Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06a3b5bf14 | ||
|
|
1a9cd35176 | ||
|
|
26ba4a409b | ||
|
|
a8c03281c6 | ||
|
|
67a33c1352 | ||
|
|
5efa8264d8 | ||
|
|
83bd85fb1a | ||
|
|
c87749cbda | ||
|
|
36b82a7776 | ||
|
|
ff03c0b718 | ||
|
|
a04ce5abfc | ||
|
|
e47ab573a7 | ||
|
|
3f18e2fabc | ||
|
|
bfd6fb00a8 | ||
|
|
1d7b3efb80 | ||
|
|
c9586bfdcf | ||
|
|
05807fe123 | ||
|
|
a8fcaf1f48 | ||
|
|
6825f8a386 | ||
|
|
b93fa4a340 | ||
|
|
44f743d52c | ||
|
|
8730049c00 | ||
|
|
2e6ad61f49 | ||
|
|
aaf3484ee5 | ||
|
|
87ef4eecb1 | ||
|
|
65fd124864 | ||
|
|
a3ac6fa498 | ||
|
|
2b2f667868 | ||
|
|
90d5dd5ad9 | ||
|
|
dbba991dd3 | ||
|
|
f76ca5643e | ||
|
|
99e3097122 | ||
|
|
55148ab3f7 | ||
|
|
f0ece1c6b7 | ||
|
|
c7996544b4 | ||
|
|
724f227842 | ||
|
|
7a47ce9d1d | ||
|
|
9f573d4299 | ||
|
|
571d35cd8b | ||
|
|
4bb4609833 | ||
|
|
f5d95ba3a1 | ||
|
|
3ef67acb92 |
@@ -0,0 +1,91 @@
|
||||
name: Build AIO Base Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.ref_name }}
|
||||
|
||||
jobs:
|
||||
base_build_setup:
|
||||
name: Build Preparation
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
|
||||
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
|
||||
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
|
||||
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
|
||||
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
|
||||
build_base: ${{ steps.changed_files.outputs.base_any_changed }}
|
||||
|
||||
steps:
|
||||
- id: set_env_variables
|
||||
name: Set Environment Variables
|
||||
run: |
|
||||
echo "BUILDX_DRIVER=cloud" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_VERSION=lab:latest" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_PLATFORMS=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
|
||||
echo "BUILDX_ENDPOINT=makeplane/plane-dev" >> $GITHUB_OUTPUT
|
||||
echo "TARGET_BRANCH=${{ env.TARGET_BRANCH }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: checkout_files
|
||||
name: Checkout Files
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get changed files
|
||||
id: changed_files
|
||||
uses: tj-actions/changed-files@v42
|
||||
with:
|
||||
files_yaml: |
|
||||
base:
|
||||
- aio/Dockerfile.base
|
||||
|
||||
base_build_push:
|
||||
if: ${{ needs.base_build_setup.outputs.build_base == 'true' || github.event_name == 'workflow_dispatch' || needs.base_build_setup.outputs.gh_branch_name == 'master' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [base_build_setup]
|
||||
env:
|
||||
BASE_IMG_TAG: ${{ secrets.DOCKERHUB_USERNAME }}/plane-aio-base:${{ needs.base_build_setup.outputs.gh_branch_name }}
|
||||
TARGET_BRANCH: ${{ needs.base_build_setup.outputs.gh_branch_name }}
|
||||
BUILDX_DRIVER: ${{ needs.base_build_setup.outputs.gh_buildx_driver }}
|
||||
BUILDX_VERSION: ${{ needs.base_build_setup.outputs.gh_buildx_version }}
|
||||
BUILDX_PLATFORMS: ${{ needs.base_build_setup.outputs.gh_buildx_platforms }}
|
||||
BUILDX_ENDPOINT: ${{ needs.base_build_setup.outputs.gh_buildx_endpoint }}
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Docker Tag
|
||||
run: |
|
||||
if [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
|
||||
TAG=${{ secrets.DOCKERHUB_USERNAME }}/plane-aio-base:latest
|
||||
else
|
||||
TAG=${{ env.BASE_IMG_TAG }}
|
||||
fi
|
||||
echo "BASE_IMG_TAG=${TAG}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: ${{ env.BUILDX_DRIVER }}
|
||||
version: ${{ env.BUILDX_VERSION }}
|
||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
with:
|
||||
context: ./aio
|
||||
file: ./aio/Dockerfile.base
|
||||
platforms: ${{ env.BUILDX_PLATFORMS }}
|
||||
tags: ${{ env.BASE_IMG_TAG }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-124
@@ -1,124 +0,0 @@
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
||||
|
||||
RUN yarn global add turbo
|
||||
RUN apk add tree
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=app --scope=plane-deploy --docker
|
||||
CMD tree -I node_modules/
|
||||
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM node:18-alpine AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install
|
||||
|
||||
# # Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
|
||||
RUN yarn turbo run build
|
||||
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
|
||||
|
||||
FROM python:3.11.1-alpine3.17 AS backend
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
RUN apk --no-cache add \
|
||||
"libpq~=15" \
|
||||
"libxslt~=1.1" \
|
||||
"nodejs-current~=19" \
|
||||
"xmlsec~=1.2" \
|
||||
"nginx" \
|
||||
"nodejs" \
|
||||
"npm" \
|
||||
"supervisor"
|
||||
|
||||
COPY apiserver/requirements.txt ./
|
||||
COPY apiserver/requirements ./requirements
|
||||
RUN apk add --no-cache libffi-dev
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
"bash~=5.2" \
|
||||
"g++~=12.2" \
|
||||
"gcc~=12.2" \
|
||||
"cargo~=1.64" \
|
||||
"git~=2" \
|
||||
"make~=4.3" \
|
||||
"postgresql13-dev~=13" \
|
||||
"libc-dev" \
|
||||
"linux-headers" \
|
||||
&& \
|
||||
pip install -r requirements.txt --compile --no-cache-dir \
|
||||
&& \
|
||||
apk del .build-deps
|
||||
|
||||
# Add in Django deps and generate Django's static files
|
||||
COPY apiserver/manage.py manage.py
|
||||
COPY apiserver/plane plane/
|
||||
COPY apiserver/templates templates/
|
||||
|
||||
RUN apk --no-cache add "bash~=5.2"
|
||||
COPY apiserver/bin ./bin/
|
||||
|
||||
RUN chmod +x ./bin/*
|
||||
RUN chmod -R 777 /code
|
||||
|
||||
# Expose container port and run entry point script
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=installer /app/apps/app/next.config.js .
|
||||
COPY --from=installer /app/apps/app/package.json .
|
||||
COPY --from=installer /app/apps/space/next.config.js .
|
||||
COPY --from=installer /app/apps/space/package.json .
|
||||
|
||||
COPY --from=installer /app/apps/app/.next/standalone ./
|
||||
|
||||
COPY --from=installer /app/apps/app/.next/static ./apps/app/.next/static
|
||||
|
||||
COPY --from=installer /app/apps/space/.next/standalone ./
|
||||
COPY --from=installer /app/apps/space/.next ./apps/space/.next
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# RUN rm /etc/nginx/conf.d/default.conf
|
||||
#######################################################################
|
||||
COPY nginx/nginx-single-docker-image.conf /etc/nginx/http.d/default.conf
|
||||
#######################################################################
|
||||
|
||||
COPY nginx/supervisor.conf /code/supervisor.conf
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
|
||||
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
COPY replace-env-vars.sh /usr/local/bin/
|
||||
COPY start.sh /usr/local/bin/
|
||||
RUN chmod +x /usr/local/bin/replace-env-vars.sh
|
||||
RUN chmod +x /usr/local/bin/start.sh
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["supervisord","-c","/code/supervisor.conf"]
|
||||
@@ -19,14 +19,14 @@ const InstanceAIPage = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Artificial Intelligence - God Mode" />
|
||||
<div className="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">AI features for all your workspaces</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Configure your AI API credentials so Plane AI features are turned on for all your workspaces.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-auto">
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceAIForm config={formattedConfig} />
|
||||
) : (
|
||||
|
||||
@@ -64,8 +64,8 @@ const InstanceGithubAuthenticationPage = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Authentication - God Mode" />
|
||||
<div className="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<AuthenticationMethodCard
|
||||
name="Github"
|
||||
description="Allow members to login or sign up to plane with their Github accounts."
|
||||
@@ -93,7 +93,7 @@ const InstanceGithubAuthenticationPage = observer(() => {
|
||||
withBorder={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-auto">
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md p-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceGithubConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
|
||||
@@ -58,8 +58,8 @@ const InstanceGoogleAuthenticationPage = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Authentication - God Mode" />
|
||||
<div className="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<AuthenticationMethodCard
|
||||
name="Google"
|
||||
description="Allow members to login or sign up to plane with their Google
|
||||
@@ -81,7 +81,7 @@ const InstanceGoogleAuthenticationPage = observer(() => {
|
||||
withBorder={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-auto">
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md p-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceGoogleConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
|
||||
@@ -119,14 +119,14 @@ const InstanceAuthenticationPage = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Authentication - God Mode" />
|
||||
<div className="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Manage authentication for your instance</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Configure authentication modes for your team and restrict sign ups to be invite only.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-auto">
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-lg font-medium">Authentication modes</div>
|
||||
|
||||
@@ -19,8 +19,8 @@ const InstanceEmailPage = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Email - God Mode" />
|
||||
<div className="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
|
||||
@@ -30,7 +30,7 @@ const InstanceEmailPage = observer(() => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-auto">
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceEmailForm config={formattedConfig} />
|
||||
) : (
|
||||
|
||||
@@ -51,7 +51,7 @@ export const SendTestEmailModal: FC<Props> = (props) => {
|
||||
setSendEmailStep(ESendEmailSteps.SUCCESS);
|
||||
})
|
||||
.catch((error) => {
|
||||
setError(error?.message || "Failed to send email");
|
||||
setError(error?.error || "Failed to send email");
|
||||
setSendEmailStep(ESendEmailSteps.FAILED);
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -10,15 +10,15 @@ function GeneralPage() {
|
||||
console.log("instance", instance);
|
||||
return (
|
||||
<>
|
||||
<div className="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">General settings</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your
|
||||
instance.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-auto">
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{instance && instanceAdmins && (
|
||||
<GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />
|
||||
)}
|
||||
|
||||
+73
-25
@@ -332,42 +332,90 @@ body {
|
||||
}
|
||||
|
||||
/* scrollbar style */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
@-moz-document url-prefix() {
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.vertical-scrollbar,
|
||||
.horizontal-scrollbar {
|
||||
scrollbar-width: initial;
|
||||
scrollbar-color: rgba(96, 100, 108, 0.1) transparent;
|
||||
}
|
||||
.vertical-scrollbar:hover,
|
||||
.horizontal-scrollbar:hover {
|
||||
scrollbar-color: rgba(96, 100, 108, 0.25) transparent;
|
||||
}
|
||||
.vertical-scrollbar:active,
|
||||
.horizontal-scrollbar:active {
|
||||
scrollbar-color: rgba(96, 100, 108, 0.7) transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-scroll-enable {
|
||||
overflow-x: scroll;
|
||||
.vertical-scrollbar {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.horizontal-scroll-enable::-webkit-scrollbar {
|
||||
.horizontal-scrollbar {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar,
|
||||
.horizontal-scrollbar::-webkit-scrollbar {
|
||||
display: block;
|
||||
height: 7px;
|
||||
width: 0;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-track,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb {
|
||||
background-clip: padding-box;
|
||||
background-color: rgba(96, 100, 108, 0.1);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
.vertical-scrollbar:hover::-webkit-scrollbar-thumb,
|
||||
.horizontal-scrollbar:hover::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(96, 100, 108, 0.25);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb:hover,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(96, 100, 108, 0.5);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-thumb:active,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-thumb:active {
|
||||
background-color: rgba(96, 100, 108, 0.7);
|
||||
}
|
||||
.vertical-scrollbar::-webkit-scrollbar-corner,
|
||||
.horizontal-scrollbar::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
.vertical-scrollbar-margin-top-md::-webkit-scrollbar-track {
|
||||
margin-top: 44px;
|
||||
}
|
||||
|
||||
.horizontal-scroll-enable::-webkit-scrollbar-track {
|
||||
height: 7px;
|
||||
background-color: rgba(var(--color-background-100));
|
||||
/* scrollbar sm size */
|
||||
.scrollbar-sm::-webkit-scrollbar {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.horizontal-scroll-enable::-webkit-scrollbar-thumb {
|
||||
border-radius: 5px;
|
||||
background-color: rgba(var(--color-scrollbar));
|
||||
.scrollbar-sm::-webkit-scrollbar-thumb {
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.vertical-scroll-enable::-webkit-scrollbar {
|
||||
display: block;
|
||||
width: 5px;
|
||||
/* scrollbar md size */
|
||||
.scrollbar-md::-webkit-scrollbar {
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.vertical-scroll-enable::-webkit-scrollbar-track {
|
||||
width: 5px;
|
||||
.scrollbar-md::-webkit-scrollbar-thumb {
|
||||
border: 3px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
/* scrollbar lg size */
|
||||
|
||||
.vertical-scroll-enable::-webkit-scrollbar-thumb {
|
||||
border-radius: 5px;
|
||||
background-color: rgba(var(--color-background-90));
|
||||
.scrollbar-lg::-webkit-scrollbar {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
.scrollbar-lg::-webkit-scrollbar-thumb {
|
||||
border: 4px solid rgba(0, 0, 0, 0);
|
||||
}
|
||||
/* end scrollbar style */
|
||||
|
||||
|
||||
@@ -19,14 +19,14 @@ const InstanceImagePage = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Image - God Mode" />
|
||||
<div className="relative container mx-auto w-full h-full p-8 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 pb-3 space-y-1 flex-shrink-0">
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Third-party image libraries</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
Let your users search and choose images from third-party libraries
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-auto">
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
{formattedConfig ? (
|
||||
<InstanceImageConfigForm config={formattedConfig} />
|
||||
) : (
|
||||
|
||||
@@ -38,7 +38,7 @@ export const HelpSection: FC = observer(() => {
|
||||
// refs
|
||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace");
|
||||
const redirectionLink = encodeURI(WEB_BASE_URL + "/");
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -56,7 +56,7 @@ export const SidebarMenu = observer(() => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-auto px-4 py-4">
|
||||
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 py-4">
|
||||
{INSTANCE_ADMIN_LINKS.map((item, index) => {
|
||||
const isActive = item.href === pathName || pathName.includes(item.href);
|
||||
return (
|
||||
|
||||
@@ -24,7 +24,7 @@ export const CopyField: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<h4 className="text-sm text-custom-text-300">{label}</h4>
|
||||
<h4 className="text-sm text-custom-text-200">{label}</h4>
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
className="flex items-center justify-between py-2"
|
||||
@@ -40,7 +40,7 @@ export const CopyField: React.FC<Props> = (props) => {
|
||||
<p className="text-sm font-medium">{url}</p>
|
||||
<Copy size={18} color="#B9B9B9" />
|
||||
</Button>
|
||||
<div className="text-xs text-custom-text-400">{description}</div>
|
||||
<div className="text-xs text-custom-text-300">{description}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -158,6 +158,7 @@ export const InstanceSetupForm: FC = (props) => {
|
||||
onError={() => setIsSubmitting(false)}
|
||||
>
|
||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||
<input type="hidden" name="is_telemetry_enabled" value={formData.is_telemetry_enabled ? "True" : "False"} />
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||
<div className="w-full space-y-1">
|
||||
@@ -319,8 +320,6 @@ export const InstanceSetupForm: FC = (props) => {
|
||||
<div>
|
||||
<Checkbox
|
||||
id="is_telemetry_enabled"
|
||||
name="is_telemetry_enabled"
|
||||
value={formData.is_telemetry_enabled ? "True" : "False"}
|
||||
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
|
||||
checked={formData.is_telemetry_enabled}
|
||||
/>
|
||||
|
||||
@@ -7,9 +7,9 @@ import { useTheme as nextUseTheme } from "next-themes";
|
||||
// ui
|
||||
import { Button, getButtonStyling } from "@plane/ui";
|
||||
// helpers
|
||||
import { resolveGeneralTheme } from "helpers/common.helper";
|
||||
import { WEB_BASE_URL, resolveGeneralTheme } from "helpers/common.helper";
|
||||
// hooks
|
||||
import { useInstance, useTheme } from "@/hooks/store";
|
||||
import { useTheme } from "@/hooks/store";
|
||||
// icons
|
||||
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
|
||||
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
|
||||
@@ -17,11 +17,10 @@ import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
|
||||
export const NewUserPopup: React.FC = observer(() => {
|
||||
// hooks
|
||||
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
|
||||
const { config } = useInstance();
|
||||
// theme
|
||||
const { resolvedTheme } = nextUseTheme();
|
||||
|
||||
const redirectionLink = `${config?.app_base_url ? `${config?.app_base_url}/create-workspace` : `/god-mode/`}`;
|
||||
const redirectionLink = encodeURI(WEB_BASE_URL + "/create-workspace");
|
||||
|
||||
if (!isNewUserPopup) return <></>;
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const nextConfig = {
|
||||
trailingSlash: true,
|
||||
reactStrictMode: false,
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
# *****************************************************************************
|
||||
# STAGE 1: Build the project
|
||||
# *****************************************************************************
|
||||
FROM node:18-alpine AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
||||
|
||||
RUN yarn global add turbo
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=web --scope=space --scope=admin --docker
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 2: Install dependencies & build the project
|
||||
# *****************************************************************************
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM node:18-alpine AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install
|
||||
|
||||
# # Build the project
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV TURBO_TELEMETRY_DISABLED 1
|
||||
|
||||
RUN yarn turbo run build
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
# *****************************************************************************
|
||||
# FROM makeplane/plane-aio-base AS runner
|
||||
FROM makeplane/plane-aio-base:develop AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
SHELL [ "/bin/bash", "-c" ]
|
||||
|
||||
# PYTHON APPLICATION SETUP
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
COPY apiserver/requirements.txt ./api/
|
||||
COPY apiserver/requirements ./api/requirements
|
||||
|
||||
RUN python3.12 -m venv /app/venv && \
|
||||
source /app/venv/bin/activate && \
|
||||
/app/venv/bin/pip install --upgrade pip && \
|
||||
/app/venv/bin/pip install -r ./api/requirements.txt --compile --no-cache-dir
|
||||
|
||||
# Add in Django deps and generate Django's static files
|
||||
COPY apiserver/manage.py ./api/manage.py
|
||||
COPY apiserver/plane ./api/plane/
|
||||
COPY apiserver/templates ./api/templates/
|
||||
COPY package.json ./api/package.json
|
||||
|
||||
COPY apiserver/bin ./api/bin/
|
||||
|
||||
RUN chmod +x ./api/bin/*
|
||||
RUN chmod -R 777 ./api/
|
||||
|
||||
# NEXTJS BUILDS
|
||||
|
||||
COPY --from=installer /app/web/next.config.js ./web/
|
||||
COPY --from=installer /app/web/package.json ./web/
|
||||
COPY --from=installer /app/web/.next/standalone ./web
|
||||
COPY --from=installer /app/web/.next/static ./web/web/.next/static
|
||||
COPY --from=installer /app/web/public ./web/web/public
|
||||
|
||||
COPY --from=installer /app/space/next.config.js ./space/
|
||||
COPY --from=installer /app/space/package.json ./space/
|
||||
COPY --from=installer /app/space/.next/standalone ./space
|
||||
COPY --from=installer /app/space/.next/static ./space/space/.next/static
|
||||
COPY --from=installer /app/space/public ./space/space/public
|
||||
|
||||
COPY --from=installer /app/admin/next.config.js ./admin/
|
||||
COPY --from=installer /app/admin/package.json ./admin/
|
||||
COPY --from=installer /app/admin/.next/standalone ./admin
|
||||
COPY --from=installer /app/admin/.next/static ./admin/admin/.next/static
|
||||
COPY --from=installer /app/admin/public ./admin/admin/public
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
ENV TURBO_TELEMETRY_DISABLED 1
|
||||
|
||||
COPY aio/supervisord.conf /app/supervisord.conf
|
||||
|
||||
COPY aio/aio.sh /app/aio.sh
|
||||
RUN chmod +x /app/aio.sh
|
||||
|
||||
COPY aio/pg-setup.sh /app/pg-setup.sh
|
||||
RUN chmod +x /app/pg-setup.sh
|
||||
|
||||
COPY deploy/selfhost/variables.env /app/plane.env
|
||||
|
||||
# NGINX Conf Copy
|
||||
COPY ./aio/nginx.conf.aio /etc/nginx/nginx.conf.template
|
||||
COPY ./nginx/env.sh /app/nginx-start.sh
|
||||
RUN chmod +x /app/nginx-start.sh
|
||||
|
||||
RUN ./pg-setup.sh
|
||||
|
||||
VOLUME [ "/app/data/minio/uploads", "/var/lib/postgresql/data" ]
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/app/supervisord.conf"]
|
||||
@@ -0,0 +1,92 @@
|
||||
FROM --platform=$BUILDPLATFORM tonistiigi/binfmt as binfmt
|
||||
|
||||
FROM debian:12-slim
|
||||
|
||||
# Set environment variables to non-interactive for apt
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
SHELL [ "/bin/bash", "-c" ]
|
||||
|
||||
# Update the package list and install prerequisites
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
gnupg2 curl ca-certificates lsb-release software-properties-common \
|
||||
build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \
|
||||
libsqlite3-dev wget llvm libncurses5-dev libncursesw5-dev xz-utils \
|
||||
tk-dev libffi-dev liblzma-dev supervisor nginx nano vim ncdu
|
||||
|
||||
# Install Redis 7.2
|
||||
RUN echo "deb http://deb.debian.org/debian $(lsb_release -cs)-backports main" > /etc/apt/sources.list.d/backports.list && \
|
||||
curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" > /etc/apt/sources.list.d/redis.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y redis-server
|
||||
|
||||
# Install PostgreSQL 15
|
||||
ENV POSTGRES_VERSION 15
|
||||
RUN curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/pgdg-archive-keyring.gpg && \
|
||||
echo "deb [signed-by=/usr/share/keyrings/pgdg-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y postgresql-$POSTGRES_VERSION postgresql-client-$POSTGRES_VERSION && \
|
||||
mkdir -p /var/lib/postgresql/data && \
|
||||
chown -R postgres:postgres /var/lib/postgresql
|
||||
|
||||
# Install MinIO
|
||||
ARG TARGETARCH
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
curl -fSl https://dl.min.io/server/minio/release/linux-amd64/minio -o /usr/local/bin/minio; \
|
||||
elif [ "$TARGETARCH" = "arm64" ]; then \
|
||||
curl -fSl https://dl.min.io/server/minio/release/linux-arm64/minio -o /usr/local/bin/minio; \
|
||||
else \
|
||||
echo "Unsupported architecture: $TARGETARCH"; exit 1; \
|
||||
fi && \
|
||||
chmod +x /usr/local/bin/minio
|
||||
|
||||
|
||||
# Install Node.js 18
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Install Python 3.12 from source
|
||||
RUN cd /usr/src && \
|
||||
wget https://www.python.org/ftp/python/3.12.0/Python-3.12.0.tgz && \
|
||||
tar xzf Python-3.12.0.tgz && \
|
||||
cd Python-3.12.0 && \
|
||||
./configure --enable-optimizations && \
|
||||
make altinstall && \
|
||||
rm -f /usr/src/Python-3.12.0.tgz
|
||||
|
||||
RUN python3.12 -m pip install --upgrade pip
|
||||
|
||||
RUN echo "alias python=/usr/local/bin/python3.12" >> ~/.bashrc && \
|
||||
echo "alias pip=/usr/local/bin/pip3.12" >> ~/.bashrc
|
||||
|
||||
# Clean up
|
||||
RUN apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /usr/src/Python-3.12.0
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN mkdir -p /app/{data,logs} && \
|
||||
mkdir -p /app/data/{redis,pg,minio,nginx} && \
|
||||
mkdir -p /app/logs/{access,error} && \
|
||||
mkdir -p /etc/supervisor/conf.d
|
||||
|
||||
# Create Supervisor configuration file
|
||||
COPY supervisord.base /app/supervisord.conf
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y sudo lsof net-tools libpq-dev procps gettext && \
|
||||
apt-get clean
|
||||
|
||||
RUN sudo -u postgres /usr/lib/postgresql/$POSTGRES_VERSION/bin/initdb -D /var/lib/postgresql/data
|
||||
COPY postgresql.conf /etc/postgresql/postgresql.conf
|
||||
|
||||
RUN echo "alias python=/usr/local/bin/python3.12" >> ~/.bashrc && \
|
||||
echo "alias pip=/usr/local/bin/pip3.12" >> ~/.bashrc
|
||||
|
||||
# Expose ports for Redis, PostgreSQL, and MinIO
|
||||
EXPOSE 6379 5432 9000 80
|
||||
|
||||
# Start Supervisor
|
||||
CMD ["/usr/bin/supervisord", "-c", "/app/supervisord.conf"]
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
|
||||
if [ "$1" = 'api' ]; then
|
||||
source /app/venv/bin/activate
|
||||
cd /app/api
|
||||
exec ./bin/docker-entrypoint-api.sh
|
||||
elif [ "$1" = 'worker' ]; then
|
||||
source /app/venv/bin/activate
|
||||
cd /app/api
|
||||
exec ./bin/docker-entrypoint-worker.sh
|
||||
elif [ "$1" = 'beat' ]; then
|
||||
source /app/venv/bin/activate
|
||||
cd /app/api
|
||||
exec ./bin/docker-entrypoint-beat.sh
|
||||
elif [ "$1" = 'migrator' ]; then
|
||||
source /app/venv/bin/activate
|
||||
cd /app/api
|
||||
exec ./bin/docker-entrypoint-migrator.sh
|
||||
elif [ "$1" = 'web' ]; then
|
||||
node /app/web/web/server.js
|
||||
elif [ "$1" = 'space' ]; then
|
||||
node /app/space/space/server.js
|
||||
elif [ "$1" = 'admin' ]; then
|
||||
node /app/admin/admin/server.js
|
||||
else
|
||||
echo "Command not found"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,73 @@
|
||||
events {
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
root /www/data/;
|
||||
access_log /var/log/nginx/access.log;
|
||||
|
||||
client_max_body_size ${FILE_SIZE_LIMIT};
|
||||
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Permissions-Policy "interest-cohort=()" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Forwarded-Proto "${dollar}scheme";
|
||||
add_header X-Forwarded-Host "${dollar}host";
|
||||
add_header X-Forwarded-For "${dollar}proxy_add_x_forwarded_for";
|
||||
add_header X-Real-IP "${dollar}remote_addr";
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:3001/;
|
||||
}
|
||||
|
||||
location /spaces/ {
|
||||
rewrite ^/spaces/?$ /spaces/login break;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:3002/spaces/;
|
||||
}
|
||||
|
||||
|
||||
location /god-mode/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:3003/god-mode/;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:8000/api/;
|
||||
}
|
||||
|
||||
location /auth/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:8000/auth/;
|
||||
}
|
||||
|
||||
location /${BUCKET_NAME}/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade ${dollar}http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host ${dollar}http_host;
|
||||
proxy_pass http://localhost:9000/uploads/;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
# Variables
|
||||
set -o allexport
|
||||
source plane.env set
|
||||
set +o allexport
|
||||
|
||||
export PGHOST=localhost
|
||||
|
||||
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/pg_ctl" -D /var/lib/postgresql/data start
|
||||
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/psql" --command "CREATE USER $POSTGRES_USER WITH SUPERUSER PASSWORD '$POSTGRES_PASSWORD';" && \
|
||||
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/createdb" -O "$POSTGRES_USER" "$POSTGRES_DB" && \
|
||||
sudo -u postgres "/usr/lib/postgresql/${POSTGRES_VERSION}/bin/pg_ctl" -D /var/lib/postgresql/data stop
|
||||
@@ -0,0 +1,12 @@
|
||||
# PostgreSQL configuration file
|
||||
|
||||
# Allow connections from any IP address
|
||||
listen_addresses = '*'
|
||||
|
||||
# Set the maximum number of connections
|
||||
max_connections = 100
|
||||
|
||||
# Set the shared buffers size
|
||||
shared_buffers = 128MB
|
||||
|
||||
# Other custom configurations can be added here
|
||||
@@ -0,0 +1,37 @@
|
||||
[supervisord]
|
||||
user=root
|
||||
nodaemon=true
|
||||
stderr_logfile=/app/logs/error/supervisor.err.log
|
||||
stdout_logfile=/app/logs/access/supervisor.out.log
|
||||
|
||||
[program:redis]
|
||||
directory=/app/data/redis
|
||||
command=redis-server
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/app/logs/error/redis.err.log
|
||||
stdout_logfile=/app/logs/access/redis.out.log
|
||||
|
||||
[program:postgresql]
|
||||
user=postgres
|
||||
command=/usr/lib/postgresql/15/bin/postgres --config-file=/etc/postgresql/15/main/postgresql.conf
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/app/logs/error/postgresql.err.log
|
||||
stdout_logfile=/app/logs/access/postgresql.out.log
|
||||
|
||||
[program:minio]
|
||||
directory=/app/data/minio
|
||||
command=minio server /app/data/minio
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/app/logs/error/minio.err.log
|
||||
stdout_logfile=/app/logs/access/minio.out.log
|
||||
|
||||
[program:nginx]
|
||||
directory=/app/data/nginx
|
||||
command=/usr/sbin/nginx -g 'daemon off;'
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/app/logs/error/nginx.err.log
|
||||
stdout_logfile=/app/logs/access/nginx.out.log
|
||||
@@ -0,0 +1,115 @@
|
||||
[supervisord]
|
||||
user=root
|
||||
nodaemon=true
|
||||
priority=1
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:redis]
|
||||
directory=/app/data/redis
|
||||
command=redis-server
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=1
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:postgresql]
|
||||
user=postgres
|
||||
command=/usr/lib/postgresql/15/bin/postgres -D /var/lib/postgresql/data --config-file=/etc/postgresql/postgresql.conf
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=1
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:minio]
|
||||
directory=/app/data/minio
|
||||
command=minio server /app/data/minio
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=1
|
||||
stdout_logfile=/app/logs/access/minio.log
|
||||
stderr_logfile=/app/logs/error/minio.err.log
|
||||
|
||||
[program:nginx]
|
||||
command=/app/nginx-start.sh
|
||||
autostart=true
|
||||
autorestart=true
|
||||
priority=1
|
||||
stdout_logfile=/app/logs/access/nginx.log
|
||||
stderr_logfile=/app/logs/error/nginx.err.log
|
||||
|
||||
|
||||
[program:web]
|
||||
command=/app/aio.sh web
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
environment=PORT=3001,HOSTNAME=0.0.0.0
|
||||
|
||||
[program:space]
|
||||
command=/app/aio.sh space
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
environment=PORT=3002,HOSTNAME=0.0.0.0
|
||||
|
||||
[program:admin]
|
||||
command=/app/aio.sh admin
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
environment=PORT=3003,HOSTNAME=0.0.0.0
|
||||
|
||||
[program:migrator]
|
||||
command=/app/aio.sh migrator
|
||||
autostart=true
|
||||
autorestart=false
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:api]
|
||||
command=/app/aio.sh api
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:worker]
|
||||
command=/app/aio.sh worker
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:beat]
|
||||
command=/app/aio.sh beat
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stdout
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
@@ -22,7 +22,7 @@ from plane.db.models import (
|
||||
IssueProperty,
|
||||
Module,
|
||||
Project,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
ProjectMember,
|
||||
State,
|
||||
Workspace,
|
||||
@@ -99,7 +99,7 @@ class ProjectAPIEndpoint(BaseAPIView):
|
||||
)
|
||||
.annotate(
|
||||
is_deployed=Exists(
|
||||
ProjectDeployBoard.objects.filter(
|
||||
DeployBoard.objects.filter(
|
||||
project_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ from .project import (
|
||||
ProjectIdentifierSerializer,
|
||||
ProjectLiteSerializer,
|
||||
ProjectMemberLiteSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
DeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
ProjectPublicMemberSerializer,
|
||||
ProjectMemberRoleSerializer,
|
||||
|
||||
@@ -106,7 +106,9 @@ class PageDetailSerializer(PageSerializer):
|
||||
description_html = serializers.CharField()
|
||||
|
||||
class Meta(PageSerializer.Meta):
|
||||
fields = PageSerializer.Meta.fields + ["description_html"]
|
||||
fields = PageSerializer.Meta.fields + [
|
||||
"description_html",
|
||||
]
|
||||
|
||||
|
||||
class SubPageSerializer(BaseSerializer):
|
||||
|
||||
@@ -13,7 +13,7 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
ProjectMemberInvite,
|
||||
ProjectIdentifier,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
|
||||
@@ -206,14 +206,14 @@ class ProjectMemberLiteSerializer(BaseSerializer):
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class ProjectDeployBoardSerializer(BaseSerializer):
|
||||
class DeployBoardSerializer(BaseSerializer):
|
||||
project_details = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(
|
||||
read_only=True, source="workspace"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ProjectDeployBoard
|
||||
model = DeployBoard
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
|
||||
@@ -6,6 +6,7 @@ from plane.app.views import (
|
||||
PageFavoriteViewSet,
|
||||
PageLogEndpoint,
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
)
|
||||
|
||||
|
||||
@@ -79,4 +80,14 @@ urlpatterns = [
|
||||
SubPagesEndpoint.as_view(),
|
||||
name="sub-page",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:pk>/description/",
|
||||
PagesDescriptionViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
}
|
||||
),
|
||||
name="page-description",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -12,7 +12,7 @@ from plane.app.views import (
|
||||
ProjectFavoritesViewSet,
|
||||
UserProjectInvitationsViewset,
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
DeployBoardViewSet,
|
||||
UserProjectRolesEndpoint,
|
||||
ProjectArchiveUnarchiveEndpoint,
|
||||
)
|
||||
@@ -157,7 +157,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
|
||||
ProjectDeployBoardViewSet.as_view(
|
||||
DeployBoardViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
@@ -167,7 +167,7 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
|
||||
ProjectDeployBoardViewSet.as_view(
|
||||
DeployBoardViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
|
||||
@@ -4,7 +4,7 @@ from .project.base import (
|
||||
ProjectUserViewsEndpoint,
|
||||
ProjectFavoritesViewSet,
|
||||
ProjectPublicCoverImagesEndpoint,
|
||||
ProjectDeployBoardViewSet,
|
||||
DeployBoardViewSet,
|
||||
ProjectArchiveUnarchiveEndpoint,
|
||||
)
|
||||
|
||||
@@ -177,6 +177,7 @@ from .page.base import (
|
||||
PageFavoriteViewSet,
|
||||
PageLogEndpoint,
|
||||
SubPagesEndpoint,
|
||||
PagesDescriptionViewSet,
|
||||
)
|
||||
|
||||
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Python imports
|
||||
import json
|
||||
import base64
|
||||
from datetime import datetime
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
@@ -8,6 +9,7 @@ from django.db import connection
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.gzip import gzip_page
|
||||
from django.http import StreamingHttpResponse
|
||||
|
||||
# Third party imports
|
||||
from rest_framework import status
|
||||
@@ -388,3 +390,48 @@ class SubPagesEndpoint(BaseAPIView):
|
||||
return Response(
|
||||
SubPageSerializer(pages, many=True).data, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
|
||||
class PagesDescriptionViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
]
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
binary_data = page.description_binary
|
||||
|
||||
def stream_data():
|
||||
if binary_data:
|
||||
yield binary_data
|
||||
else:
|
||||
yield b""
|
||||
|
||||
response = StreamingHttpResponse(
|
||||
stream_data(), content_type="application/octet-stream"
|
||||
)
|
||||
response["Content-Disposition"] = (
|
||||
'attachment; filename="page_description.bin"'
|
||||
)
|
||||
return response
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk):
|
||||
page = Page.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
base64_data = request.data.get("description_binary")
|
||||
|
||||
if base64_data:
|
||||
# Decode the base64 data to bytes
|
||||
new_binary_data = base64.b64decode(base64_data)
|
||||
|
||||
# Store the updated binary data
|
||||
page.description_binary = new_binary_data
|
||||
page.description_html = request.data.get("description_html")
|
||||
page.save()
|
||||
return Response({"message": "Updated successfully"})
|
||||
else:
|
||||
return Response({"error": "No binary data provided"})
|
||||
|
||||
@@ -28,7 +28,7 @@ from plane.app.views.base import BaseViewSet, BaseAPIView
|
||||
from plane.app.serializers import (
|
||||
ProjectSerializer,
|
||||
ProjectListSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
DeployBoardSerializer,
|
||||
)
|
||||
|
||||
from plane.app.permissions import (
|
||||
@@ -46,7 +46,7 @@ from plane.db.models import (
|
||||
Module,
|
||||
Cycle,
|
||||
Inbox,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
IssueProperty,
|
||||
Issue,
|
||||
)
|
||||
@@ -138,7 +138,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
.annotate(
|
||||
is_deployed=Exists(
|
||||
ProjectDeployBoard.objects.filter(
|
||||
DeployBoard.objects.filter(
|
||||
project_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
@@ -639,12 +639,12 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||
return Response(files, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ProjectDeployBoardViewSet(BaseViewSet):
|
||||
class DeployBoardViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectMemberPermission,
|
||||
]
|
||||
serializer_class = ProjectDeployBoardSerializer
|
||||
model = ProjectDeployBoard
|
||||
serializer_class = DeployBoardSerializer
|
||||
model = DeployBoard
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
@@ -673,7 +673,7 @@ class ProjectDeployBoardViewSet(BaseViewSet):
|
||||
},
|
||||
)
|
||||
|
||||
project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create(
|
||||
project_deploy_board, _ = DeployBoard.objects.get_or_create(
|
||||
anchor=f"{slug}/{project_id}",
|
||||
project_id=project_id,
|
||||
)
|
||||
@@ -685,5 +685,5 @@ class ProjectDeployBoardViewSet(BaseViewSet):
|
||||
|
||||
project_deploy_board.save()
|
||||
|
||||
serializer = ProjectDeployBoardSerializer(project_deploy_board)
|
||||
serializer = DeployBoardSerializer(project_deploy_board)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Python imports
|
||||
# import uuid
|
||||
import uuid
|
||||
|
||||
# Django imports
|
||||
from django.db.models import Case, Count, IntegerField, Q, When
|
||||
@@ -183,8 +183,8 @@ class UserEndpoint(BaseViewSet):
|
||||
profile.save()
|
||||
|
||||
# Reset password
|
||||
# user.is_password_autoset = True
|
||||
# user.set_password(uuid.uuid4().hex)
|
||||
user.is_password_autoset = True
|
||||
user.set_password(uuid.uuid4().hex)
|
||||
|
||||
# Deactivate the user
|
||||
user.is_active = False
|
||||
|
||||
@@ -17,6 +17,7 @@ AUTHENTICATION_ERROR_CODES = {
|
||||
"INVALID_EMAIL_SIGN_UP": 5045,
|
||||
"INVALID_EMAIL_MAGIC_SIGN_UP": 5050,
|
||||
"MAGIC_SIGN_UP_EMAIL_CODE_REQUIRED": 5055,
|
||||
"EMAIL_PASSWORD_AUTHENTICATION_DISABLED": 5056,
|
||||
# Sign In
|
||||
"USER_DOES_NOT_EXIST": 5060,
|
||||
"AUTHENTICATION_FAILED_SIGN_IN": 5065,
|
||||
|
||||
@@ -8,6 +8,10 @@ from django.utils import timezone
|
||||
from plane.db.models import Account
|
||||
|
||||
from .base import Adapter
|
||||
from plane.authentication.adapter.error import (
|
||||
AuthenticationException,
|
||||
AUTHENTICATION_ERROR_CODES,
|
||||
)
|
||||
|
||||
|
||||
class OauthAdapter(Adapter):
|
||||
@@ -50,20 +54,42 @@ class OauthAdapter(Adapter):
|
||||
return self.complete_login_or_signup()
|
||||
|
||||
def get_user_token(self, data, headers=None):
|
||||
headers = headers or {}
|
||||
response = requests.post(
|
||||
self.get_token_url(), data=data, headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
try:
|
||||
headers = headers or {}
|
||||
response = requests.post(
|
||||
self.get_token_url(), data=data, headers=headers
|
||||
)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException:
|
||||
code = (
|
||||
"GOOGLE_OAUTH_PROVIDER_ERROR"
|
||||
if self.provider == "google"
|
||||
else "GITHUB_OAUTH_PROVIDER_ERROR"
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[code],
|
||||
error_message=str(code),
|
||||
)
|
||||
|
||||
def get_user_response(self):
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.token_data.get('access_token')}"
|
||||
}
|
||||
response = requests.get(self.get_user_info_url(), headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
try:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.token_data.get('access_token')}"
|
||||
}
|
||||
response = requests.get(self.get_user_info_url(), headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except requests.RequestException:
|
||||
code = (
|
||||
"GOOGLE_OAUTH_PROVIDER_ERROR"
|
||||
if self.provider == "google"
|
||||
else "GITHUB_OAUTH_PROVIDER_ERROR"
|
||||
)
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[code],
|
||||
error_message=str(code),
|
||||
)
|
||||
|
||||
def set_user_data(self, data):
|
||||
self.user_data = data
|
||||
|
||||
@@ -41,8 +41,10 @@ class EmailProvider(CredentialAdapter):
|
||||
|
||||
if ENABLE_EMAIL_PASSWORD == "0":
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES["ENABLE_EMAIL_PASSWORD"],
|
||||
error_message="ENABLE_EMAIL_PASSWORD",
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"EMAIL_PASSWORD_AUTHENTICATION_DISABLED"
|
||||
],
|
||||
error_message="EMAIL_PASSWORD_AUTHENTICATION_DISABLED",
|
||||
)
|
||||
|
||||
def set_user_data(self):
|
||||
|
||||
@@ -105,14 +105,26 @@ class GitHubOAuthProvider(OauthAdapter):
|
||||
)
|
||||
|
||||
def __get_email(self, headers):
|
||||
# Github does not provide email in user response
|
||||
emails_url = "https://api.github.com/user/emails"
|
||||
emails_response = requests.get(emails_url, headers=headers).json()
|
||||
email = next(
|
||||
(email["email"] for email in emails_response if email["primary"]),
|
||||
None,
|
||||
)
|
||||
return email
|
||||
try:
|
||||
# Github does not provide email in user response
|
||||
emails_url = "https://api.github.com/user/emails"
|
||||
emails_response = requests.get(emails_url, headers=headers).json()
|
||||
email = next(
|
||||
(
|
||||
email["email"]
|
||||
for email in emails_response
|
||||
if email["primary"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
return email
|
||||
except requests.RequestException:
|
||||
raise AuthenticationException(
|
||||
error_code=AUTHENTICATION_ERROR_CODES[
|
||||
"GITHUB_OAUTH_PROVIDER_ERROR"
|
||||
],
|
||||
error_message="GITHUB_OAUTH_PROVIDER_ERROR",
|
||||
)
|
||||
|
||||
def set_user_data(self):
|
||||
user_info_response = self.get_user_response()
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# Django imports
|
||||
from django.core.management import BaseCommand, CommandError
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import User
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Make the user with the given email active"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
# Positional argument
|
||||
parser.add_argument("email", type=str, help="user email")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# get the user email from console
|
||||
email = options.get("email", False)
|
||||
|
||||
# raise error if email is not present
|
||||
if not email:
|
||||
raise CommandError("Error: Email is required")
|
||||
|
||||
# filter the user
|
||||
user = User.objects.filter(email=email).first()
|
||||
|
||||
# Raise error if the user is not present
|
||||
if not user:
|
||||
raise CommandError(f"Error: User with {email} does not exists")
|
||||
|
||||
# Activate the user
|
||||
user.is_active = True
|
||||
user.save()
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("User activated succesfully"))
|
||||
@@ -0,0 +1,221 @@
|
||||
# # Generated by Django 4.2.7 on 2024-05-24 09:47
|
||||
import uuid
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import plane.db.models.deploy_board
|
||||
|
||||
|
||||
def issue_estimate_point(apps, schema_editor):
|
||||
Project = apps.get_model("db", "Project")
|
||||
EstimatePoint = apps.get_model("db", "EstimatePoint")
|
||||
Issue = apps.get_model("db", "Issue")
|
||||
updated_estimate_point = []
|
||||
|
||||
# loop through all the projects
|
||||
for project in Project.objects.filter(estimate__isnull=False):
|
||||
estimate_points = EstimatePoint.objects.filter(
|
||||
estimate=project.estimate, project=project
|
||||
)
|
||||
for issue in Issue.objects.filter(
|
||||
point__isnull=False, project=project
|
||||
):
|
||||
# get the estimate id for the corresponding estimate point in the issue
|
||||
estimate = estimate_points.filter(key=issue.point).first()
|
||||
issue.estimate_point = estimate
|
||||
updated_estimate_point.append(issue)
|
||||
|
||||
Issue.objects.bulk_update(
|
||||
updated_estimate_point, ["estimate_point"], batch_size=1000
|
||||
)
|
||||
|
||||
|
||||
def last_used_estimate(apps, schema_editor):
|
||||
Project = apps.get_model("db", "Project")
|
||||
Estimate = apps.get_model("db", "Estimate")
|
||||
|
||||
# Get all estimate ids used in projects
|
||||
estimate_ids = Project.objects.filter(estimate__isnull=False).values_list(
|
||||
"estimate", flat=True
|
||||
)
|
||||
|
||||
# Update all matching estimates
|
||||
Estimate.objects.filter(id__in=estimate_ids).update(last_used=True)
|
||||
|
||||
|
||||
def populate_deploy_board(apps, schema_editor):
|
||||
DeployBoard = apps.get_model("db", "DeployBoard")
|
||||
ProjectDeployBoard = apps.get_model("db", "ProjectDeployBoard")
|
||||
|
||||
DeployBoard.objects.bulk_create(
|
||||
[
|
||||
DeployBoard(
|
||||
entity_identifier=deploy_board.project_id,
|
||||
project_id=deploy_board.project_id,
|
||||
entity_name="project",
|
||||
anchor=deploy_board.anchor,
|
||||
comments=deploy_board.comments,
|
||||
reactions=deploy_board.reactions,
|
||||
inbox=deploy_board.inbox,
|
||||
votes=deploy_board.votes,
|
||||
views=deploy_board.views,
|
||||
workspace_id=deploy_board.workspace_id,
|
||||
created_at=deploy_board.created_at,
|
||||
updated_at=deploy_board.updated_at,
|
||||
created_by_id=deploy_board.created_by_id,
|
||||
updated_by_id=deploy_board.updated_by_id,
|
||||
)
|
||||
for deploy_board in ProjectDeployBoard.objects.all()
|
||||
],
|
||||
batch_size=100,
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0066_account_id_token_cycle_logo_props_module_logo_props"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="DeployBoard",
|
||||
fields=[
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(
|
||||
auto_now_add=True, verbose_name="Created At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(
|
||||
auto_now=True, verbose_name="Last Modified At"
|
||||
),
|
||||
),
|
||||
(
|
||||
"id",
|
||||
models.UUIDField(
|
||||
db_index=True,
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("entity_identifier", models.UUIDField(null=True)),
|
||||
(
|
||||
"entity_name",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("project", "Project"),
|
||||
("issue", "Issue"),
|
||||
("module", "Module"),
|
||||
("cycle", "Task"),
|
||||
("page", "Page"),
|
||||
("view", "View"),
|
||||
],
|
||||
max_length=30,
|
||||
),
|
||||
),
|
||||
(
|
||||
"anchor",
|
||||
models.CharField(
|
||||
db_index=True,
|
||||
default=plane.db.models.deploy_board.get_anchor,
|
||||
max_length=255,
|
||||
unique=True,
|
||||
),
|
||||
),
|
||||
("comments", models.BooleanField(default=False)),
|
||||
("reactions", models.BooleanField(default=False)),
|
||||
("votes", models.BooleanField(default=False)),
|
||||
("views", models.JSONField(default=dict)),
|
||||
(
|
||||
"created_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_created_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Created By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"inbox",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="board_inbox",
|
||||
to="db.inbox",
|
||||
),
|
||||
),
|
||||
(
|
||||
"project",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="project_%(class)s",
|
||||
to="db.project",
|
||||
),
|
||||
),
|
||||
(
|
||||
"updated_by",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="%(class)s_updated_by",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
verbose_name="Last Modified By",
|
||||
),
|
||||
),
|
||||
(
|
||||
"workspace",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="workspace_%(class)s",
|
||||
to="db.workspace",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Deploy Board",
|
||||
"verbose_name_plural": "Deploy Boards",
|
||||
"db_table": "deploy_boards",
|
||||
"ordering": ("-created_at",),
|
||||
"unique_together": {("entity_name", "entity_identifier")},
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="estimate",
|
||||
name="last_used",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
# Rename the existing field
|
||||
migrations.RenameField(
|
||||
model_name="issue",
|
||||
old_name="estimate_point",
|
||||
new_name="point",
|
||||
),
|
||||
# Add a new field with the original name as a foreign key
|
||||
migrations.AddField(
|
||||
model_name="issue",
|
||||
name="estimate_point",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="issue_estimate",
|
||||
to="db.EstimatePoint",
|
||||
blank=True,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="estimate",
|
||||
name="type",
|
||||
field=models.CharField(default="categories", max_length=255),
|
||||
),
|
||||
migrations.RunPython(issue_estimate_point),
|
||||
migrations.RunPython(last_used_estimate),
|
||||
migrations.RunPython(populate_deploy_board),
|
||||
]
|
||||
@@ -4,6 +4,7 @@ from .asset import FileAsset
|
||||
from .base import BaseModel
|
||||
from .cycle import Cycle, CycleFavorite, CycleIssue, CycleUserProperties
|
||||
from .dashboard import Dashboard, DashboardWidget, Widget
|
||||
from .deploy_board import DeployBoard
|
||||
from .estimate import Estimate, EstimatePoint
|
||||
from .exporter import ExporterHistory
|
||||
from .importer import Importer
|
||||
@@ -53,7 +54,6 @@ from .page import Page, PageFavorite, PageLabel, PageLog
|
||||
from .project import (
|
||||
Project,
|
||||
ProjectBaseModel,
|
||||
ProjectDeployBoard,
|
||||
ProjectFavorite,
|
||||
ProjectIdentifier,
|
||||
ProjectMember,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# Python imports
|
||||
from uuid import uuid4
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
|
||||
# Module imports
|
||||
from .workspace import WorkspaceBaseModel
|
||||
|
||||
|
||||
def get_anchor():
|
||||
return uuid4().hex
|
||||
|
||||
class DeployBoard(WorkspaceBaseModel):
|
||||
TYPE_CHOICES = (
|
||||
("project", "Project"),
|
||||
("issue", "Issue"),
|
||||
("module", "Module"),
|
||||
("cycle", "Task"),
|
||||
("page", "Page"),
|
||||
("view", "View"),
|
||||
)
|
||||
|
||||
entity_identifier = models.UUIDField(null=True)
|
||||
entity_name = models.CharField(
|
||||
max_length=30,
|
||||
choices=TYPE_CHOICES,
|
||||
)
|
||||
anchor = models.CharField(
|
||||
max_length=255, default=get_anchor, unique=True, db_index=True
|
||||
)
|
||||
comments = models.BooleanField(default=False)
|
||||
reactions = models.BooleanField(default=False)
|
||||
inbox = models.ForeignKey(
|
||||
"db.Inbox",
|
||||
related_name="board_inbox",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
)
|
||||
votes = models.BooleanField(default=False)
|
||||
views = models.JSONField(default=dict)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the deploy board"""
|
||||
return f"{self.entity_identifier} <{self.entity_name}>"
|
||||
|
||||
class Meta:
|
||||
unique_together = ["entity_name", "entity_identifier"]
|
||||
verbose_name = "Deploy Board"
|
||||
verbose_name_plural = "Deploy Boards"
|
||||
db_table = "deploy_boards"
|
||||
ordering = ("-created_at",)
|
||||
@@ -11,7 +11,8 @@ class Estimate(ProjectBaseModel):
|
||||
description = models.TextField(
|
||||
verbose_name="Estimate Description", blank=True
|
||||
)
|
||||
type = models.CharField(max_length=255, default="Categories")
|
||||
type = models.CharField(max_length=255, default="categories")
|
||||
last_used = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the estimate"""
|
||||
|
||||
@@ -119,11 +119,18 @@ class Issue(ProjectBaseModel):
|
||||
blank=True,
|
||||
related_name="state_issue",
|
||||
)
|
||||
estimate_point = models.IntegerField(
|
||||
point = models.IntegerField(
|
||||
validators=[MinValueValidator(0), MaxValueValidator(12)],
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
estimate_point = models.ForeignKey(
|
||||
"db.EstimatePoint",
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="issue_estimate",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
name = models.CharField(max_length=255, verbose_name="Issue Name")
|
||||
description = models.JSONField(blank=True, default=dict)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
|
||||
@@ -18,6 +18,7 @@ def get_view_props():
|
||||
class Page(ProjectBaseModel):
|
||||
name = models.CharField(max_length=255, blank=True)
|
||||
description = models.JSONField(default=dict, blank=True)
|
||||
description_binary = models.BinaryField(null=True)
|
||||
description_html = models.TextField(blank=True, default="<p></p>")
|
||||
description_stripped = models.TextField(blank=True, null=True)
|
||||
owned_by = models.ForeignKey(
|
||||
@@ -43,7 +44,6 @@ class Page(ProjectBaseModel):
|
||||
is_locked = models.BooleanField(default=False)
|
||||
view_props = models.JSONField(default=get_view_props)
|
||||
logo_props = models.JSONField(default=dict)
|
||||
description_binary = models.BinaryField(null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Page"
|
||||
|
||||
@@ -260,6 +260,7 @@ def get_default_views():
|
||||
}
|
||||
|
||||
|
||||
# DEPRECATED TODO: - Remove in next release
|
||||
class ProjectDeployBoard(ProjectBaseModel):
|
||||
anchor = models.CharField(
|
||||
max_length=255, default=get_anchor, unique=True, db_index=True
|
||||
|
||||
@@ -14,7 +14,6 @@ class InstanceSerializer(BaseSerializer):
|
||||
exclude = [
|
||||
"license_key",
|
||||
"api_key",
|
||||
"version",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
|
||||
@@ -148,7 +148,7 @@ class InstanceEndpoint(BaseAPIView):
|
||||
data["app_base_url"] = settings.APP_BASE_URL
|
||||
|
||||
instance_data = serializer.data
|
||||
instance_data["workspaces_exist"] = Workspace.objects.count() > 1
|
||||
instance_data["workspaces_exist"] = Workspace.objects.count() >= 1
|
||||
|
||||
response_data = {"config": data, "instance": instance_data}
|
||||
return Response(response_data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -50,7 +50,7 @@ class Command(BaseCommand):
|
||||
instance_id=secrets.token_hex(12),
|
||||
license_key=None,
|
||||
api_key=secrets.token_hex(8),
|
||||
version=payload.get("version"),
|
||||
current_version=payload.get("version"),
|
||||
last_checked_at=timezone.now(),
|
||||
user_count=payload.get("user_count", 0),
|
||||
)
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 4.2.7 on 2024-05-28 09:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('license', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='instance',
|
||||
old_name='version',
|
||||
new_name='current_version',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='instance',
|
||||
name='new_version',
|
||||
field=models.CharField(blank=True, max_length=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='instance',
|
||||
name='product',
|
||||
field=models.CharField(blank=True, max_length=50, null=True),
|
||||
),
|
||||
]
|
||||
@@ -15,8 +15,10 @@ class Instance(BaseModel):
|
||||
instance_id = models.CharField(max_length=25, unique=True)
|
||||
license_key = models.CharField(max_length=256, null=True, blank=True)
|
||||
api_key = models.CharField(max_length=16)
|
||||
version = models.CharField(max_length=10)
|
||||
# Instnace specifics
|
||||
current_version = models.CharField(max_length=10)
|
||||
new_version = models.CharField(max_length=10, blank=True, null=True)
|
||||
product = models.CharField(max_length=50, blank=True, null=True)
|
||||
# Instance specifics
|
||||
last_checked_at = models.DateTimeField()
|
||||
namespace = models.CharField(max_length=50, blank=True, null=True)
|
||||
# telemetry and support
|
||||
|
||||
@@ -18,7 +18,7 @@ from plane.db.models import (
|
||||
State,
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
)
|
||||
from plane.app.serializers import (
|
||||
IssueSerializer,
|
||||
@@ -39,7 +39,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
@@ -59,7 +59,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
return InboxIssue.objects.none()
|
||||
|
||||
def list(self, request, slug, project_id, inbox_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if project_deploy_board.inbox is None:
|
||||
@@ -118,7 +118,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id, inbox_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if project_deploy_board.inbox is None:
|
||||
@@ -189,7 +189,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if project_deploy_board.inbox is None:
|
||||
@@ -256,7 +256,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if project_deploy_board.inbox is None:
|
||||
@@ -280,7 +280,7 @@ class InboxIssuePublicViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, slug, project_id, inbox_id, pk):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if project_deploy_board.inbox is None:
|
||||
|
||||
@@ -44,7 +44,7 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
IssueVote,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
@@ -76,7 +76,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
@@ -103,11 +103,11 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
).order_by("created_at")
|
||||
return IssueComment.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
except DeployBoard.DoesNotExist:
|
||||
return IssueComment.objects.none()
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@@ -151,7 +151,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def partial_update(self, request, slug, project_id, issue_id, pk):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@@ -184,7 +184,7 @@ class IssueCommentPublicViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def destroy(self, request, slug, project_id, issue_id, pk):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@@ -221,7 +221,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
@@ -236,11 +236,11 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
return IssueReaction.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
except DeployBoard.DoesNotExist:
|
||||
return IssueReaction.objects.none()
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@@ -280,7 +280,7 @@ class IssueReactionPublicViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@@ -319,7 +319,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
@@ -334,11 +334,11 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
||||
.distinct()
|
||||
)
|
||||
return CommentReaction.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
except DeployBoard.DoesNotExist:
|
||||
return CommentReaction.objects.none()
|
||||
|
||||
def create(self, request, slug, project_id, comment_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
|
||||
@@ -380,7 +380,7 @@ class CommentReactionPublicViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
if not project_deploy_board.reactions:
|
||||
@@ -421,7 +421,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
@@ -434,7 +434,7 @@ class IssueVotePublicViewSet(BaseViewSet):
|
||||
.filter(project_id=self.kwargs.get("project_id"))
|
||||
)
|
||||
return IssueVote.objects.none()
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
except DeployBoard.DoesNotExist:
|
||||
return IssueVote.objects.none()
|
||||
|
||||
def create(self, request, slug, project_id, issue_id):
|
||||
@@ -513,7 +513,7 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
if not ProjectDeployBoard.objects.filter(
|
||||
if not DeployBoard.objects.filter(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
).exists():
|
||||
return Response(
|
||||
|
||||
@@ -11,10 +11,10 @@ from rest_framework.permissions import AllowAny
|
||||
|
||||
# Module imports
|
||||
from .base import BaseAPIView
|
||||
from plane.app.serializers import ProjectDeployBoardSerializer
|
||||
from plane.app.serializers import DeployBoardSerializer
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
ProjectDeployBoard,
|
||||
DeployBoard,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
|
||||
]
|
||||
|
||||
def get(self, request, slug, project_id):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(
|
||||
project_deploy_board = DeployBoard.objects.get(
|
||||
workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serializer = ProjectDeployBoardSerializer(project_deploy_board)
|
||||
serializer = DeployBoardSerializer(project_deploy_board)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
|
||||
Project.objects.filter(workspace__slug=slug)
|
||||
.annotate(
|
||||
is_public=Exists(
|
||||
ProjectDeployBoard.objects.filter(
|
||||
DeployBoard.objects.filter(
|
||||
workspace__slug=slug, project_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
|
||||
@@ -128,7 +128,7 @@ services:
|
||||
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
|
||||
platform: ${DOCKER_PLATFORM:-}
|
||||
pull_policy: ${PULL_POLICY:-always}
|
||||
restart: no
|
||||
restart: "no"
|
||||
command: ./bin/docker-entrypoint-migrator.sh
|
||||
volumes:
|
||||
- logs_migrator:/code/plane/logs
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
[supervisord] ## This is the main process for the Supervisor
|
||||
nodaemon=true
|
||||
|
||||
[program:node]
|
||||
command=sh /usr/local/bin/start.sh
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/node.err.log
|
||||
stdout_logfile=/var/log/node.out.log
|
||||
|
||||
[program:python]
|
||||
directory=/code
|
||||
command=sh bin/docker-entrypoint-api.sh
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/python.err.log
|
||||
stdout_logfile=/var/log/python.out.log
|
||||
|
||||
[program:nginx]
|
||||
command=nginx -g "daemon off;"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/nginx.err.log
|
||||
stdout_logfile=/var/log/nginx.out.log
|
||||
|
||||
[program:worker]
|
||||
directory=/code
|
||||
command=sh bin/worker
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stderr_logfile=/var/log/worker.err.log
|
||||
stdout_logfile=/var/log/worker.out.log
|
||||
@@ -13,17 +13,21 @@ import { EditorMenuItemNames, getEditorMenuItems } from "src/ui/menus/menu-items
|
||||
import { EditorRefApi } from "src/types/editor-ref-api";
|
||||
import { IMarking, scrollSummary } from "src/helpers/scroll-to-node";
|
||||
|
||||
interface CustomEditorProps {
|
||||
export type TFileHandler = {
|
||||
cancel: () => void;
|
||||
delete: DeleteImage;
|
||||
upload: UploadImage;
|
||||
restore: RestoreImage;
|
||||
};
|
||||
|
||||
export interface CustomEditorProps {
|
||||
id?: string;
|
||||
uploadFile: UploadImage;
|
||||
restoreFile: RestoreImage;
|
||||
deleteFile: DeleteImage;
|
||||
cancelUploadImage?: () => void;
|
||||
initialValue: string;
|
||||
fileHandler: TFileHandler;
|
||||
initialValue?: string;
|
||||
editorClassName: string;
|
||||
// undefined when prop is not passed, null if intentionally passed to stop
|
||||
// swr syncing
|
||||
value: string | null | undefined;
|
||||
value?: string | null | undefined;
|
||||
onChange?: (json: object, html: string) => void;
|
||||
extensions?: any;
|
||||
editorProps?: EditorProps;
|
||||
@@ -38,19 +42,16 @@ interface CustomEditorProps {
|
||||
}
|
||||
|
||||
export const useEditor = ({
|
||||
uploadFile,
|
||||
id = "",
|
||||
deleteFile,
|
||||
cancelUploadImage,
|
||||
editorProps = {},
|
||||
initialValue,
|
||||
editorClassName,
|
||||
value,
|
||||
extensions = [],
|
||||
fileHandler,
|
||||
onChange,
|
||||
forwardedRef,
|
||||
tabIndex,
|
||||
restoreFile,
|
||||
handleEditorReady,
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
@@ -67,10 +68,10 @@ export const useEditor = ({
|
||||
mentionHighlights: mentionHandler.highlights ?? [],
|
||||
},
|
||||
fileConfig: {
|
||||
deleteFile,
|
||||
restoreFile,
|
||||
cancelUploadImage,
|
||||
uploadFile,
|
||||
uploadFile: fileHandler.upload,
|
||||
deleteFile: fileHandler.delete,
|
||||
restoreFile: fileHandler.restore,
|
||||
cancelUploadImage: fileHandler.cancel,
|
||||
},
|
||||
placeholder,
|
||||
tabIndex,
|
||||
@@ -139,7 +140,7 @@ export const useEditor = ({
|
||||
}
|
||||
},
|
||||
executeMenuItemCommand: (itemName: EditorMenuItemNames) => {
|
||||
const editorItems = getEditorMenuItems(editorRef.current, uploadFile);
|
||||
const editorItems = getEditorMenuItems(editorRef.current, fileHandler.upload);
|
||||
|
||||
const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.key === itemName);
|
||||
|
||||
@@ -155,7 +156,7 @@ export const useEditor = ({
|
||||
}
|
||||
},
|
||||
isMenuItemActive: (itemName: EditorMenuItemNames): boolean => {
|
||||
const editorItems = getEditorMenuItems(editorRef.current, uploadFile);
|
||||
const editorItems = getEditorMenuItems(editorRef.current, fileHandler.upload);
|
||||
|
||||
const getEditorMenuItem = (itemName: EditorMenuItemNames) => editorItems.find((item) => item.key === itemName);
|
||||
const item = getEditorMenuItem(itemName);
|
||||
@@ -177,6 +178,10 @@ export const useEditor = ({
|
||||
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
|
||||
return markdownOutput;
|
||||
},
|
||||
getHTML: (): string => {
|
||||
const htmlOutput = editorRef.current?.getHTML() ?? "<p></p>";
|
||||
return htmlOutput;
|
||||
},
|
||||
scrollSummary: (marking: IMarking): void => {
|
||||
if (!editorRef.current) return;
|
||||
scrollSummary(editorRef.current, marking);
|
||||
@@ -199,7 +204,7 @@ export const useEditor = ({
|
||||
}
|
||||
},
|
||||
}),
|
||||
[editorRef, savedSelection, uploadFile]
|
||||
[editorRef, savedSelection, fileHandler.upload]
|
||||
);
|
||||
|
||||
if (!editor) {
|
||||
|
||||
@@ -68,6 +68,10 @@ export const useReadOnlyEditor = ({
|
||||
const markdownOutput = editorRef.current?.storage.markdown.getMarkdown();
|
||||
return markdownOutput;
|
||||
},
|
||||
getHTML: (): string => {
|
||||
const htmlOutput = editorRef.current?.getHTML() ?? "<p></p>";
|
||||
return htmlOutput;
|
||||
},
|
||||
scrollSummary: (marking: IMarking): void => {
|
||||
if (!editorRef.current) return;
|
||||
scrollSummary(editorRef.current, marking);
|
||||
|
||||
@@ -24,6 +24,7 @@ export * from "src/ui/menus/menu-items";
|
||||
export * from "src/lib/editor-commands";
|
||||
|
||||
// types
|
||||
export type { CustomEditorProps, TFileHandler } from "src/hooks/use-editor";
|
||||
export type { DeleteImage } from "src/types/delete-image";
|
||||
export type { UploadImage } from "src/types/upload-image";
|
||||
export type { EditorRefApi, EditorReadOnlyRefApi } from "src/types/editor-ref-api";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Extensions, generateJSON, getSchema } from "@tiptap/core";
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { CoreEditorExtensionsWithoutProps } from "src/ui/extensions/core-without-props";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
interface EditorClassNames {
|
||||
noBorder?: boolean;
|
||||
@@ -58,3 +60,20 @@ export const isValidHttpUrl = (string: string): boolean => {
|
||||
|
||||
return url.protocol === "http:" || url.protocol === "https:";
|
||||
};
|
||||
|
||||
/**
|
||||
* @description return an object with contentJSON and editorSchema
|
||||
* @description contentJSON- ProseMirror JSON from HTML content
|
||||
* @description editorSchema- editor schema from extensions
|
||||
* @param {string} html
|
||||
* @returns {object} {contentJSON, editorSchema}
|
||||
*/
|
||||
export const generateJSONfromHTML = (html: string) => {
|
||||
const extensions = CoreEditorExtensionsWithoutProps();
|
||||
const contentJSON = generateJSON(html ?? "<p></p>", extensions as Extensions);
|
||||
const editorSchema = getSchema(extensions as Extensions);
|
||||
return {
|
||||
contentJSON,
|
||||
editorSchema,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { EditorMenuItemNames } from "src/ui/menus/menu-items";
|
||||
|
||||
export type EditorReadOnlyRefApi = {
|
||||
getMarkDown: () => string;
|
||||
getHTML: () => string;
|
||||
clearEditor: () => void;
|
||||
setEditorValue: (content: string) => void;
|
||||
scrollSummary: (marking: IMarking) => void;
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import TaskItem from "@tiptap/extension-task-item";
|
||||
import TaskList from "@tiptap/extension-task-list";
|
||||
import TextStyle from "@tiptap/extension-text-style";
|
||||
import TiptapUnderline from "@tiptap/extension-underline";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
|
||||
import { Table } from "src/ui/extensions/table/table";
|
||||
import { TableCell } from "src/ui/extensions/table/table-cell/table-cell";
|
||||
import { TableHeader } from "src/ui/extensions/table/table-header/table-header";
|
||||
import { TableRow } from "src/ui/extensions/table/table-row/table-row";
|
||||
|
||||
import { isValidHttpUrl } from "src/lib/utils";
|
||||
|
||||
import { CustomCodeBlockExtension } from "src/ui/extensions/code";
|
||||
import { CustomKeymap } from "src/ui/extensions/keymap";
|
||||
import { CustomQuoteExtension } from "src/ui/extensions/quote";
|
||||
|
||||
import { CustomLinkExtension } from "src/ui/extensions/custom-link";
|
||||
import { CustomCodeInlineExtension } from "src/ui/extensions/code-inline";
|
||||
import { CustomTypographyExtension } from "src/ui/extensions/typography";
|
||||
import { CustomHorizontalRule } from "src/ui/extensions/horizontal-rule/horizontal-rule";
|
||||
import { CustomCodeMarkPlugin } from "src/ui/extensions/custom-code-inline/inline-code-plugin";
|
||||
import { MentionsWithoutProps } from "src/ui/mentions/mention-without-props";
|
||||
import { ImageExtensionWithoutProps } from "src/ui/extensions/image/image-extension-without-props";
|
||||
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
|
||||
export const CoreEditorExtensionsWithoutProps = () => [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-disc pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
orderedList: {
|
||||
HTMLAttributes: {
|
||||
class: "list-decimal pl-7 space-y-2",
|
||||
},
|
||||
},
|
||||
listItem: {
|
||||
HTMLAttributes: {
|
||||
class: "not-prose space-y-2",
|
||||
},
|
||||
},
|
||||
code: false,
|
||||
codeBlock: false,
|
||||
horizontalRule: false,
|
||||
blockquote: false,
|
||||
dropcursor: {
|
||||
color: "rgba(var(--color-text-100))",
|
||||
width: 1,
|
||||
},
|
||||
}),
|
||||
CustomQuoteExtension,
|
||||
CustomHorizontalRule.configure({
|
||||
HTMLAttributes: {
|
||||
class: "my-4 border-custom-border-400",
|
||||
},
|
||||
}),
|
||||
CustomKeymap,
|
||||
// ListKeymap,
|
||||
CustomLinkExtension.configure({
|
||||
openOnClick: true,
|
||||
autolink: true,
|
||||
linkOnPaste: true,
|
||||
protocols: ["http", "https"],
|
||||
validate: (url: string) => isValidHttpUrl(url),
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||
},
|
||||
}),
|
||||
CustomTypographyExtension,
|
||||
ImageExtensionWithoutProps().configure({
|
||||
HTMLAttributes: {
|
||||
class: "rounded-md",
|
||||
},
|
||||
}),
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
||||
TaskList.configure({
|
||||
HTMLAttributes: {
|
||||
class: "not-prose pl-2 space-y-2",
|
||||
},
|
||||
}),
|
||||
TaskItem.configure({
|
||||
HTMLAttributes: {
|
||||
class: "flex",
|
||||
},
|
||||
nested: true,
|
||||
}),
|
||||
CustomCodeBlockExtension.configure({
|
||||
HTMLAttributes: {
|
||||
class: "",
|
||||
},
|
||||
}),
|
||||
CustomCodeMarkPlugin,
|
||||
CustomCodeInlineExtension,
|
||||
Markdown.configure({
|
||||
html: true,
|
||||
transformPastedText: true,
|
||||
}),
|
||||
Table,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
MentionsWithoutProps(),
|
||||
Placeholder.configure({
|
||||
placeholder: ({ editor, node }) => {
|
||||
if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
|
||||
|
||||
const shouldHidePlaceholder =
|
||||
editor.isActive("table") || editor.isActive("codeBlock") || editor.isActive("image");
|
||||
if (shouldHidePlaceholder) return "";
|
||||
|
||||
return "Press '/' for commands...";
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,33 @@
|
||||
import ImageExt from "@tiptap/extension-image";
|
||||
import { insertLineBelowImageAction } from "./utilities/insert-line-below-image";
|
||||
import { insertLineAboveImageAction } from "./utilities/insert-line-above-image";
|
||||
|
||||
export const ImageExtensionWithoutProps = () =>
|
||||
ImageExt.extend({
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
ArrowDown: insertLineBelowImageAction,
|
||||
ArrowUp: insertLineAboveImageAction,
|
||||
};
|
||||
},
|
||||
|
||||
// storage to keep track of image states Map<src, isDeleted>
|
||||
addStorage() {
|
||||
return {
|
||||
images: new Map<string, boolean>(),
|
||||
uploadInProgress: false,
|
||||
};
|
||||
},
|
||||
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
width: {
|
||||
default: "35%",
|
||||
},
|
||||
height: {
|
||||
default: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { CustomMention } from "./custom";
|
||||
import { ReactRenderer } from "@tiptap/react";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import tippy from "tippy.js";
|
||||
|
||||
import { MentionList } from "./mention-list";
|
||||
|
||||
export const MentionsWithoutProps = () =>
|
||||
CustomMention.configure({
|
||||
HTMLAttributes: {
|
||||
class: "mention",
|
||||
},
|
||||
// mentionHighlights: mentionHighlights,
|
||||
suggestion: {
|
||||
// @ts-expect-error - Tiptap types are incorrect
|
||||
render: () => {
|
||||
let component: ReactRenderer | null = null;
|
||||
let popup: any | null = null;
|
||||
|
||||
return {
|
||||
onStart: (props: { editor: Editor; clientRect: DOMRect }) => {
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
component = new ReactRenderer(MentionList, {
|
||||
props: { ...props },
|
||||
editor: props.editor,
|
||||
});
|
||||
props.editor.storage.mentionsOpen = true;
|
||||
// @ts-expect-error - Tippy types are incorrect
|
||||
popup = tippy("body", {
|
||||
getReferenceClientRect: props.clientRect,
|
||||
appendTo: () => document.querySelector(".active-editor") ?? document.querySelector("#editor-container"),
|
||||
content: component.element,
|
||||
showOnCreate: true,
|
||||
interactive: true,
|
||||
trigger: "manual",
|
||||
placement: "bottom-start",
|
||||
});
|
||||
},
|
||||
onUpdate: (props: { editor: Editor; clientRect: DOMRect }) => {
|
||||
component?.updateProps(props);
|
||||
|
||||
if (!props.clientRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
popup &&
|
||||
popup[0].setProps({
|
||||
getReferenceClientRect: props.clientRect,
|
||||
});
|
||||
},
|
||||
|
||||
onKeyDown: (props: { event: KeyboardEvent }) => {
|
||||
if (props.event.key === "Escape") {
|
||||
popup?.[0].hide();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"];
|
||||
|
||||
if (navigationKeys.includes(props.event.key)) {
|
||||
// @ts-expect-error - Tippy types are incorrect
|
||||
component?.ref?.onKeyDown(props);
|
||||
event?.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onExit: (props: { editor: Editor; event: KeyboardEvent }) => {
|
||||
props.editor.storage.mentionsOpen = false;
|
||||
popup?.[0].destroy();
|
||||
component?.destroy();
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -34,12 +34,17 @@
|
||||
"@plane/ui": "*",
|
||||
"@tippyjs/react": "^4.2.6",
|
||||
"@tiptap/core": "^2.1.13",
|
||||
"@tiptap/extension-collaboration": "^2.3.2",
|
||||
"@tiptap/pm": "^2.1.13",
|
||||
"@tiptap/suggestion": "^2.1.13",
|
||||
"lucide-react": "^0.378.0",
|
||||
"react-popper": "^2.3.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"uuid": "^9.0.1"
|
||||
"uuid": "^9.0.1",
|
||||
"y-indexeddb": "^9.0.12",
|
||||
"y-prosemirror": "^1.2.5",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "18.15.3",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useEffect, useLayoutEffect, useMemo } from "react";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { IndexeddbPersistence } from "y-indexeddb";
|
||||
import * as Y from "yjs";
|
||||
// editor-core
|
||||
import { EditorRefApi, IMentionHighlight, IMentionSuggestion, TFileHandler, useEditor } from "@plane/editor-core";
|
||||
// custom provider
|
||||
import { CollaborationProvider } from "src/providers/collaboration-provider";
|
||||
// extensions
|
||||
import { DocumentEditorExtensions } from "src/ui/extensions";
|
||||
|
||||
type DocumentEditorProps = {
|
||||
id: string;
|
||||
fileHandler: TFileHandler;
|
||||
value: Uint8Array;
|
||||
editorClassName: string;
|
||||
onChange: (updates: Uint8Array) => void;
|
||||
editorProps?: EditorProps;
|
||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
suggestions?: () => Promise<IMentionSuggestion[]>;
|
||||
};
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
setHideDragHandleFunction: (hideDragHandlerFromDragDrop: () => void) => void;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export const useDocumentEditor = ({
|
||||
id,
|
||||
editorProps = {},
|
||||
value,
|
||||
editorClassName,
|
||||
fileHandler,
|
||||
onChange,
|
||||
forwardedRef,
|
||||
tabIndex,
|
||||
handleEditorReady,
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
setHideDragHandleFunction,
|
||||
}: DocumentEditorProps) => {
|
||||
const provider = useMemo(
|
||||
() =>
|
||||
new CollaborationProvider({
|
||||
name: id,
|
||||
onChange,
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[id]
|
||||
);
|
||||
|
||||
// update document on value change
|
||||
useEffect(() => {
|
||||
if (value.byteLength > 0) Y.applyUpdate(provider.document, value);
|
||||
}, [value, provider.document]);
|
||||
|
||||
// indexedDB provider
|
||||
useLayoutEffect(() => {
|
||||
const localProvider = new IndexeddbPersistence(id, provider.document);
|
||||
return () => {
|
||||
localProvider?.destroy();
|
||||
};
|
||||
}, [provider, id]);
|
||||
|
||||
const editor = useEditor({
|
||||
id,
|
||||
editorProps,
|
||||
editorClassName,
|
||||
fileHandler,
|
||||
handleEditorReady,
|
||||
forwardedRef,
|
||||
mentionHandler,
|
||||
extensions: DocumentEditorExtensions({
|
||||
uploadFile: fileHandler.upload,
|
||||
setHideDragHandle: setHideDragHandleFunction,
|
||||
provider,
|
||||
}),
|
||||
placeholder,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
return editor;
|
||||
};
|
||||
@@ -3,6 +3,8 @@ export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } from "src/ui/re
|
||||
|
||||
// hooks
|
||||
export { useEditorMarkings } from "src/hooks/use-editor-markings";
|
||||
// utils
|
||||
export { proseMirrorJSONToBinaryString, applyUpdates, mergeUpdates } from "src/utils/yjs";
|
||||
|
||||
export type { EditorRefApi, EditorReadOnlyRefApi, EditorMenuItem, EditorMenuItemNames } from "@plane/editor-core";
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import * as Y from "yjs";
|
||||
|
||||
export interface CompleteCollaboratorProviderConfiguration {
|
||||
/**
|
||||
* The identifier/name of your document
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The actual Y.js document
|
||||
*/
|
||||
document: Y.Doc;
|
||||
/**
|
||||
* onChange callback
|
||||
*/
|
||||
onChange: (updates: Uint8Array) => void;
|
||||
}
|
||||
|
||||
export type CollaborationProviderConfiguration = Required<Pick<CompleteCollaboratorProviderConfiguration, "name">> &
|
||||
Partial<CompleteCollaboratorProviderConfiguration>;
|
||||
|
||||
export class CollaborationProvider {
|
||||
public configuration: CompleteCollaboratorProviderConfiguration = {
|
||||
name: "",
|
||||
// @ts-expect-error cannot be undefined
|
||||
document: undefined,
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
constructor(configuration: CollaborationProviderConfiguration) {
|
||||
this.setConfiguration(configuration);
|
||||
|
||||
this.configuration.document = configuration.document ?? new Y.Doc();
|
||||
this.document.on("update", this.documentUpdateHandler.bind(this));
|
||||
this.document.on("destroy", this.documentDestroyHandler.bind(this));
|
||||
}
|
||||
|
||||
public setConfiguration(configuration: Partial<CompleteCollaboratorProviderConfiguration> = {}): void {
|
||||
this.configuration = {
|
||||
...this.configuration,
|
||||
...configuration,
|
||||
};
|
||||
}
|
||||
|
||||
get document() {
|
||||
return this.configuration.document;
|
||||
}
|
||||
|
||||
documentUpdateHandler(update: Uint8Array, origin: any) {
|
||||
// return if the update is from the provider itself
|
||||
if (origin === this) return;
|
||||
|
||||
// call onChange with the update
|
||||
this.configuration.onChange?.(update);
|
||||
}
|
||||
|
||||
documentDestroyHandler() {
|
||||
this.document.off("update", this.documentUpdateHandler);
|
||||
this.document.off("destroy", this.documentDestroyHandler);
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,20 @@ import { IssueWidgetPlaceholder } from "src/ui/extensions/widgets/issue-embed-wi
|
||||
|
||||
import { SlashCommand, DragAndDrop } from "@plane/editor-extensions";
|
||||
import { UploadImage } from "@plane/editor-core";
|
||||
import { CollaborationProvider } from "src/providers/collaboration-provider";
|
||||
import Collaboration from "@tiptap/extension-collaboration";
|
||||
|
||||
type TArguments = {
|
||||
uploadFile: UploadImage;
|
||||
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
|
||||
provider: CollaborationProvider;
|
||||
};
|
||||
|
||||
export const DocumentEditorExtensions = ({ uploadFile, setHideDragHandle }: TArguments) => [
|
||||
export const DocumentEditorExtensions = ({ uploadFile, setHideDragHandle, provider }: TArguments) => [
|
||||
SlashCommand(uploadFile),
|
||||
DragAndDrop(setHideDragHandle),
|
||||
IssueWidgetPlaceholder(),
|
||||
Collaboration.configure({
|
||||
document: provider.document,
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1,30 +1,25 @@
|
||||
import React, { useState } from "react";
|
||||
// editor-core
|
||||
import {
|
||||
UploadImage,
|
||||
DeleteImage,
|
||||
RestoreImage,
|
||||
getEditorClassNames,
|
||||
useEditor,
|
||||
EditorRefApi,
|
||||
IMentionHighlight,
|
||||
IMentionSuggestion,
|
||||
TFileHandler,
|
||||
} from "@plane/editor-core";
|
||||
import { DocumentEditorExtensions } from "src/ui/extensions";
|
||||
// components
|
||||
import { PageRenderer } from "src/ui/components/page-renderer";
|
||||
// hooks
|
||||
import { useDocumentEditor } from "src/hooks/use-document-editor";
|
||||
|
||||
interface IDocumentEditor {
|
||||
initialValue: string;
|
||||
value?: string;
|
||||
fileHandler: {
|
||||
cancel: () => void;
|
||||
delete: DeleteImage;
|
||||
upload: UploadImage;
|
||||
restore: RestoreImage;
|
||||
};
|
||||
id: string;
|
||||
value: Uint8Array;
|
||||
fileHandler: TFileHandler;
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
containerClassName?: string;
|
||||
editorClassName?: string;
|
||||
onChange: (json: object, html: string) => void;
|
||||
onChange: (updates: Uint8Array) => void;
|
||||
forwardedRef?: React.MutableRefObject<EditorRefApi | null>;
|
||||
mentionHandler: {
|
||||
highlights: () => Promise<IMentionHighlight[]>;
|
||||
@@ -37,7 +32,7 @@ interface IDocumentEditor {
|
||||
const DocumentEditor = (props: IDocumentEditor) => {
|
||||
const {
|
||||
onChange,
|
||||
initialValue,
|
||||
id,
|
||||
value,
|
||||
fileHandler,
|
||||
containerClassName,
|
||||
@@ -50,32 +45,24 @@ const DocumentEditor = (props: IDocumentEditor) => {
|
||||
} = props;
|
||||
// states
|
||||
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = useState<() => void>(() => {});
|
||||
|
||||
// this essentially sets the hideDragHandle function from the DragAndDrop extension as the Plugin
|
||||
// loads such that we can invoke it from react when the cursor leaves the container
|
||||
const setHideDragHandleFunction = (hideDragHandlerFromDragDrop: () => void) => {
|
||||
setHideDragHandleOnMouseLeave(() => hideDragHandlerFromDragDrop);
|
||||
};
|
||||
// use editor
|
||||
const editor = useEditor({
|
||||
onChange(json, html) {
|
||||
onChange(json, html);
|
||||
},
|
||||
|
||||
// use document editor
|
||||
const editor = useDocumentEditor({
|
||||
id,
|
||||
editorClassName,
|
||||
restoreFile: fileHandler.restore,
|
||||
uploadFile: fileHandler.upload,
|
||||
deleteFile: fileHandler.delete,
|
||||
cancelUploadImage: fileHandler.cancel,
|
||||
initialValue,
|
||||
fileHandler,
|
||||
value,
|
||||
onChange,
|
||||
handleEditorReady,
|
||||
forwardedRef,
|
||||
mentionHandler,
|
||||
extensions: DocumentEditorExtensions({
|
||||
uploadFile: fileHandler.upload,
|
||||
setHideDragHandle: setHideDragHandleFunction,
|
||||
}),
|
||||
placeholder,
|
||||
setHideDragHandleFunction,
|
||||
tabIndex,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Schema } from "@tiptap/pm/model";
|
||||
import { prosemirrorJSONToYDoc } from "y-prosemirror";
|
||||
import * as Y from "yjs";
|
||||
|
||||
const defaultSchema: Schema = new Schema({
|
||||
nodes: {
|
||||
text: {},
|
||||
doc: { content: "text*" },
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* @description converts ProseMirror JSON to Yjs document
|
||||
* @param document prosemirror JSON
|
||||
* @param fieldName
|
||||
* @param schema
|
||||
* @returns {Y.Doc} Yjs document
|
||||
*/
|
||||
export const proseMirrorJSONToBinaryString = (
|
||||
document: any,
|
||||
fieldName: string | Array<string> = "default",
|
||||
schema?: Schema
|
||||
): string => {
|
||||
if (!document) {
|
||||
throw new Error(
|
||||
`You've passed an empty or invalid document to the Transformer. Make sure to pass ProseMirror-compatible JSON. Actually passed JSON: ${document}`
|
||||
);
|
||||
}
|
||||
|
||||
// allow a single field name
|
||||
if (typeof fieldName === "string") {
|
||||
const yDoc = prosemirrorJSONToYDoc(schema ?? defaultSchema, document, fieldName);
|
||||
const docAsUint8Array = Y.encodeStateAsUpdate(yDoc);
|
||||
const base64Doc = Buffer.from(docAsUint8Array).toString("base64");
|
||||
return base64Doc;
|
||||
}
|
||||
|
||||
const yDoc = new Y.Doc();
|
||||
|
||||
fieldName.forEach((field) => {
|
||||
const update = Y.encodeStateAsUpdate(prosemirrorJSONToYDoc(schema ?? defaultSchema, document, field));
|
||||
|
||||
Y.applyUpdate(yDoc, update);
|
||||
});
|
||||
|
||||
const docAsUint8Array = Y.encodeStateAsUpdate(yDoc);
|
||||
const base64Doc = Buffer.from(docAsUint8Array).toString("base64");
|
||||
|
||||
return base64Doc;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description apply updates to a doc and return the updated doc in base64(binary) format
|
||||
* @param {Uint8Array} document
|
||||
* @param {Uint8Array} updates
|
||||
* @returns {string} base64(binary) form of the updated doc
|
||||
*/
|
||||
export const applyUpdates = (document: Uint8Array, updates: Uint8Array): string => {
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, document);
|
||||
Y.applyUpdate(yDoc, updates);
|
||||
|
||||
const encodedDoc = Y.encodeStateAsUpdate(yDoc);
|
||||
const base64Updates = Buffer.from(encodedDoc).toString("base64");
|
||||
return base64Updates;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description merge multiple updates into one single update
|
||||
* @param {Uint8Array[]} updates
|
||||
* @returns {Uint8Array} merged updates
|
||||
*/
|
||||
export const mergeUpdates = (updates: Uint8Array[]): Uint8Array => {
|
||||
const mergedUpdates = Y.mergeUpdates(updates);
|
||||
return mergedUpdates;
|
||||
};
|
||||
@@ -1,27 +1,22 @@
|
||||
import * as React from "react";
|
||||
// editor-core
|
||||
import {
|
||||
UploadImage,
|
||||
DeleteImage,
|
||||
IMentionSuggestion,
|
||||
RestoreImage,
|
||||
EditorContainer,
|
||||
EditorContentWrapper,
|
||||
getEditorClassNames,
|
||||
useEditor,
|
||||
IMentionHighlight,
|
||||
EditorRefApi,
|
||||
TFileHandler,
|
||||
} from "@plane/editor-core";
|
||||
// extensions
|
||||
import { LiteTextEditorExtensions } from "src/ui/extensions";
|
||||
|
||||
export interface ILiteTextEditor {
|
||||
initialValue: string;
|
||||
value?: string | null;
|
||||
fileHandler: {
|
||||
cancel: () => void;
|
||||
delete: DeleteImage;
|
||||
upload: UploadImage;
|
||||
restore: RestoreImage;
|
||||
};
|
||||
fileHandler: TFileHandler;
|
||||
containerClassName?: string;
|
||||
editorClassName?: string;
|
||||
onChange?: (json: object, html: string) => void;
|
||||
@@ -58,10 +53,7 @@ const LiteTextEditor = (props: ILiteTextEditor) => {
|
||||
value,
|
||||
id,
|
||||
editorClassName,
|
||||
restoreFile: fileHandler.restore,
|
||||
uploadFile: fileHandler.upload,
|
||||
deleteFile: fileHandler.delete,
|
||||
cancelUploadImage: fileHandler.cancel,
|
||||
fileHandler,
|
||||
forwardedRef,
|
||||
extensions: LiteTextEditorExtensions(onEnterKeyPress),
|
||||
mentionHandler,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
|
||||
export const EnterKeyExtension = (onEnterKeyPress?: () => void) =>
|
||||
Extension.create({
|
||||
name: "enterKey",
|
||||
|
||||
addKeyboardShortcuts(this) {
|
||||
return {
|
||||
Enter: () => {
|
||||
if (onEnterKeyPress) {
|
||||
onEnterKeyPress();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
"Shift-Enter": ({ editor }) =>
|
||||
editor.commands.first(({ commands }) => [
|
||||
() => commands.newlineInCode(),
|
||||
() => commands.splitListItem("listItem"),
|
||||
() => commands.createParagraphNear(),
|
||||
() => commands.liftEmptyBlock(),
|
||||
() => commands.splitBlock(),
|
||||
]),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,13 +1,21 @@
|
||||
import { UploadImage } from "@plane/editor-core";
|
||||
import { DragAndDrop, SlashCommand } from "@plane/editor-extensions";
|
||||
import { EnterKeyExtension } from "./enter-key-extension";
|
||||
|
||||
type TArguments = {
|
||||
uploadFile: UploadImage;
|
||||
dragDropEnabled?: boolean;
|
||||
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
|
||||
onEnterKeyPress?: () => void;
|
||||
};
|
||||
|
||||
export const RichTextEditorExtensions = ({ uploadFile, dragDropEnabled, setHideDragHandle }: TArguments) => [
|
||||
export const RichTextEditorExtensions = ({
|
||||
uploadFile,
|
||||
dragDropEnabled,
|
||||
setHideDragHandle,
|
||||
onEnterKeyPress,
|
||||
}: TArguments) => [
|
||||
SlashCommand(uploadFile),
|
||||
dragDropEnabled === true && DragAndDrop(setHideDragHandle),
|
||||
EnterKeyExtension(onEnterKeyPress),
|
||||
];
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
// editor-core
|
||||
import {
|
||||
DeleteImage,
|
||||
EditorContainer,
|
||||
EditorContentWrapper,
|
||||
getEditorClassNames,
|
||||
IMentionHighlight,
|
||||
IMentionSuggestion,
|
||||
RestoreImage,
|
||||
UploadImage,
|
||||
useEditor,
|
||||
EditorRefApi,
|
||||
TFileHandler,
|
||||
} from "@plane/editor-core";
|
||||
import * as React from "react";
|
||||
// extensions
|
||||
import { RichTextEditorExtensions } from "src/ui/extensions";
|
||||
// components
|
||||
import { EditorBubbleMenu } from "src/ui/menus/bubble-menu";
|
||||
|
||||
export type IRichTextEditor = {
|
||||
initialValue: string;
|
||||
value?: string | null;
|
||||
dragDropEnabled?: boolean;
|
||||
fileHandler: {
|
||||
cancel: () => void;
|
||||
delete: DeleteImage;
|
||||
upload: UploadImage;
|
||||
restore: RestoreImage;
|
||||
};
|
||||
fileHandler: TFileHandler;
|
||||
id?: string;
|
||||
containerClassName?: string;
|
||||
editorClassName?: string;
|
||||
@@ -37,6 +33,7 @@ export type IRichTextEditor = {
|
||||
};
|
||||
placeholder?: string | ((isFocused: boolean, value: string) => string);
|
||||
tabIndex?: number;
|
||||
onEnterKeyPress?: (e?: any) => void;
|
||||
};
|
||||
|
||||
const RichTextEditor = (props: IRichTextEditor) => {
|
||||
@@ -54,6 +51,7 @@ const RichTextEditor = (props: IRichTextEditor) => {
|
||||
placeholder,
|
||||
tabIndex,
|
||||
mentionHandler,
|
||||
onEnterKeyPress,
|
||||
} = props;
|
||||
|
||||
const [hideDragHandleOnMouseLeave, setHideDragHandleOnMouseLeave] = React.useState<() => void>(() => {});
|
||||
@@ -67,10 +65,7 @@ const RichTextEditor = (props: IRichTextEditor) => {
|
||||
const editor = useEditor({
|
||||
id,
|
||||
editorClassName,
|
||||
restoreFile: fileHandler.restore,
|
||||
uploadFile: fileHandler.upload,
|
||||
deleteFile: fileHandler.delete,
|
||||
cancelUploadImage: fileHandler.cancel,
|
||||
fileHandler,
|
||||
onChange,
|
||||
initialValue,
|
||||
value,
|
||||
@@ -80,6 +75,7 @@ const RichTextEditor = (props: IRichTextEditor) => {
|
||||
uploadFile: fileHandler.upload,
|
||||
dragDropEnabled,
|
||||
setHideDragHandle: setHideDragHandleFunction,
|
||||
onEnterKeyPress,
|
||||
}),
|
||||
tabIndex,
|
||||
mentionHandler,
|
||||
|
||||
@@ -19,4 +19,3 @@ export * from "./priority-icon";
|
||||
export * from "./related-icon";
|
||||
export * from "./side-panel-icon";
|
||||
export * from "./transfer-icon";
|
||||
export * from "./user-group-icon";
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { ISvgIcons } from "./type";
|
||||
|
||||
export const UserGroupIcon: React.FC<ISvgIcons> = ({ className = "text-current", ...rest }) => (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
className={`${className} stroke-2`}
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...rest}
|
||||
>
|
||||
<path
|
||||
d="M18 19C18 17.4087 17.3679 15.8826 16.2426 14.7574C15.1174 13.6321 13.5913 13 12 13C10.4087 13 8.88258 13.6321 7.75736 14.7574C6.63214 15.8826 6 17.4087 6 19"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M12 13C14.2091 13 16 11.2091 16 9C16 6.79086 14.2091 5 12 5C9.79086 5 8 6.79086 8 9C8 11.2091 9.79086 13 12 13Z"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M23 18C23 16.636 22.4732 15.3279 21.5355 14.3635C20.5979 13.399 19.3261 12.8571 18 12.8571C18.8841 12.8571 19.7319 12.4959 20.357 11.8529C20.9821 11.21 21.3333 10.3379 21.3333 9.42857C21.3333 8.51926 20.9821 7.64719 20.357 7.00421C19.7319 6.36122 18.8841 6 18 6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M1 18C1 16.636 1.52678 15.3279 2.46447 14.3635C3.40215 13.399 4.67392 12.8571 6 12.8571C5.11595 12.8571 4.2681 12.4959 3.64298 11.8529C3.01786 11.21 2.66667 10.3379 2.66667 9.42857C2.66667 8.51926 3.01786 7.64719 3.64298 7.00421C4.2681 6.36122 5.11595 6 6 6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -37,3 +37,6 @@ next-env.d.ts
|
||||
|
||||
# env
|
||||
.env
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export async function register() {
|
||||
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||
await import('./sentry.server.config');
|
||||
}
|
||||
|
||||
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||
await import('./sentry.edge.config');
|
||||
}
|
||||
}
|
||||
+40
-6
@@ -28,12 +28,46 @@ const nextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
if (parseInt(process.env.NEXT_PUBLIC_ENABLE_SENTRY || "0", 10)) {
|
||||
module.exports = withSentryConfig(
|
||||
nextConfig,
|
||||
{ silent: true, authToken: process.env.SENTRY_AUTH_TOKEN },
|
||||
{ hideSourceMaps: true }
|
||||
);
|
||||
|
||||
const sentryConfig = {
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
|
||||
org: process.env.SENTRY_ORG_ID || "plane-hq",
|
||||
project: process.env.SENTRY_PROJECT_ID || "plane-space",
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
// Only print logs for uploading source maps in CI
|
||||
silent: true,
|
||||
|
||||
// For all available options, see:
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||
// This can increase your server load as well as your hosting bill.
|
||||
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||
// side errors will fail.
|
||||
tunnelRoute: "/monitoring",
|
||||
|
||||
// Hides source maps from generated client bundles
|
||||
hideSourceMaps: true,
|
||||
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
disableLogger: true,
|
||||
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||
// See the following for more information:
|
||||
// https://docs.sentry.io/product/crons/
|
||||
// https://vercel.com/docs/cron-jobs
|
||||
automaticVercelMonitors: true,
|
||||
}
|
||||
|
||||
|
||||
if (parseInt(process.env.SENTRY_MONITORING_ENABLED || "0", 10)) {
|
||||
module.exports = withSentryConfig(nextConfig, sentryConfig);
|
||||
} else {
|
||||
module.exports = nextConfig;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
"@plane/rich-text-editor": "*",
|
||||
"@plane/types": "*",
|
||||
"@plane/ui": "*",
|
||||
"@sentry/nextjs": "^7.108.0",
|
||||
"@sentry/nextjs": "^8",
|
||||
"axios": "^1.3.4",
|
||||
"clsx": "^2.0.0",
|
||||
"dompurify": "^3.0.11",
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// This file configures the initialization of Sentry on the browser.
|
||||
// The config you add here will be used whenever a page is visited.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
// This file configures the initialization of Sentry on the client.
|
||||
// The config you add here will be used whenever a users loads a page in their browser.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
|
||||
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
// in development and sample at a lower rate in production
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
|
||||
integrations: [
|
||||
Sentry.replayIntegration({
|
||||
// Additional Replay configuration goes in here, for example:
|
||||
maskAllText: true,
|
||||
blockAllMedia: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
// This file configures the initialization of Sentry on the server.
|
||||
// The config you add here will be used whenever middleware or an Edge route handles a request.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
|
||||
// The config you add here will be used whenever one of the edge features is loaded.
|
||||
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
});
|
||||
@@ -4,15 +4,16 @@
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN;
|
||||
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
environment: process.env.NEXT_PUBLIC_SENTRY_ENVIRONMENT || "development",
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 1.0,
|
||||
// ...
|
||||
// Note: if you want to override the automatic release value, do not set a
|
||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
tracesSampleRate: 1,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
|
||||
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
|
||||
// spotlight: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
+6
-5
@@ -8,10 +8,6 @@
|
||||
"NEXT_PUBLIC_SPACE_BASE_URL",
|
||||
"NEXT_PUBLIC_SPACE_BASE_PATH",
|
||||
"NEXT_PUBLIC_WEB_BASE_URL",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||
"NEXT_PUBLIC_ENABLE_SENTRY",
|
||||
"NEXT_PUBLIC_TRACK_EVENTS",
|
||||
"NEXT_PUBLIC_PLAUSIBLE_DOMAIN",
|
||||
"NEXT_PUBLIC_CRISP_ID",
|
||||
"NEXT_PUBLIC_ENABLE_SESSION_RECORDER",
|
||||
@@ -21,7 +17,12 @@
|
||||
"NEXT_PUBLIC_POSTHOG_HOST",
|
||||
"NEXT_PUBLIC_POSTHOG_DEBUG",
|
||||
"NEXT_PUBLIC_SUPPORT_EMAIL",
|
||||
"SENTRY_AUTH_TOKEN"
|
||||
"SENTRY_AUTH_TOKEN",
|
||||
"SENTRY_ORG_ID",
|
||||
"SENTRY_PROJECT_ID",
|
||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"SENTRY_MONITORING_ENABLED"
|
||||
],
|
||||
"pipeline": {
|
||||
"build": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { ICycle, IModule, IProject } from "@plane/types";
|
||||
@@ -20,20 +20,21 @@ export const ProjectAnalyticsModalMainContent: React.FC<Props> = observer((props
|
||||
|
||||
return (
|
||||
<Tab.Group as={React.Fragment}>
|
||||
<Tab.List as="div" className="flex space-x-2 border-b border-custom-border-200 px-0 md:px-5 py-0 md:py-3">
|
||||
<Tab.List as="div" className="flex space-x-2 border-b h-[50px] border-custom-border-200 px-0 md:px-3">
|
||||
{ANALYTICS_TABS.map((tab) => (
|
||||
<Tab
|
||||
key={tab.key}
|
||||
className={({ selected }) =>
|
||||
`rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-custom-border-200 focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-custom-background-80 ${
|
||||
selected
|
||||
? "border-custom-primary-100 text-custom-primary-100 md:bg-custom-background-80 md:text-custom-text-200 md:border-custom-border-200"
|
||||
: "border-transparent"
|
||||
}`
|
||||
}
|
||||
onClick={() => {}}
|
||||
>
|
||||
{tab.title}
|
||||
<Tab key={tab.key} as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button
|
||||
className={`text-sm group relative flex items-center gap-1 h-[50px] px-3 cursor-pointer transition-all font-medium outline-none ${
|
||||
selected ? "text-custom-primary-100 " : "hover:text-custom-text-200"
|
||||
}`}
|
||||
>
|
||||
{tab.title}
|
||||
<div
|
||||
className={`border absolute bottom-0 right-0 left-0 rounded-t-md ${selected ? "border-custom-primary-100" : "border-transparent group-hover:border-custom-border-200"}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Command } from "cmdk";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2 } from "lucide-react";
|
||||
import { LinkIcon, Signal, Trash2, UserMinus2, UserPlus2, Users } from "lucide-react";
|
||||
import { TIssue } from "@plane/types";
|
||||
// hooks
|
||||
import { DoubleCircleIcon, UserGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { DoubleCircleIcon, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
// helpers
|
||||
@@ -115,7 +115,7 @@ export const CommandPaletteIssueActions: React.FC<Props> = observer((props) => {
|
||||
className="focus:outline-none"
|
||||
>
|
||||
<div className="flex items-center gap-2 text-custom-text-200">
|
||||
<UserGroupIcon className="h-3.5 w-3.5" />
|
||||
<Users className="h-3.5 w-3.5" />
|
||||
Assign to...
|
||||
</div>
|
||||
</Command.Item>
|
||||
|
||||
@@ -502,7 +502,7 @@ const activityDetails: {
|
||||
name: {
|
||||
message: (activity, showIssue) => (
|
||||
<>
|
||||
set the name to <span className="break-all">{activity.new_value}</span>
|
||||
set the title to <span className="break-all">{activity.new_value}</span>
|
||||
{showIssue && (
|
||||
<>
|
||||
{" "}
|
||||
|
||||
@@ -2,6 +2,8 @@ import React, { FC } from "react";
|
||||
import Link from "next/link";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
interface IListItemProps {
|
||||
title: string;
|
||||
@@ -12,6 +14,7 @@ interface IListItemProps {
|
||||
actionableItems?: JSX.Element;
|
||||
isMobile?: boolean;
|
||||
parentRef: React.RefObject<HTMLDivElement>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ListItem: FC<IListItemProps> = (props) => {
|
||||
@@ -24,12 +27,18 @@ export const ListItem: FC<IListItemProps> = (props) => {
|
||||
onItemClick,
|
||||
isMobile = false,
|
||||
parentRef,
|
||||
className = "",
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div ref={parentRef} className="relative">
|
||||
<Link href={itemLink} onClick={onItemClick}>
|
||||
<div className="group h-24 sm:h-[52px] flex w-full flex-col items-center justify-between gap-3 sm:gap-5 px-6 py-4 sm:py-0 text-sm border-b border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90 sm:flex-row">
|
||||
<div
|
||||
className={cn(
|
||||
"group h-24 sm:h-[52px] flex w-full flex-col items-center justify-between gap-3 sm:gap-5 px-6 py-4 sm:py-0 text-sm border-b border-custom-border-200 bg-custom-background-100 hover:bg-custom-background-90 sm:flex-row",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="relative flex w-full items-center justify-between gap-3 overflow-hidden">
|
||||
<div className="relative flex w-full items-center gap-3 overflow-hidden">
|
||||
<div className="flex items-center gap-4 truncate">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
// headless ui
|
||||
import { Tab } from "@headlessui/react";
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
// hooks
|
||||
import { Avatar, StateGroupIcon } from "@plane/ui";
|
||||
import { SingleProgressStats } from "@/components/core";
|
||||
import { useProjectState } from "@/hooks/store";
|
||||
import useLocalStorage from "@/hooks/use-local-storage";
|
||||
// images
|
||||
import emptyLabel from "public/empty-state/empty_label.svg";
|
||||
@@ -44,20 +45,23 @@ type Props = {
|
||||
handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void;
|
||||
};
|
||||
|
||||
export const SidebarProgressStats: React.FC<Props> = ({
|
||||
distribution,
|
||||
groupedIssues,
|
||||
totalIssues,
|
||||
module,
|
||||
roundedTab,
|
||||
noBackground,
|
||||
isPeekView = false,
|
||||
isCompleted = false,
|
||||
filters,
|
||||
handleFiltersUpdate,
|
||||
}) => {
|
||||
export const SidebarProgressStats: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
distribution,
|
||||
groupedIssues,
|
||||
totalIssues,
|
||||
module,
|
||||
roundedTab,
|
||||
noBackground,
|
||||
isPeekView = false,
|
||||
isCompleted = false,
|
||||
filters,
|
||||
handleFiltersUpdate,
|
||||
} = props;
|
||||
const { storedValue: tab, setValue: setTab } = useLocalStorage("tab", "Assignees");
|
||||
|
||||
const { groupedProjectStates } = useProjectState();
|
||||
|
||||
const currentValue = (tab: string | null) => {
|
||||
switch (tab) {
|
||||
case "Assignees":
|
||||
@@ -71,6 +75,12 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const getStateGroupState = (stateGroup: string) => {
|
||||
const stateGroupStates = groupedProjectStates?.[stateGroup];
|
||||
const stateGroupStatesId = stateGroupStates?.map((state) => state.id);
|
||||
return stateGroupStatesId;
|
||||
};
|
||||
|
||||
return (
|
||||
<Tab.Group
|
||||
defaultIndex={currentValue(tab)}
|
||||
@@ -261,10 +271,14 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
||||
}
|
||||
completed={groupedIssues[group]}
|
||||
total={totalIssues}
|
||||
{...(!isPeekView &&
|
||||
!isCompleted && {
|
||||
onClick: () => handleFiltersUpdate("state", getStateGroupState(group) ?? []),
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
// types
|
||||
import { ICycle } from "@plane/types";
|
||||
// components
|
||||
@@ -8,14 +9,19 @@ import { EmptyState } from "@/components/empty-state";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
|
||||
export type ActiveCycleProductivityProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
cycle: ICycle;
|
||||
};
|
||||
|
||||
export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props) => {
|
||||
const { cycle } = props;
|
||||
const { workspaceSlug, projectId, cycle } = props;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}
|
||||
className="flex flex-col justify-center min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-base text-custom-text-300 font-semibold">Issue burndown</h3>
|
||||
</div>
|
||||
@@ -53,6 +59,6 @@ export const ActiveCycleProductivity: FC<ActiveCycleProductivityProps> = (props)
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
// types
|
||||
import { ICycle } from "@plane/types";
|
||||
// ui
|
||||
@@ -10,11 +11,13 @@ import { CYCLE_STATE_GROUPS_DETAILS } from "@/constants/cycle";
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
|
||||
export type ActiveCycleProgressProps = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
cycle: ICycle;
|
||||
};
|
||||
|
||||
export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
||||
const { cycle } = props;
|
||||
const { workspaceSlug, projectId, cycle } = props;
|
||||
|
||||
const progressIndicatorData = CYCLE_STATE_GROUPS_DETAILS.map((group, index) => ({
|
||||
id: index,
|
||||
@@ -31,7 +34,10 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg">
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${projectId}/cycles/${cycle?.id}`}
|
||||
className="flex flex-col min-h-[17rem] gap-5 py-4 px-3.5 bg-custom-background-100 border border-custom-border-200 rounded-lg"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h3 className="text-base text-custom-text-300 font-semibold">Progress</h3>
|
||||
@@ -85,6 +91,6 @@ export const ActiveCycleProgress: FC<ActiveCycleProgressProps> = (props) => {
|
||||
<EmptyState type={EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE} layout="screen-simple" size="sm" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -62,13 +62,18 @@ export const ActiveCycleRoot: React.FC<IActiveCycleDetails> = observer((props) =
|
||||
cycleId={currentProjectActiveCycleId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
className="!border-b-transparent"
|
||||
/>
|
||||
)}
|
||||
<div className="bg-custom-background-90 py-6 px-8">
|
||||
<div className="grid grid-cols-1 bg-custom-background-90 gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<ActiveCycleProgress cycle={activeCycle} />
|
||||
<ActiveCycleProductivity cycle={activeCycle} />
|
||||
<ActiveCycleStats cycle={activeCycle} workspaceSlug={workspaceSlug} projectId={projectId} />
|
||||
<div className="bg-custom-background-100 pt-3 pb-6 px-6">
|
||||
<div className="grid grid-cols-1 bg-custom-background-100 gap-3 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<ActiveCycleProgress workspaceSlug={workspaceSlug} projectId={projectId} cycle={activeCycle} />
|
||||
<ActiveCycleProductivity
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
cycle={activeCycle}
|
||||
/>
|
||||
<ActiveCycleStats workspaceSlug={workspaceSlug} projectId={projectId} cycle={activeCycle} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { User2 } from "lucide-react";
|
||||
import { Users } from "lucide-react";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, setPromiseToast } from "@plane/ui";
|
||||
// components
|
||||
@@ -112,9 +112,7 @@ export const UpcomingCycleListItem: React.FC<Props> = observer((props) => {
|
||||
})}
|
||||
</AvatarGroup>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||
</span>
|
||||
<Users className="h-4 w-4 text-custom-text-300" />
|
||||
)}
|
||||
|
||||
<FavoriteStar
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FC, MouseEvent } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { CalendarCheck2, CalendarClock, MoveRight, User2 } from "lucide-react";
|
||||
import { CalendarCheck2, CalendarClock, MoveRight, Users } from "lucide-react";
|
||||
// types
|
||||
import { ICycle, TCycleGroups } from "@plane/types";
|
||||
// ui
|
||||
@@ -146,9 +146,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
|
||||
})}
|
||||
</AvatarGroup>
|
||||
) : (
|
||||
<span className="flex h-5 w-5 items-end justify-center rounded-full border border-dashed border-custom-text-400 bg-custom-background-80">
|
||||
<User2 className="h-4 w-4 text-custom-text-400" />
|
||||
</span>
|
||||
<Users className="h-4 w-4 text-custom-text-300" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
@@ -22,10 +22,11 @@ type TCyclesListItem = {
|
||||
handleRemoveFromFavorites?: () => void;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
||||
const { cycleId, workspaceSlug, projectId } = props;
|
||||
const { cycleId, workspaceSlug, projectId, className = "" } = props;
|
||||
// refs
|
||||
const parentRef = useRef(null);
|
||||
// router
|
||||
@@ -83,6 +84,7 @@ export const CyclesListItem: FC<TCyclesListItem> = observer((props) => {
|
||||
onItemClick={(e) => {
|
||||
if (cycleDetails.archived_at) openCycleOverview(e);
|
||||
}}
|
||||
className={className}
|
||||
prependTitleElement={
|
||||
<CircularProgressIndicator size={30} percentage={progress} strokeWidth={3}>
|
||||
{isCompleted ? (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
@@ -9,10 +10,10 @@ import {
|
||||
ChevronDown,
|
||||
LinkIcon,
|
||||
Trash2,
|
||||
UserCircle2,
|
||||
AlertCircle,
|
||||
ChevronRight,
|
||||
CalendarClock,
|
||||
SquareUser,
|
||||
} from "lucide-react";
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// types
|
||||
@@ -199,14 +200,18 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const handleFiltersUpdate = useCallback(
|
||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
const newValues = issueFilters?.filters?.[key] ?? [];
|
||||
let newValues = issueFilters?.filters?.[key] ?? [];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// this validation is majorly for the filter start_date, target_date custom
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
if (key === "state") {
|
||||
if (isEqual(newValues, value)) newValues = [];
|
||||
else newValues = value;
|
||||
} else {
|
||||
value.forEach((val) => {
|
||||
if (!newValues.includes(val)) newValues.push(val);
|
||||
else newValues.splice(newValues.indexOf(val), 1);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||
else newValues.push(value);
|
||||
@@ -427,7 +432,7 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
<div className="flex items-center justify-start gap-1">
|
||||
<div className="flex w-2/5 items-center justify-start gap-2 text-custom-text-300">
|
||||
<UserCircle2 className="h-4 w-4" />
|
||||
<SquareUser className="h-4 w-4" />
|
||||
<span className="text-base">Lead</span>
|
||||
</div>
|
||||
<div className="flex w-3/5 items-center rounded-sm">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user