From a194d9826d1c93b64993fda7684afca51ddd5f9a Mon Sep 17 00:00:00 2001 From: sharma01ketan Date: Tue, 8 Oct 2024 15:12:53 +0530 Subject: [PATCH 1/7] added setPromiseToast to issue addition --- .../calendar/quick-add-issue-actions.tsx | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx b/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx index a7a0a5c0acb..7b33215e44d 100644 --- a/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx +++ b/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx @@ -8,7 +8,7 @@ import { PlusIcon } from "lucide-react"; // types import { ISearchIssueResponse, TIssue } from "@plane/types"; // ui -import { TOAST_TYPE, setToast, CustomMenu } from "@plane/ui"; +import { TOAST_TYPE, setToast, CustomMenu, setPromiseToast } from "@plane/ui"; // components import { ExistingIssuesListModal } from "@/components/core"; import { QuickAddIssueRoot } from "@/components/issues"; @@ -45,21 +45,26 @@ export const CalendarQuickAddIssueActions: FC = o if (!workspaceSlug || !projectId) return; const issueIds = data.map((i) => i.id); + const updatePromise = Promise.all( + data.map((issue) => updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {})) + ).then(() => addIssuesToView?.(issueIds)) + console.log(issueIds.length) + setPromiseToast(updatePromise, { + loading: `Adding ${issueIds.length>1 ? "issues" : "issue" } to cycle...`, + success: { + title: "Success!", + message: () => `${issueIds.length>1 ? "Issues" : "Issue" } added to cycle successfully.`, + }, + error: { + title: "Error!", + message: (err) => err?.message || "Something went wrong. Please try again.", + }, + }); try { - // To handle all updates in parallel - await Promise.all( - data.map((issue) => - updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {}) - ) - ); - await addIssuesToView?.(issueIds); + await updatePromise; } catch (error) { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Error!", - message: "Something went wrong. Please try again.", - }); + console.error(error) } }; From 323c414de9b572d0e2618d67ebbb834214f28415 Mon Sep 17 00:00:00 2001 From: sharma01ketan Date: Tue, 8 Oct 2024 15:32:16 +0530 Subject: [PATCH 2/7] made minor improvements --- .../calendar/quick-add-issue-actions.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx b/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx index 7b33215e44d..9fa0d4cf329 100644 --- a/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx +++ b/web/core/components/issues/issue-layouts/calendar/quick-add-issue-actions.tsx @@ -8,7 +8,7 @@ import { PlusIcon } from "lucide-react"; // types import { ISearchIssueResponse, TIssue } from "@plane/types"; // ui -import { TOAST_TYPE, setToast, CustomMenu, setPromiseToast } from "@plane/ui"; +import { CustomMenu, setPromiseToast } from "@plane/ui"; // components import { ExistingIssuesListModal } from "@/components/core"; import { QuickAddIssueRoot } from "@/components/issues"; @@ -47,25 +47,19 @@ export const CalendarQuickAddIssueActions: FC = o const issueIds = data.map((i) => i.id); const updatePromise = Promise.all( data.map((issue) => updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, prePopulatedData ?? {})) - ).then(() => addIssuesToView?.(issueIds)) - console.log(issueIds.length) + ).then(() => addIssuesToView?.(issueIds)); + setPromiseToast(updatePromise, { - loading: `Adding ${issueIds.length>1 ? "issues" : "issue" } to cycle...`, + loading: `Adding ${issueIds.length > 1 ? "issues" : "issue"} to cycle...`, success: { title: "Success!", - message: () => `${issueIds.length>1 ? "Issues" : "Issue" } added to cycle successfully.`, + message: () => `${issueIds.length > 1 ? "Issues" : "Issue"} added to cycle successfully.`, }, error: { title: "Error!", message: (err) => err?.message || "Something went wrong. Please try again.", }, }); - - try { - await updatePromise; - } catch (error) { - console.error(error) - } }; const handleNewIssue = () => { From d84eb9b1abaf7d0213b3ce7c23372e83631ed221 Mon Sep 17 00:00:00 2001 From: sharma01ketan Date: Fri, 11 Oct 2024 17:24:19 +0530 Subject: [PATCH 3/7] Revert "[WEB-2605] fix: update URL regex pattern to allow complex links. (#5767)" This reverts commit 328b6961a26f8a0a40e630b6f9c4e0070928a32a. --- web/helpers/string.helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index e4b4dc6651e..1182feeb0a7 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -270,7 +270,7 @@ export const isCommentEmpty = (comment: string | undefined): boolean => { export const checkURLValidity = (url: string): boolean => { if (!url) return false; // regex to match valid URLs (with or without http/https) - const urlPattern = /^(https?:\/\/)?([\w.-]+\.[a-z]{2,6})(\/[\w\-.~:/?#[\]@!$&'()*+,;=%]*)?$/i; + const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z]{2,6})(\/[\w.-]*)*\/?(\?[=&\w.-]*)?$/i; // test if the URL matches the pattern return urlPattern.test(url); }; From 21dbf6c6ded0c20278d886a889cbb7b6d5d24337 Mon Sep 17 00:00:00 2001 From: sharma01ketan Date: Fri, 11 Oct 2024 17:24:24 +0530 Subject: [PATCH 4/7] Revert "chore: only admin can changed the project settings (#5766)" This reverts commit 39eabc28b5d6d5a770a99f00b94421e7d45f5f93. --- apiserver/plane/app/views/project/base.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index f5ddb224587..6a9afb65237 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -413,20 +413,9 @@ def create(self, request, slug): status=status.HTTP_410_GONE, ) + @allow_permission([ROLE.ADMIN]) def partial_update(self, request, slug, pk=None): try: - if not ProjectMember.objects.filter( - member=request.user, - workspace__slug=slug, - project_id=pk, - role=20, - is_active=True, - ).exists(): - return Response( - {"error": "You don't have the required permissions."}, - status=status.HTTP_403_FORBIDDEN, - ) - workspace = Workspace.objects.get(slug=slug) project = Project.objects.get(pk=pk) From 1aed308053d613d0151e84c274667ebd2d74edaf Mon Sep 17 00:00:00 2001 From: sharma01ketan Date: Fri, 11 Oct 2024 17:29:13 +0530 Subject: [PATCH 5/7] Revert "Merge branch 'preview' of https://github.com/makeplane/plane into fix/add-existing-issue-to-cycle-toast" This reverts commit 25bd32c09c4cae32cd7bb150c3d5db079c4c78c7, reversing changes made to 21dbf6c6ded0c20278d886a889cbb7b6d5d24337. --- admin/package.json | 2 +- apiserver/package.json | 2 +- apiserver/plane/api/views/cycle.py | 12 +- apiserver/plane/api/views/inbox.py | 2 +- apiserver/plane/app/serializers/__init__.py | 6 - apiserver/plane/app/serializers/draft.py | 292 --- apiserver/plane/app/urls/issue.py | 23 + apiserver/plane/app/urls/workspace.py | 27 - apiserver/plane/app/views/__init__.py | 4 +- apiserver/plane/app/views/cycle/archive.py | 2 +- apiserver/plane/app/views/cycle/base.py | 13 +- apiserver/plane/app/views/cycle/issue.py | 2 +- apiserver/plane/app/views/dashboard/base.py | 2 + apiserver/plane/app/views/inbox/base.py | 4 +- apiserver/plane/app/views/issue/draft.py | 410 ++++ apiserver/plane/app/views/module/base.py | 26 +- apiserver/plane/app/views/project/base.py | 38 - apiserver/plane/app/views/view/base.py | 18 +- apiserver/plane/app/views/workspace/draft.py | 332 --- apiserver/plane/app/views/workspace/user.py | 6 +- .../plane/bgtasks/issue_automation_task.py | 12 +- apiserver/plane/celery.py | 4 - ...timezone_project_user_timezone_and_more.py | 2036 ----------------- apiserver/plane/db/models/__init__.py | 1 - apiserver/plane/db/models/cycle.py | 15 +- apiserver/plane/db/models/draft.py | 253 -- apiserver/plane/db/models/project.py | 8 +- apiserver/plane/utils/analytics_plot.py | 4 +- apiserver/plane/utils/paginator.py | 5 +- apiserver/requirements/base.txt | 2 +- live/package.json | 2 +- live/src/ce/lib/authentication.ts | 15 + live/src/core/hocuspocus-server.ts | 8 +- live/src/core/lib/authentication.ts | 47 +- live/src/core/services/user.service.ts | 35 +- package.json | 2 +- packages/constants/package.json | 2 +- packages/editor/package.json | 6 +- .../src/ce/extensions/document-extensions.tsx | 4 +- .../components/editors/rich-text/editor.tsx | 4 +- .../menus/bubble-menu/color-selector.tsx | 118 - .../components/menus/bubble-menu/index.ts | 1 - .../menus/bubble-menu/link-selector.tsx | 25 +- .../menus/bubble-menu/node-selector.tsx | 15 +- .../components/menus/bubble-menu/root.tsx | 115 +- .../src/core/components/menus/menu-items.ts | 49 +- packages/editor/src/core/constants/common.ts | 51 - .../src/core/extensions/core-without-props.ts | 6 - .../custom-image/components/image-block.tsx | 8 +- .../editor/src/core/extensions/extensions.tsx | 6 - packages/editor/src/core/extensions/index.ts | 1 - .../core/extensions/read-only-extensions.tsx | 6 - .../editor/src/core/extensions/side-menu.tsx | 2 +- .../src/core/extensions/slash-commands.tsx | 422 ++++ .../slash-commands/command-items-list.tsx | 294 --- .../slash-commands/command-menu-item.tsx | 37 - .../slash-commands/command-menu.tsx | 127 - .../core/extensions/slash-commands/index.ts | 1 - .../core/extensions/slash-commands/root.tsx | 113 - .../src/core/helpers/editor-commands.ts | 39 - packages/editor/src/core/hooks/use-editor.ts | 20 +- .../editor/src/core/plugins/drag-handle.ts | 44 +- packages/editor/src/core/types/editor.ts | 25 +- .../core/types/slash-commands-suggestion.ts | 15 +- packages/editor/src/index.ts | 3 - packages/editor/src/styles/editor.css | 2 +- packages/eslint-config/package.json | 2 +- packages/helpers/package.json | 2 +- packages/tailwind-config-custom/package.json | 2 +- packages/types/package.json | 2 +- packages/types/src/index.d.ts | 1 - packages/types/src/issues/base.d.ts | 1 - .../src/workspace-draft-issues/base.d.ts | 61 - packages/typescript-config/package.json | 2 +- packages/ui/package.json | 2 +- packages/ui/src/modals/constants.ts | 3 - .../components/editor/lite-text-editor.tsx | 6 +- space/core/components/editor/toolbar.tsx | 6 +- space/package.json | 2 +- .../(projects)/drafts/header.tsx | 65 - .../(projects)/drafts/layout.tsx | 13 - .../(projects)/drafts/page.tsx | 27 - .../cycles/(detail)/mobile-header.tsx | 2 +- .../issues/(list)/mobile-header.tsx | 2 +- .../modules/(detail)/mobile-header.tsx | 2 +- web/app/profile/appearance/page.tsx | 9 +- .../cycles/analytics-sidebar/base.tsx | 69 - .../cycles/analytics-sidebar/index.ts | 2 +- .../cycles/analytics-sidebar/root.tsx | 12 - .../analytics-sidebar/sidebar-chart.tsx | 57 + .../issues/issue-details/issue-identifier.tsx | 54 +- .../issue-details/issue-type-switcher.tsx | 2 +- .../analytics-sidebar/issue-progress.tsx | 41 +- .../analytics-sidebar/sidebar-details.tsx | 4 +- web/core/components/editor/index.ts | 1 - .../lite-text-editor/lite-text-editor.tsx | 6 +- .../editor/lite-text-editor/toolbar.tsx | 6 +- web/core/components/editor/pdf/document.tsx | 53 - web/core/components/editor/pdf/index.ts | 1 - .../inbox/content/inbox-issue-header.tsx | 55 +- .../content/inbox-issue-mobile-header.tsx | 44 +- web/core/components/inbox/content/root.tsx | 8 +- .../roots/workspace-draft-root.tsx | 49 - .../issue-layouts/list/base-list-root.tsx | 15 +- .../issues/issue-layouts/list/default.tsx | 5 +- .../issue-layouts/list/list-view-types.d.ts | 1 - .../properties/all-properties.tsx | 4 +- .../quick-action-dropdowns/index.ts | 1 - .../issue-layouts/spreadsheet/issue-row.tsx | 9 +- .../components/issues/issue-modal/base.tsx | 41 +- .../issues/issue-modal/draft-issue-layout.tsx | 11 +- .../components/issues/issue-modal/form.tsx | 39 +- .../issues/peek-overview/issue-detail.tsx | 11 +- .../issues/workspace-draft/delete-modal.tsx | 105 - .../workspace-draft/draft-issue-block.tsx | 127 - .../draft-issue-properties.tsx | 299 --- .../issues/workspace-draft/empty-state.tsx | 33 - .../issues/workspace-draft/index.ts | 4 - .../issues/workspace-draft/loader.tsx | 20 - .../issues/workspace-draft/quick-action.tsx | 160 -- .../issues/workspace-draft/root.tsx | 90 - .../pages/editor/header/color-dropdown.tsx | 129 -- .../components/pages/editor/header/index.ts | 1 - .../pages/editor/header/options-dropdown.tsx | 59 +- .../pages/editor/header/toolbar.tsx | 43 +- web/core/components/pages/editor/title.tsx | 12 +- .../pages/modals/export-page-modal.tsx | 282 --- web/core/components/pages/modals/index.ts | 1 - .../header/options/menu-option/root.tsx | 44 +- .../sidebar/header/options/root.tsx | 39 +- .../workspace/sidebar/projects-list-item.tsx | 4 +- .../workspace/sidebar/quick-actions.tsx | 4 +- web/core/constants/cycle.ts | 4 +- web/core/constants/dashboard.ts | 10 +- web/core/constants/editor.ts | 179 -- web/core/constants/empty-state.ts | 13 - web/core/constants/issue.ts | 15 +- web/core/constants/module.ts | 4 +- web/core/constants/workspace-drafts.ts | 6 - web/core/hooks/store/index.ts | 1 - web/core/hooks/store/use-issues.ts | 15 - web/core/hooks/store/workspace-draft/index.ts | 2 - .../use-workspace-draft-issue-filters.ts | 12 - .../use-workspace-draft-issue.ts | 12 - web/core/hooks/use-group-dragndrop.ts | 3 +- web/core/hooks/use-issues-actions.tsx | 81 - web/core/lib/wrappers/store-wrapper.tsx | 30 +- web/core/services/issue/index.ts | 1 - .../services/issue/workspace_draft.service.ts | 73 - web/core/store/cycle.store.ts | 17 +- web/core/store/inbox/project-inbox.store.ts | 2 +- web/core/store/issue/root.store.ts | 17 +- .../issue/workspace-draft/filter.store.ts | 254 -- web/core/store/issue/workspace-draft/index.ts | 2 - .../issue/workspace-draft/issue.store.ts | 377 --- web/helpers/common.helper.ts | 2 - web/helpers/editor.helper.ts | 157 -- web/helpers/file.helper.ts | 42 - web/helpers/theme.helper.ts | 19 +- web/package.json | 4 +- .../workspace-draft/issue-dark.webp | Bin 80938 -> 0 bytes .../workspace-draft/issue-light.webp | Bin 91768 -> 0 bytes web/public/fonts/inter/bold-italic.ttf | Bin 348180 -> 0 bytes web/public/fonts/inter/bold.ttf | Bin 344152 -> 0 bytes web/public/fonts/inter/heavy-italic.ttf | Bin 348712 -> 0 bytes web/public/fonts/inter/heavy.ttf | Bin 344820 -> 0 bytes web/public/fonts/inter/light-italic.ttf | Bin 347316 -> 0 bytes web/public/fonts/inter/light.ttf | Bin 343704 -> 0 bytes web/public/fonts/inter/medium-italic.ttf | Bin 346884 -> 0 bytes web/public/fonts/inter/medium.ttf | Bin 343200 -> 0 bytes web/public/fonts/inter/regular-italic.ttf | Bin 346480 -> 0 bytes web/public/fonts/inter/regular.ttf | Bin 342680 -> 0 bytes web/public/fonts/inter/semibold-italic.ttf | Bin 347760 -> 0 bytes web/public/fonts/inter/semibold.ttf | Bin 343828 -> 0 bytes web/public/fonts/inter/thin-italic.ttf | Bin 346916 -> 0 bytes web/public/fonts/inter/thin.ttf | Bin 343088 -> 0 bytes web/public/fonts/inter/ultrabold-italic.ttf | Bin 349064 -> 0 bytes web/public/fonts/inter/ultrabold.ttf | Bin 345008 -> 0 bytes web/public/fonts/inter/ultralight-italic.ttf | Bin 347452 -> 0 bytes web/public/fonts/inter/ultralight.ttf | Bin 343532 -> 0 bytes yarn.lock | 704 ++---- 181 files changed, 1659 insertions(+), 7901 deletions(-) delete mode 100644 apiserver/plane/app/serializers/draft.py create mode 100644 apiserver/plane/app/views/issue/draft.py delete mode 100644 apiserver/plane/app/views/workspace/draft.py delete mode 100644 apiserver/plane/db/migrations/0077_draftissue_cycle_user_timezone_project_user_timezone_and_more.py delete mode 100644 apiserver/plane/db/models/draft.py create mode 100644 live/src/ce/lib/authentication.ts delete mode 100644 packages/editor/src/core/components/menus/bubble-menu/color-selector.tsx delete mode 100644 packages/editor/src/core/constants/common.ts create mode 100644 packages/editor/src/core/extensions/slash-commands.tsx delete mode 100644 packages/editor/src/core/extensions/slash-commands/command-items-list.tsx delete mode 100644 packages/editor/src/core/extensions/slash-commands/command-menu-item.tsx delete mode 100644 packages/editor/src/core/extensions/slash-commands/command-menu.tsx delete mode 100644 packages/editor/src/core/extensions/slash-commands/index.ts delete mode 100644 packages/editor/src/core/extensions/slash-commands/root.tsx delete mode 100644 packages/types/src/workspace-draft-issues/base.d.ts delete mode 100644 web/app/[workspaceSlug]/(projects)/drafts/header.tsx delete mode 100644 web/app/[workspaceSlug]/(projects)/drafts/layout.tsx delete mode 100644 web/app/[workspaceSlug]/(projects)/drafts/page.tsx delete mode 100644 web/ce/components/cycles/analytics-sidebar/base.tsx delete mode 100644 web/ce/components/cycles/analytics-sidebar/root.tsx create mode 100644 web/ce/components/cycles/analytics-sidebar/sidebar-chart.tsx delete mode 100644 web/core/components/editor/pdf/document.tsx delete mode 100644 web/core/components/editor/pdf/index.ts delete mode 100644 web/core/components/issues/issue-layouts/filters/applied-filters/roots/workspace-draft-root.tsx delete mode 100644 web/core/components/issues/workspace-draft/delete-modal.tsx delete mode 100644 web/core/components/issues/workspace-draft/draft-issue-block.tsx delete mode 100644 web/core/components/issues/workspace-draft/draft-issue-properties.tsx delete mode 100644 web/core/components/issues/workspace-draft/empty-state.tsx delete mode 100644 web/core/components/issues/workspace-draft/index.ts delete mode 100644 web/core/components/issues/workspace-draft/loader.tsx delete mode 100644 web/core/components/issues/workspace-draft/quick-action.tsx delete mode 100644 web/core/components/issues/workspace-draft/root.tsx delete mode 100644 web/core/components/pages/editor/header/color-dropdown.tsx delete mode 100644 web/core/components/pages/modals/export-page-modal.tsx delete mode 100644 web/core/constants/workspace-drafts.ts delete mode 100644 web/core/hooks/store/workspace-draft/index.ts delete mode 100644 web/core/hooks/store/workspace-draft/use-workspace-draft-issue-filters.ts delete mode 100644 web/core/hooks/store/workspace-draft/use-workspace-draft-issue.ts delete mode 100644 web/core/services/issue/workspace_draft.service.ts delete mode 100644 web/core/store/issue/workspace-draft/filter.store.ts delete mode 100644 web/core/store/issue/workspace-draft/index.ts delete mode 100644 web/core/store/issue/workspace-draft/issue.store.ts delete mode 100644 web/helpers/editor.helper.ts delete mode 100644 web/helpers/file.helper.ts delete mode 100644 web/public/empty-state/workspace-draft/issue-dark.webp delete mode 100644 web/public/empty-state/workspace-draft/issue-light.webp delete mode 100644 web/public/fonts/inter/bold-italic.ttf delete mode 100644 web/public/fonts/inter/bold.ttf delete mode 100644 web/public/fonts/inter/heavy-italic.ttf delete mode 100644 web/public/fonts/inter/heavy.ttf delete mode 100644 web/public/fonts/inter/light-italic.ttf delete mode 100644 web/public/fonts/inter/light.ttf delete mode 100644 web/public/fonts/inter/medium-italic.ttf delete mode 100644 web/public/fonts/inter/medium.ttf delete mode 100644 web/public/fonts/inter/regular-italic.ttf delete mode 100644 web/public/fonts/inter/regular.ttf delete mode 100644 web/public/fonts/inter/semibold-italic.ttf delete mode 100644 web/public/fonts/inter/semibold.ttf delete mode 100644 web/public/fonts/inter/thin-italic.ttf delete mode 100644 web/public/fonts/inter/thin.ttf delete mode 100644 web/public/fonts/inter/ultrabold-italic.ttf delete mode 100644 web/public/fonts/inter/ultrabold.ttf delete mode 100644 web/public/fonts/inter/ultralight-italic.ttf delete mode 100644 web/public/fonts/inter/ultralight.ttf diff --git a/admin/package.json b/admin/package.json index 023636a2533..1246bc912d8 100644 --- a/admin/package.json +++ b/admin/package.json @@ -1,6 +1,6 @@ { "name": "admin", - "version": "0.23.1", + "version": "0.23.0", "private": true, "scripts": { "dev": "turbo run develop", diff --git a/apiserver/package.json b/apiserver/package.json index f26382c8938..b9c13498125 100644 --- a/apiserver/package.json +++ b/apiserver/package.json @@ -1,4 +1,4 @@ { "name": "plane-api", - "version": "0.23.1" + "version": "0.23.0" } diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 48e7f6d1f67..3814466322e 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -207,7 +207,8 @@ def get(self, request, slug, project_id, pk=None): # Incomplete Cycles if cycle_view == "incomplete": queryset = queryset.filter( - Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True), + Q(end_date__gte=timezone.now().date()) + | Q(end_date__isnull=True), ) return self.paginate( request=request, @@ -308,7 +309,10 @@ def patch(self, request, slug, project_id, pk): request_data = request.data - if cycle.end_date is not None and cycle.end_date < timezone.now(): + if ( + cycle.end_date is not None + and cycle.end_date < timezone.now().date() + ): if "sort_order" in request_data: # Can only change sort order request_data = { @@ -533,7 +537,7 @@ def post(self, request, slug, project_id, cycle_id): cycle = Cycle.objects.get( pk=cycle_id, project_id=project_id, workspace__slug=slug ) - if cycle.end_date >= timezone.now(): + if cycle.end_date >= timezone.now().date(): return Response( {"error": "Only completed cycles can be archived"}, status=status.HTTP_400_BAD_REQUEST, @@ -1142,7 +1146,7 @@ def post(self, request, slug, project_id, cycle_id): if ( new_cycle.end_date is not None - and new_cycle.end_date < timezone.now() + and new_cycle.end_date < timezone.now().date() ): return Response( { diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index f7e18dd76ff..24eac569d68 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -285,7 +285,7 @@ def patch(self, request, slug, project_id, issue_id): ) # Only project admins and members can edit inbox issue attributes - if project_member.role > 15: + if project_member.role > 5: serializer = InboxIssueSerializer( inbox_issue, data=request.data, partial=True ) diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index b3c3d794994..618a9ec20f1 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -124,9 +124,3 @@ from .dashboard import DashboardSerializer, WidgetSerializer from .favorite import UserFavoriteSerializer - -from .draft import ( - DraftIssueCreateSerializer, - DraftIssueSerializer, - DraftIssueDetailSerializer, -) diff --git a/apiserver/plane/app/serializers/draft.py b/apiserver/plane/app/serializers/draft.py deleted file mode 100644 index 10929e453de..00000000000 --- a/apiserver/plane/app/serializers/draft.py +++ /dev/null @@ -1,292 +0,0 @@ -# Django imports -from django.utils import timezone - -# Third Party imports -from rest_framework import serializers - -# Module imports -from .base import BaseSerializer -from plane.db.models import ( - User, - Issue, - Label, - State, - DraftIssue, - DraftIssueAssignee, - DraftIssueLabel, - DraftIssueCycle, - DraftIssueModule, -) - - -class DraftIssueCreateSerializer(BaseSerializer): - # ids - state_id = serializers.PrimaryKeyRelatedField( - source="state", - queryset=State.objects.all(), - required=False, - allow_null=True, - ) - parent_id = serializers.PrimaryKeyRelatedField( - source="parent", - queryset=Issue.objects.all(), - required=False, - allow_null=True, - ) - label_ids = serializers.ListField( - child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()), - write_only=True, - required=False, - ) - assignee_ids = serializers.ListField( - child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), - write_only=True, - required=False, - ) - - class Meta: - model = DraftIssue - fields = "__all__" - read_only_fields = [ - "workspace", - "created_by", - "updated_by", - "created_at", - "updated_at", - ] - - def to_representation(self, instance): - data = super().to_representation(instance) - assignee_ids = self.initial_data.get("assignee_ids") - data["assignee_ids"] = assignee_ids if assignee_ids else [] - label_ids = self.initial_data.get("label_ids") - data["label_ids"] = label_ids if label_ids else [] - return data - - def validate(self, data): - if ( - data.get("start_date", None) is not None - and data.get("target_date", None) is not None - and data.get("start_date", None) > data.get("target_date", None) - ): - raise serializers.ValidationError( - "Start date cannot exceed target date" - ) - return data - - def create(self, validated_data): - assignees = validated_data.pop("assignee_ids", None) - labels = validated_data.pop("label_ids", None) - modules = validated_data.pop("module_ids", None) - cycle_id = self.initial_data.get("cycle_id", None) - modules = self.initial_data.get("module_ids", None) - - workspace_id = self.context["workspace_id"] - project_id = self.context["project_id"] - - # Create Issue - issue = DraftIssue.objects.create( - **validated_data, - workspace_id=workspace_id, - project_id=project_id, - ) - - # Issue Audit Users - created_by_id = issue.created_by_id - updated_by_id = issue.updated_by_id - - if assignees is not None and len(assignees): - DraftIssueAssignee.objects.bulk_create( - [ - DraftIssueAssignee( - assignee=user, - draft_issue=issue, - workspace_id=workspace_id, - project_id=project_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for user in assignees - ], - batch_size=10, - ) - - if labels is not None and len(labels): - DraftIssueLabel.objects.bulk_create( - [ - DraftIssueLabel( - label=label, - draft_issue=issue, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for label in labels - ], - batch_size=10, - ) - - if cycle_id is not None: - DraftIssueCycle.objects.create( - cycle_id=cycle_id, - draft_issue=issue, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - - if modules is not None and len(modules): - DraftIssueModule.objects.bulk_create( - [ - DraftIssueModule( - module_id=module_id, - draft_issue=issue, - project_id=project_id, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for module_id in modules - ], - batch_size=10, - ) - - return issue - - def update(self, instance, validated_data): - assignees = validated_data.pop("assignee_ids", None) - labels = validated_data.pop("label_ids", None) - cycle_id = self.context.get("cycle_id", None) - modules = self.initial_data.get("module_ids", None) - - # Related models - workspace_id = instance.workspace_id - project_id = instance.project_id - - created_by_id = instance.created_by_id - updated_by_id = instance.updated_by_id - - if assignees is not None: - DraftIssueAssignee.objects.filter(draft_issue=instance).delete() - DraftIssueAssignee.objects.bulk_create( - [ - DraftIssueAssignee( - assignee=user, - draft_issue=instance, - workspace_id=workspace_id, - project_id=project_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for user in assignees - ], - batch_size=10, - ) - - if labels is not None: - DraftIssueLabel.objects.filter(draft_issue=instance).delete() - DraftIssueLabel.objects.bulk_create( - [ - DraftIssueLabel( - label=label, - draft_issue=instance, - workspace_id=workspace_id, - project_id=project_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for label in labels - ], - batch_size=10, - ) - - if cycle_id != "not_provided": - DraftIssueCycle.objects.filter(draft_issue=instance).delete() - if cycle_id is not None: - DraftIssueCycle.objects.create( - cycle_id=cycle_id, - draft_issue=instance, - workspace_id=workspace_id, - project_id=project_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - - if modules is not None: - DraftIssueModule.objects.filter(draft_issue=instance).delete() - DraftIssueModule.objects.bulk_create( - [ - DraftIssueModule( - module_id=module_id, - draft_issue=instance, - workspace_id=workspace_id, - project_id=project_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) - for module_id in modules - ], - batch_size=10, - ) - - # Time updation occurs even when other related models are updated - instance.updated_at = timezone.now() - return super().update(instance, validated_data) - - -class DraftIssueSerializer(BaseSerializer): - # ids - cycle_id = serializers.PrimaryKeyRelatedField(read_only=True) - module_ids = serializers.ListField( - child=serializers.UUIDField(), - required=False, - ) - - # Many to many - label_ids = serializers.ListField( - child=serializers.UUIDField(), - required=False, - ) - assignee_ids = serializers.ListField( - child=serializers.UUIDField(), - required=False, - ) - - class Meta: - model = DraftIssue - fields = [ - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "created_at", - "updated_at", - "created_by", - "updated_by", - "type_id", - "description_html", - ] - read_only_fields = fields - - -class DraftIssueDetailSerializer(DraftIssueSerializer): - description_html = serializers.CharField() - - class Meta(DraftIssueSerializer.Meta): - fields = DraftIssueSerializer.Meta.fields + [ - "description_html", - ] - read_only_fields = fields diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index e6007862fda..564725e8391 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -11,6 +11,7 @@ IssueActivityEndpoint, IssueArchiveViewSet, IssueCommentViewSet, + IssueDraftViewSet, IssueListEndpoint, IssueReactionViewSet, IssueRelationViewSet, @@ -289,6 +290,28 @@ name="issue-relation", ), ## End Issue Relation + ## Issue Drafts + path( + "workspaces//projects//issue-drafts/", + IssueDraftViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-issue-draft", + ), + path( + "workspaces//projects//issue-drafts//", + IssueDraftViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-issue-draft", + ), path( "workspaces//projects//deleted-issues/", DeletedIssuesListViewSet.as_view(), diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index fb6f4c13acc..3f1e000e473 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -27,7 +27,6 @@ WorkspaceCyclesEndpoint, WorkspaceFavoriteEndpoint, WorkspaceFavoriteGroupEndpoint, - WorkspaceDraftIssueViewSet, ) @@ -255,30 +254,4 @@ WorkspaceFavoriteGroupEndpoint.as_view(), name="workspace-user-favorites-groups", ), - path( - "workspaces//draft-issues/", - WorkspaceDraftIssueViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="workspace-draft-issues", - ), - path( - "workspaces//draft-issues//", - WorkspaceDraftIssueViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="workspace-drafts-issues", - ), - path( - "workspaces//draft-to-issue//", - WorkspaceDraftIssueViewSet.as_view({"post": "create_draft_to_issue"}), - name="workspace-drafts-issues", - ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 872b511a049..6c4cc12c894 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -40,8 +40,6 @@ ExportWorkspaceUserActivityEndpoint, ) -from .workspace.draft import WorkspaceDraftIssueViewSet - from .workspace.favorite import ( WorkspaceFavoriteEndpoint, WorkspaceFavoriteGroupEndpoint, @@ -135,6 +133,8 @@ CommentReactionViewSet, ) +from .issue.draft import IssueDraftViewSet + from .issue.label import ( LabelViewSet, BulkCreateIssueLabelsEndpoint, diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py index 9d7f79b0ec1..25ad8a2eb6e 100644 --- a/apiserver/plane/app/views/cycle/archive.py +++ b/apiserver/plane/app/views/cycle/archive.py @@ -604,7 +604,7 @@ def post(self, request, slug, project_id, cycle_id): pk=cycle_id, project_id=project_id, workspace__slug=slug ) - if cycle.end_date >= timezone.now(): + if cycle.end_date >= timezone.now().date(): return Response( {"error": "Only completed cycles can be archived"}, status=status.HTTP_400_BAD_REQUEST, diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 3a372d36ded..fc04abe35d2 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -187,7 +187,6 @@ def list(self, request, slug, project_id): "completed_issues", "assignee_ids", "status", - "version", "created_by", ) @@ -217,7 +216,6 @@ def list(self, request, slug, project_id): "completed_issues", "assignee_ids", "status", - "version", "created_by", ) return Response(data, status=status.HTTP_200_OK) @@ -257,7 +255,6 @@ def create(self, request, slug, project_id): "external_id", "progress_snapshot", "logo_props", - "version", # meta fields "is_favorite", "total_issues", @@ -309,7 +306,10 @@ def partial_update(self, request, slug, project_id, pk): request_data = request.data - if cycle.end_date is not None and cycle.end_date < timezone.now(): + if ( + cycle.end_date is not None + and cycle.end_date < timezone.now().date() + ): if "sort_order" in request_data: # Can only change sort order for a completed cycle`` request_data = { @@ -347,7 +347,6 @@ def partial_update(self, request, slug, project_id, pk): "external_id", "progress_snapshot", "logo_props", - "version", # meta fields "is_favorite", "total_issues", @@ -413,7 +412,6 @@ def retrieve(self, request, slug, project_id, pk): "progress_snapshot", "sub_issues", "logo_props", - "version", # meta fields "is_favorite", "total_issues", @@ -927,7 +925,7 @@ def post(self, request, slug, project_id, cycle_id): if ( new_cycle.end_date is not None - and new_cycle.end_date < timezone.now() + and new_cycle.end_date < timezone.now().date() ): return Response( { @@ -1150,7 +1148,6 @@ def get(self, request, slug, project_id, cycle_id): status=status.HTTP_200_OK, ) - class CycleAnalyticsEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) diff --git a/apiserver/plane/app/views/cycle/issue.py b/apiserver/plane/app/views/cycle/issue.py index 6be2c9ea943..211f5a88acf 100644 --- a/apiserver/plane/app/views/cycle/issue.py +++ b/apiserver/plane/app/views/cycle/issue.py @@ -248,7 +248,7 @@ def create(self, request, slug, project_id, cycle_id): if ( cycle.end_date is not None - and cycle.end_date < timezone.now() + and cycle.end_date < timezone.now().date() ): return Response( { diff --git a/apiserver/plane/app/views/dashboard/base.py b/apiserver/plane/app/views/dashboard/base.py index 6a72fed2898..4a760ca3b14 100644 --- a/apiserver/plane/app/views/dashboard/base.py +++ b/apiserver/plane/app/views/dashboard/base.py @@ -40,6 +40,8 @@ IssueLink, IssueRelation, Project, + ProjectMember, + User, Widget, WorkspaceMember, ) diff --git a/apiserver/plane/app/views/inbox/base.py b/apiserver/plane/app/views/inbox/base.py index 4a32d993034..3bd5332dc27 100644 --- a/apiserver/plane/app/views/inbox/base.py +++ b/apiserver/plane/app/views/inbox/base.py @@ -323,7 +323,7 @@ def create(self, request, slug, project_id): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=Issue) + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) def partial_update(self, request, slug, project_id, pk): inbox_id = Inbox.objects.filter( workspace__slug=slug, project_id=project_id @@ -418,7 +418,7 @@ def partial_update(self, request, slug, project_id, pk): ) # Only project admins and members can edit inbox issue attributes - if project_member.role > 15: + if project_member.role > 5: serializer = InboxIssueSerializer( inbox_issue, data=request.data, partial=True ) diff --git a/apiserver/plane/app/views/issue/draft.py b/apiserver/plane/app/views/issue/draft.py new file mode 100644 index 00000000000..c5899d9725d --- /dev/null +++ b/apiserver/plane/app/views/issue/draft.py @@ -0,0 +1,410 @@ +# Python imports +import json + +# Django imports +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.core.serializers.json import DjangoJSONEncoder +from django.db.models import ( + Exists, + F, + Func, + OuterRef, + Prefetch, + Q, + UUIDField, + Value, +) +from django.db.models.functions import Coalesce +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views.decorators.gzip import gzip_page + +# Third Party imports +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.app.permissions import ProjectEntityPermission +from plane.app.serializers import ( + IssueCreateSerializer, + IssueDetailSerializer, + IssueFlatSerializer, + IssueSerializer, +) +from plane.bgtasks.issue_activities_task import issue_activity +from plane.db.models import ( + Issue, + IssueAttachment, + IssueLink, + IssueReaction, + IssueSubscriber, + Project, + ProjectMember, +) +from plane.utils.grouper import ( + issue_group_values, + issue_on_results, + issue_queryset_grouper, +) +from plane.utils.issue_filters import issue_filters +from plane.utils.order_queryset import order_issue_queryset +from plane.utils.paginator import ( + GroupedOffsetPaginator, + SubGroupedOffsetPaginator, +) +from .. import BaseViewSet + + +class IssueDraftViewSet(BaseViewSet): + permission_classes = [ + ProjectEntityPermission, + ] + serializer_class = IssueFlatSerializer + model = Issue + + def get_queryset(self): + return ( + Issue.objects.filter(project_id=self.kwargs.get("project_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(is_draft=True) + .filter(deleted_at__isnull=True) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + ).distinct() + + @method_decorator(gzip_page) + def list(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = self.get_queryset().filter(**filters) + # Issue queryset + issue_queryset, order_by_param = order_issue_queryset( + issue_queryset=issue_queryset, + order_by_param=order_by_param, + ) + + # Group by + group_by = request.GET.get("group_by", False) + sub_group_by = request.GET.get("sub_group_by", False) + + # issue queryset + issue_queryset = issue_queryset_grouper( + queryset=issue_queryset, + group_by=group_by, + sub_group_by=sub_group_by, + ) + + if group_by: + # Check group and sub group value paginate + if sub_group_by: + if group_by == sub_group_by: + return Response( + { + "error": "Group by and sub group by cannot have same parameters" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + else: + # group and sub group pagination + return self.paginate( + request=request, + order_by=order_by_param, + queryset=issue_queryset, + on_results=lambda issues: issue_on_results( + group_by=group_by, + issues=issues, + sub_group_by=sub_group_by, + ), + paginator_cls=SubGroupedOffsetPaginator, + group_by_fields=issue_group_values( + field=group_by, + slug=slug, + project_id=project_id, + filters=filters, + ), + sub_group_by_fields=issue_group_values( + field=sub_group_by, + slug=slug, + project_id=project_id, + filters=filters, + ), + group_by_field_name=group_by, + sub_group_by_field_name=sub_group_by, + count_filter=Q( + Q(issue_inbox__status=1) + | Q(issue_inbox__status=-1) + | Q(issue_inbox__status=2) + | Q(issue_inbox__isnull=True), + archived_at__isnull=True, + is_draft=False, + ), + ) + # Group Paginate + else: + # Group paginate + return self.paginate( + request=request, + order_by=order_by_param, + queryset=issue_queryset, + on_results=lambda issues: issue_on_results( + group_by=group_by, + issues=issues, + sub_group_by=sub_group_by, + ), + paginator_cls=GroupedOffsetPaginator, + group_by_fields=issue_group_values( + field=group_by, + slug=slug, + project_id=project_id, + filters=filters, + ), + group_by_field_name=group_by, + count_filter=Q( + Q(issue_inbox__status=1) + | Q(issue_inbox__status=-1) + | Q(issue_inbox__status=2) + | Q(issue_inbox__isnull=True), + archived_at__isnull=True, + is_draft=False, + ), + ) + else: + # List Paginate + return self.paginate( + order_by=order_by_param, + request=request, + queryset=issue_queryset, + on_results=lambda issues: issue_on_results( + group_by=group_by, issues=issues, sub_group_by=sub_group_by + ), + ) + + def create(self, request, slug, project_id): + project = Project.objects.get(pk=project_id) + + serializer = IssueCreateSerializer( + data=request.data, + context={ + "project_id": project_id, + "workspace_id": project.workspace_id, + "default_assignee_id": project.default_assignee_id, + }, + ) + + if serializer.is_valid(): + serializer.save(is_draft=True) + + # Track the issue + issue_activity.delay( + type="issue_draft.activity.created", + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), + actor_id=str(request.user.id), + issue_id=str(serializer.data.get("id", None)), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + issue = ( + issue_queryset_grouper( + queryset=self.get_queryset().filter( + pk=serializer.data["id"] + ), + group_by=None, + sub_group_by=None, + ) + .values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + .first() + ) + return Response(issue, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, pk): + issue = self.get_queryset().filter(pk=pk).first() + + if not issue: + return Response( + {"error": "Issue does not exist"}, + status=status.HTTP_404_NOT_FOUND, + ) + + serializer = IssueCreateSerializer( + issue, data=request.data, partial=True + ) + + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="issue_draft.activity.updated", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("pk", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + IssueSerializer(issue).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request, slug, project_id, pk=None): + issue = ( + self.get_queryset() + .filter(pk=pk) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True) + & Q(assignees__member_project__is_active=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related( + "issue", "actor" + ), + ) + ) + .prefetch_related( + Prefetch( + "issue_attachment", + queryset=IssueAttachment.objects.select_related("issue"), + ) + ) + .prefetch_related( + Prefetch( + "issue_link", + queryset=IssueLink.objects.select_related("created_by"), + ) + ) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + workspace__slug=slug, + project_id=project_id, + issue_id=OuterRef("pk"), + subscriber=request.user, + ) + ) + ) + ).first() + + if not issue: + return Response( + {"error": "The required object does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + serializer = IssueDetailSerializer(issue, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) + + def destroy(self, request, slug, project_id, pk=None): + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=pk + ) + if issue.created_by_id != request.user.id and ( + not ProjectMember.objects.filter( + workspace__slug=slug, + member=request.user, + role=20, + project_id=project_id, + is_active=True, + ).exists() + ): + return Response( + {"error": "Only admin or creator can delete the issue"}, + status=status.HTTP_403_FORBIDDEN, + ) + issue.delete() + issue_activity.delay( + type="issue_draft.activity.deleted", + requested_data=json.dumps({"issue_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(pk), + project_id=str(project_id), + current_instance={}, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index eafb63b0e41..d09848fd94c 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -30,7 +30,6 @@ # Module imports from plane.app.permissions import ( ProjectEntityPermission, - ProjectLitePermission, allow_permission, ROLE, ) @@ -318,12 +317,13 @@ def get_queryset(self): .order_by("-is_favorite", "-created_at") ) - @allow_permission( + allow_permission( [ ROLE.ADMIN, ROLE.MEMBER, ] ) + def create(self, request, slug, project_id): project = Project.objects.get(workspace__slug=slug, pk=project_id) serializer = ModuleWriteSerializer( @@ -386,7 +386,8 @@ def create(self, request, slug, project_id): return Response(module, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + def list(self, request, slug, project_id): queryset = self.get_queryset().filter(archived_at__isnull=True) if self.fields: @@ -434,7 +435,13 @@ def list(self, request, slug, project_id): ) return Response(modules, status=status.HTTP_200_OK) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + allow_permission( + [ + ROLE.ADMIN, + ROLE.MEMBER, + ] + ) + def retrieve(self, request, slug, project_id, pk): queryset = ( self.get_queryset() @@ -665,13 +672,7 @@ def retrieve(self, request, slug, project_id, pk): "labels": label_distribution, "completion_chart": {}, } - - if ( - modules - and modules.start_date - and modules.target_date - and modules.total_issues > 0 - ): + if modules and modules.start_date and modules.target_date: data["distribution"]["completion_chart"] = burndown_plot( queryset=modules, slug=slug, @@ -837,9 +838,6 @@ def get_queryset(self): class ModuleFavoriteViewSet(BaseViewSet): model = UserFavorite - permission_classes = [ - ProjectLitePermission, - ] def get_queryset(self): return self.filter_queryset( diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 9095e4e3be2..6a9afb65237 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -497,44 +497,6 @@ def partial_update(self, request, slug, pk=None): status=status.HTTP_410_GONE, ) - def destroy(self, request, slug, pk): - if ( - WorkspaceMember.objects.filter( - member=request.user, - workspace__slug=slug, - is_active=True, - role=20, - ).exists() - or ProjectMember.objects.filter( - member=request.user, - workspace__slug=slug, - project_id=pk, - role=20, - is_active=True, - ).exists() - ): - project = Project.objects.get(pk=pk) - project.delete() - - # Delete the project members - DeployBoard.objects.filter( - project_id=pk, - workspace__slug=slug, - ).delete() - - # Delete the user favorite - UserFavorite.objects.filter( - project_id=pk, - workspace__slug=slug, - ).delete() - - return Response(status=status.HTTP_204_NO_CONTENT) - else: - return Response( - {"error": "You don't have the required permissions."}, - status=status.HTTP_403_FORBIDDEN, - ) - class ProjectArchiveUnarchiveEndpoint(BaseAPIView): diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index e48871e3b6e..861aa4292ed 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -431,7 +431,8 @@ def get_queryset(self): .distinct() ) - @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + def list(self, request, slug, project_id): queryset = self.get_queryset() project = Project.objects.get(id=project_id) @@ -456,7 +457,8 @@ def list(self, request, slug, project_id): ).data return Response(views, status=status.HTTP_200_OK) - @allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) + def retrieve(self, request, slug, project_id, pk): issue_view = ( self.get_queryset().filter(pk=pk, project_id=project_id).first() @@ -496,7 +498,8 @@ def retrieve(self, request, slug, project_id, pk): status=status.HTTP_200_OK, ) - @allow_permission(allowed_roles=[], creator=True, model=IssueView) + allow_permission(allowed_roles=[], creator=True, model=IssueView) + def partial_update(self, request, slug, project_id, pk): with transaction.atomic(): issue_view = IssueView.objects.select_for_update().get( @@ -529,7 +532,8 @@ def partial_update(self, request, slug, project_id, pk): serializer.errors, status=status.HTTP_400_BAD_REQUEST ) - @allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView) + allow_permission(allowed_roles=[ROLE.ADMIN], creator=True, model=IssueView) + def destroy(self, request, slug, project_id, pk): project_view = IssueView.objects.get( pk=pk, @@ -574,7 +578,8 @@ def get_queryset(self): .select_related("view") ) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + def create(self, request, slug, project_id): _ = UserFavorite.objects.create( user=request.user, @@ -584,7 +589,8 @@ def create(self, request, slug, project_id): ) return Response(status=status.HTTP_204_NO_CONTENT) - @allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + allow_permission([ROLE.ADMIN, ROLE.MEMBER]) + def destroy(self, request, slug, project_id, view_id): view_favorite = UserFavorite.objects.get( project=project_id, diff --git a/apiserver/plane/app/views/workspace/draft.py b/apiserver/plane/app/views/workspace/draft.py deleted file mode 100644 index 5bd8c2dfae7..00000000000 --- a/apiserver/plane/app/views/workspace/draft.py +++ /dev/null @@ -1,332 +0,0 @@ -# Python imports -import json - -# Django imports -from django.utils import timezone -from django.core import serializers -from django.core.serializers.json import DjangoJSONEncoder -from django.contrib.postgres.aggregates import ArrayAgg -from django.contrib.postgres.fields import ArrayField -from django.db.models import ( - F, - Q, - UUIDField, - Value, -) -from django.db.models.functions import Coalesce -from django.utils.decorators import method_decorator -from django.views.decorators.gzip import gzip_page - -# Third Party imports -from rest_framework import status -from rest_framework.response import Response - -# Module imports -from plane.app.permissions import allow_permission, ROLE -from plane.app.serializers import ( - IssueCreateSerializer, - DraftIssueCreateSerializer, - DraftIssueSerializer, - DraftIssueDetailSerializer, -) -from plane.db.models import ( - Issue, - DraftIssue, - CycleIssue, - ModuleIssue, - DraftIssueModule, - DraftIssueCycle, - Workspace, -) -from .. import BaseViewSet -from plane.bgtasks.issue_activities_task import issue_activity -from plane.utils.issue_filters import issue_filters - - -class WorkspaceDraftIssueViewSet(BaseViewSet): - model = DraftIssue - - def get_queryset(self): - return ( - DraftIssue.objects.filter(workspace__slug=self.kwargs.get("slug")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related( - "assignees", "labels", "draft_issue_module__module" - ) - .annotate(cycle_id=F("draft_issue_cycle__cycle_id")) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True) - & Q(assignees__member_project__is_active=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "draft_issue_module__module_id", - distinct=True, - filter=~Q(draft_issue_module__module_id__isnull=True) - & Q( - draft_issue_module__module__archived_at__isnull=True - ), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ).distinct() - - @method_decorator(gzip_page) - @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" - ) - def list(self, request, slug): - filters = issue_filters(request.query_params, "GET") - issues = ( - self.get_queryset() - .filter(created_by=request.user) - .order_by("-created_at") - ) - - issues = issues.filter(**filters) - # List Paginate - return self.paginate( - request=request, - queryset=(issues), - on_results=lambda issues: DraftIssueSerializer( - issues, - many=True, - ).data, - ) - - @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" - ) - def create(self, request, slug): - workspace = Workspace.objects.get(slug=slug) - - serializer = DraftIssueCreateSerializer( - data=request.data, - context={ - "workspace_id": workspace.id, - "project_id": request.data.get("project_id", None), - }, - ) - if serializer.is_valid(): - serializer.save() - issue = ( - self.get_queryset() - .filter(pk=serializer.data.get("id")) - .values( - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "created_at", - "updated_at", - "created_by", - "updated_by", - ) - .first() - ) - - return Response(issue, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], - creator=True, - model=Issue, - level="WORKSPACE", - ) - def partial_update(self, request, slug, pk): - issue = ( - self.get_queryset().filter(pk=pk, created_by=request.user).first() - ) - - if not issue: - return Response( - {"error": "Issue not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - serializer = DraftIssueCreateSerializer( - issue, - data=request.data, - partial=True, - context={ - "project_id": request.data.get("project_id", None), - "cycle_id": request.data.get("cycle_id", "not_provided"), - }, - ) - - if serializer.is_valid(): - serializer.save() - - return Response(status=status.HTTP_204_NO_CONTENT) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - @allow_permission( - allowed_roles=[ROLE.ADMIN], - creator=True, - model=Issue, - level="WORKSPACE", - ) - def retrieve(self, request, slug, pk=None): - issue = ( - self.get_queryset().filter(pk=pk, created_by=request.user).first() - ) - - if not issue: - return Response( - {"error": "The required object does not exist."}, - status=status.HTTP_404_NOT_FOUND, - ) - - serializer = DraftIssueDetailSerializer(issue) - return Response(serializer.data, status=status.HTTP_200_OK) - - @allow_permission( - allowed_roles=[ROLE.ADMIN], - creator=True, - model=Issue, - level="WORKSPACE", - ) - def destroy(self, request, slug, pk=None): - draft_issue = DraftIssue.objects.get(workspace__slug=slug, pk=pk) - draft_issue.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], - level="WORKSPACE", - ) - def create_draft_to_issue(self, request, slug, draft_id): - draft_issue = self.get_queryset().filter(pk=draft_id).first() - - if not draft_issue.project_id: - return Response( - {"error": "Project is required to create an issue."}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = IssueCreateSerializer( - data=request.data, - context={ - "project_id": draft_issue.project_id, - "workspace_id": draft_issue.project.workspace_id, - "default_assignee_id": draft_issue.project.default_assignee_id, - }, - ) - - if serializer.is_valid(): - serializer.save() - - issue_activity.delay( - type="issue.activity.created", - requested_data=json.dumps( - self.request.data, cls=DjangoJSONEncoder - ), - actor_id=str(request.user.id), - issue_id=str(serializer.data.get("id", None)), - project_id=str(draft_issue.project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - - if draft_issue.cycle_id: - created_records = CycleIssue.objects.create( - cycle_id=draft_issue.cycle_id, - issue_id=serializer.data.get("id", None), - project_id=draft_issue.project_id, - workspace_id=draft_issue.workspace_id, - created_by_id=draft_issue.created_by_id, - updated_by_id=draft_issue.updated_by_id, - ) - # Capture Issue Activity - issue_activity.delay( - type="cycle.activity.created", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=None, - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "updated_cycle_issues": None, - "created_cycle_issues": serializers.serialize( - "json", created_records - ), - } - ), - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - - if draft_issue.module_ids: - # bulk create the module - ModuleIssue.objects.bulk_create( - [ - ModuleIssue( - module_id=module, - issue_id=serializer.data.get("id", None), - workspace_id=draft_issue.workspace_id, - project_id=draft_issue.project_id, - created_by_id=draft_issue.created_by_id, - updated_by_id=draft_issue.updated_by_id, - ) - for module in draft_issue.module_ids - ], - batch_size=10, - ) - # Bulk Update the activity - _ = [ - issue_activity.delay( - type="module.activity.created", - requested_data=json.dumps({"module_id": str(module)}), - actor_id=str(request.user.id), - issue_id=serializer.data.get("id", None), - project_id=draft_issue.project_id, - current_instance=None, - epoch=int(timezone.now().timestamp()), - notification=True, - origin=request.META.get("HTTP_ORIGIN"), - ) - for module in draft_issue.module_ids - ] - - # delete the draft issue - draft_issue.delete() - - # delete the draft issue module - DraftIssueModule.objects.filter(draft_issue=draft_issue).delete() - - # delete the draft issue cycle - DraftIssueCycle.objects.filter(draft_issue=draft_issue).delete() - - return Response(serializer.data, status=status.HTTP_201_CREATED) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/apiserver/plane/app/views/workspace/user.py b/apiserver/plane/app/views/workspace/user.py index 15e2d01da03..5c173f2021f 100644 --- a/apiserver/plane/app/views/workspace/user.py +++ b/apiserver/plane/app/views/workspace/user.py @@ -504,7 +504,7 @@ def get(self, request, slug, user_id): upcoming_cycles = CycleIssue.objects.filter( workspace__slug=slug, - cycle__start_date__gt=timezone.now(), + cycle__start_date__gt=timezone.now().date(), issue__assignees__in=[ user_id, ], @@ -512,8 +512,8 @@ def get(self, request, slug, user_id): present_cycle = CycleIssue.objects.filter( workspace__slug=slug, - cycle__start_date__lt=timezone.now(), - cycle__end_date__gt=timezone.now(), + cycle__start_date__lt=timezone.now().date(), + cycle__end_date__gt=timezone.now().date(), issue__assignees__in=[ user_id, ], diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index e7ca16a984b..8e648c16b0b 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -42,12 +42,14 @@ def archive_old_issues(): ), Q(issue_cycle__isnull=True) | ( - Q(issue_cycle__cycle__end_date__lt=timezone.now()) + Q(issue_cycle__cycle__end_date__lt=timezone.now().date()) & Q(issue_cycle__isnull=False) ), Q(issue_module__isnull=True) | ( - Q(issue_module__module__target_date__lt=timezone.now()) + Q( + issue_module__module__target_date__lt=timezone.now().date() + ) & Q(issue_module__isnull=False) ), ).filter( @@ -120,12 +122,14 @@ def close_old_issues(): ), Q(issue_cycle__isnull=True) | ( - Q(issue_cycle__cycle__end_date__lt=timezone.now()) + Q(issue_cycle__cycle__end_date__lt=timezone.now().date()) & Q(issue_cycle__isnull=False) ), Q(issue_module__isnull=True) | ( - Q(issue_module__module__target_date__lt=timezone.now()) + Q( + issue_module__module__target_date__lt=timezone.now().date() + ) & Q(issue_module__isnull=False) ), ).filter( diff --git a/apiserver/plane/celery.py b/apiserver/plane/celery.py index 459cb8ed671..4d651255683 100644 --- a/apiserver/plane/celery.py +++ b/apiserver/plane/celery.py @@ -40,10 +40,6 @@ "task": "plane.bgtasks.deletion_task.hard_delete", "schedule": crontab(hour=0, minute=0), }, - "run-every-6-hours-for-instance-trace": { - "task": "plane.license.bgtasks.tracer.instance_traces", - "schedule": crontab(hour="*/6"), - }, } # Load task modules from all registered Django app configs. diff --git a/apiserver/plane/db/migrations/0077_draftissue_cycle_user_timezone_project_user_timezone_and_more.py b/apiserver/plane/db/migrations/0077_draftissue_cycle_user_timezone_project_user_timezone_and_more.py deleted file mode 100644 index ee70f661528..00000000000 --- a/apiserver/plane/db/migrations/0077_draftissue_cycle_user_timezone_project_user_timezone_and_more.py +++ /dev/null @@ -1,2036 +0,0 @@ -# Generated by Django 4.2.15 on 2024-09-24 08:44 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid -from django.db.models import Prefetch - - -def migrate_draft_issues(apps, schema_editor): - Issue = apps.get_model("db", "Issue") - DraftIssue = apps.get_model("db", "DraftIssue") - IssueAssignee = apps.get_model("db", "IssueAssignee") - DraftIssueAssignee = apps.get_model("db", "DraftIssueAssignee") - IssueLabel = apps.get_model("db", "IssueLabel") - DraftIssueLabel = apps.get_model("db", "DraftIssueLabel") - ModuleIssue = apps.get_model("db", "ModuleIssue") - DraftIssueModule = apps.get_model("db", "DraftIssueModule") - DraftIssueCycle = apps.get_model("db", "DraftIssueCycle") - - # Fetch all draft issues with their related assignees and labels - issues = ( - Issue.objects.filter(is_draft=True) - .select_related("issue_cycle__cycle") - .prefetch_related( - Prefetch( - "issue_assignee", - queryset=IssueAssignee.objects.select_related("assignee"), - ), - Prefetch( - "label_issue", - queryset=IssueLabel.objects.select_related("label"), - ), - Prefetch( - "issue_module", - queryset=ModuleIssue.objects.select_related("module"), - ), - ) - ) - - draft_issues = [] - draft_issue_cycle = [] - draft_issue_labels = [] - draft_issue_modules = [] - draft_issue_assignees = [] - # issue_ids_to_delete = [] - - for issue in issues: - draft_issue = DraftIssue( - parent_id=issue.parent_id, - state_id=issue.state_id, - estimate_point_id=issue.estimate_point_id, - name=issue.name, - description=issue.description, - description_html=issue.description_html, - description_stripped=issue.description_stripped, - description_binary=issue.description_binary, - priority=issue.priority, - start_date=issue.start_date, - target_date=issue.target_date, - workspace_id=issue.workspace_id, - project_id=issue.project_id, - created_by_id=issue.created_by_id, - updated_by_id=issue.updated_by_id, - ) - draft_issues.append(draft_issue) - - for assignee in issue.issue_assignee.all(): - draft_issue_assignees.append( - DraftIssueAssignee( - draft_issue=draft_issue, - assignee=assignee.assignee, - workspace_id=issue.workspace_id, - project_id=issue.project_id, - ) - ) - - # Prepare labels for bulk insert - for label in issue.label_issue.all(): - draft_issue_labels.append( - DraftIssueLabel( - draft_issue=draft_issue, - label=label.label, - workspace_id=issue.workspace_id, - project_id=issue.project_id, - ) - ) - - for module_issue in issue.issue_module.all(): - draft_issue_modules.append( - DraftIssueModule( - draft_issue=draft_issue, - module=module_issue.module, - workspace_id=issue.workspace_id, - project_id=issue.project_id, - ) - ) - - if hasattr(issue, "issue_cycle") and issue.issue_cycle: - draft_issue_cycle.append( - DraftIssueCycle( - draft_issue=draft_issue, - cycle=issue.issue_cycle.cycle, - workspace_id=issue.workspace_id, - project_id=issue.project_id, - ) - ) - - # issue_ids_to_delete.append(issue.id) - - # Bulk create draft issues - DraftIssue.objects.bulk_create(draft_issues) - - # Bulk create draft assignees and labels - DraftIssueLabel.objects.bulk_create(draft_issue_labels) - DraftIssueAssignee.objects.bulk_create(draft_issue_assignees) - - # Bulk create draft modules - DraftIssueCycle.objects.bulk_create(draft_issue_cycle) - DraftIssueModule.objects.bulk_create(draft_issue_modules) - - # Delete original issues - # Issue.objects.filter(id__in=issue_ids_to_delete).delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ("db", "0076_alter_projectmember_role_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="DraftIssue", - 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" - ), - ), - ( - "deleted_at", - models.DateTimeField( - blank=True, null=True, verbose_name="Deleted At" - ), - ), - ( - "id", - models.UUIDField( - db_index=True, - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ( - "name", - models.CharField( - blank=True, - max_length=255, - null=True, - verbose_name="Issue Name", - ), - ), - ("description", models.JSONField(blank=True, default=dict)), - ( - "description_html", - models.TextField(blank=True, default="

"), - ), - ( - "description_stripped", - models.TextField(blank=True, null=True), - ), - ("description_binary", models.BinaryField(null=True)), - ( - "priority", - models.CharField( - choices=[ - ("urgent", "Urgent"), - ("high", "High"), - ("medium", "Medium"), - ("low", "Low"), - ("none", "None"), - ], - default="none", - max_length=30, - verbose_name="Issue Priority", - ), - ), - ("start_date", models.DateField(blank=True, null=True)), - ("target_date", models.DateField(blank=True, null=True)), - ("sort_order", models.FloatField(default=65535)), - ("completed_at", models.DateTimeField(null=True)), - ( - "external_source", - models.CharField(blank=True, max_length=255, null=True), - ), - ( - "external_id", - models.CharField(blank=True, max_length=255, null=True), - ), - ], - options={ - "verbose_name": "DraftIssue", - "verbose_name_plural": "DraftIssues", - "db_table": "draft_issues", - "ordering": ("-created_at",), - }, - ), - migrations.AddField( - model_name="cycle", - name="timezone", - field=models.CharField( - choices=[ - ("Africa/Abidjan", "Africa/Abidjan"), - ("Africa/Accra", "Africa/Accra"), - ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), - ("Africa/Algiers", "Africa/Algiers"), - ("Africa/Asmara", "Africa/Asmara"), - ("Africa/Asmera", "Africa/Asmera"), - ("Africa/Bamako", "Africa/Bamako"), - ("Africa/Bangui", "Africa/Bangui"), - ("Africa/Banjul", "Africa/Banjul"), - ("Africa/Bissau", "Africa/Bissau"), - ("Africa/Blantyre", "Africa/Blantyre"), - ("Africa/Brazzaville", "Africa/Brazzaville"), - ("Africa/Bujumbura", "Africa/Bujumbura"), - ("Africa/Cairo", "Africa/Cairo"), - ("Africa/Casablanca", "Africa/Casablanca"), - ("Africa/Ceuta", "Africa/Ceuta"), - ("Africa/Conakry", "Africa/Conakry"), - ("Africa/Dakar", "Africa/Dakar"), - ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), - ("Africa/Djibouti", "Africa/Djibouti"), - ("Africa/Douala", "Africa/Douala"), - ("Africa/El_Aaiun", "Africa/El_Aaiun"), - ("Africa/Freetown", "Africa/Freetown"), - ("Africa/Gaborone", "Africa/Gaborone"), - ("Africa/Harare", "Africa/Harare"), - ("Africa/Johannesburg", "Africa/Johannesburg"), - ("Africa/Juba", "Africa/Juba"), - ("Africa/Kampala", "Africa/Kampala"), - ("Africa/Khartoum", "Africa/Khartoum"), - ("Africa/Kigali", "Africa/Kigali"), - ("Africa/Kinshasa", "Africa/Kinshasa"), - ("Africa/Lagos", "Africa/Lagos"), - ("Africa/Libreville", "Africa/Libreville"), - ("Africa/Lome", "Africa/Lome"), - ("Africa/Luanda", "Africa/Luanda"), - ("Africa/Lubumbashi", "Africa/Lubumbashi"), - ("Africa/Lusaka", "Africa/Lusaka"), - ("Africa/Malabo", "Africa/Malabo"), - ("Africa/Maputo", "Africa/Maputo"), - ("Africa/Maseru", "Africa/Maseru"), - ("Africa/Mbabane", "Africa/Mbabane"), - ("Africa/Mogadishu", "Africa/Mogadishu"), - ("Africa/Monrovia", "Africa/Monrovia"), - ("Africa/Nairobi", "Africa/Nairobi"), - ("Africa/Ndjamena", "Africa/Ndjamena"), - ("Africa/Niamey", "Africa/Niamey"), - ("Africa/Nouakchott", "Africa/Nouakchott"), - ("Africa/Ouagadougou", "Africa/Ouagadougou"), - ("Africa/Porto-Novo", "Africa/Porto-Novo"), - ("Africa/Sao_Tome", "Africa/Sao_Tome"), - ("Africa/Timbuktu", "Africa/Timbuktu"), - ("Africa/Tripoli", "Africa/Tripoli"), - ("Africa/Tunis", "Africa/Tunis"), - ("Africa/Windhoek", "Africa/Windhoek"), - ("America/Adak", "America/Adak"), - ("America/Anchorage", "America/Anchorage"), - ("America/Anguilla", "America/Anguilla"), - ("America/Antigua", "America/Antigua"), - ("America/Araguaina", "America/Araguaina"), - ( - "America/Argentina/Buenos_Aires", - "America/Argentina/Buenos_Aires", - ), - ( - "America/Argentina/Catamarca", - "America/Argentina/Catamarca", - ), - ( - "America/Argentina/ComodRivadavia", - "America/Argentina/ComodRivadavia", - ), - ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), - ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), - ( - "America/Argentina/La_Rioja", - "America/Argentina/La_Rioja", - ), - ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), - ( - "America/Argentina/Rio_Gallegos", - "America/Argentina/Rio_Gallegos", - ), - ("America/Argentina/Salta", "America/Argentina/Salta"), - ( - "America/Argentina/San_Juan", - "America/Argentina/San_Juan", - ), - ( - "America/Argentina/San_Luis", - "America/Argentina/San_Luis", - ), - ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), - ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), - ("America/Aruba", "America/Aruba"), - ("America/Asuncion", "America/Asuncion"), - ("America/Atikokan", "America/Atikokan"), - ("America/Atka", "America/Atka"), - ("America/Bahia", "America/Bahia"), - ("America/Bahia_Banderas", "America/Bahia_Banderas"), - ("America/Barbados", "America/Barbados"), - ("America/Belem", "America/Belem"), - ("America/Belize", "America/Belize"), - ("America/Blanc-Sablon", "America/Blanc-Sablon"), - ("America/Boa_Vista", "America/Boa_Vista"), - ("America/Bogota", "America/Bogota"), - ("America/Boise", "America/Boise"), - ("America/Buenos_Aires", "America/Buenos_Aires"), - ("America/Cambridge_Bay", "America/Cambridge_Bay"), - ("America/Campo_Grande", "America/Campo_Grande"), - ("America/Cancun", "America/Cancun"), - ("America/Caracas", "America/Caracas"), - ("America/Catamarca", "America/Catamarca"), - ("America/Cayenne", "America/Cayenne"), - ("America/Cayman", "America/Cayman"), - ("America/Chicago", "America/Chicago"), - ("America/Chihuahua", "America/Chihuahua"), - ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), - ("America/Coral_Harbour", "America/Coral_Harbour"), - ("America/Cordoba", "America/Cordoba"), - ("America/Costa_Rica", "America/Costa_Rica"), - ("America/Creston", "America/Creston"), - ("America/Cuiaba", "America/Cuiaba"), - ("America/Curacao", "America/Curacao"), - ("America/Danmarkshavn", "America/Danmarkshavn"), - ("America/Dawson", "America/Dawson"), - ("America/Dawson_Creek", "America/Dawson_Creek"), - ("America/Denver", "America/Denver"), - ("America/Detroit", "America/Detroit"), - ("America/Dominica", "America/Dominica"), - ("America/Edmonton", "America/Edmonton"), - ("America/Eirunepe", "America/Eirunepe"), - ("America/El_Salvador", "America/El_Salvador"), - ("America/Ensenada", "America/Ensenada"), - ("America/Fort_Nelson", "America/Fort_Nelson"), - ("America/Fort_Wayne", "America/Fort_Wayne"), - ("America/Fortaleza", "America/Fortaleza"), - ("America/Glace_Bay", "America/Glace_Bay"), - ("America/Godthab", "America/Godthab"), - ("America/Goose_Bay", "America/Goose_Bay"), - ("America/Grand_Turk", "America/Grand_Turk"), - ("America/Grenada", "America/Grenada"), - ("America/Guadeloupe", "America/Guadeloupe"), - ("America/Guatemala", "America/Guatemala"), - ("America/Guayaquil", "America/Guayaquil"), - ("America/Guyana", "America/Guyana"), - ("America/Halifax", "America/Halifax"), - ("America/Havana", "America/Havana"), - ("America/Hermosillo", "America/Hermosillo"), - ( - "America/Indiana/Indianapolis", - "America/Indiana/Indianapolis", - ), - ("America/Indiana/Knox", "America/Indiana/Knox"), - ("America/Indiana/Marengo", "America/Indiana/Marengo"), - ( - "America/Indiana/Petersburg", - "America/Indiana/Petersburg", - ), - ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), - ("America/Indiana/Vevay", "America/Indiana/Vevay"), - ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), - ("America/Indiana/Winamac", "America/Indiana/Winamac"), - ("America/Indianapolis", "America/Indianapolis"), - ("America/Inuvik", "America/Inuvik"), - ("America/Iqaluit", "America/Iqaluit"), - ("America/Jamaica", "America/Jamaica"), - ("America/Jujuy", "America/Jujuy"), - ("America/Juneau", "America/Juneau"), - ( - "America/Kentucky/Louisville", - "America/Kentucky/Louisville", - ), - ( - "America/Kentucky/Monticello", - "America/Kentucky/Monticello", - ), - ("America/Knox_IN", "America/Knox_IN"), - ("America/Kralendijk", "America/Kralendijk"), - ("America/La_Paz", "America/La_Paz"), - ("America/Lima", "America/Lima"), - ("America/Los_Angeles", "America/Los_Angeles"), - ("America/Louisville", "America/Louisville"), - ("America/Lower_Princes", "America/Lower_Princes"), - ("America/Maceio", "America/Maceio"), - ("America/Managua", "America/Managua"), - ("America/Manaus", "America/Manaus"), - ("America/Marigot", "America/Marigot"), - ("America/Martinique", "America/Martinique"), - ("America/Matamoros", "America/Matamoros"), - ("America/Mazatlan", "America/Mazatlan"), - ("America/Mendoza", "America/Mendoza"), - ("America/Menominee", "America/Menominee"), - ("America/Merida", "America/Merida"), - ("America/Metlakatla", "America/Metlakatla"), - ("America/Mexico_City", "America/Mexico_City"), - ("America/Miquelon", "America/Miquelon"), - ("America/Moncton", "America/Moncton"), - ("America/Monterrey", "America/Monterrey"), - ("America/Montevideo", "America/Montevideo"), - ("America/Montreal", "America/Montreal"), - ("America/Montserrat", "America/Montserrat"), - ("America/Nassau", "America/Nassau"), - ("America/New_York", "America/New_York"), - ("America/Nipigon", "America/Nipigon"), - ("America/Nome", "America/Nome"), - ("America/Noronha", "America/Noronha"), - ( - "America/North_Dakota/Beulah", - "America/North_Dakota/Beulah", - ), - ( - "America/North_Dakota/Center", - "America/North_Dakota/Center", - ), - ( - "America/North_Dakota/New_Salem", - "America/North_Dakota/New_Salem", - ), - ("America/Nuuk", "America/Nuuk"), - ("America/Ojinaga", "America/Ojinaga"), - ("America/Panama", "America/Panama"), - ("America/Pangnirtung", "America/Pangnirtung"), - ("America/Paramaribo", "America/Paramaribo"), - ("America/Phoenix", "America/Phoenix"), - ("America/Port-au-Prince", "America/Port-au-Prince"), - ("America/Port_of_Spain", "America/Port_of_Spain"), - ("America/Porto_Acre", "America/Porto_Acre"), - ("America/Porto_Velho", "America/Porto_Velho"), - ("America/Puerto_Rico", "America/Puerto_Rico"), - ("America/Punta_Arenas", "America/Punta_Arenas"), - ("America/Rainy_River", "America/Rainy_River"), - ("America/Rankin_Inlet", "America/Rankin_Inlet"), - ("America/Recife", "America/Recife"), - ("America/Regina", "America/Regina"), - ("America/Resolute", "America/Resolute"), - ("America/Rio_Branco", "America/Rio_Branco"), - ("America/Rosario", "America/Rosario"), - ("America/Santa_Isabel", "America/Santa_Isabel"), - ("America/Santarem", "America/Santarem"), - ("America/Santiago", "America/Santiago"), - ("America/Santo_Domingo", "America/Santo_Domingo"), - ("America/Sao_Paulo", "America/Sao_Paulo"), - ("America/Scoresbysund", "America/Scoresbysund"), - ("America/Shiprock", "America/Shiprock"), - ("America/Sitka", "America/Sitka"), - ("America/St_Barthelemy", "America/St_Barthelemy"), - ("America/St_Johns", "America/St_Johns"), - ("America/St_Kitts", "America/St_Kitts"), - ("America/St_Lucia", "America/St_Lucia"), - ("America/St_Thomas", "America/St_Thomas"), - ("America/St_Vincent", "America/St_Vincent"), - ("America/Swift_Current", "America/Swift_Current"), - ("America/Tegucigalpa", "America/Tegucigalpa"), - ("America/Thule", "America/Thule"), - ("America/Thunder_Bay", "America/Thunder_Bay"), - ("America/Tijuana", "America/Tijuana"), - ("America/Toronto", "America/Toronto"), - ("America/Tortola", "America/Tortola"), - ("America/Vancouver", "America/Vancouver"), - ("America/Virgin", "America/Virgin"), - ("America/Whitehorse", "America/Whitehorse"), - ("America/Winnipeg", "America/Winnipeg"), - ("America/Yakutat", "America/Yakutat"), - ("America/Yellowknife", "America/Yellowknife"), - ("Antarctica/Casey", "Antarctica/Casey"), - ("Antarctica/Davis", "Antarctica/Davis"), - ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), - ("Antarctica/Macquarie", "Antarctica/Macquarie"), - ("Antarctica/Mawson", "Antarctica/Mawson"), - ("Antarctica/McMurdo", "Antarctica/McMurdo"), - ("Antarctica/Palmer", "Antarctica/Palmer"), - ("Antarctica/Rothera", "Antarctica/Rothera"), - ("Antarctica/South_Pole", "Antarctica/South_Pole"), - ("Antarctica/Syowa", "Antarctica/Syowa"), - ("Antarctica/Troll", "Antarctica/Troll"), - ("Antarctica/Vostok", "Antarctica/Vostok"), - ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), - ("Asia/Aden", "Asia/Aden"), - ("Asia/Almaty", "Asia/Almaty"), - ("Asia/Amman", "Asia/Amman"), - ("Asia/Anadyr", "Asia/Anadyr"), - ("Asia/Aqtau", "Asia/Aqtau"), - ("Asia/Aqtobe", "Asia/Aqtobe"), - ("Asia/Ashgabat", "Asia/Ashgabat"), - ("Asia/Ashkhabad", "Asia/Ashkhabad"), - ("Asia/Atyrau", "Asia/Atyrau"), - ("Asia/Baghdad", "Asia/Baghdad"), - ("Asia/Bahrain", "Asia/Bahrain"), - ("Asia/Baku", "Asia/Baku"), - ("Asia/Bangkok", "Asia/Bangkok"), - ("Asia/Barnaul", "Asia/Barnaul"), - ("Asia/Beirut", "Asia/Beirut"), - ("Asia/Bishkek", "Asia/Bishkek"), - ("Asia/Brunei", "Asia/Brunei"), - ("Asia/Calcutta", "Asia/Calcutta"), - ("Asia/Chita", "Asia/Chita"), - ("Asia/Choibalsan", "Asia/Choibalsan"), - ("Asia/Chongqing", "Asia/Chongqing"), - ("Asia/Chungking", "Asia/Chungking"), - ("Asia/Colombo", "Asia/Colombo"), - ("Asia/Dacca", "Asia/Dacca"), - ("Asia/Damascus", "Asia/Damascus"), - ("Asia/Dhaka", "Asia/Dhaka"), - ("Asia/Dili", "Asia/Dili"), - ("Asia/Dubai", "Asia/Dubai"), - ("Asia/Dushanbe", "Asia/Dushanbe"), - ("Asia/Famagusta", "Asia/Famagusta"), - ("Asia/Gaza", "Asia/Gaza"), - ("Asia/Harbin", "Asia/Harbin"), - ("Asia/Hebron", "Asia/Hebron"), - ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), - ("Asia/Hong_Kong", "Asia/Hong_Kong"), - ("Asia/Hovd", "Asia/Hovd"), - ("Asia/Irkutsk", "Asia/Irkutsk"), - ("Asia/Istanbul", "Asia/Istanbul"), - ("Asia/Jakarta", "Asia/Jakarta"), - ("Asia/Jayapura", "Asia/Jayapura"), - ("Asia/Jerusalem", "Asia/Jerusalem"), - ("Asia/Kabul", "Asia/Kabul"), - ("Asia/Kamchatka", "Asia/Kamchatka"), - ("Asia/Karachi", "Asia/Karachi"), - ("Asia/Kashgar", "Asia/Kashgar"), - ("Asia/Kathmandu", "Asia/Kathmandu"), - ("Asia/Katmandu", "Asia/Katmandu"), - ("Asia/Khandyga", "Asia/Khandyga"), - ("Asia/Kolkata", "Asia/Kolkata"), - ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), - ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), - ("Asia/Kuching", "Asia/Kuching"), - ("Asia/Kuwait", "Asia/Kuwait"), - ("Asia/Macao", "Asia/Macao"), - ("Asia/Macau", "Asia/Macau"), - ("Asia/Magadan", "Asia/Magadan"), - ("Asia/Makassar", "Asia/Makassar"), - ("Asia/Manila", "Asia/Manila"), - ("Asia/Muscat", "Asia/Muscat"), - ("Asia/Nicosia", "Asia/Nicosia"), - ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), - ("Asia/Novosibirsk", "Asia/Novosibirsk"), - ("Asia/Omsk", "Asia/Omsk"), - ("Asia/Oral", "Asia/Oral"), - ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), - ("Asia/Pontianak", "Asia/Pontianak"), - ("Asia/Pyongyang", "Asia/Pyongyang"), - ("Asia/Qatar", "Asia/Qatar"), - ("Asia/Qostanay", "Asia/Qostanay"), - ("Asia/Qyzylorda", "Asia/Qyzylorda"), - ("Asia/Rangoon", "Asia/Rangoon"), - ("Asia/Riyadh", "Asia/Riyadh"), - ("Asia/Saigon", "Asia/Saigon"), - ("Asia/Sakhalin", "Asia/Sakhalin"), - ("Asia/Samarkand", "Asia/Samarkand"), - ("Asia/Seoul", "Asia/Seoul"), - ("Asia/Shanghai", "Asia/Shanghai"), - ("Asia/Singapore", "Asia/Singapore"), - ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), - ("Asia/Taipei", "Asia/Taipei"), - ("Asia/Tashkent", "Asia/Tashkent"), - ("Asia/Tbilisi", "Asia/Tbilisi"), - ("Asia/Tehran", "Asia/Tehran"), - ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), - ("Asia/Thimbu", "Asia/Thimbu"), - ("Asia/Thimphu", "Asia/Thimphu"), - ("Asia/Tokyo", "Asia/Tokyo"), - ("Asia/Tomsk", "Asia/Tomsk"), - ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), - ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), - ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), - ("Asia/Urumqi", "Asia/Urumqi"), - ("Asia/Ust-Nera", "Asia/Ust-Nera"), - ("Asia/Vientiane", "Asia/Vientiane"), - ("Asia/Vladivostok", "Asia/Vladivostok"), - ("Asia/Yakutsk", "Asia/Yakutsk"), - ("Asia/Yangon", "Asia/Yangon"), - ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), - ("Asia/Yerevan", "Asia/Yerevan"), - ("Atlantic/Azores", "Atlantic/Azores"), - ("Atlantic/Bermuda", "Atlantic/Bermuda"), - ("Atlantic/Canary", "Atlantic/Canary"), - ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), - ("Atlantic/Faeroe", "Atlantic/Faeroe"), - ("Atlantic/Faroe", "Atlantic/Faroe"), - ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), - ("Atlantic/Madeira", "Atlantic/Madeira"), - ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), - ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), - ("Atlantic/St_Helena", "Atlantic/St_Helena"), - ("Atlantic/Stanley", "Atlantic/Stanley"), - ("Australia/ACT", "Australia/ACT"), - ("Australia/Adelaide", "Australia/Adelaide"), - ("Australia/Brisbane", "Australia/Brisbane"), - ("Australia/Broken_Hill", "Australia/Broken_Hill"), - ("Australia/Canberra", "Australia/Canberra"), - ("Australia/Currie", "Australia/Currie"), - ("Australia/Darwin", "Australia/Darwin"), - ("Australia/Eucla", "Australia/Eucla"), - ("Australia/Hobart", "Australia/Hobart"), - ("Australia/LHI", "Australia/LHI"), - ("Australia/Lindeman", "Australia/Lindeman"), - ("Australia/Lord_Howe", "Australia/Lord_Howe"), - ("Australia/Melbourne", "Australia/Melbourne"), - ("Australia/NSW", "Australia/NSW"), - ("Australia/North", "Australia/North"), - ("Australia/Perth", "Australia/Perth"), - ("Australia/Queensland", "Australia/Queensland"), - ("Australia/South", "Australia/South"), - ("Australia/Sydney", "Australia/Sydney"), - ("Australia/Tasmania", "Australia/Tasmania"), - ("Australia/Victoria", "Australia/Victoria"), - ("Australia/West", "Australia/West"), - ("Australia/Yancowinna", "Australia/Yancowinna"), - ("Brazil/Acre", "Brazil/Acre"), - ("Brazil/DeNoronha", "Brazil/DeNoronha"), - ("Brazil/East", "Brazil/East"), - ("Brazil/West", "Brazil/West"), - ("CET", "CET"), - ("CST6CDT", "CST6CDT"), - ("Canada/Atlantic", "Canada/Atlantic"), - ("Canada/Central", "Canada/Central"), - ("Canada/Eastern", "Canada/Eastern"), - ("Canada/Mountain", "Canada/Mountain"), - ("Canada/Newfoundland", "Canada/Newfoundland"), - ("Canada/Pacific", "Canada/Pacific"), - ("Canada/Saskatchewan", "Canada/Saskatchewan"), - ("Canada/Yukon", "Canada/Yukon"), - ("Chile/Continental", "Chile/Continental"), - ("Chile/EasterIsland", "Chile/EasterIsland"), - ("Cuba", "Cuba"), - ("EET", "EET"), - ("EST", "EST"), - ("EST5EDT", "EST5EDT"), - ("Egypt", "Egypt"), - ("Eire", "Eire"), - ("Etc/GMT", "Etc/GMT"), - ("Etc/GMT+0", "Etc/GMT+0"), - ("Etc/GMT+1", "Etc/GMT+1"), - ("Etc/GMT+10", "Etc/GMT+10"), - ("Etc/GMT+11", "Etc/GMT+11"), - ("Etc/GMT+12", "Etc/GMT+12"), - ("Etc/GMT+2", "Etc/GMT+2"), - ("Etc/GMT+3", "Etc/GMT+3"), - ("Etc/GMT+4", "Etc/GMT+4"), - ("Etc/GMT+5", "Etc/GMT+5"), - ("Etc/GMT+6", "Etc/GMT+6"), - ("Etc/GMT+7", "Etc/GMT+7"), - ("Etc/GMT+8", "Etc/GMT+8"), - ("Etc/GMT+9", "Etc/GMT+9"), - ("Etc/GMT-0", "Etc/GMT-0"), - ("Etc/GMT-1", "Etc/GMT-1"), - ("Etc/GMT-10", "Etc/GMT-10"), - ("Etc/GMT-11", "Etc/GMT-11"), - ("Etc/GMT-12", "Etc/GMT-12"), - ("Etc/GMT-13", "Etc/GMT-13"), - ("Etc/GMT-14", "Etc/GMT-14"), - ("Etc/GMT-2", "Etc/GMT-2"), - ("Etc/GMT-3", "Etc/GMT-3"), - ("Etc/GMT-4", "Etc/GMT-4"), - ("Etc/GMT-5", "Etc/GMT-5"), - ("Etc/GMT-6", "Etc/GMT-6"), - ("Etc/GMT-7", "Etc/GMT-7"), - ("Etc/GMT-8", "Etc/GMT-8"), - ("Etc/GMT-9", "Etc/GMT-9"), - ("Etc/GMT0", "Etc/GMT0"), - ("Etc/Greenwich", "Etc/Greenwich"), - ("Etc/UCT", "Etc/UCT"), - ("Etc/UTC", "Etc/UTC"), - ("Etc/Universal", "Etc/Universal"), - ("Etc/Zulu", "Etc/Zulu"), - ("Europe/Amsterdam", "Europe/Amsterdam"), - ("Europe/Andorra", "Europe/Andorra"), - ("Europe/Astrakhan", "Europe/Astrakhan"), - ("Europe/Athens", "Europe/Athens"), - ("Europe/Belfast", "Europe/Belfast"), - ("Europe/Belgrade", "Europe/Belgrade"), - ("Europe/Berlin", "Europe/Berlin"), - ("Europe/Bratislava", "Europe/Bratislava"), - ("Europe/Brussels", "Europe/Brussels"), - ("Europe/Bucharest", "Europe/Bucharest"), - ("Europe/Budapest", "Europe/Budapest"), - ("Europe/Busingen", "Europe/Busingen"), - ("Europe/Chisinau", "Europe/Chisinau"), - ("Europe/Copenhagen", "Europe/Copenhagen"), - ("Europe/Dublin", "Europe/Dublin"), - ("Europe/Gibraltar", "Europe/Gibraltar"), - ("Europe/Guernsey", "Europe/Guernsey"), - ("Europe/Helsinki", "Europe/Helsinki"), - ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), - ("Europe/Istanbul", "Europe/Istanbul"), - ("Europe/Jersey", "Europe/Jersey"), - ("Europe/Kaliningrad", "Europe/Kaliningrad"), - ("Europe/Kiev", "Europe/Kiev"), - ("Europe/Kirov", "Europe/Kirov"), - ("Europe/Kyiv", "Europe/Kyiv"), - ("Europe/Lisbon", "Europe/Lisbon"), - ("Europe/Ljubljana", "Europe/Ljubljana"), - ("Europe/London", "Europe/London"), - ("Europe/Luxembourg", "Europe/Luxembourg"), - ("Europe/Madrid", "Europe/Madrid"), - ("Europe/Malta", "Europe/Malta"), - ("Europe/Mariehamn", "Europe/Mariehamn"), - ("Europe/Minsk", "Europe/Minsk"), - ("Europe/Monaco", "Europe/Monaco"), - ("Europe/Moscow", "Europe/Moscow"), - ("Europe/Nicosia", "Europe/Nicosia"), - ("Europe/Oslo", "Europe/Oslo"), - ("Europe/Paris", "Europe/Paris"), - ("Europe/Podgorica", "Europe/Podgorica"), - ("Europe/Prague", "Europe/Prague"), - ("Europe/Riga", "Europe/Riga"), - ("Europe/Rome", "Europe/Rome"), - ("Europe/Samara", "Europe/Samara"), - ("Europe/San_Marino", "Europe/San_Marino"), - ("Europe/Sarajevo", "Europe/Sarajevo"), - ("Europe/Saratov", "Europe/Saratov"), - ("Europe/Simferopol", "Europe/Simferopol"), - ("Europe/Skopje", "Europe/Skopje"), - ("Europe/Sofia", "Europe/Sofia"), - ("Europe/Stockholm", "Europe/Stockholm"), - ("Europe/Tallinn", "Europe/Tallinn"), - ("Europe/Tirane", "Europe/Tirane"), - ("Europe/Tiraspol", "Europe/Tiraspol"), - ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), - ("Europe/Uzhgorod", "Europe/Uzhgorod"), - ("Europe/Vaduz", "Europe/Vaduz"), - ("Europe/Vatican", "Europe/Vatican"), - ("Europe/Vienna", "Europe/Vienna"), - ("Europe/Vilnius", "Europe/Vilnius"), - ("Europe/Volgograd", "Europe/Volgograd"), - ("Europe/Warsaw", "Europe/Warsaw"), - ("Europe/Zagreb", "Europe/Zagreb"), - ("Europe/Zaporozhye", "Europe/Zaporozhye"), - ("Europe/Zurich", "Europe/Zurich"), - ("GB", "GB"), - ("GB-Eire", "GB-Eire"), - ("GMT", "GMT"), - ("GMT+0", "GMT+0"), - ("GMT-0", "GMT-0"), - ("GMT0", "GMT0"), - ("Greenwich", "Greenwich"), - ("HST", "HST"), - ("Hongkong", "Hongkong"), - ("Iceland", "Iceland"), - ("Indian/Antananarivo", "Indian/Antananarivo"), - ("Indian/Chagos", "Indian/Chagos"), - ("Indian/Christmas", "Indian/Christmas"), - ("Indian/Cocos", "Indian/Cocos"), - ("Indian/Comoro", "Indian/Comoro"), - ("Indian/Kerguelen", "Indian/Kerguelen"), - ("Indian/Mahe", "Indian/Mahe"), - ("Indian/Maldives", "Indian/Maldives"), - ("Indian/Mauritius", "Indian/Mauritius"), - ("Indian/Mayotte", "Indian/Mayotte"), - ("Indian/Reunion", "Indian/Reunion"), - ("Iran", "Iran"), - ("Israel", "Israel"), - ("Jamaica", "Jamaica"), - ("Japan", "Japan"), - ("Kwajalein", "Kwajalein"), - ("Libya", "Libya"), - ("MET", "MET"), - ("MST", "MST"), - ("MST7MDT", "MST7MDT"), - ("Mexico/BajaNorte", "Mexico/BajaNorte"), - ("Mexico/BajaSur", "Mexico/BajaSur"), - ("Mexico/General", "Mexico/General"), - ("NZ", "NZ"), - ("NZ-CHAT", "NZ-CHAT"), - ("Navajo", "Navajo"), - ("PRC", "PRC"), - ("PST8PDT", "PST8PDT"), - ("Pacific/Apia", "Pacific/Apia"), - ("Pacific/Auckland", "Pacific/Auckland"), - ("Pacific/Bougainville", "Pacific/Bougainville"), - ("Pacific/Chatham", "Pacific/Chatham"), - ("Pacific/Chuuk", "Pacific/Chuuk"), - ("Pacific/Easter", "Pacific/Easter"), - ("Pacific/Efate", "Pacific/Efate"), - ("Pacific/Enderbury", "Pacific/Enderbury"), - ("Pacific/Fakaofo", "Pacific/Fakaofo"), - ("Pacific/Fiji", "Pacific/Fiji"), - ("Pacific/Funafuti", "Pacific/Funafuti"), - ("Pacific/Galapagos", "Pacific/Galapagos"), - ("Pacific/Gambier", "Pacific/Gambier"), - ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), - ("Pacific/Guam", "Pacific/Guam"), - ("Pacific/Honolulu", "Pacific/Honolulu"), - ("Pacific/Johnston", "Pacific/Johnston"), - ("Pacific/Kanton", "Pacific/Kanton"), - ("Pacific/Kiritimati", "Pacific/Kiritimati"), - ("Pacific/Kosrae", "Pacific/Kosrae"), - ("Pacific/Kwajalein", "Pacific/Kwajalein"), - ("Pacific/Majuro", "Pacific/Majuro"), - ("Pacific/Marquesas", "Pacific/Marquesas"), - ("Pacific/Midway", "Pacific/Midway"), - ("Pacific/Nauru", "Pacific/Nauru"), - ("Pacific/Niue", "Pacific/Niue"), - ("Pacific/Norfolk", "Pacific/Norfolk"), - ("Pacific/Noumea", "Pacific/Noumea"), - ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), - ("Pacific/Palau", "Pacific/Palau"), - ("Pacific/Pitcairn", "Pacific/Pitcairn"), - ("Pacific/Pohnpei", "Pacific/Pohnpei"), - ("Pacific/Ponape", "Pacific/Ponape"), - ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), - ("Pacific/Rarotonga", "Pacific/Rarotonga"), - ("Pacific/Saipan", "Pacific/Saipan"), - ("Pacific/Samoa", "Pacific/Samoa"), - ("Pacific/Tahiti", "Pacific/Tahiti"), - ("Pacific/Tarawa", "Pacific/Tarawa"), - ("Pacific/Tongatapu", "Pacific/Tongatapu"), - ("Pacific/Truk", "Pacific/Truk"), - ("Pacific/Wake", "Pacific/Wake"), - ("Pacific/Wallis", "Pacific/Wallis"), - ("Pacific/Yap", "Pacific/Yap"), - ("Poland", "Poland"), - ("Portugal", "Portugal"), - ("ROC", "ROC"), - ("ROK", "ROK"), - ("Singapore", "Singapore"), - ("Turkey", "Turkey"), - ("UCT", "UCT"), - ("US/Alaska", "US/Alaska"), - ("US/Aleutian", "US/Aleutian"), - ("US/Arizona", "US/Arizona"), - ("US/Central", "US/Central"), - ("US/East-Indiana", "US/East-Indiana"), - ("US/Eastern", "US/Eastern"), - ("US/Hawaii", "US/Hawaii"), - ("US/Indiana-Starke", "US/Indiana-Starke"), - ("US/Michigan", "US/Michigan"), - ("US/Mountain", "US/Mountain"), - ("US/Pacific", "US/Pacific"), - ("US/Samoa", "US/Samoa"), - ("UTC", "UTC"), - ("Universal", "Universal"), - ("W-SU", "W-SU"), - ("WET", "WET"), - ("Zulu", "Zulu"), - ], - default="UTC", - max_length=255, - ), - ), - migrations.AddField( - model_name="project", - name="timezone", - field=models.CharField( - choices=[ - ("Africa/Abidjan", "Africa/Abidjan"), - ("Africa/Accra", "Africa/Accra"), - ("Africa/Addis_Ababa", "Africa/Addis_Ababa"), - ("Africa/Algiers", "Africa/Algiers"), - ("Africa/Asmara", "Africa/Asmara"), - ("Africa/Asmera", "Africa/Asmera"), - ("Africa/Bamako", "Africa/Bamako"), - ("Africa/Bangui", "Africa/Bangui"), - ("Africa/Banjul", "Africa/Banjul"), - ("Africa/Bissau", "Africa/Bissau"), - ("Africa/Blantyre", "Africa/Blantyre"), - ("Africa/Brazzaville", "Africa/Brazzaville"), - ("Africa/Bujumbura", "Africa/Bujumbura"), - ("Africa/Cairo", "Africa/Cairo"), - ("Africa/Casablanca", "Africa/Casablanca"), - ("Africa/Ceuta", "Africa/Ceuta"), - ("Africa/Conakry", "Africa/Conakry"), - ("Africa/Dakar", "Africa/Dakar"), - ("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"), - ("Africa/Djibouti", "Africa/Djibouti"), - ("Africa/Douala", "Africa/Douala"), - ("Africa/El_Aaiun", "Africa/El_Aaiun"), - ("Africa/Freetown", "Africa/Freetown"), - ("Africa/Gaborone", "Africa/Gaborone"), - ("Africa/Harare", "Africa/Harare"), - ("Africa/Johannesburg", "Africa/Johannesburg"), - ("Africa/Juba", "Africa/Juba"), - ("Africa/Kampala", "Africa/Kampala"), - ("Africa/Khartoum", "Africa/Khartoum"), - ("Africa/Kigali", "Africa/Kigali"), - ("Africa/Kinshasa", "Africa/Kinshasa"), - ("Africa/Lagos", "Africa/Lagos"), - ("Africa/Libreville", "Africa/Libreville"), - ("Africa/Lome", "Africa/Lome"), - ("Africa/Luanda", "Africa/Luanda"), - ("Africa/Lubumbashi", "Africa/Lubumbashi"), - ("Africa/Lusaka", "Africa/Lusaka"), - ("Africa/Malabo", "Africa/Malabo"), - ("Africa/Maputo", "Africa/Maputo"), - ("Africa/Maseru", "Africa/Maseru"), - ("Africa/Mbabane", "Africa/Mbabane"), - ("Africa/Mogadishu", "Africa/Mogadishu"), - ("Africa/Monrovia", "Africa/Monrovia"), - ("Africa/Nairobi", "Africa/Nairobi"), - ("Africa/Ndjamena", "Africa/Ndjamena"), - ("Africa/Niamey", "Africa/Niamey"), - ("Africa/Nouakchott", "Africa/Nouakchott"), - ("Africa/Ouagadougou", "Africa/Ouagadougou"), - ("Africa/Porto-Novo", "Africa/Porto-Novo"), - ("Africa/Sao_Tome", "Africa/Sao_Tome"), - ("Africa/Timbuktu", "Africa/Timbuktu"), - ("Africa/Tripoli", "Africa/Tripoli"), - ("Africa/Tunis", "Africa/Tunis"), - ("Africa/Windhoek", "Africa/Windhoek"), - ("America/Adak", "America/Adak"), - ("America/Anchorage", "America/Anchorage"), - ("America/Anguilla", "America/Anguilla"), - ("America/Antigua", "America/Antigua"), - ("America/Araguaina", "America/Araguaina"), - ( - "America/Argentina/Buenos_Aires", - "America/Argentina/Buenos_Aires", - ), - ( - "America/Argentina/Catamarca", - "America/Argentina/Catamarca", - ), - ( - "America/Argentina/ComodRivadavia", - "America/Argentina/ComodRivadavia", - ), - ("America/Argentina/Cordoba", "America/Argentina/Cordoba"), - ("America/Argentina/Jujuy", "America/Argentina/Jujuy"), - ( - "America/Argentina/La_Rioja", - "America/Argentina/La_Rioja", - ), - ("America/Argentina/Mendoza", "America/Argentina/Mendoza"), - ( - "America/Argentina/Rio_Gallegos", - "America/Argentina/Rio_Gallegos", - ), - ("America/Argentina/Salta", "America/Argentina/Salta"), - ( - "America/Argentina/San_Juan", - "America/Argentina/San_Juan", - ), - ( - "America/Argentina/San_Luis", - "America/Argentina/San_Luis", - ), - ("America/Argentina/Tucuman", "America/Argentina/Tucuman"), - ("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"), - ("America/Aruba", "America/Aruba"), - ("America/Asuncion", "America/Asuncion"), - ("America/Atikokan", "America/Atikokan"), - ("America/Atka", "America/Atka"), - ("America/Bahia", "America/Bahia"), - ("America/Bahia_Banderas", "America/Bahia_Banderas"), - ("America/Barbados", "America/Barbados"), - ("America/Belem", "America/Belem"), - ("America/Belize", "America/Belize"), - ("America/Blanc-Sablon", "America/Blanc-Sablon"), - ("America/Boa_Vista", "America/Boa_Vista"), - ("America/Bogota", "America/Bogota"), - ("America/Boise", "America/Boise"), - ("America/Buenos_Aires", "America/Buenos_Aires"), - ("America/Cambridge_Bay", "America/Cambridge_Bay"), - ("America/Campo_Grande", "America/Campo_Grande"), - ("America/Cancun", "America/Cancun"), - ("America/Caracas", "America/Caracas"), - ("America/Catamarca", "America/Catamarca"), - ("America/Cayenne", "America/Cayenne"), - ("America/Cayman", "America/Cayman"), - ("America/Chicago", "America/Chicago"), - ("America/Chihuahua", "America/Chihuahua"), - ("America/Ciudad_Juarez", "America/Ciudad_Juarez"), - ("America/Coral_Harbour", "America/Coral_Harbour"), - ("America/Cordoba", "America/Cordoba"), - ("America/Costa_Rica", "America/Costa_Rica"), - ("America/Creston", "America/Creston"), - ("America/Cuiaba", "America/Cuiaba"), - ("America/Curacao", "America/Curacao"), - ("America/Danmarkshavn", "America/Danmarkshavn"), - ("America/Dawson", "America/Dawson"), - ("America/Dawson_Creek", "America/Dawson_Creek"), - ("America/Denver", "America/Denver"), - ("America/Detroit", "America/Detroit"), - ("America/Dominica", "America/Dominica"), - ("America/Edmonton", "America/Edmonton"), - ("America/Eirunepe", "America/Eirunepe"), - ("America/El_Salvador", "America/El_Salvador"), - ("America/Ensenada", "America/Ensenada"), - ("America/Fort_Nelson", "America/Fort_Nelson"), - ("America/Fort_Wayne", "America/Fort_Wayne"), - ("America/Fortaleza", "America/Fortaleza"), - ("America/Glace_Bay", "America/Glace_Bay"), - ("America/Godthab", "America/Godthab"), - ("America/Goose_Bay", "America/Goose_Bay"), - ("America/Grand_Turk", "America/Grand_Turk"), - ("America/Grenada", "America/Grenada"), - ("America/Guadeloupe", "America/Guadeloupe"), - ("America/Guatemala", "America/Guatemala"), - ("America/Guayaquil", "America/Guayaquil"), - ("America/Guyana", "America/Guyana"), - ("America/Halifax", "America/Halifax"), - ("America/Havana", "America/Havana"), - ("America/Hermosillo", "America/Hermosillo"), - ( - "America/Indiana/Indianapolis", - "America/Indiana/Indianapolis", - ), - ("America/Indiana/Knox", "America/Indiana/Knox"), - ("America/Indiana/Marengo", "America/Indiana/Marengo"), - ( - "America/Indiana/Petersburg", - "America/Indiana/Petersburg", - ), - ("America/Indiana/Tell_City", "America/Indiana/Tell_City"), - ("America/Indiana/Vevay", "America/Indiana/Vevay"), - ("America/Indiana/Vincennes", "America/Indiana/Vincennes"), - ("America/Indiana/Winamac", "America/Indiana/Winamac"), - ("America/Indianapolis", "America/Indianapolis"), - ("America/Inuvik", "America/Inuvik"), - ("America/Iqaluit", "America/Iqaluit"), - ("America/Jamaica", "America/Jamaica"), - ("America/Jujuy", "America/Jujuy"), - ("America/Juneau", "America/Juneau"), - ( - "America/Kentucky/Louisville", - "America/Kentucky/Louisville", - ), - ( - "America/Kentucky/Monticello", - "America/Kentucky/Monticello", - ), - ("America/Knox_IN", "America/Knox_IN"), - ("America/Kralendijk", "America/Kralendijk"), - ("America/La_Paz", "America/La_Paz"), - ("America/Lima", "America/Lima"), - ("America/Los_Angeles", "America/Los_Angeles"), - ("America/Louisville", "America/Louisville"), - ("America/Lower_Princes", "America/Lower_Princes"), - ("America/Maceio", "America/Maceio"), - ("America/Managua", "America/Managua"), - ("America/Manaus", "America/Manaus"), - ("America/Marigot", "America/Marigot"), - ("America/Martinique", "America/Martinique"), - ("America/Matamoros", "America/Matamoros"), - ("America/Mazatlan", "America/Mazatlan"), - ("America/Mendoza", "America/Mendoza"), - ("America/Menominee", "America/Menominee"), - ("America/Merida", "America/Merida"), - ("America/Metlakatla", "America/Metlakatla"), - ("America/Mexico_City", "America/Mexico_City"), - ("America/Miquelon", "America/Miquelon"), - ("America/Moncton", "America/Moncton"), - ("America/Monterrey", "America/Monterrey"), - ("America/Montevideo", "America/Montevideo"), - ("America/Montreal", "America/Montreal"), - ("America/Montserrat", "America/Montserrat"), - ("America/Nassau", "America/Nassau"), - ("America/New_York", "America/New_York"), - ("America/Nipigon", "America/Nipigon"), - ("America/Nome", "America/Nome"), - ("America/Noronha", "America/Noronha"), - ( - "America/North_Dakota/Beulah", - "America/North_Dakota/Beulah", - ), - ( - "America/North_Dakota/Center", - "America/North_Dakota/Center", - ), - ( - "America/North_Dakota/New_Salem", - "America/North_Dakota/New_Salem", - ), - ("America/Nuuk", "America/Nuuk"), - ("America/Ojinaga", "America/Ojinaga"), - ("America/Panama", "America/Panama"), - ("America/Pangnirtung", "America/Pangnirtung"), - ("America/Paramaribo", "America/Paramaribo"), - ("America/Phoenix", "America/Phoenix"), - ("America/Port-au-Prince", "America/Port-au-Prince"), - ("America/Port_of_Spain", "America/Port_of_Spain"), - ("America/Porto_Acre", "America/Porto_Acre"), - ("America/Porto_Velho", "America/Porto_Velho"), - ("America/Puerto_Rico", "America/Puerto_Rico"), - ("America/Punta_Arenas", "America/Punta_Arenas"), - ("America/Rainy_River", "America/Rainy_River"), - ("America/Rankin_Inlet", "America/Rankin_Inlet"), - ("America/Recife", "America/Recife"), - ("America/Regina", "America/Regina"), - ("America/Resolute", "America/Resolute"), - ("America/Rio_Branco", "America/Rio_Branco"), - ("America/Rosario", "America/Rosario"), - ("America/Santa_Isabel", "America/Santa_Isabel"), - ("America/Santarem", "America/Santarem"), - ("America/Santiago", "America/Santiago"), - ("America/Santo_Domingo", "America/Santo_Domingo"), - ("America/Sao_Paulo", "America/Sao_Paulo"), - ("America/Scoresbysund", "America/Scoresbysund"), - ("America/Shiprock", "America/Shiprock"), - ("America/Sitka", "America/Sitka"), - ("America/St_Barthelemy", "America/St_Barthelemy"), - ("America/St_Johns", "America/St_Johns"), - ("America/St_Kitts", "America/St_Kitts"), - ("America/St_Lucia", "America/St_Lucia"), - ("America/St_Thomas", "America/St_Thomas"), - ("America/St_Vincent", "America/St_Vincent"), - ("America/Swift_Current", "America/Swift_Current"), - ("America/Tegucigalpa", "America/Tegucigalpa"), - ("America/Thule", "America/Thule"), - ("America/Thunder_Bay", "America/Thunder_Bay"), - ("America/Tijuana", "America/Tijuana"), - ("America/Toronto", "America/Toronto"), - ("America/Tortola", "America/Tortola"), - ("America/Vancouver", "America/Vancouver"), - ("America/Virgin", "America/Virgin"), - ("America/Whitehorse", "America/Whitehorse"), - ("America/Winnipeg", "America/Winnipeg"), - ("America/Yakutat", "America/Yakutat"), - ("America/Yellowknife", "America/Yellowknife"), - ("Antarctica/Casey", "Antarctica/Casey"), - ("Antarctica/Davis", "Antarctica/Davis"), - ("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"), - ("Antarctica/Macquarie", "Antarctica/Macquarie"), - ("Antarctica/Mawson", "Antarctica/Mawson"), - ("Antarctica/McMurdo", "Antarctica/McMurdo"), - ("Antarctica/Palmer", "Antarctica/Palmer"), - ("Antarctica/Rothera", "Antarctica/Rothera"), - ("Antarctica/South_Pole", "Antarctica/South_Pole"), - ("Antarctica/Syowa", "Antarctica/Syowa"), - ("Antarctica/Troll", "Antarctica/Troll"), - ("Antarctica/Vostok", "Antarctica/Vostok"), - ("Arctic/Longyearbyen", "Arctic/Longyearbyen"), - ("Asia/Aden", "Asia/Aden"), - ("Asia/Almaty", "Asia/Almaty"), - ("Asia/Amman", "Asia/Amman"), - ("Asia/Anadyr", "Asia/Anadyr"), - ("Asia/Aqtau", "Asia/Aqtau"), - ("Asia/Aqtobe", "Asia/Aqtobe"), - ("Asia/Ashgabat", "Asia/Ashgabat"), - ("Asia/Ashkhabad", "Asia/Ashkhabad"), - ("Asia/Atyrau", "Asia/Atyrau"), - ("Asia/Baghdad", "Asia/Baghdad"), - ("Asia/Bahrain", "Asia/Bahrain"), - ("Asia/Baku", "Asia/Baku"), - ("Asia/Bangkok", "Asia/Bangkok"), - ("Asia/Barnaul", "Asia/Barnaul"), - ("Asia/Beirut", "Asia/Beirut"), - ("Asia/Bishkek", "Asia/Bishkek"), - ("Asia/Brunei", "Asia/Brunei"), - ("Asia/Calcutta", "Asia/Calcutta"), - ("Asia/Chita", "Asia/Chita"), - ("Asia/Choibalsan", "Asia/Choibalsan"), - ("Asia/Chongqing", "Asia/Chongqing"), - ("Asia/Chungking", "Asia/Chungking"), - ("Asia/Colombo", "Asia/Colombo"), - ("Asia/Dacca", "Asia/Dacca"), - ("Asia/Damascus", "Asia/Damascus"), - ("Asia/Dhaka", "Asia/Dhaka"), - ("Asia/Dili", "Asia/Dili"), - ("Asia/Dubai", "Asia/Dubai"), - ("Asia/Dushanbe", "Asia/Dushanbe"), - ("Asia/Famagusta", "Asia/Famagusta"), - ("Asia/Gaza", "Asia/Gaza"), - ("Asia/Harbin", "Asia/Harbin"), - ("Asia/Hebron", "Asia/Hebron"), - ("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"), - ("Asia/Hong_Kong", "Asia/Hong_Kong"), - ("Asia/Hovd", "Asia/Hovd"), - ("Asia/Irkutsk", "Asia/Irkutsk"), - ("Asia/Istanbul", "Asia/Istanbul"), - ("Asia/Jakarta", "Asia/Jakarta"), - ("Asia/Jayapura", "Asia/Jayapura"), - ("Asia/Jerusalem", "Asia/Jerusalem"), - ("Asia/Kabul", "Asia/Kabul"), - ("Asia/Kamchatka", "Asia/Kamchatka"), - ("Asia/Karachi", "Asia/Karachi"), - ("Asia/Kashgar", "Asia/Kashgar"), - ("Asia/Kathmandu", "Asia/Kathmandu"), - ("Asia/Katmandu", "Asia/Katmandu"), - ("Asia/Khandyga", "Asia/Khandyga"), - ("Asia/Kolkata", "Asia/Kolkata"), - ("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"), - ("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"), - ("Asia/Kuching", "Asia/Kuching"), - ("Asia/Kuwait", "Asia/Kuwait"), - ("Asia/Macao", "Asia/Macao"), - ("Asia/Macau", "Asia/Macau"), - ("Asia/Magadan", "Asia/Magadan"), - ("Asia/Makassar", "Asia/Makassar"), - ("Asia/Manila", "Asia/Manila"), - ("Asia/Muscat", "Asia/Muscat"), - ("Asia/Nicosia", "Asia/Nicosia"), - ("Asia/Novokuznetsk", "Asia/Novokuznetsk"), - ("Asia/Novosibirsk", "Asia/Novosibirsk"), - ("Asia/Omsk", "Asia/Omsk"), - ("Asia/Oral", "Asia/Oral"), - ("Asia/Phnom_Penh", "Asia/Phnom_Penh"), - ("Asia/Pontianak", "Asia/Pontianak"), - ("Asia/Pyongyang", "Asia/Pyongyang"), - ("Asia/Qatar", "Asia/Qatar"), - ("Asia/Qostanay", "Asia/Qostanay"), - ("Asia/Qyzylorda", "Asia/Qyzylorda"), - ("Asia/Rangoon", "Asia/Rangoon"), - ("Asia/Riyadh", "Asia/Riyadh"), - ("Asia/Saigon", "Asia/Saigon"), - ("Asia/Sakhalin", "Asia/Sakhalin"), - ("Asia/Samarkand", "Asia/Samarkand"), - ("Asia/Seoul", "Asia/Seoul"), - ("Asia/Shanghai", "Asia/Shanghai"), - ("Asia/Singapore", "Asia/Singapore"), - ("Asia/Srednekolymsk", "Asia/Srednekolymsk"), - ("Asia/Taipei", "Asia/Taipei"), - ("Asia/Tashkent", "Asia/Tashkent"), - ("Asia/Tbilisi", "Asia/Tbilisi"), - ("Asia/Tehran", "Asia/Tehran"), - ("Asia/Tel_Aviv", "Asia/Tel_Aviv"), - ("Asia/Thimbu", "Asia/Thimbu"), - ("Asia/Thimphu", "Asia/Thimphu"), - ("Asia/Tokyo", "Asia/Tokyo"), - ("Asia/Tomsk", "Asia/Tomsk"), - ("Asia/Ujung_Pandang", "Asia/Ujung_Pandang"), - ("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"), - ("Asia/Ulan_Bator", "Asia/Ulan_Bator"), - ("Asia/Urumqi", "Asia/Urumqi"), - ("Asia/Ust-Nera", "Asia/Ust-Nera"), - ("Asia/Vientiane", "Asia/Vientiane"), - ("Asia/Vladivostok", "Asia/Vladivostok"), - ("Asia/Yakutsk", "Asia/Yakutsk"), - ("Asia/Yangon", "Asia/Yangon"), - ("Asia/Yekaterinburg", "Asia/Yekaterinburg"), - ("Asia/Yerevan", "Asia/Yerevan"), - ("Atlantic/Azores", "Atlantic/Azores"), - ("Atlantic/Bermuda", "Atlantic/Bermuda"), - ("Atlantic/Canary", "Atlantic/Canary"), - ("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"), - ("Atlantic/Faeroe", "Atlantic/Faeroe"), - ("Atlantic/Faroe", "Atlantic/Faroe"), - ("Atlantic/Jan_Mayen", "Atlantic/Jan_Mayen"), - ("Atlantic/Madeira", "Atlantic/Madeira"), - ("Atlantic/Reykjavik", "Atlantic/Reykjavik"), - ("Atlantic/South_Georgia", "Atlantic/South_Georgia"), - ("Atlantic/St_Helena", "Atlantic/St_Helena"), - ("Atlantic/Stanley", "Atlantic/Stanley"), - ("Australia/ACT", "Australia/ACT"), - ("Australia/Adelaide", "Australia/Adelaide"), - ("Australia/Brisbane", "Australia/Brisbane"), - ("Australia/Broken_Hill", "Australia/Broken_Hill"), - ("Australia/Canberra", "Australia/Canberra"), - ("Australia/Currie", "Australia/Currie"), - ("Australia/Darwin", "Australia/Darwin"), - ("Australia/Eucla", "Australia/Eucla"), - ("Australia/Hobart", "Australia/Hobart"), - ("Australia/LHI", "Australia/LHI"), - ("Australia/Lindeman", "Australia/Lindeman"), - ("Australia/Lord_Howe", "Australia/Lord_Howe"), - ("Australia/Melbourne", "Australia/Melbourne"), - ("Australia/NSW", "Australia/NSW"), - ("Australia/North", "Australia/North"), - ("Australia/Perth", "Australia/Perth"), - ("Australia/Queensland", "Australia/Queensland"), - ("Australia/South", "Australia/South"), - ("Australia/Sydney", "Australia/Sydney"), - ("Australia/Tasmania", "Australia/Tasmania"), - ("Australia/Victoria", "Australia/Victoria"), - ("Australia/West", "Australia/West"), - ("Australia/Yancowinna", "Australia/Yancowinna"), - ("Brazil/Acre", "Brazil/Acre"), - ("Brazil/DeNoronha", "Brazil/DeNoronha"), - ("Brazil/East", "Brazil/East"), - ("Brazil/West", "Brazil/West"), - ("CET", "CET"), - ("CST6CDT", "CST6CDT"), - ("Canada/Atlantic", "Canada/Atlantic"), - ("Canada/Central", "Canada/Central"), - ("Canada/Eastern", "Canada/Eastern"), - ("Canada/Mountain", "Canada/Mountain"), - ("Canada/Newfoundland", "Canada/Newfoundland"), - ("Canada/Pacific", "Canada/Pacific"), - ("Canada/Saskatchewan", "Canada/Saskatchewan"), - ("Canada/Yukon", "Canada/Yukon"), - ("Chile/Continental", "Chile/Continental"), - ("Chile/EasterIsland", "Chile/EasterIsland"), - ("Cuba", "Cuba"), - ("EET", "EET"), - ("EST", "EST"), - ("EST5EDT", "EST5EDT"), - ("Egypt", "Egypt"), - ("Eire", "Eire"), - ("Etc/GMT", "Etc/GMT"), - ("Etc/GMT+0", "Etc/GMT+0"), - ("Etc/GMT+1", "Etc/GMT+1"), - ("Etc/GMT+10", "Etc/GMT+10"), - ("Etc/GMT+11", "Etc/GMT+11"), - ("Etc/GMT+12", "Etc/GMT+12"), - ("Etc/GMT+2", "Etc/GMT+2"), - ("Etc/GMT+3", "Etc/GMT+3"), - ("Etc/GMT+4", "Etc/GMT+4"), - ("Etc/GMT+5", "Etc/GMT+5"), - ("Etc/GMT+6", "Etc/GMT+6"), - ("Etc/GMT+7", "Etc/GMT+7"), - ("Etc/GMT+8", "Etc/GMT+8"), - ("Etc/GMT+9", "Etc/GMT+9"), - ("Etc/GMT-0", "Etc/GMT-0"), - ("Etc/GMT-1", "Etc/GMT-1"), - ("Etc/GMT-10", "Etc/GMT-10"), - ("Etc/GMT-11", "Etc/GMT-11"), - ("Etc/GMT-12", "Etc/GMT-12"), - ("Etc/GMT-13", "Etc/GMT-13"), - ("Etc/GMT-14", "Etc/GMT-14"), - ("Etc/GMT-2", "Etc/GMT-2"), - ("Etc/GMT-3", "Etc/GMT-3"), - ("Etc/GMT-4", "Etc/GMT-4"), - ("Etc/GMT-5", "Etc/GMT-5"), - ("Etc/GMT-6", "Etc/GMT-6"), - ("Etc/GMT-7", "Etc/GMT-7"), - ("Etc/GMT-8", "Etc/GMT-8"), - ("Etc/GMT-9", "Etc/GMT-9"), - ("Etc/GMT0", "Etc/GMT0"), - ("Etc/Greenwich", "Etc/Greenwich"), - ("Etc/UCT", "Etc/UCT"), - ("Etc/UTC", "Etc/UTC"), - ("Etc/Universal", "Etc/Universal"), - ("Etc/Zulu", "Etc/Zulu"), - ("Europe/Amsterdam", "Europe/Amsterdam"), - ("Europe/Andorra", "Europe/Andorra"), - ("Europe/Astrakhan", "Europe/Astrakhan"), - ("Europe/Athens", "Europe/Athens"), - ("Europe/Belfast", "Europe/Belfast"), - ("Europe/Belgrade", "Europe/Belgrade"), - ("Europe/Berlin", "Europe/Berlin"), - ("Europe/Bratislava", "Europe/Bratislava"), - ("Europe/Brussels", "Europe/Brussels"), - ("Europe/Bucharest", "Europe/Bucharest"), - ("Europe/Budapest", "Europe/Budapest"), - ("Europe/Busingen", "Europe/Busingen"), - ("Europe/Chisinau", "Europe/Chisinau"), - ("Europe/Copenhagen", "Europe/Copenhagen"), - ("Europe/Dublin", "Europe/Dublin"), - ("Europe/Gibraltar", "Europe/Gibraltar"), - ("Europe/Guernsey", "Europe/Guernsey"), - ("Europe/Helsinki", "Europe/Helsinki"), - ("Europe/Isle_of_Man", "Europe/Isle_of_Man"), - ("Europe/Istanbul", "Europe/Istanbul"), - ("Europe/Jersey", "Europe/Jersey"), - ("Europe/Kaliningrad", "Europe/Kaliningrad"), - ("Europe/Kiev", "Europe/Kiev"), - ("Europe/Kirov", "Europe/Kirov"), - ("Europe/Kyiv", "Europe/Kyiv"), - ("Europe/Lisbon", "Europe/Lisbon"), - ("Europe/Ljubljana", "Europe/Ljubljana"), - ("Europe/London", "Europe/London"), - ("Europe/Luxembourg", "Europe/Luxembourg"), - ("Europe/Madrid", "Europe/Madrid"), - ("Europe/Malta", "Europe/Malta"), - ("Europe/Mariehamn", "Europe/Mariehamn"), - ("Europe/Minsk", "Europe/Minsk"), - ("Europe/Monaco", "Europe/Monaco"), - ("Europe/Moscow", "Europe/Moscow"), - ("Europe/Nicosia", "Europe/Nicosia"), - ("Europe/Oslo", "Europe/Oslo"), - ("Europe/Paris", "Europe/Paris"), - ("Europe/Podgorica", "Europe/Podgorica"), - ("Europe/Prague", "Europe/Prague"), - ("Europe/Riga", "Europe/Riga"), - ("Europe/Rome", "Europe/Rome"), - ("Europe/Samara", "Europe/Samara"), - ("Europe/San_Marino", "Europe/San_Marino"), - ("Europe/Sarajevo", "Europe/Sarajevo"), - ("Europe/Saratov", "Europe/Saratov"), - ("Europe/Simferopol", "Europe/Simferopol"), - ("Europe/Skopje", "Europe/Skopje"), - ("Europe/Sofia", "Europe/Sofia"), - ("Europe/Stockholm", "Europe/Stockholm"), - ("Europe/Tallinn", "Europe/Tallinn"), - ("Europe/Tirane", "Europe/Tirane"), - ("Europe/Tiraspol", "Europe/Tiraspol"), - ("Europe/Ulyanovsk", "Europe/Ulyanovsk"), - ("Europe/Uzhgorod", "Europe/Uzhgorod"), - ("Europe/Vaduz", "Europe/Vaduz"), - ("Europe/Vatican", "Europe/Vatican"), - ("Europe/Vienna", "Europe/Vienna"), - ("Europe/Vilnius", "Europe/Vilnius"), - ("Europe/Volgograd", "Europe/Volgograd"), - ("Europe/Warsaw", "Europe/Warsaw"), - ("Europe/Zagreb", "Europe/Zagreb"), - ("Europe/Zaporozhye", "Europe/Zaporozhye"), - ("Europe/Zurich", "Europe/Zurich"), - ("GB", "GB"), - ("GB-Eire", "GB-Eire"), - ("GMT", "GMT"), - ("GMT+0", "GMT+0"), - ("GMT-0", "GMT-0"), - ("GMT0", "GMT0"), - ("Greenwich", "Greenwich"), - ("HST", "HST"), - ("Hongkong", "Hongkong"), - ("Iceland", "Iceland"), - ("Indian/Antananarivo", "Indian/Antananarivo"), - ("Indian/Chagos", "Indian/Chagos"), - ("Indian/Christmas", "Indian/Christmas"), - ("Indian/Cocos", "Indian/Cocos"), - ("Indian/Comoro", "Indian/Comoro"), - ("Indian/Kerguelen", "Indian/Kerguelen"), - ("Indian/Mahe", "Indian/Mahe"), - ("Indian/Maldives", "Indian/Maldives"), - ("Indian/Mauritius", "Indian/Mauritius"), - ("Indian/Mayotte", "Indian/Mayotte"), - ("Indian/Reunion", "Indian/Reunion"), - ("Iran", "Iran"), - ("Israel", "Israel"), - ("Jamaica", "Jamaica"), - ("Japan", "Japan"), - ("Kwajalein", "Kwajalein"), - ("Libya", "Libya"), - ("MET", "MET"), - ("MST", "MST"), - ("MST7MDT", "MST7MDT"), - ("Mexico/BajaNorte", "Mexico/BajaNorte"), - ("Mexico/BajaSur", "Mexico/BajaSur"), - ("Mexico/General", "Mexico/General"), - ("NZ", "NZ"), - ("NZ-CHAT", "NZ-CHAT"), - ("Navajo", "Navajo"), - ("PRC", "PRC"), - ("PST8PDT", "PST8PDT"), - ("Pacific/Apia", "Pacific/Apia"), - ("Pacific/Auckland", "Pacific/Auckland"), - ("Pacific/Bougainville", "Pacific/Bougainville"), - ("Pacific/Chatham", "Pacific/Chatham"), - ("Pacific/Chuuk", "Pacific/Chuuk"), - ("Pacific/Easter", "Pacific/Easter"), - ("Pacific/Efate", "Pacific/Efate"), - ("Pacific/Enderbury", "Pacific/Enderbury"), - ("Pacific/Fakaofo", "Pacific/Fakaofo"), - ("Pacific/Fiji", "Pacific/Fiji"), - ("Pacific/Funafuti", "Pacific/Funafuti"), - ("Pacific/Galapagos", "Pacific/Galapagos"), - ("Pacific/Gambier", "Pacific/Gambier"), - ("Pacific/Guadalcanal", "Pacific/Guadalcanal"), - ("Pacific/Guam", "Pacific/Guam"), - ("Pacific/Honolulu", "Pacific/Honolulu"), - ("Pacific/Johnston", "Pacific/Johnston"), - ("Pacific/Kanton", "Pacific/Kanton"), - ("Pacific/Kiritimati", "Pacific/Kiritimati"), - ("Pacific/Kosrae", "Pacific/Kosrae"), - ("Pacific/Kwajalein", "Pacific/Kwajalein"), - ("Pacific/Majuro", "Pacific/Majuro"), - ("Pacific/Marquesas", "Pacific/Marquesas"), - ("Pacific/Midway", "Pacific/Midway"), - ("Pacific/Nauru", "Pacific/Nauru"), - ("Pacific/Niue", "Pacific/Niue"), - ("Pacific/Norfolk", "Pacific/Norfolk"), - ("Pacific/Noumea", "Pacific/Noumea"), - ("Pacific/Pago_Pago", "Pacific/Pago_Pago"), - ("Pacific/Palau", "Pacific/Palau"), - ("Pacific/Pitcairn", "Pacific/Pitcairn"), - ("Pacific/Pohnpei", "Pacific/Pohnpei"), - ("Pacific/Ponape", "Pacific/Ponape"), - ("Pacific/Port_Moresby", "Pacific/Port_Moresby"), - ("Pacific/Rarotonga", "Pacific/Rarotonga"), - ("Pacific/Saipan", "Pacific/Saipan"), - ("Pacific/Samoa", "Pacific/Samoa"), - ("Pacific/Tahiti", "Pacific/Tahiti"), - ("Pacific/Tarawa", "Pacific/Tarawa"), - ("Pacific/Tongatapu", "Pacific/Tongatapu"), - ("Pacific/Truk", "Pacific/Truk"), - ("Pacific/Wake", "Pacific/Wake"), - ("Pacific/Wallis", "Pacific/Wallis"), - ("Pacific/Yap", "Pacific/Yap"), - ("Poland", "Poland"), - ("Portugal", "Portugal"), - ("ROC", "ROC"), - ("ROK", "ROK"), - ("Singapore", "Singapore"), - ("Turkey", "Turkey"), - ("UCT", "UCT"), - ("US/Alaska", "US/Alaska"), - ("US/Aleutian", "US/Aleutian"), - ("US/Arizona", "US/Arizona"), - ("US/Central", "US/Central"), - ("US/East-Indiana", "US/East-Indiana"), - ("US/Eastern", "US/Eastern"), - ("US/Hawaii", "US/Hawaii"), - ("US/Indiana-Starke", "US/Indiana-Starke"), - ("US/Michigan", "US/Michigan"), - ("US/Mountain", "US/Mountain"), - ("US/Pacific", "US/Pacific"), - ("US/Samoa", "US/Samoa"), - ("UTC", "UTC"), - ("Universal", "Universal"), - ("W-SU", "W-SU"), - ("WET", "WET"), - ("Zulu", "Zulu"), - ], - default="UTC", - max_length=255, - ), - ), - migrations.AlterField( - model_name="cycle", - name="end_date", - field=models.DateTimeField( - blank=True, null=True, verbose_name="End Date" - ), - ), - migrations.AlterField( - model_name="cycle", - name="start_date", - field=models.DateTimeField( - blank=True, null=True, verbose_name="Start Date" - ), - ), - migrations.CreateModel( - name="DraftIssueModule", - 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" - ), - ), - ( - "deleted_at", - models.DateTimeField( - blank=True, null=True, verbose_name="Deleted At" - ), - ), - ( - "id", - models.UUIDField( - db_index=True, - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ( - "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", - ), - ), - ( - "draft_issue", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="draft_issue_module", - to="db.draftissue", - ), - ), - ( - "module", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="draft_issue_module", - to="db.module", - ), - ), - ( - "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": "Draft Issue Module", - "verbose_name_plural": "Draft Issue Modules", - "db_table": "draft_issue_modules", - "ordering": ("-created_at",), - }, - ), - migrations.CreateModel( - name="DraftIssueLabel", - 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" - ), - ), - ( - "deleted_at", - models.DateTimeField( - blank=True, null=True, verbose_name="Deleted At" - ), - ), - ( - "id", - models.UUIDField( - db_index=True, - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ( - "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", - ), - ), - ( - "draft_issue", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="draft_label_issue", - to="db.draftissue", - ), - ), - ( - "label", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="draft_label_issue", - to="db.label", - ), - ), - ( - "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": "Draft Issue Label", - "verbose_name_plural": "Draft Issue Labels", - "db_table": "draft_issue_labels", - "ordering": ("-created_at",), - }, - ), - migrations.CreateModel( - name="DraftIssueCycle", - 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" - ), - ), - ( - "deleted_at", - models.DateTimeField( - blank=True, null=True, verbose_name="Deleted At" - ), - ), - ( - "id", - models.UUIDField( - db_index=True, - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ( - "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", - ), - ), - ( - "cycle", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="draft_issue_cycle", - to="db.cycle", - ), - ), - ( - "draft_issue", - models.OneToOneField( - on_delete=django.db.models.deletion.CASCADE, - related_name="draft_issue_cycle", - to="db.draftissue", - ), - ), - ( - "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": "Draft Issue Cycle", - "verbose_name_plural": "Draft Issue Cycles", - "db_table": "draft_issue_cycles", - "ordering": ("-created_at",), - }, - ), - migrations.CreateModel( - name="DraftIssueAssignee", - 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" - ), - ), - ( - "deleted_at", - models.DateTimeField( - blank=True, null=True, verbose_name="Deleted At" - ), - ), - ( - "id", - models.UUIDField( - db_index=True, - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ( - "assignee", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="draft_issue_assignee", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "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", - ), - ), - ( - "draft_issue", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="draft_issue_assignee", - to="db.draftissue", - ), - ), - ( - "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": "Draft Issue Assignee", - "verbose_name_plural": "Draft Issue Assignees", - "db_table": "draft_issue_assignees", - "ordering": ("-created_at",), - }, - ), - migrations.AddField( - model_name="draftissue", - name="assignees", - field=models.ManyToManyField( - blank=True, - related_name="draft_assignee", - through="db.DraftIssueAssignee", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="draftissue", - name="created_by", - field=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", - ), - ), - migrations.AddField( - model_name="draftissue", - name="estimate_point", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="draft_issue_estimates", - to="db.estimatepoint", - ), - ), - migrations.AddField( - model_name="draftissue", - name="labels", - field=models.ManyToManyField( - blank=True, - related_name="draft_labels", - through="db.DraftIssueLabel", - to="db.label", - ), - ), - migrations.AddField( - model_name="draftissue", - name="parent", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="draft_parent_issue", - to="db.issue", - ), - ), - migrations.AddField( - model_name="draftissue", - name="project", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="project_%(class)s", - to="db.project", - ), - ), - migrations.AddField( - model_name="draftissue", - name="state", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="state_draft_issue", - to="db.state", - ), - ), - migrations.AddField( - model_name="draftissue", - name="type", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="draft_issue_type", - to="db.issuetype", - ), - ), - migrations.AddField( - model_name="draftissue", - name="updated_by", - field=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", - ), - ), - migrations.AddField( - model_name="draftissue", - name="workspace", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="workspace_%(class)s", - to="db.workspace", - ), - ), - migrations.AddConstraint( - model_name="draftissuemodule", - constraint=models.UniqueConstraint( - condition=models.Q(("deleted_at__isnull", True)), - fields=("draft_issue", "module"), - name="module_draft_issue_unique_issue_module_when_deleted_at_null", - ), - ), - migrations.AlterUniqueTogether( - name="draftissuemodule", - unique_together={("draft_issue", "module", "deleted_at")}, - ), - migrations.AddConstraint( - model_name="draftissueassignee", - constraint=models.UniqueConstraint( - condition=models.Q(("deleted_at__isnull", True)), - fields=("draft_issue", "assignee"), - name="draft_issue_assignee_unique_issue_assignee_when_deleted_at_null", - ), - ), - migrations.AlterUniqueTogether( - name="draftissueassignee", - unique_together={("draft_issue", "assignee", "deleted_at")}, - ), - migrations.AddField( - model_name="cycle", - name="version", - field=models.IntegerField(default=1), - ), - migrations.RunPython(migrate_draft_issues), - ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index a6fa6dddb76..e7def641d5c 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -5,7 +5,6 @@ from .cycle import Cycle, CycleFavorite, CycleIssue, CycleUserProperties from .dashboard import Dashboard, DashboardWidget, Widget from .deploy_board import DeployBoard -from .draft import DraftIssue, DraftIssueAssignee, DraftIssueLabel, DraftIssueModule, DraftIssueCycle from .estimate import Estimate, EstimatePoint from .exporter import ExporterHistory from .importer import Importer diff --git a/apiserver/plane/db/models/cycle.py b/apiserver/plane/db/models/cycle.py index 7c6ac8e3935..b3ce49e01a9 100644 --- a/apiserver/plane/db/models/cycle.py +++ b/apiserver/plane/db/models/cycle.py @@ -1,6 +1,3 @@ -# Python imports -import pytz - # Django imports from django.conf import settings from django.db import models @@ -58,12 +55,10 @@ class Cycle(ProjectBaseModel): description = models.TextField( verbose_name="Cycle Description", blank=True ) - start_date = models.DateTimeField( + start_date = models.DateField( verbose_name="Start Date", blank=True, null=True ) - end_date = models.DateTimeField( - verbose_name="End Date", blank=True, null=True - ) + end_date = models.DateField(verbose_name="End Date", blank=True, null=True) owned_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, @@ -76,12 +71,6 @@ class Cycle(ProjectBaseModel): progress_snapshot = models.JSONField(default=dict) archived_at = models.DateTimeField(null=True) logo_props = models.JSONField(default=dict) - # timezone - TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) - timezone = models.CharField( - max_length=255, default="UTC", choices=TIMEZONE_CHOICES - ) - version = models.IntegerField(default=1) class Meta: verbose_name = "Cycle" diff --git a/apiserver/plane/db/models/draft.py b/apiserver/plane/db/models/draft.py deleted file mode 100644 index 671b89ff1fa..00000000000 --- a/apiserver/plane/db/models/draft.py +++ /dev/null @@ -1,253 +0,0 @@ -# Django imports -from django.conf import settings -from django.db import models -from django.utils import timezone - -# Module imports -from plane.utils.html_processor import strip_tags - -from .workspace import WorkspaceBaseModel - - -class DraftIssue(WorkspaceBaseModel): - PRIORITY_CHOICES = ( - ("urgent", "Urgent"), - ("high", "High"), - ("medium", "Medium"), - ("low", "Low"), - ("none", "None"), - ) - parent = models.ForeignKey( - "db.Issue", - on_delete=models.CASCADE, - null=True, - blank=True, - related_name="draft_parent_issue", - ) - state = models.ForeignKey( - "db.State", - on_delete=models.CASCADE, - null=True, - blank=True, - related_name="state_draft_issue", - ) - estimate_point = models.ForeignKey( - "db.EstimatePoint", - on_delete=models.SET_NULL, - related_name="draft_issue_estimates", - null=True, - blank=True, - ) - name = models.CharField( - max_length=255, verbose_name="Issue Name", blank=True, null=True - ) - description = models.JSONField(blank=True, default=dict) - description_html = models.TextField(blank=True, default="

") - description_stripped = models.TextField(blank=True, null=True) - description_binary = models.BinaryField(null=True) - priority = models.CharField( - max_length=30, - choices=PRIORITY_CHOICES, - verbose_name="Issue Priority", - default="none", - ) - start_date = models.DateField(null=True, blank=True) - target_date = models.DateField(null=True, blank=True) - assignees = models.ManyToManyField( - settings.AUTH_USER_MODEL, - blank=True, - related_name="draft_assignee", - through="DraftIssueAssignee", - through_fields=("draft_issue", "assignee"), - ) - labels = models.ManyToManyField( - "db.Label", - blank=True, - related_name="draft_labels", - through="DraftIssueLabel", - ) - sort_order = models.FloatField(default=65535) - completed_at = models.DateTimeField(null=True) - external_source = models.CharField(max_length=255, null=True, blank=True) - external_id = models.CharField(max_length=255, blank=True, null=True) - type = models.ForeignKey( - "db.IssueType", - on_delete=models.SET_NULL, - related_name="draft_issue_type", - null=True, - blank=True, - ) - - class Meta: - verbose_name = "DraftIssue" - verbose_name_plural = "DraftIssues" - db_table = "draft_issues" - ordering = ("-created_at",) - - def save(self, *args, **kwargs): - if self.state is None: - try: - from plane.db.models import State - - default_state = State.objects.filter( - ~models.Q(is_triage=True), - project=self.project, - default=True, - ).first() - if default_state is None: - random_state = State.objects.filter( - ~models.Q(is_triage=True), project=self.project - ).first() - self.state = random_state - else: - self.state = default_state - except ImportError: - pass - else: - try: - from plane.db.models import State - - if self.state.group == "completed": - self.completed_at = timezone.now() - else: - self.completed_at = None - except ImportError: - pass - - if self._state.adding: - # Strip the html tags using html parser - self.description_stripped = ( - None - if ( - self.description_html == "" - or self.description_html is None - ) - else strip_tags(self.description_html) - ) - largest_sort_order = DraftIssue.objects.filter( - project=self.project, state=self.state - ).aggregate(largest=models.Max("sort_order"))["largest"] - if largest_sort_order is not None: - self.sort_order = largest_sort_order + 10000 - - super(DraftIssue, self).save(*args, **kwargs) - - else: - # Strip the html tags using html parser - self.description_stripped = ( - None - if ( - self.description_html == "" - or self.description_html is None - ) - else strip_tags(self.description_html) - ) - super(DraftIssue, self).save(*args, **kwargs) - - def __str__(self): - """Return name of the draft issue""" - return f"{self.name} <{self.project.name}>" - - -class DraftIssueAssignee(WorkspaceBaseModel): - draft_issue = models.ForeignKey( - DraftIssue, - on_delete=models.CASCADE, - related_name="draft_issue_assignee", - ) - assignee = models.ForeignKey( - settings.AUTH_USER_MODEL, - on_delete=models.CASCADE, - related_name="draft_issue_assignee", - ) - - class Meta: - unique_together = ["draft_issue", "assignee", "deleted_at"] - constraints = [ - models.UniqueConstraint( - fields=["draft_issue", "assignee"], - condition=models.Q(deleted_at__isnull=True), - name="draft_issue_assignee_unique_issue_assignee_when_deleted_at_null", - ) - ] - verbose_name = "Draft Issue Assignee" - verbose_name_plural = "Draft Issue Assignees" - db_table = "draft_issue_assignees" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.draft_issue.name} {self.assignee.email}" - - -class DraftIssueLabel(WorkspaceBaseModel): - draft_issue = models.ForeignKey( - "db.DraftIssue", - on_delete=models.CASCADE, - related_name="draft_label_issue", - ) - label = models.ForeignKey( - "db.Label", on_delete=models.CASCADE, related_name="draft_label_issue" - ) - - class Meta: - verbose_name = "Draft Issue Label" - verbose_name_plural = "Draft Issue Labels" - db_table = "draft_issue_labels" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.draft_issue.name} {self.label.name}" - - -class DraftIssueModule(WorkspaceBaseModel): - module = models.ForeignKey( - "db.Module", - on_delete=models.CASCADE, - related_name="draft_issue_module", - ) - draft_issue = models.ForeignKey( - "db.DraftIssue", - on_delete=models.CASCADE, - related_name="draft_issue_module", - ) - - class Meta: - unique_together = ["draft_issue", "module", "deleted_at"] - constraints = [ - models.UniqueConstraint( - fields=["draft_issue", "module"], - condition=models.Q(deleted_at__isnull=True), - name="module_draft_issue_unique_issue_module_when_deleted_at_null", - ) - ] - verbose_name = "Draft Issue Module" - verbose_name_plural = "Draft Issue Modules" - db_table = "draft_issue_modules" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.module.name} {self.draft_issue.name}" - - -class DraftIssueCycle(WorkspaceBaseModel): - """ - Draft Issue Cycles - """ - - draft_issue = models.OneToOneField( - "db.DraftIssue", - on_delete=models.CASCADE, - related_name="draft_issue_cycle", - ) - cycle = models.ForeignKey( - "db.Cycle", on_delete=models.CASCADE, related_name="draft_issue_cycle" - ) - - class Meta: - verbose_name = "Draft Issue Cycle" - verbose_name_plural = "Draft Issue Cycles" - db_table = "draft_issue_cycles" - ordering = ("-created_at",) - - def __str__(self): - return f"{self.cycle}" diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index 3f784b399ec..bcc16822739 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -1,5 +1,4 @@ # Python imports -import pytz from uuid import uuid4 # Django imports @@ -8,7 +7,7 @@ from django.db import models from django.db.models import Q -# Module imports +# Modeule imports from plane.db.mixins import AuditModel # Module imports @@ -120,11 +119,6 @@ class Project(BaseModel): related_name="default_state", ) archived_at = models.DateTimeField(null=True) - # timezone - TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones)) - timezone = models.CharField( - max_length=255, default="UTC", choices=TIMEZONE_CHOICES - ) def __str__(self): """Return name of the project""" diff --git a/apiserver/plane/utils/analytics_plot.py b/apiserver/plane/utils/analytics_plot.py index 1770e95ae95..eda3b30ac9c 100644 --- a/apiserver/plane/utils/analytics_plot.py +++ b/apiserver/plane/utils/analytics_plot.py @@ -163,7 +163,7 @@ def burndown_plot( if queryset.end_date and queryset.start_date: # Get all dates between the two dates date_range = [ - (queryset.start_date + timedelta(days=x)).date() + queryset.start_date + timedelta(days=x) for x in range( (queryset.end_date - queryset.start_date).days + 1 ) @@ -203,7 +203,7 @@ def burndown_plot( if module_id: # Get all dates between the two dates date_range = [ - (queryset.start_date + timedelta(days=x)) + queryset.start_date + timedelta(days=x) for x in range( (queryset.target_date - queryset.start_date).days + 1 ) diff --git a/apiserver/plane/utils/paginator.py b/apiserver/plane/utils/paginator.py index 219b646b25b..65f0aa7f746 100644 --- a/apiserver/plane/utils/paginator.py +++ b/apiserver/plane/utils/paginator.py @@ -150,7 +150,7 @@ def get_result(self, limit=1000, cursor=None): raise BadPaginationError("Pagination offset cannot be negative") results = queryset[offset:stop] - print(limit, "limit") + if cursor.value != limit: results = results[-(limit + 1) :] @@ -761,7 +761,7 @@ def paginate( ): """Paginate the request""" per_page = self.get_per_page(request, default_per_page, max_per_page) - print(per_page, "per_page") + # Convert the cursor value to integer and float from string input_cursor = None try: @@ -788,7 +788,6 @@ def paginate( paginator = paginator_cls(**paginator_kwargs) try: - print(per_page, "per_page 2") cursor_result = paginator.get_result( limit=per_page, cursor=input_cursor ) diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index fbe6680d43f..8b96cb99798 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -1,7 +1,7 @@ # base requirements # django -Django==4.2.16 +Django==4.2.15 # rest framework djangorestframework==3.15.2 # postgres diff --git a/live/package.json b/live/package.json index 07d8a053fde..9344475bb29 100644 --- a/live/package.json +++ b/live/package.json @@ -1,6 +1,6 @@ { "name": "live", - "version": "0.23.1", + "version": "0.23.0", "description": "", "main": "./src/server.ts", "private": true, diff --git a/live/src/ce/lib/authentication.ts b/live/src/ce/lib/authentication.ts new file mode 100644 index 00000000000..3d5a1ea48e5 --- /dev/null +++ b/live/src/ce/lib/authentication.ts @@ -0,0 +1,15 @@ +import { ConnectionConfiguration } from "@hocuspocus/server"; +// types +import { TDocumentTypes } from "@/core/types/common.js"; + +type TArgs = { + connection: ConnectionConfiguration + cookie: string; + documentType: TDocumentTypes | undefined; + params: URLSearchParams; +} + +export const authenticateUser = async (args: TArgs): Promise => { + const { documentType } = args; + throw Error(`Authentication failed: Invalid document type ${documentType} provided.`); +} \ No newline at end of file diff --git a/live/src/core/hocuspocus-server.ts b/live/src/core/hocuspocus-server.ts index 0aa411b9334..fb30c8f8281 100644 --- a/live/src/core/hocuspocus-server.ts +++ b/live/src/core/hocuspocus-server.ts @@ -12,11 +12,15 @@ export const getHocusPocusServer = async () => { name: serverName, onAuthenticate: async ({ requestHeaders, + requestParameters, + connection, // user id used as token for authentication token, }) => { // request headers const cookie = requestHeaders.cookie?.toString(); + // params + const params = requestParameters; if (!cookie) { throw Error("Credentials not provided"); @@ -24,7 +28,9 @@ export const getHocusPocusServer = async () => { try { await handleAuthentication({ + connection, cookie, + params, token, }); } catch (error) { @@ -32,6 +38,6 @@ export const getHocusPocusServer = async () => { } }, extensions, - debounce: 10000, + debounce: 10000 }); }; diff --git a/live/src/core/lib/authentication.ts b/live/src/core/lib/authentication.ts index ee01b020908..dbde17959ad 100644 --- a/live/src/core/lib/authentication.ts +++ b/live/src/core/lib/authentication.ts @@ -1,17 +1,28 @@ +import { ConnectionConfiguration } from "@hocuspocus/server"; // services import { UserService } from "@/core/services/user.service.js"; +// types +import { TDocumentTypes } from "@/core/types/common.js"; +// plane live lib +import { authenticateUser } from "@/plane-live/lib/authentication.js"; // core helpers import { manualLogger } from "@/core/helpers/logger.js"; const userService = new UserService(); type Props = { + connection: ConnectionConfiguration; cookie: string; + params: URLSearchParams; token: string; }; export const handleAuthentication = async (props: Props) => { - const { cookie, token } = props; + const { connection, cookie, params, token } = props; + // params + const documentType = params.get("documentType")?.toString() as + | TDocumentTypes + | undefined; // fetch current user info let response; try { @@ -24,6 +35,40 @@ export const handleAuthentication = async (props: Props) => { throw Error("Authentication failed: Token doesn't match the current user."); } + if (documentType === "project_page") { + // params + const workspaceSlug = params.get("workspaceSlug")?.toString(); + const projectId = params.get("projectId")?.toString(); + if (!workspaceSlug || !projectId) { + throw Error( + "Authentication failed: Incomplete query params. Either workspaceSlug or projectId is missing." + ); + } + // fetch current user's project membership info + try { + const projectMembershipInfo = await userService.getUserProjectMembership( + workspaceSlug, + projectId, + cookie + ); + const projectRole = projectMembershipInfo.role; + // make the connection read only for roles lower than a member + if (projectRole < 15) { + connection.readOnly = true; + } + } catch (error) { + manualLogger.error("Failed to fetch project membership info:", error); + throw error; + } + } else { + await authenticateUser({ + connection, + cookie, + documentType, + params, + }); + } + return { user: { id: response.id, diff --git a/live/src/core/services/user.service.ts b/live/src/core/services/user.service.ts index 39d200919ac..09412aa532c 100644 --- a/live/src/core/services/user.service.ts +++ b/live/src/core/services/user.service.ts @@ -1,5 +1,5 @@ // types -import type { IUser } from "@plane/types"; +import type { IProjectMember, IUser } from "@plane/types"; // services import { API_BASE_URL, APIService } from "@/core/services/api.service.js"; @@ -25,4 +25,37 @@ export class UserService extends APIService { throw error; }); } + + async getUserWorkspaceMembership( + workspaceSlug: string, + cookie: string + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/workspace-members/me/`, + { + headers: { + Cookie: cookie, + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async getUserProjectMembership( + workspaceSlug: string, + projectId: string, + cookie: string + ): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/project-members/me/`, + { + headers: { + Cookie: cookie, + }, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } } diff --git a/package.json b/package.json index 372fdc937f6..27a5389d2a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "repository": "https://github.com/makeplane/plane.git", - "version": "0.23.1", + "version": "0.23.0", "license": "AGPL-3.0", "private": true, "workspaces": [ diff --git a/packages/constants/package.json b/packages/constants/package.json index cdf51bbaf13..55306a6efda 100644 --- a/packages/constants/package.json +++ b/packages/constants/package.json @@ -1,6 +1,6 @@ { "name": "@plane/constants", - "version": "0.23.1", + "version": "0.23.0", "private": true, "main": "./index.ts" } diff --git a/packages/editor/package.json b/packages/editor/package.json index 58f99a1e9e3..bfb379f8b60 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@plane/editor", - "version": "0.23.1", + "version": "0.23.0", "description": "Core Editor that powers Plane", "private": true, "main": "./dist/index.mjs", @@ -42,15 +42,13 @@ "@tiptap/extension-blockquote": "^2.1.13", "@tiptap/extension-character-count": "^2.6.5", "@tiptap/extension-collaboration": "^2.3.2", - "@tiptap/extension-color": "^2.7.1", - "@tiptap/extension-highlight": "^2.7.1", "@tiptap/extension-image": "^2.1.13", "@tiptap/extension-list-item": "^2.1.13", "@tiptap/extension-mention": "^2.1.13", "@tiptap/extension-placeholder": "^2.3.0", "@tiptap/extension-task-item": "^2.1.13", "@tiptap/extension-task-list": "^2.1.13", - "@tiptap/extension-text-style": "^2.7.1", + "@tiptap/extension-text-style": "^2.1.13", "@tiptap/extension-underline": "^2.1.13", "@tiptap/pm": "^2.1.13", "@tiptap/react": "^2.1.13", diff --git a/packages/editor/src/ce/extensions/document-extensions.tsx b/packages/editor/src/ce/extensions/document-extensions.tsx index 2809fcee4ef..93900700b27 100644 --- a/packages/editor/src/ce/extensions/document-extensions.tsx +++ b/packages/editor/src/ce/extensions/document-extensions.tsx @@ -1,6 +1,6 @@ import { HocuspocusProvider } from "@hocuspocus/provider"; import { Extensions } from "@tiptap/core"; -import { SlashCommands } from "@/extensions"; +import { SlashCommand } from "@/extensions"; // plane editor types import { TIssueEmbedConfig } from "@/plane-editor/types"; // types @@ -14,7 +14,7 @@ type Props = { }; export const DocumentEditorAdditionalExtensions = (_props: Props) => { - const extensions: Extensions = [SlashCommands()]; + const extensions: Extensions = [SlashCommand()]; return extensions; }; diff --git a/packages/editor/src/core/components/editors/rich-text/editor.tsx b/packages/editor/src/core/components/editors/rich-text/editor.tsx index 53f766ee21a..fe4d2d51373 100644 --- a/packages/editor/src/core/components/editors/rich-text/editor.tsx +++ b/packages/editor/src/core/components/editors/rich-text/editor.tsx @@ -3,7 +3,7 @@ import { forwardRef, useCallback } from "react"; import { EditorWrapper } from "@/components/editors"; import { EditorBubbleMenu } from "@/components/menus"; // extensions -import { SideMenuExtension, SlashCommands } from "@/extensions"; +import { SideMenuExtension, SlashCommand } from "@/extensions"; // types import { EditorRefApi, IRichTextEditor } from "@/types"; @@ -11,7 +11,7 @@ const RichTextEditor = (props: IRichTextEditor) => { const { dragDropEnabled } = props; const getExtensions = useCallback(() => { - const extensions = [SlashCommands()]; + const extensions = [SlashCommand()]; extensions.push( SideMenuExtension({ diff --git a/packages/editor/src/core/components/menus/bubble-menu/color-selector.tsx b/packages/editor/src/core/components/menus/bubble-menu/color-selector.tsx deleted file mode 100644 index cc3eb5412e2..00000000000 --- a/packages/editor/src/core/components/menus/bubble-menu/color-selector.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { Dispatch, FC, SetStateAction } from "react"; -import { Editor } from "@tiptap/react"; -import { ALargeSmall, Ban } from "lucide-react"; -// constants -import { COLORS_LIST } from "@/constants/common"; -// helpers -import { cn } from "@/helpers/common"; -import { BackgroundColorItem, TextColorItem } from "../menu-items"; - -type Props = { - editor: Editor; - isOpen: boolean; - setIsOpen: Dispatch>; -}; - -export const BubbleMenuColorSelector: FC = (props) => { - const { editor, isOpen, setIsOpen } = props; - - const activeTextColor = COLORS_LIST.find((c) => editor.getAttributes("textStyle").color === c.textColor); - const activeBackgroundColor = COLORS_LIST.find((c) => - editor.isActive("highlight", { - color: c.backgroundColor, - }) - ); - - return ( -
- - {isOpen && ( -
-
-

Text colors

-
- {COLORS_LIST.map((color) => ( - -
-
-
-

Background colors

-
- {COLORS_LIST.map((color) => ( - -
-
-
- )} -
- ); -}; diff --git a/packages/editor/src/core/components/menus/bubble-menu/index.ts b/packages/editor/src/core/components/menus/bubble-menu/index.ts index 526feed3d1e..71a98bada08 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/index.ts +++ b/packages/editor/src/core/components/menus/bubble-menu/index.ts @@ -1,4 +1,3 @@ -export * from "./color-selector"; export * from "./link-selector"; export * from "./node-selector"; export * from "./root"; diff --git a/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx b/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx index eaa20ed26bb..20335e8abb6 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/link-selector.tsx @@ -1,6 +1,6 @@ import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react"; import { Editor } from "@tiptap/core"; -import { Check, Link, Trash } from "lucide-react"; +import { Check, Trash } from "lucide-react"; // helpers import { cn, isValidHttpUrl } from "@/helpers/common"; import { setLinkEditor, unsetLinkEditor } from "@/helpers/editor-commands"; @@ -11,9 +11,7 @@ type Props = { setIsOpen: Dispatch>; }; -export const BubbleMenuLinkSelector: FC = (props) => { - const { editor, isOpen, setIsOpen } = props; - // refs +export const BubbleMenuLinkSelector: FC = ({ editor, isOpen, setIsOpen }) => { const inputRef = useRef(null); const onLinkSubmit = useCallback(() => { @@ -30,23 +28,26 @@ export const BubbleMenuLinkSelector: FC = (props) => { }); return ( -
+
{isOpen && (
>; }; -export const BubbleMenuNodeSelector: FC = (props) => { - const { editor, isOpen, setIsOpen } = props; - - const items: EditorMenuItem[] = [ +export const BubbleMenuNodeSelector: FC = ({ editor, isOpen, setIsOpen }) => { + const items: BubbleMenuItem[] = [ TextItem(editor), HeadingOneItem(editor), HeadingTwoItem(editor), @@ -44,7 +42,7 @@ export const BubbleMenuNodeSelector: FC = (props) => { CodeItem(editor), ]; - const activeItem = items.filter((item) => item.isActive("")).pop() ?? { + const activeItem = items.filter((item) => item.isActive()).pop() ?? { name: "Multiple", }; @@ -56,11 +54,12 @@ export const BubbleMenuNodeSelector: FC = (props) => { setIsOpen(!isOpen); e.stopPropagation(); }} - className="flex items-center gap-1 h-full whitespace-nowrap px-3 text-sm font-medium text-custom-text-300 hover:bg-custom-background-80 active:bg-custom-background-80 rounded transition-colors" + className="flex h-full items-center gap-1 whitespace-nowrap p-2 text-sm font-medium text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5" > {activeItem?.name} - + + {isOpen && (
{items.map((item) => ( diff --git a/packages/editor/src/core/components/menus/bubble-menu/root.tsx b/packages/editor/src/core/components/menus/bubble-menu/root.tsx index 0f789dd8a1b..ec72f154086 100644 --- a/packages/editor/src/core/components/menus/bubble-menu/root.tsx +++ b/packages/editor/src/core/components/menus/bubble-menu/root.tsx @@ -1,13 +1,12 @@ import { FC, useEffect, useState } from "react"; import { BubbleMenu, BubbleMenuProps, isNodeSelection } from "@tiptap/react"; +import { LucideIcon } from "lucide-react"; // components import { BoldItem, - BubbleMenuColorSelector, BubbleMenuLinkSelector, BubbleMenuNodeSelector, CodeItem, - EditorMenuItem, ItalicItem, StrikeThroughItem, UnderLineItem, @@ -17,23 +16,34 @@ import { isCellSelection } from "@/extensions/table/table/utilities/is-cell-sele // helpers import { cn } from "@/helpers/common"; +export interface BubbleMenuItem { + key: string; + name: string; + isActive: () => boolean; + command: () => void; + icon: LucideIcon; +} + type EditorBubbleMenuProps = Omit; export const EditorBubbleMenu: FC = (props: any) => { - // states - const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false); - const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); - const [isColorSelectorOpen, setIsColorSelectorOpen] = useState(false); - const [isSelecting, setIsSelecting] = useState(false); - - const items: EditorMenuItem[] = props.editor.isActive("code") - ? [CodeItem(props.editor)] - : [BoldItem(props.editor), ItalicItem(props.editor), UnderLineItem(props.editor), StrikeThroughItem(props.editor)]; + const items: BubbleMenuItem[] = [ + ...(props.editor.isActive("code") + ? [] + : [ + BoldItem(props.editor), + ItalicItem(props.editor), + UnderLineItem(props.editor), + StrikeThroughItem(props.editor), + ]), + CodeItem(props.editor), + ]; const bubbleMenuProps: EditorBubbleMenuProps = { ...props, shouldShow: ({ state, editor }) => { const { selection } = state; + const { empty } = selection; if ( @@ -53,11 +63,15 @@ export const EditorBubbleMenu: FC = (props: any) => { onHidden: () => { setIsNodeSelectorOpen(false); setIsLinkSelectorOpen(false); - setIsColorSelectorOpen(false); }, }, }; + const [isNodeSelectorOpen, setIsNodeSelectorOpen] = useState(false); + const [isLinkSelectorOpen, setIsLinkSelectorOpen] = useState(false); + + const [isSelecting, setIsSelecting] = useState(false); + useEffect(() => { function handleMouseDown() { function handleMouseMove() { @@ -88,66 +102,51 @@ export const EditorBubbleMenu: FC = (props: any) => { return ( - {!isSelecting && ( + {isSelecting ? null : ( <> -
- {!props.editor.isActive("table") && ( - { - setIsNodeSelectorOpen((prev) => !prev); - setIsLinkSelectorOpen(false); - setIsColorSelectorOpen(false); - }} - /> - )} -
-
- {!props.editor.isActive("code") && ( - { - setIsLinkSelectorOpen((prev) => !prev); - setIsNodeSelectorOpen(false); - setIsColorSelectorOpen(false); - }} - /> - )} -
-
- {!props.editor.isActive("code") && ( - { - setIsColorSelectorOpen((prev) => !prev); - setIsNodeSelectorOpen(false); - setIsLinkSelectorOpen(false); - }} - /> - )} -
-
+ {!props.editor.isActive("table") && ( + { + setIsNodeSelectorOpen(!isNodeSelectorOpen); + setIsLinkSelectorOpen(false); + }} + /> + )} + {!props.editor.isActive("code") && ( + { + setIsLinkSelectorOpen(!isLinkSelectorOpen); + setIsNodeSelectorOpen(false); + }} + /> + )} +
{items.map((item) => ( ))}
diff --git a/packages/editor/src/core/components/menus/menu-items.ts b/packages/editor/src/core/components/menus/menu-items.ts index f7082c12de0..cf10081f1e5 100644 --- a/packages/editor/src/core/components/menus/menu-items.ts +++ b/packages/editor/src/core/components/menus/menu-items.ts @@ -20,14 +20,12 @@ import { Heading6, CaseSensitive, LucideIcon, - Palette, } from "lucide-react"; // helpers import { insertImage, insertTableCommand, setText, - toggleBackgroundColor, toggleBlockquote, toggleBold, toggleBulletList, @@ -42,26 +40,18 @@ import { toggleOrderedList, toggleStrike, toggleTaskList, - toggleTextColor, toggleUnderline, } from "@/helpers/editor-commands"; // types -import { TColorEditorCommands, TNonColorEditorCommands } from "@/types"; +import { TEditorCommands } from "@/types"; -export type EditorMenuItem = { +export interface EditorMenuItem { + key: TEditorCommands; name: string; - command: (...args: any) => void; + isActive: () => boolean; + command: () => void; icon: LucideIcon; -} & ( - | { - key: TNonColorEditorCommands; - isActive: () => boolean; - } - | { - key: TColorEditorCommands; - isActive: (color: string | undefined) => boolean; - } -); +} export const TextItem = (editor: Editor): EditorMenuItem => ({ key: "text", @@ -208,25 +198,10 @@ export const ImageItem = (editor: Editor) => icon: ImageIcon, }) as const; -export const TextColorItem = (editor: Editor): EditorMenuItem => ({ - key: "text-color", - name: "Color", - isActive: (color) => editor.getAttributes("textStyle").color === color, - command: (color: string) => toggleTextColor(color, editor), - icon: Palette, -}); - -export const BackgroundColorItem = (editor: Editor): EditorMenuItem => ({ - key: "background-color", - name: "Background color", - isActive: (color) => editor.isActive("highlight", { color }), - command: (color: string) => toggleBackgroundColor(color, editor), - icon: Palette, -}); - -export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem[] => { - if (!editor) return []; - +export function getEditorMenuItems(editor: Editor | null) { + if (!editor) { + return []; + } return [ TextItem(editor), HeadingOneItem(editor), @@ -246,7 +221,5 @@ export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem[] => { QuoteItem(editor), TableItem(editor), ImageItem(editor), - TextColorItem(editor), - BackgroundColorItem(editor), ]; -}; +} diff --git a/packages/editor/src/core/constants/common.ts b/packages/editor/src/core/constants/common.ts deleted file mode 100644 index 4e46fb83769..00000000000 --- a/packages/editor/src/core/constants/common.ts +++ /dev/null @@ -1,51 +0,0 @@ -export const COLORS_LIST: { - backgroundColor: string; - textColor: string; - label: string; -}[] = [ - // { - // backgroundColor: "#1c202426", - // textColor: "#1c2024", - // label: "Black", - // }, - { - backgroundColor: "#5c5e6326", - textColor: "#5c5e63", - label: "Gray", - }, - { - backgroundColor: "#ff5b5926", - textColor: "#ff5b59", - label: "Peach", - }, - { - backgroundColor: "#f6538526", - textColor: "#f65385", - label: "Pink", - }, - { - backgroundColor: "#fd903826", - textColor: "#fd9038", - label: "Orange", - }, - { - backgroundColor: "#0fc27b26", - textColor: "#0fc27b", - label: "Green", - }, - { - backgroundColor: "#17bee926", - textColor: "#17bee9", - label: "Light blue", - }, - { - backgroundColor: "#266df026", - textColor: "#266df0", - label: "Dark blue", - }, - { - backgroundColor: "#9162f926", - textColor: "#9162f9", - label: "Purple", - }, -]; diff --git a/packages/editor/src/core/extensions/core-without-props.ts b/packages/editor/src/core/extensions/core-without-props.ts index 7fc0ae6d052..1cedd513966 100644 --- a/packages/editor/src/core/extensions/core-without-props.ts +++ b/packages/editor/src/core/extensions/core-without-props.ts @@ -1,5 +1,3 @@ -import { Color } from "@tiptap/extension-color"; -import Highlight from "@tiptap/extension-highlight"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import TextStyle from "@tiptap/extension-text-style"; @@ -85,10 +83,6 @@ export const CoreEditorExtensionsWithoutProps = [ TableCell, TableRow, CustomMentionWithoutProps(), - Color, - Highlight.configure({ - multicolor: true, - }), ]; export const DocumentEditorExtensionsWithoutProps = [IssueWidgetWithoutProps()]; diff --git a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx index ed60f3dab08..f067ee94780 100644 --- a/packages/editor/src/core/extensions/custom-image/components/image-block.tsx +++ b/packages/editor/src/core/extensions/custom-image/components/image-block.tsx @@ -201,10 +201,8 @@ export const CustomImageBlock: React.FC = (props) => { // show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or) // if the initial resize (from 35% width and "auto" height attrs to the actual size in px) is not complete const showImageLoader = !(remoteImageSrc || imageFromFileSystem) || !initialResizeComplete; - // show the image utils only if the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) - const showImageUtils = remoteImageSrc && initialResizeComplete; - // show the image resizer only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) - const showImageResizer = editor.isEditable && remoteImageSrc && initialResizeComplete; + // show the image utils only if the editor is editable, the remote image's (post upload) src is set and the initial resize is complete (but not while we're showing the preview imageFromFileSystem) + const showImageUtils = editor.isEditable && remoteImageSrc && initialResizeComplete; // show the preview image from the file system if the remote image's src is not set const displayedImageSrc = remoteImageSrc ?? imageFromFileSystem; @@ -260,7 +258,7 @@ export const CustomImageBlock: React.FC = (props) => { {selected && displayedImageSrc === remoteImageSrc && (
)} - {showImageResizer && ( + {showImageUtils && ( <>
{ ai: aiEnabled, dragDrop: dragDropEnabled, }, - scrollThreshold: { up: 200, down: 100 }, + scrollThreshold: { up: 300, down: 100 }, }), ]; }, diff --git a/packages/editor/src/core/extensions/slash-commands.tsx b/packages/editor/src/core/extensions/slash-commands.tsx new file mode 100644 index 00000000000..2be8d89d96b --- /dev/null +++ b/packages/editor/src/core/extensions/slash-commands.tsx @@ -0,0 +1,422 @@ +import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react"; +import { Editor, Range, Extension } from "@tiptap/core"; +import { ReactRenderer } from "@tiptap/react"; +import Suggestion, { SuggestionOptions } from "@tiptap/suggestion"; +import tippy from "tippy.js"; +import { + CaseSensitive, + Code2, + Heading1, + Heading2, + Heading3, + Heading4, + Heading5, + Heading6, + ImageIcon, + List, + ListOrdered, + ListTodo, + MinusSquare, + Quote, + Table, +} from "lucide-react"; +// helpers +import { cn } from "@/helpers/common"; +import { + insertTableCommand, + toggleBlockquote, + toggleBulletList, + toggleOrderedList, + toggleTaskList, + toggleHeadingOne, + toggleHeadingTwo, + toggleHeadingThree, + toggleHeadingFour, + toggleHeadingFive, + toggleHeadingSix, + insertImage, +} from "@/helpers/editor-commands"; +// types +import { CommandProps, ISlashCommandItem } from "@/types"; + +interface CommandItemProps { + key: string; + title: string; + description: string; + icon: ReactNode; +} + +export type SlashCommandOptions = { + suggestion: Omit; +}; + +const Command = Extension.create({ + name: "slash-command", + addOptions() { + return { + suggestion: { + char: "/", + command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => { + props.command({ editor, range }); + }, + allow({ editor }: { editor: Editor }) { + const { selection } = editor.state; + + const parentNode = selection.$from.node(selection.$from.depth); + const blockType = parentNode.type.name; + + if (blockType === "codeBlock") { + return false; + } + + if (editor.isActive("table")) { + return false; + } + + return true; + }, + }, + }; + }, + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + ...this.options.suggestion, + }), + ]; + }, +}); + +const getSuggestionItems = + (additionalOptions?: Array) => + ({ query }: { query: string }) => { + let slashCommands: ISlashCommandItem[] = [ + { + key: "text", + title: "Text", + description: "Just start typing with plain text.", + searchTerms: ["p", "paragraph"], + icon: , + command: ({ editor, range }: CommandProps) => { + if (range) { + editor.chain().focus().deleteRange(range).clearNodes().run(); + } + editor.chain().focus().clearNodes().run(); + }, + }, + { + key: "h1", + title: "Heading 1", + description: "Big section heading.", + searchTerms: ["title", "big", "large"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingOne(editor, range); + }, + }, + { + key: "h2", + title: "Heading 2", + description: "Medium section heading.", + searchTerms: ["subtitle", "medium"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingTwo(editor, range); + }, + }, + { + key: "h3", + title: "Heading 3", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingThree(editor, range); + }, + }, + { + key: "h4", + title: "Heading 4", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingFour(editor, range); + }, + }, + { + key: "h5", + title: "Heading 5", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingFive(editor, range); + }, + }, + { + key: "h6", + title: "Heading 6", + description: "Small section heading.", + searchTerms: ["subtitle", "small"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleHeadingSix(editor, range); + }, + }, + { + key: "to-do-list", + title: "To do", + description: "Track tasks with a to-do list.", + searchTerms: ["todo", "task", "list", "check", "checkbox"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleTaskList(editor, range); + }, + }, + { + key: "bulleted-list", + title: "Bullet list", + description: "Create a simple bullet list.", + searchTerms: ["unordered", "point"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleBulletList(editor, range); + }, + }, + { + key: "numbered-list", + title: "Numbered list", + description: "Create a list with numbering.", + searchTerms: ["ordered"], + icon: , + command: ({ editor, range }: CommandProps) => { + toggleOrderedList(editor, range); + }, + }, + { + key: "table", + title: "Table", + description: "Create a table", + searchTerms: ["table", "cell", "db", "data", "tabular"], + icon: , + command: ({ editor, range }: CommandProps) => { + insertTableCommand(editor, range); + }, + }, + { + key: "quote", + title: "Quote", + description: "Capture a quote.", + searchTerms: ["blockquote"], + icon: , + command: ({ editor, range }: CommandProps) => toggleBlockquote(editor, range), + }, + { + key: "code", + title: "Code", + description: "Capture a code snippet.", + searchTerms: ["codeblock"], + icon: , + command: ({ editor, range }: CommandProps) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), + }, + { + key: "image", + title: "Image", + icon: , + description: "Insert an image", + searchTerms: ["img", "photo", "picture", "media", "upload"], + command: ({ editor, range }: CommandProps) => insertImage({ editor, event: "insert", range }), + }, + { + key: "divider", + title: "Divider", + description: "Visually divide blocks.", + searchTerms: ["line", "divider", "horizontal", "rule", "separate"], + icon: , + command: ({ editor, range }: CommandProps) => { + editor.chain().focus().deleteRange(range).setHorizontalRule().run(); + }, + }, + ]; + + if (additionalOptions) { + additionalOptions.map((item) => { + slashCommands.push(item); + }); + } + + slashCommands = slashCommands.filter((item) => { + if (typeof query === "string" && query.length > 0) { + const search = query.toLowerCase(); + return ( + item.title.toLowerCase().includes(search) || + item.description.toLowerCase().includes(search) || + (item.searchTerms && item.searchTerms.some((term: string) => term.includes(search))) + ); + } + return true; + }); + + return slashCommands; + }; + +export const updateScrollView = (container: HTMLElement, item: HTMLElement) => { + const containerHeight = container.offsetHeight; + const itemHeight = item ? item.offsetHeight : 0; + + const top = item.offsetTop; + const bottom = top + itemHeight; + + if (top < container.scrollTop) { + container.scrollTop -= container.scrollTop - top + 5; + } else if (bottom > containerHeight + container.scrollTop) { + container.scrollTop += bottom - containerHeight - container.scrollTop + 5; + } +}; + +const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => { + // states + const [selectedIndex, setSelectedIndex] = useState(0); + // refs + const commandListContainer = useRef(null); + + const selectItem = useCallback( + (index: number) => { + const item = items[index]; + if (item) command(item); + }, + [command, items] + ); + + useEffect(() => { + const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"]; + const onKeyDown = (e: KeyboardEvent) => { + if (navigationKeys.includes(e.key)) { + e.preventDefault(); + if (e.key === "ArrowUp") { + setSelectedIndex((selectedIndex + items.length - 1) % items.length); + return true; + } + if (e.key === "ArrowDown") { + setSelectedIndex((selectedIndex + 1) % items.length); + return true; + } + if (e.key === "Enter") { + selectItem(selectedIndex); + return true; + } + return false; + } + }; + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("keydown", onKeyDown); + }; + }, [items, selectedIndex, setSelectedIndex, selectItem]); + + useEffect(() => { + setSelectedIndex(0); + }, [items]); + + useLayoutEffect(() => { + const container = commandListContainer?.current; + + const item = container?.children[selectedIndex] as HTMLElement; + + if (item && container) updateScrollView(container, item); + }, [selectedIndex]); + + if (items.length <= 0) return null; + + return ( +
+ {items.map((item, index) => ( + + ))} +
+ ); +}; + +interface CommandListInstance { + onKeyDown: (props: { event: KeyboardEvent }) => boolean; +} + +const renderItems = () => { + let component: ReactRenderer | null = null; + let popup: any | null = null; + return { + onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { + component = new ReactRenderer(CommandList, { + props, + editor: props.editor, + }); + + const tippyContainer = + document.querySelector(".active-editor") ?? document.querySelector('[id^="editor-container"]'); + + // @ts-expect-error Tippy overloads are messed up + popup = tippy("body", { + getReferenceClientRect: props.clientRect, + appendTo: tippyContainer, + content: component.element, + showOnCreate: true, + interactive: true, + trigger: "manual", + placement: "bottom-start", + }); + }, + onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { + component?.updateProps(props); + + popup && + popup[0].setProps({ + getReferenceClientRect: props.clientRect, + }); + }, + onKeyDown: (props: { event: KeyboardEvent }) => { + if (props.event.key === "Escape") { + popup?.[0].hide(); + + return true; + } + + if (component?.ref?.onKeyDown(props)) { + return true; + } + return false; + }, + onExit: () => { + popup?.[0].destroy(); + component?.destroy(); + }, + }; +}; + +export const SlashCommand = (additionalOptions?: Array) => + Command.configure({ + suggestion: { + items: getSuggestionItems(additionalOptions), + render: renderItems, + }, + }); diff --git a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx b/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx deleted file mode 100644 index 5f443ee332c..00000000000 --- a/packages/editor/src/core/extensions/slash-commands/command-items-list.tsx +++ /dev/null @@ -1,294 +0,0 @@ -import { - ALargeSmall, - CaseSensitive, - Code2, - Heading1, - Heading2, - Heading3, - Heading4, - Heading5, - Heading6, - ImageIcon, - List, - ListOrdered, - ListTodo, - MinusSquare, - Quote, - Table, -} from "lucide-react"; -// constants -import { COLORS_LIST } from "@/constants/common"; -// helpers -import { - insertTableCommand, - toggleBlockquote, - toggleBulletList, - toggleOrderedList, - toggleTaskList, - toggleHeadingOne, - toggleHeadingTwo, - toggleHeadingThree, - toggleHeadingFour, - toggleHeadingFive, - toggleHeadingSix, - toggleTextColor, - toggleBackgroundColor, - insertImage, -} from "@/helpers/editor-commands"; -// types -import { CommandProps, ISlashCommandItem } from "@/types"; - -export type TSlashCommandSection = { - key: string; - title?: string; - items: ISlashCommandItem[]; -}; - -const SLASH_COMMAND_SECTIONS: TSlashCommandSection[] = [ - { - key: "general", - items: [ - { - commandKey: "text", - key: "text", - title: "Text", - description: "Just start typing with plain text.", - searchTerms: ["p", "paragraph"], - icon: , - command: ({ editor, range }: CommandProps) => { - if (range) { - editor.chain().focus().deleteRange(range).clearNodes().run(); - } - editor.chain().focus().clearNodes().run(); - }, - }, - { - commandKey: "h1", - key: "h1", - title: "Heading 1", - description: "Big section heading.", - searchTerms: ["title", "big", "large"], - icon: , - command: ({ editor, range }) => toggleHeadingOne(editor, range), - }, - { - commandKey: "h2", - key: "h2", - title: "Heading 2", - description: "Medium section heading.", - searchTerms: ["subtitle", "medium"], - icon: , - command: ({ editor, range }) => toggleHeadingTwo(editor, range), - }, - { - commandKey: "h3", - key: "h3", - title: "Heading 3", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }) => toggleHeadingThree(editor, range), - }, - { - commandKey: "h4", - key: "h4", - title: "Heading 4", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }) => toggleHeadingFour(editor, range), - }, - { - commandKey: "h5", - key: "h5", - title: "Heading 5", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }) => toggleHeadingFive(editor, range), - }, - { - commandKey: "h6", - key: "h6", - title: "Heading 6", - description: "Small section heading.", - searchTerms: ["subtitle", "small"], - icon: , - command: ({ editor, range }) => toggleHeadingSix(editor, range), - }, - { - commandKey: "to-do-list", - key: "to-do-list", - title: "To do", - description: "Track tasks with a to-do list.", - searchTerms: ["todo", "task", "list", "check", "checkbox"], - icon: , - command: ({ editor, range }) => toggleTaskList(editor, range), - }, - { - commandKey: "bulleted-list", - key: "bulleted-list", - title: "Bullet list", - description: "Create a simple bullet list.", - searchTerms: ["unordered", "point"], - icon: , - command: ({ editor, range }) => toggleBulletList(editor, range), - }, - { - commandKey: "numbered-list", - key: "numbered-list", - title: "Numbered list", - description: "Create a list with numbering.", - searchTerms: ["ordered"], - icon: , - command: ({ editor, range }) => toggleOrderedList(editor, range), - }, - { - commandKey: "table", - key: "table", - title: "Table", - description: "Create a table", - searchTerms: ["table", "cell", "db", "data", "tabular"], - icon:
, - command: ({ editor, range }) => insertTableCommand(editor, range), - }, - { - commandKey: "quote", - key: "quote", - title: "Quote", - description: "Capture a quote.", - searchTerms: ["blockquote"], - icon: , - command: ({ editor, range }) => toggleBlockquote(editor, range), - }, - { - commandKey: "code", - key: "code", - title: "Code", - description: "Capture a code snippet.", - searchTerms: ["codeblock"], - icon: , - command: ({ editor, range }) => editor.chain().focus().deleteRange(range).toggleCodeBlock().run(), - }, - { - commandKey: "image", - key: "image", - title: "Image", - icon: , - description: "Insert an image", - searchTerms: ["img", "photo", "picture", "media", "upload"], - command: ({ editor, range }: CommandProps) => insertImage({ editor, event: "insert", range }), - }, - { - commandKey: "divider", - key: "divider", - title: "Divider", - description: "Visually divide blocks.", - searchTerms: ["line", "divider", "horizontal", "rule", "separate"], - icon: , - command: ({ editor, range }) => editor.chain().focus().deleteRange(range).setHorizontalRule().run(), - }, - ], - }, - { - key: "text-color", - title: "Colors", - items: [ - { - commandKey: "text-color", - key: "text-color-default", - title: "Default", - description: "Change text color", - searchTerms: ["color", "text", "default"], - icon: ( - - ), - command: ({ editor, range }) => toggleTextColor(undefined, editor, range), - }, - ...COLORS_LIST.map( - (color) => - ({ - commandKey: "text-color", - key: `text-color-${color.textColor}`, - title: color.label, - description: "Change text color", - searchTerms: ["color", "text", color.label], - icon: ( - - ), - command: ({ editor, range }) => toggleTextColor(color.textColor, editor, range), - }) as ISlashCommandItem - ), - ], - }, - { - key: "background-color", - title: "Background colors", - items: [ - { - commandKey: "background-color", - key: "background-color-default", - title: "Default background", - description: "Change background color", - searchTerms: ["color", "bg", "background", "default"], - icon: , - iconContainerStyle: { - borderRadius: "4px", - backgroundColor: "rgba(var(--color-background-100))", - border: "1px solid rgba(var(--color-border-300))", - }, - command: ({ editor, range }) => toggleTextColor(undefined, editor, range), - }, - ...COLORS_LIST.map( - (color) => - ({ - commandKey: "background-color", - key: `background-color-${color.backgroundColor}`, - title: `${color.label} background`, - description: "Change background color", - searchTerms: ["color", "bg", "background", color.label], - icon: , - iconContainerStyle: { - borderRadius: "4px", - backgroundColor: color.backgroundColor, - }, - command: ({ editor, range }) => toggleBackgroundColor(color.backgroundColor, editor, range), - }) as ISlashCommandItem - ), - ], - }, -]; - -export const getSlashCommandFilteredSections = - (additionalOptions?: ISlashCommandItem[]) => - ({ query }: { query: string }): TSlashCommandSection[] => { - if (additionalOptions) { - additionalOptions.map((item) => SLASH_COMMAND_SECTIONS?.[0]?.items.push(item)); - } - - const filteredSlashSections = SLASH_COMMAND_SECTIONS.map((section) => ({ - ...section, - items: section.items.filter((item) => { - if (typeof query !== "string") return; - - const lowercaseQuery = query.toLowerCase(); - return ( - item.title.toLowerCase().includes(lowercaseQuery) || - item.description.toLowerCase().includes(lowercaseQuery) || - item.searchTerms.some((t) => t.includes(lowercaseQuery)) - ); - }), - })); - - return filteredSlashSections.filter((s) => s.items.length !== 0); - }; diff --git a/packages/editor/src/core/extensions/slash-commands/command-menu-item.tsx b/packages/editor/src/core/extensions/slash-commands/command-menu-item.tsx deleted file mode 100644 index 3a03c3b6a70..00000000000 --- a/packages/editor/src/core/extensions/slash-commands/command-menu-item.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// helpers -import { cn } from "@/helpers/common"; -// types -import { ISlashCommandItem } from "@/types"; - -type Props = { - isSelected: boolean; - item: ISlashCommandItem; - itemIndex: number; - onClick: (e: React.MouseEvent) => void; - onMouseEnter: () => void; - sectionIndex: number; -}; - -export const CommandMenuItem: React.FC = (props) => { - const { isSelected, item, itemIndex, onClick, onMouseEnter, sectionIndex } = props; - - return ( - - ); -}; diff --git a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx b/packages/editor/src/core/extensions/slash-commands/command-menu.tsx deleted file mode 100644 index 977f688286e..00000000000 --- a/packages/editor/src/core/extensions/slash-commands/command-menu.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; -// components -import { TSlashCommandSection } from "./command-items-list"; -import { CommandMenuItem } from "./command-menu-item"; - -type Props = { - items: TSlashCommandSection[]; - command: any; - editor: any; - range: any; -}; - -export const SlashCommandsMenu = (props: Props) => { - const { items: sections, command } = props; - // states - const [selectedIndex, setSelectedIndex] = useState({ - section: 0, - item: 0, - }); - // refs - const commandListContainer = useRef(null); - - const selectItem = useCallback( - (sectionIndex: number, itemIndex: number) => { - const item = sections[sectionIndex].items[itemIndex]; - if (item) command(item); - }, - [command, sections] - ); - // handle arrow key navigation - useEffect(() => { - const navigationKeys = ["ArrowUp", "ArrowDown", "Enter"]; - const onKeyDown = (e: KeyboardEvent) => { - if (navigationKeys.includes(e.key)) { - e.preventDefault(); - const currentSection = selectedIndex.section; - const currentItem = selectedIndex.item; - let nextSection = currentSection; - let nextItem = currentItem; - - if (e.key === "ArrowUp") { - nextItem = currentItem - 1; - if (nextItem < 0) { - nextSection = currentSection - 1; - if (nextSection < 0) nextSection = sections.length - 1; - nextItem = sections[nextSection].items.length - 1; - } - } - if (e.key === "ArrowDown") { - nextItem = currentItem + 1; - if (nextItem >= sections[currentSection].items.length) { - nextSection = currentSection + 1; - if (nextSection >= sections.length) nextSection = 0; - nextItem = 0; - } - } - if (e.key === "Enter") { - selectItem(currentSection, currentItem); - } - setSelectedIndex({ - section: nextSection, - item: nextItem, - }); - } - }; - document.addEventListener("keydown", onKeyDown); - return () => { - document.removeEventListener("keydown", onKeyDown); - }; - }, [sections, selectedIndex, setSelectedIndex, selectItem]); - // initialize the select index to 0 by default - useEffect(() => { - setSelectedIndex({ - section: 0, - item: 0, - }); - }, [sections]); - // scroll to the dropdown item when navigating via keyboard - useLayoutEffect(() => { - const container = commandListContainer?.current; - if (!container) return; - - const item = container.querySelector(`#item-${selectedIndex.section}-${selectedIndex.item}`) as HTMLElement; - - // use scroll into view to bring the item in view if it is not in view - item?.scrollIntoView({ block: "nearest" }); - }, [sections, selectedIndex]); - - const areSearchResultsEmpty = sections.map((s) => s.items.length).reduce((acc, curr) => acc + curr, 0) === 0; - - if (areSearchResultsEmpty) return null; - - return ( -
- {sections.map((section, sectionIndex) => ( -
- {section.title &&
{section.title}
} -
- {section.items.map((item, itemIndex) => ( - { - e.stopPropagation(); - selectItem(sectionIndex, itemIndex); - }} - onMouseEnter={() => - setSelectedIndex({ - section: sectionIndex, - item: itemIndex, - }) - } - sectionIndex={sectionIndex} - /> - ))} -
-
- ))} -
- ); -}; diff --git a/packages/editor/src/core/extensions/slash-commands/index.ts b/packages/editor/src/core/extensions/slash-commands/index.ts deleted file mode 100644 index 1efe34c51ec..00000000000 --- a/packages/editor/src/core/extensions/slash-commands/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./root"; diff --git a/packages/editor/src/core/extensions/slash-commands/root.tsx b/packages/editor/src/core/extensions/slash-commands/root.tsx deleted file mode 100644 index ac88f20d3ac..00000000000 --- a/packages/editor/src/core/extensions/slash-commands/root.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Editor, Range, Extension } from "@tiptap/core"; -import { ReactRenderer } from "@tiptap/react"; -import Suggestion, { SuggestionOptions } from "@tiptap/suggestion"; -import tippy from "tippy.js"; -// types -import { ISlashCommandItem } from "@/types"; -// components -import { getSlashCommandFilteredSections } from "./command-items-list"; -import { SlashCommandsMenu } from "./command-menu"; - -export type SlashCommandOptions = { - suggestion: Omit; -}; - -const Command = Extension.create({ - name: "slash-command", - addOptions() { - return { - suggestion: { - char: "/", - command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => { - props.command({ editor, range }); - }, - allow({ editor }: { editor: Editor }) { - const { selection } = editor.state; - - const parentNode = selection.$from.node(selection.$from.depth); - const blockType = parentNode.type.name; - - if (blockType === "codeBlock") { - return false; - } - - if (editor.isActive("table")) { - return false; - } - - return true; - }, - }, - }; - }, - addProseMirrorPlugins() { - return [ - Suggestion({ - editor: this.editor, - ...this.options.suggestion, - }), - ]; - }, -}); - -interface CommandListInstance { - onKeyDown: (props: { event: KeyboardEvent }) => boolean; -} - -const renderItems = () => { - let component: ReactRenderer | null = null; - let popup: any | null = null; - return { - onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { - component = new ReactRenderer(SlashCommandsMenu, { - props, - editor: props.editor, - }); - - const tippyContainer = - document.querySelector(".active-editor") ?? document.querySelector('[id^="editor-container"]'); - - // @ts-expect-error Tippy overloads are messed up - popup = tippy("body", { - getReferenceClientRect: props.clientRect, - appendTo: tippyContainer, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: "manual", - placement: "bottom-start", - }); - }, - onUpdate: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => { - component?.updateProps(props); - - popup?.[0]?.setProps({ - getReferenceClientRect: props.clientRect, - }); - }, - onKeyDown: (props: { event: KeyboardEvent }) => { - if (props.event.key === "Escape") { - popup?.[0].hide(); - - return true; - } - - if (component?.ref?.onKeyDown(props)) { - return true; - } - return false; - }, - onExit: () => { - popup?.[0].destroy(); - component?.destroy(); - }, - }; -}; - -export const SlashCommands = (additionalOptions?: ISlashCommandItem[]) => - Command.configure({ - suggestion: { - items: getSlashCommandFilteredSections(additionalOptions), - render: renderItems, - }, - }); diff --git a/packages/editor/src/core/helpers/editor-commands.ts b/packages/editor/src/core/helpers/editor-commands.ts index fb63a6fbf23..66be05bb261 100644 --- a/packages/editor/src/core/helpers/editor-commands.ts +++ b/packages/editor/src/core/helpers/editor-commands.ts @@ -154,42 +154,3 @@ export const unsetLinkEditor = (editor: Editor) => { export const setLinkEditor = (editor: Editor, url: string) => { editor.chain().focus().setLink({ href: url }).run(); }; - -export const toggleTextColor = (color: string | undefined, editor: Editor, range?: Range) => { - if (color) { - if (range) editor.chain().focus().deleteRange(range).setColor(color).run(); - else editor.chain().focus().setColor(color).run(); - } else { - if (range) editor.chain().focus().deleteRange(range).unsetColor().run(); - else editor.chain().focus().unsetColor().run(); - } -}; - -export const toggleBackgroundColor = (color: string | undefined, editor: Editor, range?: Range) => { - if (color) { - if (range) { - editor - .chain() - .focus() - .deleteRange(range) - .setHighlight({ - color, - }) - .run(); - } else { - editor - .chain() - .focus() - .setHighlight({ - color, - }) - .run(); - } - } else { - if (range) { - editor.chain().focus().deleteRange(range).unsetHighlight().run(); - } else { - editor.chain().focus().unsetHighlight().run(); - } - } -}; diff --git a/packages/editor/src/core/hooks/use-editor.ts b/packages/editor/src/core/hooks/use-editor.ts index 0edb6ca50bd..65e36c01ae6 100644 --- a/packages/editor/src/core/hooks/use-editor.ts +++ b/packages/editor/src/core/hooks/use-editor.ts @@ -136,8 +136,7 @@ export const useEditor = (props: CustomEditorProps) => { insertContentAtSavedSelection(editorRef, content, savedSelection); } }, - executeMenuItemCommand: (props) => { - const { itemKey } = props; + executeMenuItemCommand: (itemKey: TEditorCommands) => { const editorItems = getEditorMenuItems(editorRef.current); const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey); @@ -146,8 +145,6 @@ export const useEditor = (props: CustomEditorProps) => { if (item) { if (item.key === "image") { item.command(savedSelectionRef.current); - } else if (itemKey === "text-color" || itemKey === "background-color") { - item.command(props.color); } else { item.command(); } @@ -155,19 +152,12 @@ export const useEditor = (props: CustomEditorProps) => { console.warn(`No command found for item: ${itemKey}`); } }, - isMenuItemActive: (props) => { - const { itemKey } = props; + isMenuItemActive: (itemName: TEditorCommands): boolean => { const editorItems = getEditorMenuItems(editorRef.current); - const getEditorMenuItem = (itemKey: TEditorCommands) => editorItems.find((item) => item.key === itemKey); - const item = getEditorMenuItem(itemKey); - if (!item) return false; - - if (itemKey === "text-color" || itemKey === "background-color") { - return item.isActive(props.color); - } else { - return item.isActive(""); - } + const getEditorMenuItem = (itemName: TEditorCommands) => editorItems.find((item) => item.key === itemName); + const item = getEditorMenuItem(itemName); + return item ? item.isActive() : false; }, onHeadingChange: (callback: (headings: IMarking[]) => void) => { // Subscribe to update event emitted from headers extension diff --git a/packages/editor/src/core/plugins/drag-handle.ts b/packages/editor/src/core/plugins/drag-handle.ts index 809802b4f92..ba390180bbe 100644 --- a/packages/editor/src/core/plugins/drag-handle.ts +++ b/packages/editor/src/core/plugins/drag-handle.ts @@ -233,46 +233,14 @@ export const DragHandlePlugin = (options: SideMenuPluginProps): SideMenuHandleOp dragHandleElement.addEventListener("click", (e) => handleClick(e, view)); dragHandleElement.addEventListener("contextmenu", (e) => handleClick(e, view)); - const isScrollable = (node: HTMLElement | SVGElement) => { - if (!(node instanceof HTMLElement || node instanceof SVGElement)) { - return false; - } - const style = getComputedStyle(node); - return ["overflow", "overflow-y"].some((propertyName) => { - const value = style.getPropertyValue(propertyName); - return value === "auto" || value === "scroll"; - }); - }; - - const getScrollParent = (node: HTMLElement | SVGElement) => { - let currentParent = node.parentElement; - while (currentParent) { - if (isScrollable(currentParent)) { - return currentParent; - } - currentParent = currentParent.parentElement; - } - return document.scrollingElement || document.documentElement; - }; - - const maxScrollSpeed = 100; - dragHandleElement.addEventListener("drag", (e) => { hideDragHandle(); - const scrollableParent = getScrollParent(dragHandleElement); - if (!scrollableParent) return; - const scrollThreshold = options.scrollThreshold; - - if (e.clientY < scrollThreshold.up) { - const overflow = scrollThreshold.up - e.clientY; - const ratio = Math.min(overflow / scrollThreshold.up, 1); - const scrollAmount = -maxScrollSpeed * ratio; - scrollableParent.scrollBy({ top: scrollAmount }); - } else if (window.innerHeight - e.clientY < scrollThreshold.down) { - const overflow = e.clientY - (window.innerHeight - scrollThreshold.down); - const ratio = Math.min(overflow / scrollThreshold.down, 1); - const scrollAmount = maxScrollSpeed * ratio; - scrollableParent.scrollBy({ top: scrollAmount }); + const frameRenderer = document.querySelector(".frame-renderer"); + if (!frameRenderer) return; + if (e.clientY < options.scrollThreshold.up) { + frameRenderer.scrollBy({ top: -70, behavior: "smooth" }); + } else if (window.innerHeight - e.clientY < options.scrollThreshold.down) { + frameRenderer.scrollBy({ top: 70, behavior: "smooth" }); } }); diff --git a/packages/editor/src/core/types/editor.ts b/packages/editor/src/core/types/editor.ts index c833cb74920..3624fa046ce 100644 --- a/packages/editor/src/core/types/editor.ts +++ b/packages/editor/src/core/types/editor.ts @@ -6,15 +6,14 @@ import { IMentionHighlight, IMentionSuggestion, TAIHandler, - TColorEditorCommands, TDisplayConfig, TEditorCommands, TEmbedConfig, TExtensions, TFileHandler, - TNonColorEditorCommands, TServerHandler, } from "@/types"; + // editor refs export type EditorReadOnlyRefApi = { getMarkDown: () => string; @@ -37,26 +36,8 @@ export type EditorReadOnlyRefApi = { export interface EditorRefApi extends EditorReadOnlyRefApi { setEditorValueAtCursorPosition: (content: string) => void; - executeMenuItemCommand: ( - props: - | { - itemKey: TNonColorEditorCommands; - } - | { - itemKey: TColorEditorCommands; - color: string | undefined; - } - ) => void; - isMenuItemActive: ( - props: - | { - itemKey: TNonColorEditorCommands; - } - | { - itemKey: TColorEditorCommands; - color: string | undefined; - } - ) => boolean; + executeMenuItemCommand: (itemKey: TEditorCommands) => void; + isMenuItemActive: (itemKey: TEditorCommands) => boolean; onStateChange: (callback: () => void) => () => void; setFocusAtPosition: (position: number) => void; isEditorReadyToDiscard: () => boolean; diff --git a/packages/editor/src/core/types/slash-commands-suggestion.ts b/packages/editor/src/core/types/slash-commands-suggestion.ts index ce3408a34f0..3cb9d76b0ea 100644 --- a/packages/editor/src/core/types/slash-commands-suggestion.ts +++ b/packages/editor/src/core/types/slash-commands-suggestion.ts @@ -1,4 +1,4 @@ -import { CSSProperties } from "react"; +import { ReactNode } from "react"; import { Editor, Range } from "@tiptap/core"; export type TEditorCommands = @@ -21,12 +21,7 @@ export type TEditorCommands = | "table" | "image" | "divider" - | "issue-embed" - | "text-color" - | "background-color"; - -export type TColorEditorCommands = Extract; -export type TNonColorEditorCommands = Exclude; + | "issue-embed"; export type CommandProps = { editor: Editor; @@ -34,12 +29,10 @@ export type CommandProps = { }; export type ISlashCommandItem = { - commandKey: TEditorCommands; - key: string; + key: TEditorCommands; title: string; description: string; searchTerms: string[]; - icon: React.ReactNode; - iconContainerStyle?: CSSProperties; + icon: ReactNode; command: ({ editor, range }: CommandProps) => void; }; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index c65f43dd4fb..fc9fe1ac603 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -18,9 +18,6 @@ export { export { isCellSelection } from "@/extensions/table/table/utilities/is-cell-selection"; -// constants -export * from "@/constants/common"; - // helpers export * from "@/helpers/common"; export * from "@/helpers/editor-commands"; diff --git a/packages/editor/src/styles/editor.css b/packages/editor/src/styles/editor.css index caffc0534a5..e5047fb0c48 100644 --- a/packages/editor/src/styles/editor.css +++ b/packages/editor/src/styles/editor.css @@ -44,7 +44,7 @@ } &.sans-serif { - --font-style: "Inter", sans-serif; + --font-style: sans-serif; } &.serif { diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 335047356ee..332e3608048 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,7 +1,7 @@ { "name": "@plane/eslint-config", "private": true, - "version": "0.23.1", + "version": "0.23.0", "files": [ "library.js", "next.js", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index b4b94db1f5f..c94c8f76342 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@plane/helpers", - "version": "0.23.1", + "version": "0.23.0", "description": "Helper functions shared across multiple apps internally", "private": true, "main": "./dist/index.js", diff --git a/packages/tailwind-config-custom/package.json b/packages/tailwind-config-custom/package.json index cec6628a644..bf35a97dcc8 100644 --- a/packages/tailwind-config-custom/package.json +++ b/packages/tailwind-config-custom/package.json @@ -1,6 +1,6 @@ { "name": "tailwind-config-custom", - "version": "0.23.1", + "version": "0.23.0", "description": "common tailwind configuration across monorepo", "main": "index.js", "private": true, diff --git a/packages/types/package.json b/packages/types/package.json index 5962ca25c9d..5a7bd93d7db 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@plane/types", - "version": "0.23.1", + "version": "0.23.0", "private": true, "types": "./src/index.d.ts", "main": "./src/index.d.ts" diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 4559e79c837..6dfddc6b638 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -29,4 +29,3 @@ export * from "./pragmatic"; export * from "./publish"; export * from "./workspace-notifications"; export * from "./favorite"; -export * from "./workspace-draft-issues/base"; diff --git a/packages/types/src/issues/base.d.ts b/packages/types/src/issues/base.d.ts index 05f679cce28..8292c111649 100644 --- a/packages/types/src/issues/base.d.ts +++ b/packages/types/src/issues/base.d.ts @@ -10,7 +10,6 @@ export * from "./issue_relation"; export * from "./issue_sub_issues"; export * from "./activity/base"; - export type TLoader = | "init-loader" | "mutation" diff --git a/packages/types/src/workspace-draft-issues/base.d.ts b/packages/types/src/workspace-draft-issues/base.d.ts deleted file mode 100644 index f0272defd5e..00000000000 --- a/packages/types/src/workspace-draft-issues/base.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { TIssuePriorities } from "../issues"; - -export type TWorkspaceDraftIssue = { - id: string; - name: string; - sort_order: number; - - state_id: string | undefined; - priority: TIssuePriorities | undefined; - label_ids: string[]; - assignee_ids: string[]; - estimate_point: string | undefined; - - project_id: string | undefined; - parent_id: string | undefined; - cycle_id: string | undefined; - module_ids: string[] | undefined; - - start_date: string | undefined; - target_date: string | undefined; - completed_at: string | undefined; - - created_at: string; - updated_at: string; - created_by: string; - updated_by: string; - - is_draft: boolean; -}; - -export type TWorkspaceDraftPaginationInfo = { - next_cursor: string | undefined; - prev_cursor: string | undefined; - next_page_results: boolean | undefined; - prev_page_results: boolean | undefined; - total_pages: number | undefined; - count: number | undefined; // current paginated results count - total_count: number | undefined; // total available results count - total_results: number | undefined; - results: T[] | undefined; - extra_stats: string | undefined; - grouped_by: string | undefined; - sub_grouped_by: string | undefined; -}; - -export type TWorkspaceDraftQueryParams = { - per_page: number; - cursor: string; -}; - -export type TWorkspaceDraftIssueLoader = - | "init-loader" - | "empty-state" - | "mutation" - | "pagination" - | "loaded" - | "create" - | "update" - | "delete" - | "move" - | undefined; diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index f6b12920c7e..5356628b73c 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@plane/typescript-config", - "version": "0.23.1", + "version": "0.23.0", "private": true, "files": [ "base.json", diff --git a/packages/ui/package.json b/packages/ui/package.json index 09019457aee..73a720c9fcb 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -2,7 +2,7 @@ "name": "@plane/ui", "description": "UI components shared across multiple apps internally", "private": true, - "version": "0.23.1", + "version": "0.23.0", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", diff --git a/packages/ui/src/modals/constants.ts b/packages/ui/src/modals/constants.ts index fe72ef7aea1..0cb268fc882 100644 --- a/packages/ui/src/modals/constants.ts +++ b/packages/ui/src/modals/constants.ts @@ -4,9 +4,6 @@ export enum EModalPosition { } export enum EModalWidth { - SM = "sm:max-w-sm", - MD = "sm:max-w-md", - LG = "sm:max-w-lg", XL = "sm:max-w-xl", XXL = "sm:max-w-2xl", XXXL = "sm:max-w-3xl", diff --git a/space/core/components/editor/lite-text-editor.tsx b/space/core/components/editor/lite-text-editor.tsx index 86b143f9ded..186f44a1011 100644 --- a/space/core/components/editor/lite-text-editor.tsx +++ b/space/core/components/editor/lite-text-editor.tsx @@ -1,6 +1,6 @@ import React from "react"; // editor -import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TNonColorEditorCommands } from "@plane/editor"; +import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; // components import { IssueCommentToolbar } from "@/components/editor"; // helpers @@ -56,9 +56,7 @@ export const LiteTextEditor = React.forwardRef { if (isMutableRefObject(ref)) { - ref.current?.executeMenuItemCommand({ - itemKey: key as TNonColorEditorCommands, - }); + ref.current?.executeMenuItemCommand(key); } }} isSubmitting={isSubmitting} diff --git a/space/core/components/editor/toolbar.tsx b/space/core/components/editor/toolbar.tsx index beccc8cb763..45e94c2d9a5 100644 --- a/space/core/components/editor/toolbar.tsx +++ b/space/core/components/editor/toolbar.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState, useCallback } from "react"; // editor -import { EditorRefApi, TEditorCommands, TNonColorEditorCommands } from "@plane/editor"; +import { EditorRefApi, TEditorCommands } from "@plane/editor"; // ui import { Button, Tooltip } from "@plane/ui"; // constants @@ -34,9 +34,7 @@ export const IssueCommentToolbar: React.FC = (props) => { .flat() .forEach((item) => { // Assert that editorRef.current is not null - newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive({ - itemKey: item.key as TNonColorEditorCommands, - }); + newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive(item.key); }); setActiveStates(newActiveStates); } diff --git a/space/package.json b/space/package.json index 99422406c02..4bd7dda492d 100644 --- a/space/package.json +++ b/space/package.json @@ -1,6 +1,6 @@ { "name": "space", - "version": "0.23.1", + "version": "0.23.0", "private": true, "scripts": { "dev": "turbo run develop", diff --git a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx deleted file mode 100644 index 6f0b90717d1..00000000000 --- a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx +++ /dev/null @@ -1,65 +0,0 @@ -"use client"; - -import { FC, useState } from "react"; -import { observer } from "mobx-react"; -import { PenSquare } from "lucide-react"; -// ui -import { Breadcrumbs, Button, Header } from "@plane/ui"; -// components -import { BreadcrumbLink, CountChip } from "@/components/common"; -import { CreateUpdateIssueModal } from "@/components/issues"; -// constants -import { EIssuesStoreType } from "@/constants/issue"; -// hooks -import { useUserPermissions, useWorkspaceDraftIssues } from "@/hooks/store"; -// plane-web -import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; - -export const WorkspaceDraftHeader: FC = observer(() => { - // state - const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); - // store hooks - const { allowPermissions } = useUserPermissions(); - const { paginationInfo } = useWorkspaceDraftIssues(); - // check if user is authorized to create draft issue - const isAuthorizedUser = allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.WORKSPACE - ); - - return ( - <> - setIsDraftIssueModalOpen(false)} - isDraft - /> -
- -
- - } />} - /> - - {paginationInfo?.count && paginationInfo?.count > 0 ? : <>} -
-
- - - - -
- - ); -}); diff --git a/web/app/[workspaceSlug]/(projects)/drafts/layout.tsx b/web/app/[workspaceSlug]/(projects)/drafts/layout.tsx deleted file mode 100644 index a5a647bfdba..00000000000 --- a/web/app/[workspaceSlug]/(projects)/drafts/layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { AppHeader, ContentWrapper } from "@/components/core"; -import { WorkspaceDraftHeader } from "./header"; - -export default function WorkspaceDraftLayout({ children }: { children: React.ReactNode }) { - return ( - <> - } /> - {children} - - ); -} diff --git a/web/app/[workspaceSlug]/(projects)/drafts/page.tsx b/web/app/[workspaceSlug]/(projects)/drafts/page.tsx deleted file mode 100644 index f94fc872aeb..00000000000 --- a/web/app/[workspaceSlug]/(projects)/drafts/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { useParams } from "next/navigation"; -// components -import { PageHead } from "@/components/core"; -import { WorkspaceDraftIssuesRoot } from "@/components/issues/workspace-draft"; - -const WorkspaceDraftPage = () => { - // router - const { workspaceSlug: routeWorkspaceSlug } = useParams(); - const pageTitle = "Workspace Draft"; - - // derived values - const workspaceSlug = (routeWorkspaceSlug as string) || undefined; - - if (!workspaceSlug) return null; - return ( - <> - -
- -
- - ); -}; - -export default WorkspaceDraftPage; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx index aa81ae5816a..27e33e2c2c5 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(detail)/mobile-header.tsx @@ -29,7 +29,7 @@ export const CycleIssuesMobileHeader = () => { const { getCycleById } = useCycle(); const layouts = [ { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Board", icon: Kanban }, + { key: "kanban", title: "Kanban", icon: Kanban }, { key: "calendar", title: "Calendar", icon: Calendar }, ]; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx index 025c9fab04f..a1255b206c2 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(list)/mobile-header.tsx @@ -28,7 +28,7 @@ import { useIssues, useLabel, useMember, useProject, useProjectState } from "@/h export const ProjectIssuesMobileHeader = observer(() => { const layouts = [ { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Board", icon: Kanban }, + { key: "kanban", title: "Kanban", icon: Kanban }, { key: "calendar", title: "Calendar", icon: Calendar }, ]; const [analyticsModal, setAnalyticsModal] = useState(false); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx index 0edfa2b5a2b..f67df642c76 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(detail)/mobile-header.tsx @@ -31,7 +31,7 @@ export const ModuleIssuesMobileHeader = observer(() => { const { getModuleById } = useModule(); const layouts = [ { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Board", icon: Kanban }, + { key: "kanban", title: "Kanban", icon: Kanban }, { key: "calendar", title: "Calendar", icon: Calendar }, ]; const { workspaceSlug, projectId, moduleId } = useParams() as { diff --git a/web/app/profile/appearance/page.tsx b/web/app/profile/appearance/page.tsx index 775ff637b04..ef19a3342cf 100644 --- a/web/app/profile/appearance/page.tsx +++ b/web/app/profile/appearance/page.tsx @@ -52,8 +52,13 @@ const ProfileAppearancePage = observer(() => { const applyThemeChange = (theme: Partial) => { setTheme(theme?.theme || "system"); - if (theme?.theme === "custom" && theme?.palette) { - applyTheme(theme?.palette !== ",,,," ? theme?.palette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", false); + const customThemeElement = window.document?.querySelector("[data-theme='custom']"); + if (theme?.theme === "custom" && theme?.palette && customThemeElement) { + applyTheme( + theme?.palette !== ",,,," ? theme?.palette : "#0d101b,#c5c5c5,#3f76ff,#0d101b,#c5c5c5", + false, + customThemeElement + ); } else unsetCustomCssVariables(); }; diff --git a/web/ce/components/cycles/analytics-sidebar/base.tsx b/web/ce/components/cycles/analytics-sidebar/base.tsx deleted file mode 100644 index 94609bc1f63..00000000000 --- a/web/ce/components/cycles/analytics-sidebar/base.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client"; -import { FC, Fragment } from "react"; -import { observer } from "mobx-react"; -// plane ui -import { Loader } from "@plane/ui"; -// components -import ProgressChart from "@/components/core/sidebar/progress-chart"; -import { validateCycleSnapshot } from "@/components/cycles"; -// helpers -import { getDate } from "@/helpers/date-time.helper"; -// hooks -import { useCycle } from "@/hooks/store"; - -type ProgressChartProps = { - workspaceSlug: string; - projectId: string; - cycleId: string; -}; -export const SidebarChart: FC = observer((props) => { - const { workspaceSlug, projectId, cycleId } = props; - - // hooks - const { getEstimateTypeByCycleId, getCycleById } = useCycle(); - - // derived data - const cycleDetails = validateCycleSnapshot(getCycleById(cycleId)); - const cycleStartDate = getDate(cycleDetails?.start_date); - const cycleEndDate = getDate(cycleDetails?.end_date); - const totalEstimatePoints = cycleDetails?.total_estimate_points || 0; - const totalIssues = cycleDetails?.total_issues || 0; - const estimateType = getEstimateTypeByCycleId(cycleId); - - const chartDistributionData = - estimateType === "points" ? cycleDetails?.estimate_distribution : cycleDetails?.distribution || undefined; - - const completionChartDistributionData = chartDistributionData?.completion_chart || undefined; - - if (!workspaceSlug || !projectId || !cycleId) return null; - - return ( -
-
-
- - Ideal -
-
- - Current -
-
- {cycleStartDate && cycleEndDate && completionChartDistributionData ? ( - - - - ) : ( - - - - )} -
- ); -}); diff --git a/web/ce/components/cycles/analytics-sidebar/index.ts b/web/ce/components/cycles/analytics-sidebar/index.ts index 1efe34c51ec..3ba38c61be5 100644 --- a/web/ce/components/cycles/analytics-sidebar/index.ts +++ b/web/ce/components/cycles/analytics-sidebar/index.ts @@ -1 +1 @@ -export * from "./root"; +export * from "./sidebar-chart"; diff --git a/web/ce/components/cycles/analytics-sidebar/root.tsx b/web/ce/components/cycles/analytics-sidebar/root.tsx deleted file mode 100644 index d18f9168dcf..00000000000 --- a/web/ce/components/cycles/analytics-sidebar/root.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; -import React, { FC } from "react"; -// components -import { SidebarChart } from "./base"; - -type Props = { - workspaceSlug: string; - projectId: string; - cycleId: string; -}; - -export const SidebarChartRoot: FC = (props) => ; diff --git a/web/ce/components/cycles/analytics-sidebar/sidebar-chart.tsx b/web/ce/components/cycles/analytics-sidebar/sidebar-chart.tsx new file mode 100644 index 00000000000..e5b69ef24b1 --- /dev/null +++ b/web/ce/components/cycles/analytics-sidebar/sidebar-chart.tsx @@ -0,0 +1,57 @@ +import { Fragment } from "react"; +import { TCycleDistribution, TCycleEstimateDistribution } from "@plane/types"; +import { Loader } from "@plane/ui"; +import ProgressChart from "@/components/core/sidebar/progress-chart"; + +type ProgressChartProps = { + chartDistributionData: TCycleEstimateDistribution | TCycleDistribution | undefined; + cycleStartDate: Date | undefined; + cycleEndDate: Date | undefined; + totalEstimatePoints: number; + totalIssues: number; + plotType: string; +}; +export const SidebarBaseChart = (props: ProgressChartProps) => { + const { chartDistributionData, cycleStartDate, cycleEndDate, totalEstimatePoints, totalIssues, plotType } = props; + const completionChartDistributionData = chartDistributionData?.completion_chart || undefined; + + return ( +
+
+
+ + Ideal +
+
+ + Current +
+
+ {cycleStartDate && cycleEndDate && completionChartDistributionData ? ( + + {plotType === "points" ? ( + + ) : ( + + )} + + ) : ( + + + + )} +
+ ); +}; diff --git a/web/ce/components/issues/issue-details/issue-identifier.tsx b/web/ce/components/issues/issue-details/issue-identifier.tsx index c461e88fa31..b12cc6de71c 100644 --- a/web/ce/components/issues/issue-details/issue-identifier.tsx +++ b/web/ce/components/issues/issue-details/issue-identifier.tsx @@ -1,8 +1,6 @@ import { observer } from "mobx-react"; // types import { IIssueDisplayProperties } from "@plane/types"; -// ui -import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui"; // helpers import { cn } from "@/helpers/common.helper"; // hooks @@ -13,7 +11,6 @@ type TIssueIdentifierBaseProps = { size?: "xs" | "sm" | "md" | "lg"; textContainerClassName?: string; displayProperties?: IIssueDisplayProperties | undefined; - enableClickToCopyIdentifier?: boolean; }; type TIssueIdentifierFromStore = TIssueIdentifierBaseProps & { @@ -26,48 +23,9 @@ type TIssueIdentifierWithDetails = TIssueIdentifierBaseProps & { issueSequenceId: string | number; }; -export type TIssueIdentifierProps = TIssueIdentifierFromStore | TIssueIdentifierWithDetails; - -type TIdentifierTextProps = { - identifier: string; - enableClickToCopyIdentifier?: boolean; - textContainerClassName?: string; -}; - -export const IdentifierText: React.FC = (props) => { - const { identifier, enableClickToCopyIdentifier = false, textContainerClassName } = props; - // handlers - const handleCopyIssueIdentifier = () => { - if (enableClickToCopyIdentifier) { - navigator.clipboard.writeText(identifier).then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Issue ID copied to clipboard", - }); - }); - } - }; - - return ( - - - {identifier} - - - ); -}; - +type TIssueIdentifierProps = TIssueIdentifierFromStore | TIssueIdentifierWithDetails; export const IssueIdentifier: React.FC = observer((props) => { - const { projectId, textContainerClassName, displayProperties, enableClickToCopyIdentifier = false } = props; + const { projectId, textContainerClassName, displayProperties } = props; // store hooks const { getProjectIdentifierById } = useProject(); const { @@ -85,11 +43,9 @@ export const IssueIdentifier: React.FC = observer((props) return (
- + + {projectIdentifier}-{issueSequenceId} +
); }); diff --git a/web/ce/components/issues/issue-details/issue-type-switcher.tsx b/web/ce/components/issues/issue-details/issue-type-switcher.tsx index 5d4adeb9558..5cbd8e6d67f 100644 --- a/web/ce/components/issues/issue-details/issue-type-switcher.tsx +++ b/web/ce/components/issues/issue-details/issue-type-switcher.tsx @@ -20,5 +20,5 @@ export const IssueTypeSwitcher: React.FC = observer((pr if (!issue || !issue.project_id) return <>; - return ; + return ; }); diff --git a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx index 55178ed2171..2f9b4b79e9d 100644 --- a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx +++ b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx @@ -1,6 +1,6 @@ "use client"; -import { FC, Fragment, useCallback, useMemo } from "react"; +import { FC, Fragment, useCallback, useMemo, useState } from "react"; import isEmpty from "lodash/isEmpty"; import isEqual from "lodash/isEqual"; import { observer } from "mobx-react"; @@ -16,9 +16,10 @@ import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; // helpers import { getDate } from "@/helpers/date-time.helper"; // hooks -import { useIssues, useCycle } from "@/hooks/store"; -// plane web components -import { SidebarChartRoot } from "@/plane-web/components/cycles"; +import { useIssues, useCycle, useProjectEstimates } from "@/hooks/store"; +// plane web constants +import { SidebarBaseChart } from "@/plane-web/components/cycles/analytics-sidebar"; +import { EEstimateSystem } from "@/plane-web/constants/estimates"; type TCycleAnalyticsProgress = { workspaceSlug: string; @@ -26,7 +27,7 @@ type TCycleAnalyticsProgress = { cycleId: string; }; -export const validateCycleSnapshot = (cycleDetails: ICycle | null): ICycle | null => { +const validateCycleSnapshot = (cycleDetails: ICycle | null): ICycle | null => { if (!cycleDetails || cycleDetails === null) return cycleDetails; const updatedCycleDetails: any = { ...cycleDetails }; @@ -59,9 +60,12 @@ export const CycleAnalyticsProgress: FC = observer((pro // router const searchParams = useSearchParams(); const peekCycle = searchParams.get("peekCycle") || undefined; + // hooks + const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates(); const { getPlotTypeByCycleId, getEstimateTypeByCycleId, + setPlotType, getCycleById, fetchCycleDetails, fetchArchivedCycleDetails, @@ -70,11 +74,17 @@ export const CycleAnalyticsProgress: FC = observer((pro const { issuesFilter: { issueFilters, updateFilters }, } = useIssues(EIssuesStoreType.CYCLE); + // state + const [loader, setLoader] = useState(false); // derived values const cycleDetails = validateCycleSnapshot(getCycleById(cycleId)); const plotType: TCyclePlotType = getPlotTypeByCycleId(cycleId); const estimateType = getEstimateTypeByCycleId(cycleId); + const isCurrentProjectEstimateEnabled = projectId && areEstimateEnabledByProjectId(projectId) ? true : false; + const estimateDetails = + isCurrentProjectEstimateEnabled && currentActiveEstimateId && estimateById(currentActiveEstimateId); + const isCurrentEstimateTypeIsPoints = estimateDetails && estimateDetails?.type === EEstimateSystem.POINTS; const completedIssues = cycleDetails?.completed_issues || 0; const totalIssues = cycleDetails?.total_issues || 0; @@ -122,13 +132,15 @@ export const CycleAnalyticsProgress: FC = observer((pro setEstimateType(cycleId, value); if (!workspaceSlug || !projectId || !cycleId) return; try { + setLoader(true); if (isArchived) { await fetchArchivedCycleDetails(workspaceSlug, projectId, cycleId); } else { await fetchCycleDetails(workspaceSlug, projectId, cycleId); } - } catch (err) { - console.error(err); + setLoader(false); + } catch (error) { + setLoader(false); setEstimateType(cycleId, estimateType); } }; @@ -206,15 +218,16 @@ export const CycleAnalyticsProgress: FC = observer((pro ))} -
-
- Done - {progressHeaderPercentage}% -
-
- +
{/* progress detailed view */} {chartDistributionData && ( diff --git a/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx b/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx index 708b9c217b3..0c6ad1fd81f 100644 --- a/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx +++ b/web/core/components/cycles/analytics-sidebar/sidebar-details.tsx @@ -3,10 +3,10 @@ import React, { FC } from "react"; import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react"; import { LayersIcon, SquareUser, Users } from "lucide-react"; -// types -import { ICycle } from "@plane/types"; // ui +import { ICycle } from "@plane/types"; import { Avatar, AvatarGroup, TextArea } from "@plane/ui"; +// types // hooks import { useMember, useProjectEstimates } from "@/hooks/store"; // plane web diff --git a/web/core/components/editor/index.ts b/web/core/components/editor/index.ts index 0b14bd13570..72e92a6a8ac 100644 --- a/web/core/components/editor/index.ts +++ b/web/core/components/editor/index.ts @@ -1,3 +1,2 @@ export * from "./lite-text-editor"; -export * from "./pdf"; export * from "./rich-text-editor"; diff --git a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx index f8e1f3bde22..8036e4c8d4e 100644 --- a/web/core/components/editor/lite-text-editor/lite-text-editor.tsx +++ b/web/core/components/editor/lite-text-editor/lite-text-editor.tsx @@ -1,6 +1,6 @@ import React from "react"; // editor -import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef, TNonColorEditorCommands } from "@plane/editor"; +import { EditorRefApi, ILiteTextEditor, LiteTextEditorWithRef } from "@plane/editor"; // types import { IUserLite } from "@plane/types"; // components @@ -87,9 +87,7 @@ export const LiteTextEditor = React.forwardRef { if (isMutableRefObject(ref)) { - ref.current?.executeMenuItemCommand({ - itemKey: key as TNonColorEditorCommands, - }); + ref.current?.executeMenuItemCommand(key); } }} handleAccessChange={handleAccessChange} diff --git a/web/core/components/editor/lite-text-editor/toolbar.tsx b/web/core/components/editor/lite-text-editor/toolbar.tsx index ecf8c3283c8..58eef6f13b5 100644 --- a/web/core/components/editor/lite-text-editor/toolbar.tsx +++ b/web/core/components/editor/lite-text-editor/toolbar.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState, useCallback } from "react"; import { Globe2, Lock, LucideIcon } from "lucide-react"; // editor -import { EditorRefApi, TEditorCommands, TNonColorEditorCommands } from "@plane/editor"; +import { EditorRefApi, TEditorCommands } from "@plane/editor"; // ui import { Button, Tooltip } from "@plane/ui"; // constants @@ -69,9 +69,7 @@ export const IssueCommentToolbar: React.FC = (props) => { .flat() .forEach((item) => { // Assert that editorRef.current is not null - newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive({ - itemKey: item.key as TNonColorEditorCommands, - }); + newActiveStates[item.key] = (editorRef.current as EditorRefApi).isMenuItemActive(item.key); }); setActiveStates(newActiveStates); } diff --git a/web/core/components/editor/pdf/document.tsx b/web/core/components/editor/pdf/document.tsx deleted file mode 100644 index 4dca9e6d53c..00000000000 --- a/web/core/components/editor/pdf/document.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import { Document, Font, Page, PageProps } from "@react-pdf/renderer"; -import { Html } from "react-pdf-html"; -// constants -import { EDITOR_PDF_DOCUMENT_STYLESHEET } from "@/constants/editor"; - -Font.register({ - family: "Inter", - fonts: [ - { src: "/fonts/inter/thin.ttf", fontWeight: "thin" }, - { src: "/fonts/inter/thin.ttf", fontWeight: "thin", fontStyle: "italic" }, - { src: "/fonts/inter/ultralight.ttf", fontWeight: "ultralight" }, - { src: "/fonts/inter/ultralight.ttf", fontWeight: "ultralight", fontStyle: "italic" }, - { src: "/fonts/inter/light.ttf", fontWeight: "light" }, - { src: "/fonts/inter/light.ttf", fontWeight: "light", fontStyle: "italic" }, - { src: "/fonts/inter/regular.ttf", fontWeight: "normal" }, - { src: "/fonts/inter/regular.ttf", fontWeight: "normal", fontStyle: "italic" }, - { src: "/fonts/inter/medium.ttf", fontWeight: "medium" }, - { src: "/fonts/inter/medium.ttf", fontWeight: "medium", fontStyle: "italic" }, - { src: "/fonts/inter/semibold.ttf", fontWeight: "semibold" }, - { src: "/fonts/inter/semibold.ttf", fontWeight: "semibold", fontStyle: "italic" }, - { src: "/fonts/inter/bold.ttf", fontWeight: "bold" }, - { src: "/fonts/inter/bold.ttf", fontWeight: "bold", fontStyle: "italic" }, - { src: "/fonts/inter/extrabold.ttf", fontWeight: "ultrabold" }, - { src: "/fonts/inter/extrabold.ttf", fontWeight: "ultrabold", fontStyle: "italic" }, - { src: "/fonts/inter/heavy.ttf", fontWeight: "heavy" }, - { src: "/fonts/inter/heavy.ttf", fontWeight: "heavy", fontStyle: "italic" }, - ], -}); - -type Props = { - content: string; - pageFormat: PageProps["size"]; -}; - -export const PDFDocument: React.FC = (props) => { - const { content, pageFormat } = props; - - return ( - - - {content} - - - ); -}; diff --git a/web/core/components/editor/pdf/index.ts b/web/core/components/editor/pdf/index.ts deleted file mode 100644 index fe6d89c0eb9..00000000000 --- a/web/core/components/editor/pdf/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./document"; diff --git a/web/core/components/inbox/content/inbox-issue-header.tsx b/web/core/components/inbox/content/inbox-issue-header.tsx index 19a39e5e92c..efd0e8ee1e3 100644 --- a/web/core/components/inbox/content/inbox-issue-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-header.tsx @@ -89,12 +89,6 @@ export const InboxIssueActionsHeader: FC = observer((p const canDelete = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT, workspaceSlug, projectId) || issue?.created_by === currentUser?.id; - const isProjectAdmin = allowPermissions( - [EUserPermissions.ADMIN], - EUserPermissionsLevel.PROJECT, - workspaceSlug, - projectId - ); const isAcceptedOrDeclined = inboxIssue?.status ? [-1, 1, 2].includes(inboxIssue.status) : undefined; // days left for snooze const numberOfDaysLeft = findHowManyDaysLeft(inboxIssue?.snoozed_till); @@ -205,17 +199,6 @@ export const InboxIssueActionsHeader: FC = observer((p [handleInboxIssueNavigation] ); - const handleActionWithPermission = (isAdmin: boolean, action: () => void, errorMessage: string) => { - if (isAdmin) action(); - else { - setToast({ - type: TOAST_TYPE.ERROR, - title: "Permission denied", - message: errorMessage, - }); - } - }; - useEffect(() => { if (!isNotificationEmbed) document.addEventListener("keydown", onKeyDown); return () => { @@ -310,13 +293,7 @@ export const InboxIssueActionsHeader: FC = observer((p size="sm" prependIcon={} className="text-green-500 border-0.5 border-green-500 bg-green-500/20 focus:bg-green-500/20 focus:text-green-500 hover:bg-green-500/40 bg-opacity-20" - onClick={() => - handleActionWithPermission( - isProjectAdmin, - () => setAcceptIssueModal(true), - "Only project admins can accept issues" - ) - } + onClick={() => setAcceptIssueModal(true)} > Accept @@ -330,13 +307,7 @@ export const InboxIssueActionsHeader: FC = observer((p size="sm" prependIcon={} className="text-red-500 border-0.5 border-red-500 bg-red-500/20 focus:bg-red-500/20 focus:text-red-500 hover:bg-red-500/40 bg-opacity-20" - onClick={() => - handleActionWithPermission( - isProjectAdmin, - () => setDeclineIssueModal(true), - "Only project admins can deny issues" - ) - } + onClick={() => setDeclineIssueModal(true)} > Decline @@ -370,15 +341,7 @@ export const InboxIssueActionsHeader: FC = observer((p {isAllowed && ( {canMarkAsAccepted && ( - - handleActionWithPermission( - isProjectAdmin, - handleIssueSnoozeAction, - "Only project admins can snooze/Un-snooze issues" - ) - } - > +
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0 @@ -388,15 +351,7 @@ export const InboxIssueActionsHeader: FC = observer((p )} {canMarkAsDuplicate && ( - - handleActionWithPermission( - isProjectAdmin, - () => setSelectDuplicateIssue(true), - "Only project admins can mark issues as duplicate" - ) - } - > + setSelectDuplicateIssue(true)}>
Mark as duplicate @@ -446,8 +401,6 @@ export const InboxIssueActionsHeader: FC = observer((p setIsMobileSidebar={setIsMobileSidebar} isNotificationEmbed={isNotificationEmbed} embedRemoveCurrentNotification={embedRemoveCurrentNotification} - isProjectAdmin={isProjectAdmin} - handleActionWithPermission={handleActionWithPermission} />
diff --git a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx index 7a66d0976f8..e87573e9be0 100644 --- a/web/core/components/inbox/content/inbox-issue-mobile-header.tsx +++ b/web/core/components/inbox/content/inbox-issue-mobile-header.tsx @@ -47,8 +47,6 @@ type Props = { setIsMobileSidebar: (value: boolean) => void; isNotificationEmbed: boolean; embedRemoveCurrentNotification?: () => void; - isProjectAdmin: boolean; - handleActionWithPermission: (isAdmin: boolean, action: () => void, errorMessage: string) => void; }; export const InboxIssueActionsMobileHeader: React.FC = observer((props) => { @@ -72,8 +70,6 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) = setIsMobileSidebar, isNotificationEmbed, embedRemoveCurrentNotification, - isProjectAdmin, - handleActionWithPermission, } = props; const router = useAppRouter(); const issue = inboxIssue?.issue; @@ -143,15 +139,7 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) =
)} {canMarkAsAccepted && !isAcceptedOrDeclined && ( - - handleActionWithPermission( - isProjectAdmin, - handleIssueSnoozeAction, - "Only project admins can snooze/Un-snooze issues" - ) - } - > +
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0 ? "Un-snooze" : "Snooze"} @@ -159,15 +147,7 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) = )} {canMarkAsDuplicate && !isAcceptedOrDeclined && ( - - handleActionWithPermission( - isProjectAdmin, - () => setSelectDuplicateIssue(true), - "Only project admins can mark issues as duplicate" - ) - } - > + setSelectDuplicateIssue(true)}>
Mark as duplicate @@ -175,15 +155,7 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) = )} {canMarkAsAccepted && ( - - handleActionWithPermission( - isProjectAdmin, - () => setAcceptIssueModal(true), - "Only project admins can accept issues" - ) - } - > + setAcceptIssueModal(true)}>
Accept @@ -191,15 +163,7 @@ export const InboxIssueActionsMobileHeader: React.FC = observer((props) = )} {canMarkAsDeclined && ( - - handleActionWithPermission( - isProjectAdmin, - () => setDeclineIssueModal(true), - "Only project admins can deny issues" - ) - } - > + setDeclineIssueModal(true)}>
Decline diff --git a/web/core/components/inbox/content/root.tsx b/web/core/components/inbox/content/root.tsx index 504b1d593a7..852be8a80b8 100644 --- a/web/core/components/inbox/content/root.tsx +++ b/web/core/components/inbox/content/root.tsx @@ -62,10 +62,10 @@ export const InboxContentRoot: FC = observer((props) => { } ); - const isEditable = - allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT) || - inboxIssue.created_by === currentUser?.id; - + const isEditable = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], + EUserPermissionsLevel.PROJECT + ); const isGuest = projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId) === EUserPermissions.GUEST; const isOwner = inboxIssue?.issue.created_by === currentUser?.id; const readOnly = !isOwner && isGuest; diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/workspace-draft-root.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/workspace-draft-root.tsx deleted file mode 100644 index feccca5f61c..00000000000 --- a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/workspace-draft-root.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React, { useCallback } from "react"; -import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import { WorkspaceDraftIssueQuickActions } from "@/components/issues/issue-layouts/quick-action-dropdowns"; -import { IssuePeekOverview } from "@/components/issues/peek-overview"; -import { EIssuesStoreType } from "@/constants/issue"; -import { useUserPermissions } from "@/hooks/store"; -import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; -import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; -import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; -import { BaseListRoot } from "../../../list/base-list-root"; - -export const WorkspaceDraftIssueLayoutRoot = observer(() => { - // router - const { workspaceSlug } = useParams(); - - //swr hook for fetching issue properties - useWorkspaceIssueProperties(workspaceSlug); - // store - const { allowPermissions } = useUserPermissions(); - - const canEditProperties = useCallback( - (projectId: string | undefined) => { - if (!projectId) return false; - return allowPermissions( - [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - EUserPermissionsLevel.PROJECT, - workspaceSlug.toString(), - projectId - ); - }, - [workspaceSlug, allowPermissions] - ); - - return ( - -
-
- - -
-
-
- ); -}); diff --git a/web/core/components/issues/issue-layouts/list/base-list-root.tsx b/web/core/components/issues/issue-layouts/list/base-list-root.tsx index d73ca6ff7be..a97ad0a8e05 100644 --- a/web/core/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/core/components/issues/issue-layouts/list/base-list-root.tsx @@ -25,8 +25,7 @@ type ListStoreType = | EIssuesStoreType.PROJECT_VIEW | EIssuesStoreType.DRAFT | EIssuesStoreType.PROFILE - | EIssuesStoreType.ARCHIVED - | EIssuesStoreType.WORKSPACE_DRAFT; + | EIssuesStoreType.ARCHIVED; interface IBaseListRoot { QuickActions: FC; addIssuesToView?: (issueIds: string[]) => Promise; @@ -62,9 +61,8 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { const showEmptyGroup = displayFilters?.show_empty_groups ?? false; const { workspaceSlug, projectId } = useParams(); - const { updateFilters } = useIssuesActions(storeType); - const collapsedGroups = - issuesFilter?.issueFilters?.kanbanFilters || ({ group_by: [], sub_group_by: [] } as TIssueKanbanFilters); + const {updateFilters} = useIssuesActions(storeType); + const collapsedGroups = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] } as TIssueKanbanFilters; useEffect(() => { fetchIssues("init-loader", { canGroup: true, perPageCount: group_by ? 50 : 100 }, viewId); @@ -124,14 +122,15 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { } else { collapsedGroups.push(value); } - updateFilters(projectId?.toString() ?? "", EIssueFilterType.KANBAN_FILTERS, { - group_by: collapsedGroups, - } as TIssueKanbanFilters); + updateFilters(projectId?.toString() ?? "", EIssueFilterType.KANBAN_FILTERS, + { group_by: collapsedGroups } as TIssueKanbanFilters + ); } }, [workspaceSlug, issuesFilter, projectId, updateFilters] ); + return (
diff --git a/web/core/components/issues/issue-layouts/list/default.tsx b/web/core/components/issues/issue-layouts/list/default.tsx index fca8d68eb25..befa1f8fdbc 100644 --- a/web/core/components/issues/issue-layouts/list/default.tsx +++ b/web/core/components/issues/issue-layouts/list/default.tsx @@ -49,7 +49,7 @@ export interface IList { isCompletedCycle?: boolean; loadMoreIssues: (groupId?: string) => void; handleCollapsedGroups: (value: string) => void; - collapsedGroups: TIssueKanbanFilters; + collapsedGroups : TIssueKanbanFilters; } export const List: React.FC = observer((props) => { @@ -71,7 +71,7 @@ export const List: React.FC = observer((props) => { isCompletedCycle = false, loadMoreIssues, handleCollapsedGroups, - collapsedGroups, + collapsedGroups } = props; const storeType = useIssueStoreType(); @@ -133,6 +133,7 @@ export const List: React.FC = observer((props) => { } else { entities = orderedGroups; } + return (
{groups && ( diff --git a/web/core/components/issues/issue-layouts/list/list-view-types.d.ts b/web/core/components/issues/issue-layouts/list/list-view-types.d.ts index 089623ed931..6597855f6f8 100644 --- a/web/core/components/issues/issue-layouts/list/list-view-types.d.ts +++ b/web/core/components/issues/issue-layouts/list/list-view-types.d.ts @@ -9,7 +9,6 @@ export interface IQuickActionProps { handleRemoveFromView?: () => Promise; handleArchive?: () => Promise; handleRestore?: () => Promise; - handleMoveToIssues?: () => Promise; customActionButton?: React.ReactElement; portalElement?: HTMLDivElement | null; readOnly?: boolean; diff --git a/web/core/components/issues/issue-layouts/properties/all-properties.tsx b/web/core/components/issues/issue-layouts/properties/all-properties.tsx index 36bb6fafccb..e439635feeb 100644 --- a/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -37,7 +37,9 @@ import { WithDisplayPropertiesHOC } from "./with-display-properties-HOC"; export interface IIssueProperties { issue: TIssue; - updateIssue: ((projectId: string | null, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null, issueId: string, data: Partial) => Promise) + | undefined; displayProperties: IIssueDisplayProperties | undefined; isReadOnly: boolean; className: string; diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts b/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts index dbc1e9f5a74..212a43f91c3 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts @@ -4,4 +4,3 @@ export * from "./project-issue"; export * from "./archived-issue"; export * from "./draft-issue"; export * from "./all-issue"; -export * from "../../workspace-draft/quick-action"; diff --git a/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx index ab4b73df4d2..0ee9d96fd44 100644 --- a/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/core/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -18,7 +18,7 @@ import { SPREADSHEET_SELECT_GROUP } from "@/constants/spreadsheet"; // helper import { cn } from "@/helpers/common.helper"; // hooks -import { useIssueDetail, useIssues, useProject } from "@/hooks/store"; +import { useIssueDetail, useProject } from "@/hooks/store"; import useIssuePeekOverviewRedirection from "@/hooks/use-issue-peek-overview-redirection"; import { TSelectionHelper } from "@/hooks/use-multiple-select"; import { usePlatformOS } from "@/hooks/use-platform-os"; @@ -26,7 +26,6 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; import { IssueIdentifier } from "@/plane-web/components/issues"; // local components import { TRenderQuickActions } from "../list/list-view-types"; -import { isIssueNew } from "../utils"; import { IssueColumn } from "./issue-column"; interface Props { @@ -43,7 +42,6 @@ interface Props { spreadsheetColumnsList: (keyof IIssueDisplayProperties)[]; spacingLeft?: number; selectionHelpers: TSelectionHelper; - shouldRenderByDefault?: boolean; } export const SpreadsheetIssueRow = observer((props: Props) => { @@ -61,14 +59,11 @@ export const SpreadsheetIssueRow = observer((props: Props) => { spreadsheetColumnsList, spacingLeft = 6, selectionHelpers, - shouldRenderByDefault, } = props; // states const [isExpanded, setExpanded] = useState(false); // store hooks const { subIssues: subIssuesStore } = useIssueDetail(); - const { issueMap } = useIssues(); - // derived values const subIssues = subIssuesStore.subIssuesByIssueId(issueId); const isIssueSelected = selectionHelpers.getIsEntitySelected(issueId); @@ -93,7 +88,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => { })} verticalOffset={100} shouldRecordHeights={false} - defaultValue={shouldRenderByDefault || isIssueNew(issueMap[issueId])} > { containerRef={containerRef} spreadsheetColumnsList={spreadsheetColumnsList} selectionHelpers={selectionHelpers} - shouldRenderByDefault={isExpanded} /> ))} diff --git a/web/core/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index afbebcebad7..0d1763011c4 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -16,6 +16,7 @@ import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { useEventTracker, useCycle, useIssues, useModule, useIssueDetail, useUser } from "@/hooks/store"; import { useIssueStoreType } from "@/hooks/use-issue-layout-store"; import { useIssuesActions } from "@/hooks/use-issues-actions"; +import useLocalStorage from "@/hooks/use-local-storage"; // local components import { DraftIssueLayout } from "./draft-issue-layout"; import { IssueFormRoot } from "./form"; @@ -49,11 +50,15 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( const { fetchModuleDetails } = useModule(); const { issues } = useIssues(storeType); const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT); - const { issues: draftIssues } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT); + const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT); const { fetchIssue } = useIssueDetail(); const { handleCreateUpdatePropertyValues } = useIssueModal(); // pathname const pathname = usePathname(); + // local storage + const { storedValue: localStorageDraftIssues, setValue: setLocalStorageDraftIssue } = useLocalStorage< + Record> + >("draftedIssue", {}); // current store details const { createIssue, updateIssue } = useIssuesActions(storeType); // derived values @@ -65,7 +70,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( if (!workspaceSlug) return; if (!projectId || issueId === undefined || !fetchIssueDetails) { - // Set description to the issue description from the props if available + // Set description to the issue description from the props if available setDescription(data?.description_html || "

"); return; } @@ -123,9 +128,14 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( setCreateMore(value); }; - const handleClose = (saveAsDraft?: boolean) => { - if (changesMade && saveAsDraft && !data) { - handleCreateIssue(changesMade, true); + const handleClose = (saveDraftIssueInLocalStorage?: boolean) => { + if (changesMade && saveDraftIssueInLocalStorage) { + // updating the current edited issue data in the local storage + let draftIssues = localStorageDraftIssues ? localStorageDraftIssues : {}; + if (workspaceSlug) { + draftIssues = { ...draftIssues, [workspaceSlug.toString()]: changesMade }; + setLocalStorageDraftIssue(draftIssues); + } } setActiveProjectId(null); @@ -140,10 +150,11 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( if (!workspaceSlug || !payload.project_id) return; try { - let response: TIssue | undefined; + let response; + // if draft issue, use draft issue store to create issue if (is_draft_issue) { - response = (await draftIssues.createIssue(workspaceSlug.toString(), payload)) as TIssue; + response = await draftIssues.createIssue(workspaceSlug.toString(), payload.project_id, payload); } // if cycle id in payload does not match the cycleId in url // or if the moduleIds in Payload does not match the moduleId in url @@ -202,8 +213,8 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( payload: { ...response, state: "SUCCESS" }, path: pathname, }); - if (!createMore) handleClose(); - if (createMore && issueTitleRef) issueTitleRef?.current?.focus(); + !createMore && handleClose(); + if (createMore) issueTitleRef && issueTitleRef?.current?.focus(); setDescription("

"); setChangesMade(null); return response; @@ -226,8 +237,9 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( if (!workspaceSlug || !payload.project_id || !data?.id) return; try { - if (isDraft) await draftIssues.updateIssue(workspaceSlug.toString(), data.id, payload); - else if (updateIssue) await updateIssue(payload.project_id, data.id, payload); + isDraft + ? await draftIssues.updateIssue(workspaceSlug.toString(), payload.project_id, data.id, payload) + : updateIssue && (await updateIssue(payload.project_id, data.id, payload)); // add other property values await handleCreateUpdatePropertyValues({ @@ -248,7 +260,6 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( }); handleClose(); } catch (error) { - console.error(error); setToast({ type: TOAST_TYPE.ERROR, title: "Error!", @@ -303,7 +314,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( issueTitleRef={issueTitleRef} onChange={handleFormChange} onClose={handleClose} - onSubmit={(payload) => handleFormSubmit(payload, isDraft)} + onSubmit={handleFormSubmit} projectId={activeProjectId} isCreateMoreToggleEnabled={createMore} onCreateMoreToggleChange={handleCreateMoreToggleChange} @@ -318,10 +329,10 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId.toString() : null, module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId.toString()] : null, }} - onClose={handleClose} + onClose={() => handleClose(false)} isCreateMoreToggleEnabled={createMore} onCreateMoreToggleChange={handleCreateMoreToggleChange} - onSubmit={(payload) => handleFormSubmit(payload, isDraft)} + onSubmit={handleFormSubmit} projectId={activeProjectId} isDraft={isDraft} /> diff --git a/web/core/components/issues/issue-modal/draft-issue-layout.tsx b/web/core/components/issues/issue-modal/draft-issue-layout.tsx index c3a7c180557..49bb1734de5 100644 --- a/web/core/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/core/components/issues/issue-modal/draft-issue-layout.tsx @@ -14,7 +14,9 @@ import { ConfirmIssueDiscard } from "@/components/issues"; import { isEmptyHtmlString } from "@/helpers/string.helper"; // hooks import { useIssueModal } from "@/hooks/context/use-issue-modal"; -import { useEventTracker, useWorkspaceDraftIssues } from "@/hooks/store"; +import { useEventTracker } from "@/hooks/store"; +// services +import { IssueDraftService } from "@/services/issue"; // local components import { IssueFormRoot } from "./form"; @@ -31,6 +33,8 @@ export interface DraftIssueProps { isDraft: boolean; } +const issueDraftService = new IssueDraftService(); + export const DraftIssueLayout: React.FC = observer((props) => { const { changesMade, @@ -53,7 +57,6 @@ export const DraftIssueLayout: React.FC = observer((props) => { // store hooks const { captureIssueEvent } = useEventTracker(); const { handleCreateUpdatePropertyValues } = useIssueModal(); - const { createIssue } = useWorkspaceDraftIssues(); const handleClose = () => { if (data?.id) { @@ -92,10 +95,10 @@ export const DraftIssueLayout: React.FC = observer((props) => { const payload = { ...changesMade, name: changesMade?.name && changesMade?.name?.trim() !== "" ? changesMade.name?.trim() : "Untitled", - project_id: projectId, }; - const response = await createIssue(workspaceSlug.toString(), payload) + const response = await issueDraftService + .createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload) .then((res) => { setToast({ type: TOAST_TYPE.SUCCESS, diff --git a/web/core/components/issues/issue-modal/form.tsx b/web/core/components/issues/issue-modal/form.tsx index 615d935f561..35785d3487d 100644 --- a/web/core/components/issues/issue-modal/form.tsx +++ b/web/core/components/issues/issue-modal/form.tsx @@ -266,9 +266,7 @@ export const IssueFormRoot: FC = observer((props) => { )}
handleFormSubmit(data))}>
-

- {data?.id ? "Update" : isDraft ? "Create draft" : "Create new"} issue -

+

{data?.id ? "Update" : "Create new"} issue

{/* Disable project selection if editing an issue */}
= observer((props) => { > Discard + {isDraft && ( + <> + {data?.id ? ( + + ) : ( + + )} + + )}
diff --git a/web/core/components/issues/peek-overview/issue-detail.tsx b/web/core/components/issues/peek-overview/issue-detail.tsx index 242ebfd0cee..20117d10f51 100644 --- a/web/core/components/issues/peek-overview/issue-detail.tsx +++ b/web/core/components/issues/peek-overview/issue-detail.tsx @@ -1,7 +1,7 @@ import { FC, useEffect } from "react"; import { observer } from "mobx-react"; // components -import { IssueParentDetail, TIssueOperations } from "@/components/issues"; +import { TIssueOperations } from "@/components/issues"; // store hooks import { useIssueDetail, useUser } from "@/hooks/store"; // hooks @@ -57,15 +57,6 @@ export const PeekOverviewIssueDetails: FC = observer( return (
- {issue.parent_id && ( - - )} void; - dataId?: string | null | undefined; - data?: TWorkspaceDraftIssue; - isSubIssue?: boolean; - onSubmit?: () => Promise; -}; - -export const WorkspaceDraftIssueDeleteIssueModal: React.FC = (props) => { - const { dataId, data, isOpen, handleClose, isSubIssue = false, onSubmit } = props; - // states - const [isDeleting, setIsDeleting] = useState(false); - // store hooks - const { issueMap } = useIssues(); - const { getProjectById } = useProject(); - const { allowPermissions } = useUserPermissions(); - - const { data: currentUser } = useUser(); - - // derived values - const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); - - useEffect(() => { - setIsDeleting(false); - }, [isOpen]); - - if (!dataId && !data) return null; - - // derived values - const issue = data ? data : issueMap[dataId!]; - const projectDetails = getProjectById(issue?.project_id); - const isIssueCreator = issue?.created_by === currentUser?.id; - const authorized = isIssueCreator || canPerformProjectAdminActions; - - const onClose = () => { - setIsDeleting(false); - handleClose(); - }; - - const handleIssueDelete = async () => { - setIsDeleting(true); - - if (!authorized) { - setToast({ - title: PROJECT_ERROR_MESSAGES.permissionError.title, - type: TOAST_TYPE.ERROR, - message: PROJECT_ERROR_MESSAGES.permissionError.message, - }); - onClose(); - return; - } - if (onSubmit) - await onSubmit() - .then(() => { - setToast({ - type: TOAST_TYPE.SUCCESS, - title: "Success!", - message: `${isSubIssue ? "Sub-issue" : "Issue"} deleted successfully`, - }); - onClose(); - }) - .catch((errors) => { - const isPermissionError = errors?.error === "Only admin or creator can delete the issue"; - const currentError = isPermissionError - ? PROJECT_ERROR_MESSAGES.permissionError - : PROJECT_ERROR_MESSAGES.issueDeleteError; - setToast({ - title: currentError.title, - type: TOAST_TYPE.ERROR, - message: currentError.message, - }); - }) - .finally(() => onClose()); - }; - - return ( - - Are you sure you want to delete issue{" "} - {projectDetails?.identifier} - {""}? All of the data related to the issue will be permanently removed. This action cannot be undone. - - } - /> - ); -}; diff --git a/web/core/components/issues/workspace-draft/draft-issue-block.tsx b/web/core/components/issues/workspace-draft/draft-issue-block.tsx deleted file mode 100644 index 8be73154d6d..00000000000 --- a/web/core/components/issues/workspace-draft/draft-issue-block.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; -import React, { FC, useRef } from "react"; -import { observer } from "mobx-react"; -// ui -import { Row, Tooltip } from "@plane/ui"; -// helper -import { cn } from "@/helpers/common.helper"; -// hooks -import { useAppTheme, useProject, useWorkspaceDraftIssues } from "@/hooks/store"; -// plane-web components -import { IdentifierText } from "ce/components/issues"; -// local components -import { WorkspaceDraftIssueQuickActions } from "../issue-layouts"; -import { DraftIssueProperties } from "./draft-issue-properties"; - -type Props = { - workspaceSlug: string; - issueId: string; -}; - -export const DraftIssueBlock: FC = observer((props) => { - // props - const { workspaceSlug, issueId } = props; - // hooks - const { getIssueById, updateIssue, deleteIssue, moveIssue } = useWorkspaceDraftIssues(); - const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); - const { getProjectIdentifierById } = useProject(); - // ref - const issueRef = useRef(null); - // derived values - const issue = getIssueById(issueId); - const projectIdentifier = (issue && issue.project_id && getProjectIdentifierById(issue.project_id)) || undefined; - if (!issue || !projectIdentifier) return null; - - return ( -
- -
-
-
- {/* {displayProperties && (displayProperties.key || displayProperties.issue_type) && ( */} -
- {issue.project_id && ( -
- -
- )} -
- {/* )} */} - - {/* sub-issues chevron */} -
-
- - -

{issue.name}

-
-
- - {/* quick actions */} -
- updateIssue(workspaceSlug, issueId, data)} - handleDelete={async () => deleteIssue(workspaceSlug, issueId)} - handleMoveToIssues={async () => moveIssue(workspaceSlug, issueId, issue)} - /> -
-
- -
- { - await updateIssue(workspaceSlug, issueId, data); - }} - activeLayout="List" - /> -
{ - e.preventDefault(); - e.stopPropagation(); - }} - > - updateIssue(workspaceSlug, issueId, data)} - handleDelete={async () => deleteIssue(workspaceSlug, issueId)} - handleMoveToIssues={async () => moveIssue(workspaceSlug, issueId, issue)} - /> -
-
- -
- ); -}); diff --git a/web/core/components/issues/workspace-draft/draft-issue-properties.tsx b/web/core/components/issues/workspace-draft/draft-issue-properties.tsx deleted file mode 100644 index 7150012764c..00000000000 --- a/web/core/components/issues/workspace-draft/draft-issue-properties.tsx +++ /dev/null @@ -1,299 +0,0 @@ -"use client"; - -import { useCallback, useMemo } from "react"; -import xor from "lodash/xor"; -import { observer } from "mobx-react"; -import { useParams, usePathname } from "next/navigation"; -// icons -import { CalendarCheck2, CalendarClock } from "lucide-react"; -// types -import { TIssue, TIssuePriorities, TWorkspaceDraftIssue } from "@plane/types"; -// components -import { - DateDropdown, - EstimateDropdown, - PriorityDropdown, - MemberDropdown, - ModuleDropdown, - CycleDropdown, - StateDropdown, -} from "@/components/dropdowns"; -// constants -import { ISSUE_UPDATED } from "@/constants/event-tracker"; -// helpers -import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; -import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; -// hooks -import { - useEventTracker, - useLabel, - useProjectState, - useProject, - useProjectEstimates, - useWorkspaceDraftIssues, -} from "@/hooks/store"; -import { usePlatformOS } from "@/hooks/use-platform-os"; -// local components -import { IssuePropertyLabels } from "../issue-layouts"; - -export interface IIssueProperties { - issue: TWorkspaceDraftIssue; - updateIssue: - | ((projectId: string | null, issueId: string, data: Partial) => Promise) - | undefined; - className: string; - activeLayout: string; -} - -export const DraftIssueProperties: React.FC = observer((props) => { - const { issue, updateIssue, activeLayout, className } = props; - // store hooks - const { getProjectById } = useProject(); - const { labelMap } = useLabel(); - const { captureIssueEvent } = useEventTracker(); - const { addCycleToIssue, addModulesToIssue } = useWorkspaceDraftIssues(); - const { areEstimateEnabledByProjectId } = useProjectEstimates(); - const { getStateById } = useProjectState(); - const { isMobile } = usePlatformOS(); - const projectDetails = getProjectById(issue.project_id); - - // router - const { workspaceSlug } = useParams(); - const pathname = usePathname(); - - const currentLayout = `${activeLayout} layout`; - // derived values - const stateDetails = getStateById(issue.state_id); - - const issueOperations = useMemo( - () => ({ - addModulesToIssue: async (moduleIds: string[]) => { - if (!workspaceSlug || !issue.id) return; - await addModulesToIssue(workspaceSlug.toString(), issue.id, moduleIds); - }, - removeModulesFromIssue: async (moduleIds: string[]) => { - if (!workspaceSlug || !issue.id) return; - await addModulesToIssue(workspaceSlug.toString(), issue.id, moduleIds); - }, - addIssueToCycle: async (cycleId: string) => { - if (!workspaceSlug || !issue.id) return; - await addCycleToIssue(workspaceSlug.toString(), issue.id, cycleId); - }, - removeIssueFromCycle: async () => { - if (!workspaceSlug || !issue.id) return; - // TODO: To be checked - await addCycleToIssue(workspaceSlug.toString(), issue.id, ""); - }, - }), - [workspaceSlug, issue, addCycleToIssue, addModulesToIssue] - ); - - const handleState = (stateId: string) => - issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { state_id: stateId }); - - const handlePriority = (value: TIssuePriorities) => - issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { priority: value }); - - const handleLabel = (ids: string[]) => - issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { label_ids: ids }); - - const handleAssignee = (ids: string[]) => - issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { assignee_ids: ids }); - - const handleModule = useCallback( - (moduleIds: string[] | null) => { - if (!issue || !issue.module_ids || !moduleIds) return; - - const updatedModuleIds = xor(issue.module_ids, moduleIds); - const modulesToAdd: string[] = []; - const modulesToRemove: string[] = []; - for (const moduleId of updatedModuleIds) - if (issue.module_ids.includes(moduleId)) modulesToRemove.push(moduleId); - else modulesToAdd.push(moduleId); - if (modulesToAdd.length > 0) issueOperations.addModulesToIssue(modulesToAdd); - if (modulesToRemove.length > 0) issueOperations.removeModulesFromIssue(modulesToRemove); - }, - [issueOperations, currentLayout, pathname, issue] - ); - - const handleCycle = useCallback( - (cycleId: string | null) => { - if (!issue || issue.cycle_id === cycleId) return; - if (cycleId) issueOperations.addIssueToCycle?.(cycleId); - else issueOperations.removeIssueFromCycle?.(); - }, - [issue, issueOperations, currentLayout, pathname] - ); - - const handleStartDate = (date: Date | null) => - issue?.project_id && - updateIssue && - updateIssue(issue.project_id, issue.id, { - start_date: date ? (renderFormattedPayloadDate(date) ?? undefined) : undefined, - }); - - const handleTargetDate = (date: Date | null) => - issue?.project_id && - updateIssue && - updateIssue(issue.project_id, issue.id, { - target_date: date ? (renderFormattedPayloadDate(date) ?? undefined) : undefined, - }); - - const handleEstimate = (value: string | undefined) => - issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { estimate_point: value }); - - if (!issue.project_id) return null; - - const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || []; - - const minDate = getDate(issue.start_date); - minDate?.setDate(minDate.getDate()); - - const maxDate = getDate(issue.target_date); - maxDate?.setDate(maxDate.getDate()); - - const handleEventPropagation = (e: React.MouseEvent) => { - e.stopPropagation(); - e.preventDefault(); - }; - - return ( -
- {/* basic properties */} - {/* state */} -
- -
- - {/* priority */} -
- -
- - {/* label */} - -
- -
- - {/* start date */} -
- } - buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} - optionsClassName="z-10" - renderByDefault={isMobile} - showTooltip - /> -
- - {/* target/due date */} -
- } - buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} - buttonClassName={ - shouldHighlightIssueDueDate(issue?.target_date || null, stateDetails?.group) ? "text-red-500" : "" - } - clearIconClassName="!text-custom-text-100" - optionsClassName="z-10" - renderByDefault={isMobile} - showTooltip - /> -
- - {/* assignee */} -
- 0 ? "transparent-without-text" : "border-without-text"} - buttonClassName={issue.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""} - showTooltip={issue?.assignee_ids?.length === 0} - placeholder="Assignees" - optionsClassName="z-10" - tooltipContent="" - renderByDefault={isMobile} - /> -
- - {/* modules */} - {projectDetails?.module_view && ( -
- -
- )} - - {/* cycles */} - {projectDetails?.cycle_view && ( -
- -
- )} - - {/* estimates */} - {issue.project_id && areEstimateEnabledByProjectId(issue.project_id?.toString()) && ( -
- -
- )} -
- ); -}); diff --git a/web/core/components/issues/workspace-draft/empty-state.tsx b/web/core/components/issues/workspace-draft/empty-state.tsx deleted file mode 100644 index 4a1292d6160..00000000000 --- a/web/core/components/issues/workspace-draft/empty-state.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import { FC, Fragment, useState } from "react"; -// components -import { EmptyState } from "@/components/empty-state"; -import { CreateUpdateIssueModal } from "@/components/issues"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; -import { EIssuesStoreType } from "@/constants/issue"; - -export const WorkspaceDraftEmptyState: FC = () => { - // state - const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); - - return ( - - setIsDraftIssueModalOpen(false)} - isDraft - /> -
- { - setIsDraftIssueModalOpen(true); - }} - /> -
-
- ); -}; diff --git a/web/core/components/issues/workspace-draft/index.ts b/web/core/components/issues/workspace-draft/index.ts deleted file mode 100644 index 07138bc0bc2..00000000000 --- a/web/core/components/issues/workspace-draft/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./draft-issue-block"; -export * from "./draft-issue-properties"; -export * from "./delete-modal"; -export * from "./root"; diff --git a/web/core/components/issues/workspace-draft/loader.tsx b/web/core/components/issues/workspace-draft/loader.tsx deleted file mode 100644 index d663a0d035e..00000000000 --- a/web/core/components/issues/workspace-draft/loader.tsx +++ /dev/null @@ -1,20 +0,0 @@ -"use client"; - -import { FC } from "react"; -// components -import { ListLoaderItemRow } from "@/components/ui"; - -type TWorkspaceDraftIssuesLoader = { - items?: number; -}; - -export const WorkspaceDraftIssuesLoader: FC = (props) => { - const { items = 14 } = props; - return ( -
- {[...Array(items)].map((_, index) => ( - - ))} -
- ); -}; diff --git a/web/core/components/issues/workspace-draft/quick-action.tsx b/web/core/components/issues/workspace-draft/quick-action.tsx deleted file mode 100644 index 5a3f8268830..00000000000 --- a/web/core/components/issues/workspace-draft/quick-action.tsx +++ /dev/null @@ -1,160 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { Placement } from "@popperjs/core"; -import omit from "lodash/omit"; -import { observer } from "mobx-react"; -// icons -import { Copy, Pencil, SquareStackIcon, Trash2 } from "lucide-react"; -// types -import { TWorkspaceDraftIssue } from "@plane/types"; -// ui -import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui"; -// components -import { CreateUpdateIssueModal } from "@/components/issues"; -// constant -import { EIssuesStoreType } from "@/constants/issue"; -// helpers -import { cn } from "@/helpers/common.helper"; -// local components -import { WorkspaceDraftIssueDeleteIssueModal } from "./delete-modal"; - -export interface IQuickActionProps { - issue: TWorkspaceDraftIssue; - handleDelete: () => Promise; - handleUpdate: (payload: Partial) => Promise; - handleMoveToIssues?: () => Promise; - customActionButton?: React.ReactElement; - portalElement?: HTMLDivElement | null; - placements?: Placement; - parentRef: React.RefObject; -} - -export const WorkspaceDraftIssueQuickActions: React.FC = observer((props) => { - const { - issue, - handleDelete, - handleUpdate, - handleMoveToIssues, - customActionButton, - portalElement, - placements = "bottom-end", - parentRef, - } = props; - // states - const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState(undefined); - const [deleteIssueModal, setDeleteIssueModal] = useState(false); - - const duplicateIssuePayload = omit( - { - ...issue, - name: `${issue.name} (copy)`, - is_draft: true, - }, - ["id"] - ); - - const MENU_ITEMS: TContextMenuItem[] = [ - { - key: "edit", - title: "Edit", - icon: Pencil, - action: () => { - setIssueToEdit(issue); - setCreateUpdateIssueModal(true); - }, - }, - { - key: "make-a-copy", - title: "Make a copy", - icon: Copy, - action: () => { - setCreateUpdateIssueModal(true); - }, - }, - { - key: "move-to-issues", - title: "Move to issues", - icon: SquareStackIcon, - action: () => handleMoveToIssues && handleMoveToIssues(), - }, - { - key: "delete", - title: "Delete", - icon: Trash2, - action: () => { - setDeleteIssueModal(true); - }, - }, - ]; - - return ( - <> - setDeleteIssueModal(false)} - onSubmit={handleDelete} - /> - { - setCreateUpdateIssueModal(false); - setIssueToEdit(undefined); - }} - data={issueToEdit ?? duplicateIssuePayload} - onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate(data as TWorkspaceDraftIssue); - }} - storeType={EIssuesStoreType.WORKSPACE_DRAFT} - fetchIssueDetails={false} - isDraft - /> - - - {MENU_ITEMS.map((item) => ( - { - e.preventDefault(); - e.stopPropagation(); - item.action(); - }} - className={cn( - "flex items-center gap-2", - { - "text-custom-text-400": item.disabled, - }, - item.className - )} - disabled={item.disabled} - > - {item.icon && } -
-
{item.title}
- {item.description && ( -

- {item.description} -

- )} -
-
- ))} -
- - ); -}); diff --git a/web/core/components/issues/workspace-draft/root.tsx b/web/core/components/issues/workspace-draft/root.tsx deleted file mode 100644 index bfb70fc45dc..00000000000 --- a/web/core/components/issues/workspace-draft/root.tsx +++ /dev/null @@ -1,90 +0,0 @@ -"use client"; - -import { FC, Fragment } from "react"; -import { observer } from "mobx-react"; -import useSWR from "swr"; -// components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; -import { EDraftIssuePaginationType } from "@/constants/workspace-drafts"; -// helpers -import { cn } from "@/helpers/common.helper"; -// hooks -import { useCommandPalette, useProject, useWorkspaceDraftIssues } from "@/hooks/store"; -// components -import { DraftIssueBlock } from "./draft-issue-block"; -import { WorkspaceDraftEmptyState } from "./empty-state"; -import { WorkspaceDraftIssuesLoader } from "./loader"; - -type TWorkspaceDraftIssuesRoot = { - workspaceSlug: string; -}; - -export const WorkspaceDraftIssuesRoot: FC = observer((props) => { - const { workspaceSlug } = props; - // hooks - const { loader, paginationInfo, fetchIssues, issueIds } = useWorkspaceDraftIssues(); - const { workspaceProjectIds } = useProject(); - const { toggleCreateProjectModal } = useCommandPalette(); - - // fetching issues - useSWR( - workspaceSlug && issueIds.length <= 0 ? `WORKSPACE_DRAFT_ISSUES_${workspaceSlug}` : null, - workspaceSlug && issueIds.length <= 0 ? async () => await fetchIssues(workspaceSlug, "init-loader") : null - ); - - // handle nest issues - const handleNextIssues = async () => { - if (!paginationInfo?.next_page_results) return; - await fetchIssues(workspaceSlug, "pagination", EDraftIssuePaginationType.NEXT); - }; - - if (loader === "init-loader" && issueIds.length <= 0) { - return ; - } - - if (workspaceProjectIds?.length === 0) - return ( - { - toggleCreateProjectModal(true); - }} - /> - ); - - if (loader === "empty-state" && issueIds.length <= 0) return ; - - return ( -
-
- {issueIds.map((issueId: string) => ( - - ))} -
- - {paginationInfo?.next_page_results && ( - - {loader === "pagination" && issueIds.length >= 0 ? ( - - ) : ( -
- Load More ↓ -
- )} -
- )} -
- ); -}); diff --git a/web/core/components/pages/editor/header/color-dropdown.tsx b/web/core/components/pages/editor/header/color-dropdown.tsx deleted file mode 100644 index 6be2b1f7b39..00000000000 --- a/web/core/components/pages/editor/header/color-dropdown.tsx +++ /dev/null @@ -1,129 +0,0 @@ -"use client"; - -import { memo } from "react"; -import { ALargeSmall, Ban } from "lucide-react"; -import { Popover } from "@headlessui/react"; -// plane editor -import { COLORS_LIST, TColorEditorCommands } from "@plane/editor"; -// helpers -import { cn } from "@/helpers/common.helper"; - -type Props = { - handleColorSelect: (key: TColorEditorCommands, color: string | undefined) => void; - isColorActive: (key: TColorEditorCommands, color: string | undefined) => boolean; -}; - -export const ColorDropdown: React.FC = memo((props) => { - const { handleColorSelect, isColorActive } = props; - - const activeTextColor = COLORS_LIST.find((c) => isColorActive("text-color", c.textColor)); - const activeBackgroundColor = COLORS_LIST.find((c) => isColorActive("background-color", c.backgroundColor)); - - return ( - - - cn("h-full", { - "outline-none": open, - }) - } - > - {({ open }) => ( - - Color - - - - - )} - - -
-

Text colors

-
- {COLORS_LIST.map((color) => ( - -
-
-
-

Background colors

-
- {COLORS_LIST.map((color) => ( - -
-
-
-
- ); -}); - -ColorDropdown.displayName = "ColorDropdown"; diff --git a/web/core/components/pages/editor/header/index.ts b/web/core/components/pages/editor/header/index.ts index d87f5d11946..219ed44d87f 100644 --- a/web/core/components/pages/editor/header/index.ts +++ b/web/core/components/pages/editor/header/index.ts @@ -1,4 +1,3 @@ -export * from "./color-dropdown"; export * from "./extra-options"; export * from "./info-popover"; export * from "./options-dropdown"; diff --git a/web/core/components/pages/editor/header/options-dropdown.tsx b/web/core/components/pages/editor/header/options-dropdown.tsx index c7cf53a5f50..0560002d840 100644 --- a/web/core/components/pages/editor/header/options-dropdown.tsx +++ b/web/core/components/pages/editor/header/options-dropdown.tsx @@ -1,15 +1,12 @@ "use client"; -import { useState } from "react"; import { observer } from "mobx-react"; import { useParams, useRouter } from "next/navigation"; -import { ArchiveRestoreIcon, ArrowUpToLine, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react"; +import { ArchiveRestoreIcon, Clipboard, Copy, History, Link, Lock, LockOpen } from "lucide-react"; // document editor import { EditorReadOnlyRefApi, EditorRefApi } from "@plane/editor"; // ui import { ArchiveIcon, CustomMenu, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui"; -// components -import { ExportPageModal } from "@/components/pages"; // helpers import { copyTextToClipboard, copyUrlToClipboard } from "@/helpers/string.helper"; // hooks @@ -30,7 +27,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { const router = useRouter(); // store values const { - name, archived_at, is_locked, id, @@ -42,8 +38,6 @@ export const PageOptionsDropdown: React.FC = observer((props) => { canCurrentUserLockPage, restore, } = page; - // states - const [isExportModalOpen, setIsExportModalOpen] = useState(false); // store hooks const { workspaceSlug, projectId } = useParams(); // page filters @@ -163,41 +157,26 @@ export const PageOptionsDropdown: React.FC = observer((props) => { icon: History, shouldRender: true, }, - { - key: "export", - action: () => setIsExportModalOpen(true), - label: "Export", - icon: ArrowUpToLine, - shouldRender: true, - }, ]; return ( - <> - setIsExportModalOpen(false)} - pageTitle={name ?? ""} - /> - - handleFullWidth(!isFullWidth)} - > - Full width - {}} /> - - {MENU_ITEMS.map((item) => { - if (!item.shouldRender) return null; - return ( - - - {item.label} - - ); - })} - - + + handleFullWidth(!isFullWidth)} + > + Full width + {}} /> + + {MENU_ITEMS.map((item) => { + if (!item.shouldRender) return null; + return ( + + + {item.label} + + ); + })} + ); }); diff --git a/web/core/components/pages/editor/header/toolbar.tsx b/web/core/components/pages/editor/header/toolbar.tsx index 447616b532f..65d484ef152 100644 --- a/web/core/components/pages/editor/header/toolbar.tsx +++ b/web/core/components/pages/editor/header/toolbar.tsx @@ -3,11 +3,9 @@ import React, { useEffect, useState, useCallback } from "react"; import { Check, ChevronDown } from "lucide-react"; // editor -import { EditorRefApi, TNonColorEditorCommands } from "@plane/editor"; +import { EditorRefApi, TEditorCommands } from "@plane/editor"; // ui import { CustomMenu, Tooltip } from "@plane/ui"; -// components -import { ColorDropdown } from "@/components/pages"; // constants import { TOOLBAR_ITEMS, TYPOGRAPHY_ITEMS, ToolbarMenuItem } from "@/constants/editor"; // helpers @@ -20,7 +18,7 @@ type Props = { type ToolbarButtonProps = { item: ToolbarMenuItem; isActive: boolean; - executeCommand: EditorRefApi["executeMenuItemCommand"]; + executeCommand: (commandKey: TEditorCommands) => void; }; const ToolbarButton: React.FC = React.memo((props) => { @@ -38,11 +36,7 @@ const ToolbarButton: React.FC = React.memo((props) => {