From 102c183a396b228218cb181ea7d83dcbd618e85b Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 22 Feb 2024 13:49:43 +0530 Subject: [PATCH 01/15] fix: issue archive without automation --- apiserver/plane/app/urls/issue.py | 3 ++- apiserver/plane/app/views/issue.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 234c2824dd7..5880102d997 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -251,10 +251,11 @@ name="project-issue-archive", ), path( - "workspaces//projects//archived-issues//", + "workspaces//projects//issues//archive/", IssueArchiveViewSet.as_view( { "get": "retrieve", + "post": "archive", "delete": "destroy", } ), diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index edefade16e4..55a1f617fa5 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -1217,6 +1217,36 @@ def retrieve(self, request, slug, project_id, pk=None): status=status.HTTP_200_OK, ) + def archive(self, request, slug, project_id, pk=None): + issue = Issue.issue_objects.get( + workspace__slug=slug, + project_id=project_id, + pk=pk, + ) + if issue.state.group not in ["completed"]: + return Response( + {"error": "Can only archive a completed issue"}, + status=status.HTTP_400_BAD_REQUEST, + ) + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps({"archived_at": str(timezone.now().date())}), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(issue).data, cls=DjangoJSONEncoder + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + issue.archived_at = timezone.now().date() + issue.save() + + return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) + + def unarchive(self, request, slug, project_id, pk=None): issue = Issue.objects.get( workspace__slug=slug, From e5310f8292e55cfdcdf00816ad4b68ae344597d3 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 22 Feb 2024 13:52:44 +0530 Subject: [PATCH 02/15] fix: unarchive issue endpoint change --- apiserver/plane/app/urls/issue.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 5880102d997..bb601e1e389 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -256,19 +256,10 @@ { "get": "retrieve", "post": "archive", - "delete": "destroy", + "delete": "unarchive", } ), - name="project-issue-archive", - ), - path( - "workspaces//projects//unarchive//", - IssueArchiveViewSet.as_view( - { - "post": "unarchive", - } - ), - name="project-issue-archive", + name="project-issue-archive-unarchive", ), ## End Issue Archives ## Issue Relation From 847ce50552fe326b6926b40dcd9f5bf7dff981b8 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 23 Feb 2024 13:16:21 +0530 Subject: [PATCH 03/15] chore: archiving logic implemented in the quick-actions dropdowns --- .../automation/auto-archive-automation.tsx | 6 +- .../automation/auto-close-automation.tsx | 6 +- .../automation/select-month-modal.tsx | 2 +- web/components/dropdowns/date.tsx | 2 +- .../project-archived-issue-details.tsx | 2 +- .../headers/project-archived-issues.tsx | 2 +- web/components/issues/archive-issue-modal.tsx | 108 +++++++++++++ web/components/issues/index.ts | 1 + .../calendar/base-calendar-root.tsx | 6 + .../calendar/roots/cycle-root.tsx | 4 + .../calendar/roots/module-root.tsx | 4 + .../calendar/roots/project-root.tsx | 5 + .../calendar/roots/project-view-root.tsx | 1 + .../issue-layouts/kanban/base-kanban-root.tsx | 4 + .../issue-layouts/kanban/roots/cycle-root.tsx | 5 + .../kanban/roots/module-root.tsx | 5 + .../kanban/roots/profile-issues-root.tsx | 5 + .../kanban/roots/project-root.tsx | 5 + .../kanban/roots/project-view-root.tsx | 1 + .../issue-layouts/list/base-list-root.tsx | 4 + .../issue-layouts/list/list-view-types.d.ts | 1 + .../issue-layouts/list/roots/cycle-root.tsx | 5 + .../issue-layouts/list/roots/module-root.tsx | 5 + .../list/roots/profile-issues-root.tsx | 5 + .../issue-layouts/list/roots/project-root.tsx | 5 + .../list/roots/project-view-root.tsx | 1 + .../quick-action-dropdowns/all-issue.tsx | 124 +++++++++------ .../quick-action-dropdowns/cycle-issue.tsx | 149 +++++++++++------- .../quick-action-dropdowns/module-issue.tsx | 147 ++++++++++------- .../quick-action-dropdowns/project-issue.tsx | 134 ++++++++++------ .../spreadsheet/base-spreadsheet-root.tsx | 4 + .../spreadsheet/roots/cycle-root.tsx | 4 + .../spreadsheet/roots/module-root.tsx | 4 + .../spreadsheet/roots/project-root.tsx | 5 + .../spreadsheet/roots/project-view-root.tsx | 1 + web/components/issues/issue-layouts/types.ts | 1 + web/components/project/sidebar-list-item.tsx | 21 ++- web/constants/empty-state.ts | 4 +- web/constants/project.ts | 10 +- .../[projectId]/archived-issues/index.tsx | 24 +-- web/services/issue/issue_archive.service.ts | 17 +- web/store/issue/archived/issue.store.ts | 2 +- web/store/issue/cycle/issue.store.ts | 29 +++- web/store/issue/draft/issue.store.ts | 2 +- web/store/issue/issue.store.ts | 9 +- web/store/issue/module/issue.store.ts | 29 +++- web/store/issue/profile/issue.store.ts | 30 +++- web/store/issue/project-views/issue.store.ts | 30 +++- web/store/issue/project/issue.store.ts | 20 ++- web/store/issue/workspace/issue.store.ts | 35 +++- 50 files changed, 749 insertions(+), 286 deletions(-) create mode 100644 web/components/issues/archive-issue-modal.tsx diff --git a/web/components/automation/auto-archive-automation.tsx b/web/components/automation/auto-archive-automation.tsx index 974efff3a1a..d871b64d047 100644 --- a/web/components/automation/auto-archive-automation.tsx +++ b/web/components/automation/auto-archive-automation.tsx @@ -48,7 +48,7 @@ export const AutoArchiveAutomation: React.FC = observer((props) => {

Auto-archive closed issues

- Plane will auto archive issues that have been completed or cancelled. + Plane will auto archive issues that have been completed or canceled.

@@ -73,7 +73,7 @@ export const AutoArchiveAutomation: React.FC = observer((props) => { { handleChange({ archive_in: val }); @@ -93,7 +93,7 @@ export const AutoArchiveAutomation: React.FC = observer((props) => { className="flex w-full select-none items-center rounded px-1 py-1.5 text-sm text-custom-text-200 hover:bg-custom-background-80" onClick={() => setmonthModal(true)} > - Customise Time Range + Customize time range diff --git a/web/components/automation/auto-close-automation.tsx b/web/components/automation/auto-close-automation.tsx index 8d6662c112d..2ae4d1f9c65 100644 --- a/web/components/automation/auto-close-automation.tsx +++ b/web/components/automation/auto-close-automation.tsx @@ -74,7 +74,7 @@ export const AutoCloseAutomation: React.FC = observer((props) => {

Auto-close issues

- Plane will automatically close issue that haven{"'"}t been completed or cancelled. + Plane will automatically close issue that haven{"'"}t been completed or canceled.

@@ -100,7 +100,7 @@ export const AutoCloseAutomation: React.FC = observer((props) => { { handleChange({ close_in: val }); @@ -119,7 +119,7 @@ export const AutoCloseAutomation: React.FC = observer((props) => { className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80" onClick={() => setmonthModal(true)} > - Customize Time Range + Customize time range diff --git a/web/components/automation/select-month-modal.tsx b/web/components/automation/select-month-modal.tsx index 1d306bb0401..01d07f64a82 100644 --- a/web/components/automation/select-month-modal.tsx +++ b/web/components/automation/select-month-modal.tsx @@ -72,7 +72,7 @@ export const SelectMonthModal: React.FC = ({ type, initialValues, isOpen,
- Customise Time Range + Customize time range
diff --git a/web/components/dropdowns/date.tsx b/web/components/dropdowns/date.tsx index 04c7d6948ed..9919c3bcba4 100644 --- a/web/components/dropdowns/date.tsx +++ b/web/components/dropdowns/date.tsx @@ -146,7 +146,7 @@ export const DateDropdown: React.FC = (props) => { {BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && ( {value ? renderFormattedDate(value) : placeholder} )} - {isClearable && isDateSelected && ( + {isClearable && !disabled && isDateSelected && ( { diff --git a/web/components/headers/project-archived-issue-details.tsx b/web/components/headers/project-archived-issue-details.tsx index 3b3e05f1a12..9d4596f8357 100644 --- a/web/components/headers/project-archived-issue-details.tsx +++ b/web/components/headers/project-archived-issue-details.tsx @@ -71,7 +71,7 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => { link={ } /> } diff --git a/web/components/headers/project-archived-issues.tsx b/web/components/headers/project-archived-issues.tsx index b7ca78ede3d..d1da1c85971 100644 --- a/web/components/headers/project-archived-issues.tsx +++ b/web/components/headers/project-archived-issues.tsx @@ -109,7 +109,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => { type="text" link={ } /> } diff --git a/web/components/issues/archive-issue-modal.tsx b/web/components/issues/archive-issue-modal.tsx new file mode 100644 index 00000000000..053929da917 --- /dev/null +++ b/web/components/issues/archive-issue-modal.tsx @@ -0,0 +1,108 @@ +import { useState, Fragment } from "react"; +import { Dialog, Transition } from "@headlessui/react"; +// hooks +import { useProject } from "hooks/store"; +import { useIssues } from "hooks/store/use-issues"; +import useToast from "hooks/use-toast"; +// ui +import { Button } from "@plane/ui"; +// types +import { TIssue } from "@plane/types"; + +type Props = { + data?: TIssue; + dataId?: string | null | undefined; + handleClose: () => void; + isOpen: boolean; + onSubmit?: () => Promise; +}; + +export const ArchiveIssueModal: React.FC = (props) => { + const { dataId, data, isOpen, handleClose, onSubmit } = props; + // states + const [isArchiving, setIsArchiving] = useState(false); + // store hooks + const { getProjectById } = useProject(); + const { issueMap } = useIssues(); + // toast alert + const { setToastAlert } = useToast(); + + if (!dataId && !data) return null; + + const issue = data ? data : issueMap[dataId!]; + const projectDetails = getProjectById(issue.project_id); + + const onClose = () => { + setIsArchiving(false); + handleClose(); + }; + + const handleArchiveIssue = async () => { + console.log("Outside"); + if (!onSubmit) return; + console.log("Inside"); + + setIsArchiving(true); + await onSubmit() + .then(() => onClose()) + .catch(() => + setToastAlert({ + title: "Error", + type: "error", + message: "Failed to delete issue", + }) + ) + .finally(() => setIsArchiving(false)); + }; + + return ( + + + +
+ + +
+
+ + +
+

+ Archive issue {projectDetails?.identifier} {issue.sequence_id} +

+

+ Are you sure you want to archive the issue? All your archived issues can be restored later. +

+
+ + +
+
+
+
+
+
+
+
+ ); +}; diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index 3904049e9c4..0750df3e3ae 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -20,4 +20,5 @@ export * from "./draft-issue-modal"; export * from "./delete-draft-issue-modal"; // archived issue +export * from "./archive-issue-modal"; export * from "./delete-archived-issue-modal"; diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index 472d6808580..c95764da7d7 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -26,6 +26,7 @@ interface IBaseCalendarRoot { [EIssueActions.DELETE]: (issue: TIssue) => Promise; [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; }; viewId?: string; isCompletedCycle?: boolean; @@ -114,6 +115,11 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.REMOVE) : undefined } + handleArchive={ + issueActions[EIssueActions.ARCHIVE] + ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.ARCHIVE) + : undefined + } readOnly={!isEditingAllowed || isCompletedCycle} /> )} diff --git a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx index 1ef08ea6158..106e28cbf5f 100644 --- a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx @@ -33,6 +33,10 @@ export const CycleCalendarLayout: React.FC = observer(() => { if (!workspaceSlug || !cycleId || !projectId) return; await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; + await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); + }, }), [issues, workspaceSlug, cycleId, projectId] ); diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index d2b23e17614..1a62f740974 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -34,6 +34,10 @@ export const ModuleCalendarLayout: React.FC = observer(() => { if (!workspaceSlug || !moduleId) return; await issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; + await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); + }, }), [issues, workspaceSlug, moduleId] ); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx index 40f72e7b871..d42a8c5d217 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx @@ -28,6 +28,11 @@ export const CalendarLayout: React.FC = observer(() => { await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug) return; + + await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); + }, }), [issues, workspaceSlug] ); diff --git a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx index 573a9cf2049..0110aea2bc1 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx @@ -16,6 +16,7 @@ export interface IViewCalendarLayout { [EIssueActions.DELETE]: (issue: TIssue) => Promise; [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; }; } diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 3b31f6b67b4..e75fc91a95f 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -41,6 +41,7 @@ export interface IBaseKanBanLayout { [EIssueActions.DELETE]: (issue: TIssue) => Promise; [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; }; showLoader?: boolean; viewId?: string; @@ -188,6 +189,9 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas handleRemoveFromView={ issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined } + handleArchive={ + issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined + } readOnly={!isEditingAllowed || isCompletedCycle} /> ), diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index 2b311f6eb95..1a66541f3f4 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -39,6 +39,11 @@ export const CycleKanBanLayout: React.FC = observer(() => { await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; + + await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); + }, }), [issues, workspaceSlug, cycleId] ); diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index c3af69e6eb1..5e8cd63c81f 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -38,6 +38,11 @@ export const ModuleKanBanLayout: React.FC = observer(() => { await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; + + await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); + }, }), [issues, workspaceSlug, moduleId] ); diff --git a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index 2e189c9f4fb..c6c04165447 100644 --- a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -35,6 +35,11 @@ export const ProfileIssuesKanBanLayout: React.FC = observer(() => { await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug || !userId) return; + + await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, userId); + }, }), [issues, workspaceSlug, userId] ); diff --git a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx index 89e2ee1872a..efd86bc8e94 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx @@ -32,6 +32,11 @@ export const KanBanLayout: React.FC = observer(() => { await issues.removeIssue(workspaceSlug, issue.project_id, issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug) return; + + await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); + }, }), [issues, workspaceSlug] ); diff --git a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx index 1cdf71d456e..8dd33b72844 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx @@ -17,6 +17,7 @@ export interface IViewKanBanLayout { [EIssueActions.DELETE]: (issue: TIssue) => Promise; [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; }; } diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index 6cec6d35869..10b4dbb952b 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -41,6 +41,7 @@ interface IBaseListRoot { [EIssueActions.DELETE]: (issue: TIssue) => Promise; [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; }; viewId?: string; storeType: TCreateModalStoreTypes; @@ -109,6 +110,9 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { handleRemoveFromView={ issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined } + handleArchive={ + issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined + } readOnly={!isEditingAllowed || isCompletedCycle} /> ), diff --git a/web/components/issues/issue-layouts/list/list-view-types.d.ts b/web/components/issues/issue-layouts/list/list-view-types.d.ts index e369410af7f..304872ffb60 100644 --- a/web/components/issues/issue-layouts/list/list-view-types.d.ts +++ b/web/components/issues/issue-layouts/list/list-view-types.d.ts @@ -5,6 +5,7 @@ export interface IQuickActionProps { handleDelete: () => Promise; handleUpdate?: (data: TIssue) => Promise; handleRemoveFromView?: () => Promise; + handleArchive?: () => Promise; customActionButton?: React.ReactElement; portalElement?: HTMLDivElement | null; readOnly?: boolean; diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index e30c207b649..b282705e66c 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -38,6 +38,11 @@ export const CycleListLayout: React.FC = observer(() => { await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; + + await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); + }, }), [issues, workspaceSlug, cycleId] ); diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index 520a2da32bc..36e04a054d1 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -37,6 +37,11 @@ export const ModuleListLayout: React.FC = observer(() => { await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; + + await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); + }, }), [issues, workspaceSlug, moduleId] ); diff --git a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index 91e80382a65..fa4a05bbcbb 100644 --- a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -36,6 +36,11 @@ export const ProfileIssuesListLayout: FC = observer(() => { await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug || !userId) return; + + await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, userId); + }, }), [issues, workspaceSlug, userId] ); diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index f0479b71ffa..9e1b5830b70 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -33,6 +33,11 @@ export const ListLayout: FC = observer(() => { await issues.removeIssue(workspaceSlug, projectId, issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; + + await issues.archiveIssue(workspaceSlug, projectId, issue.id); + }, }), // eslint-disable-next-line react-hooks/exhaustive-deps [issues] diff --git a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx index dd384ba93eb..5ecfd6da28e 100644 --- a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx @@ -17,6 +17,7 @@ export interface IViewListLayout { [EIssueActions.DELETE]: (issue: TIssue) => Promise; [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; }; } diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index bc6518911ae..6c70c077026 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -1,13 +1,13 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { CustomMenu } from "@plane/ui"; -import { Copy, Link, Pencil, Trash2 } from "lucide-react"; +import { Archive, Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; import omit from "lodash/omit"; // hooks import useToast from "hooks/use-toast"; import { useEventTracker } from "hooks/store"; // components -import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; // types @@ -17,11 +17,20 @@ import { IQuickActionProps } from "../list/list-view-types"; import { EIssuesStoreType } from "constants/issue"; export const AllIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, customActionButton, portalElement, readOnly = false } = props; + const { + issue, + handleDelete, + handleUpdate, + handleArchive, + customActionButton, + portalElement, + readOnly = false, + } = props; // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [archiveIssueModal, setArchiveIssueModal] = useState(false); // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -30,15 +39,18 @@ export const AllIssueQuickActions: React.FC = (props) => { // toast alert const { setToastAlert } = useToast(); - const handleCopyIssueLink = () => { - copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() => + const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; + + const handleOpenInNewTab = () => window.open(`/${issueLink}}`, "_blank"); + + const handleCopyIssueLink = () => + copyUrlToClipboard(issueLink).then(() => setToastAlert({ type: "success", title: "Link copied", message: "Issue link copied to clipboard", }) ); - }; const duplicateIssuePayload = omit( { @@ -50,6 +62,12 @@ export const AllIssueQuickActions: React.FC = (props) => { return ( <> + setArchiveIssueModal(false)} + onSubmit={handleArchive} + /> = (props) => { closeOnSelect ellipsis > - { - handleCopyIssueLink(); - }} - > + {!readOnly && ( + { + setTrackElement("Global issues"); + setIssueToEdit(issue); + setCreateUpdateIssueModal(true); + }} + > +
+ + Edit +
+
+ )} + +
+ + Open in new tab +
+
+
Copy link
{!readOnly && ( - <> - { - setTrackElement("Global issues"); - setIssueToEdit(issue); - setCreateUpdateIssueModal(true); - }} - > -
- - Edit issue -
-
- { - setTrackElement("Global issues"); - setCreateUpdateIssueModal(true); - }} - > -
- - Make a copy -
-
- { - setTrackElement("Global issues"); - setDeleteIssueModal(true); - }} - > -
- - Delete issue -
-
- + { + setTrackElement("Global issues"); + setCreateUpdateIssueModal(true); + }} + > +
+ + Make a copy +
+
+ )} + {!readOnly && ( + setArchiveIssueModal(true)}> +
+ + Archive +
+
+ )} + {!readOnly && ( + { + setTrackElement("Global issues"); + setDeleteIssueModal(true); + }} + > +
+ + Delete +
+
)} diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 4699b1c81e3..a6a500326f9 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -1,13 +1,13 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { CustomMenu } from "@plane/ui"; -import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react"; +import { Archive, Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; import omit from "lodash/omit"; // hooks import useToast from "hooks/use-toast"; import { useEventTracker, useIssues, useUser } from "hooks/store"; // components -import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; // types @@ -23,6 +23,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { handleDelete, handleUpdate, handleRemoveFromView, + handleArchive, customActionButton, portalElement, readOnly = false, @@ -31,6 +32,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [archiveIssueModal, setArchiveIssueModal] = useState(false); // router const router = useRouter(); const { workspaceSlug, cycleId } = router.query; @@ -45,19 +47,24 @@ export const CycleIssueQuickActions: React.FC = (props) => { membership: { currentProjectRole }, } = useUser(); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; + const isArchivingAllowed = isEditingAllowed; + const isDeletingAllowed = isEditingAllowed; const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; - const handleCopyIssueLink = () => { - copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() => + const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; + + const handleOpenInNewTab = () => window.open(`/${issueLink}}`, "_blank"); + + const handleCopyIssueLink = () => + copyUrlToClipboard(issueLink).then(() => setToastAlert({ type: "success", title: "Link copied", message: "Issue link copied to clipboard", }) ); - }; const duplicateIssuePayload = omit( { @@ -69,6 +76,12 @@ export const CycleIssueQuickActions: React.FC = (props) => { return ( <> + setArchiveIssueModal(false)} + onSubmit={handleArchive} + /> = (props) => { closeOnSelect ellipsis > - { - handleCopyIssueLink(); - }} - > + {isEditingAllowed && ( + { + setIssueToEdit({ + ...issue, + cycle_id: cycleId?.toString() ?? null, + }); + setTrackElement(activeLayout); + setCreateUpdateIssueModal(true); + }} + > +
+ + Edit +
+
+ )} + +
+ + Open in new tab +
+
+
Copy link
- {isEditingAllowed && !readOnly && ( - <> - { - setIssueToEdit({ - ...issue, - cycle_id: cycleId?.toString() ?? null, - }); - setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); - }} - > -
- - Edit issue -
-
- { - handleRemoveFromView && handleRemoveFromView(); - }} - > -
- - Remove from cycle -
-
- { - setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); - }} - > -
- - Make a copy -
-
- { - setTrackElement(activeLayout); - setDeleteIssueModal(true); - }} - > -
- - Delete issue -
-
- + {isEditingAllowed && ( + { + setTrackElement(activeLayout); + setCreateUpdateIssueModal(true); + }} + > +
+ + Make a copy +
+
+ )} + {isEditingAllowed && ( + { + handleRemoveFromView && handleRemoveFromView(); + }} + > +
+ + Remove from cycle +
+
+ )} + {isArchivingAllowed && ( + setArchiveIssueModal(true)}> +
+ + Archive +
+
+ )} + {isDeletingAllowed && ( + { + setTrackElement(activeLayout); + setDeleteIssueModal(true); + }} + > +
+ + Delete +
+
)} diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 6eabfda59fd..8e28822b8f8 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -1,13 +1,13 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { CustomMenu } from "@plane/ui"; -import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react"; +import { Archive, Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; import omit from "lodash/omit"; // hooks import useToast from "hooks/use-toast"; import { useIssues, useEventTracker, useUser } from "hooks/store"; // components -import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; // types @@ -23,6 +23,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { handleDelete, handleUpdate, handleRemoveFromView, + handleArchive, customActionButton, portalElement, readOnly = false, @@ -31,6 +32,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [archiveIssueModal, setArchiveIssueModal] = useState(false); // router const router = useRouter(); const { workspaceSlug, moduleId } = router.query; @@ -45,19 +47,24 @@ export const ModuleIssueQuickActions: React.FC = (props) => { membership: { currentProjectRole }, } = useUser(); - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; + const isArchivingAllowed = isEditingAllowed; + const isDeletingAllowed = isEditingAllowed; const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; - const handleCopyIssueLink = () => { - copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() => + const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; + + const handleOpenInNewTab = () => window.open(`/${issueLink}}`, "_blank"); + + const handleCopyIssueLink = () => + copyUrlToClipboard(issueLink).then(() => setToastAlert({ type: "success", title: "Link copied", message: "Issue link copied to clipboard", }) ); - }; const duplicateIssuePayload = omit( { @@ -69,6 +76,12 @@ export const ModuleIssueQuickActions: React.FC = (props) => { return ( <> + setArchiveIssueModal(false)} + onSubmit={handleArchive} + /> = (props) => { closeOnSelect ellipsis > - { - handleCopyIssueLink(); - }} - > + {isEditingAllowed && ( + { + setIssueToEdit({ ...issue, module_ids: moduleId ? [moduleId.toString()] : [] }); + setTrackElement(activeLayout); + setCreateUpdateIssueModal(true); + }} + > +
+ + Edit +
+
+ )} + +
+ + Open in new tab +
+
+
Copy link
- {isEditingAllowed && !readOnly && ( - <> - { - setIssueToEdit({ ...issue, module_ids: moduleId ? [moduleId.toString()] : [] }); - setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); - }} - > -
- - Edit issue -
-
- { - handleRemoveFromView && handleRemoveFromView(); - }} - > -
- - Remove from module -
-
- { - setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); - }} - > -
- - Make a copy -
-
- { - e.preventDefault(); - e.stopPropagation(); - setTrackElement(activeLayout); - setDeleteIssueModal(true); - }} - > -
- - Delete issue -
-
- + {isEditingAllowed && ( + { + setTrackElement(activeLayout); + setCreateUpdateIssueModal(true); + }} + > +
+ + Make a copy +
+
+ )} + {isEditingAllowed && ( + { + handleRemoveFromView && handleRemoveFromView(); + }} + > +
+ + Remove from module +
+
+ )} + {isArchivingAllowed && ( + setArchiveIssueModal(true)}> +
+ + Archive +
+
+ )} + {isDeletingAllowed && ( + { + e.preventDefault(); + e.stopPropagation(); + setTrackElement(activeLayout); + setDeleteIssueModal(true); + }} + > +
+ + Delete +
+
)} diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 1d6d88f2575..5f780b5113c 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -1,13 +1,13 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { CustomMenu } from "@plane/ui"; -import { Copy, Link, Pencil, Trash2 } from "lucide-react"; +import { Archive, Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; import omit from "lodash/omit"; // hooks import { useEventTracker, useIssues, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components -import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; +import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; // types @@ -18,7 +18,15 @@ import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; export const ProjectIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, customActionButton, portalElement, readOnly = false } = props; + const { + issue, + handleDelete, + handleUpdate, + handleArchive, + customActionButton, + portalElement, + readOnly = false, + } = props; // router const router = useRouter(); const { workspaceSlug } = router.query; @@ -26,6 +34,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [archiveIssueModal, setArchiveIssueModal] = useState(false); // store hooks const { membership: { currentProjectRole }, @@ -35,19 +44,24 @@ export const ProjectIssueQuickActions: React.FC = (props) => const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; + const isArchivingAllowed = isEditingAllowed; + const isDeletingAllowed = isEditingAllowed; const { setToastAlert } = useToast(); - const handleCopyIssueLink = () => { - copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() => + const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; + + const handleOpenInNewTab = () => window.open(`/${issueLink}}`, "_blank"); + + const handleCopyIssueLink = () => + copyUrlToClipboard(issueLink).then(() => setToastAlert({ type: "success", title: "Link copied", message: "Issue link copied to clipboard", }) ); - }; const duplicateIssuePayload = omit( { @@ -59,15 +73,22 @@ export const ProjectIssueQuickActions: React.FC = (props) => const isDraftIssue = router?.asPath?.includes("draft-issues") || false; + console.log("handleArchive", handleArchive); + return ( <> + setArchiveIssueModal(false)} + onSubmit={handleArchive} + /> setDeleteIssueModal(false)} onSubmit={handleDelete} /> - { @@ -81,7 +102,6 @@ export const ProjectIssueQuickActions: React.FC = (props) => storeType={EIssuesStoreType.PROJECT} isDraft={isDraftIssue} /> - = (props) => closeOnSelect ellipsis > - { - handleCopyIssueLink(); - }} - > + {isEditingAllowed && ( + { + setTrackElement(activeLayout); + setIssueToEdit(issue); + setCreateUpdateIssueModal(true); + }} + > +
+ + Edit +
+
+ )} + +
+ + Open in new tab +
+
+
Copy link
- {isEditingAllowed && !readOnly && ( - <> - { - setTrackElement(activeLayout); - setIssueToEdit(issue); - setCreateUpdateIssueModal(true); - }} - > -
- - Edit issue -
-
- { - setTrackElement(activeLayout); - setCreateUpdateIssueModal(true); - }} - > -
- - Make a copy -
-
- { - setTrackElement(activeLayout); - setDeleteIssueModal(true); - }} - > -
- - Delete issue -
-
- + {isEditingAllowed && ( + { + setTrackElement(activeLayout); + setCreateUpdateIssueModal(true); + }} + > +
+ + Make a copy +
+
+ )} + {isArchivingAllowed && ( + setArchiveIssueModal(true)}> +
+ + Archive +
+
+ )} + {isDeletingAllowed && ( + { + setTrackElement(activeLayout); + setDeleteIssueModal(true); + }} + > +
+ + Delete +
+
)}
diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index a94455a0b03..4e8909e97b2 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -26,6 +26,7 @@ interface IBaseSpreadsheetRoot { [EIssueActions.DELETE]: (issue: TIssue) => void; [EIssueActions.UPDATE]?: (issue: TIssue) => void; [EIssueActions.REMOVE]?: (issue: TIssue) => void; + [EIssueActions.ARCHIVE]?: (issue: TIssue) => void; }; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; isCompletedCycle?: boolean; @@ -103,6 +104,9 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { handleRemoveFromView={ issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined } + handleArchive={ + issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined + } portalElement={portalElement} readOnly={!isEditingAllowed || isCompletedCycle} /> diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx index 7f92bd74ca6..780b6b91084 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx @@ -32,6 +32,10 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => { if (!workspaceSlug || !cycleId) return; issues.removeIssueFromCycle(workspaceSlug, issue.project_id, cycleId, issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug || !cycleId) return; + issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); + }, }), [issues, workspaceSlug, cycleId] ); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx index 6538ab20bc4..7d011a364bb 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx @@ -31,6 +31,10 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => { if (!workspaceSlug || !moduleId) return; issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug || !moduleId) return; + issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); + }, }), [issues, workspaceSlug, moduleId] ); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx index e260daee1fd..4ce54cff5a1 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx @@ -28,6 +28,11 @@ export const ProjectSpreadsheetLayout: React.FC = observer(() => { await issues.removeIssue(workspaceSlug, issue.project_id, issue.id); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + if (!workspaceSlug) return; + + await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); + }, }), [issues, workspaceSlug] ); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx index 28b766cd12e..d8b7571e584 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx @@ -17,6 +17,7 @@ export interface IViewSpreadsheetLayout { [EIssueActions.DELETE]: (issue: TIssue) => Promise; [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; + [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; }; } diff --git a/web/components/issues/issue-layouts/types.ts b/web/components/issues/issue-layouts/types.ts index f4c2d810059..b943c159ce3 100644 --- a/web/components/issues/issue-layouts/types.ts +++ b/web/components/issues/issue-layouts/types.ts @@ -2,4 +2,5 @@ export enum EIssueActions { UPDATE = "update", DELETE = "delete", REMOVE = "remove", + ARCHIVE = "archive", } diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index f899a9b31b1..c9b91a1f9f9 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -83,7 +83,7 @@ export const ProjectSidebarListItem: React.FC = observer((props) => { // store hooks const { theme: themeStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { currentProjectDetails, addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject(); + const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject(); const { getInboxesByProjectId, getInboxById } = useInbox(); // states const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false); @@ -270,17 +270,14 @@ export const ProjectSidebarListItem: React.FC = observer((props) => {
)} - - {project.archive_in > 0 && ( - - -
- - Archived Issues -
- -
- )} + + +
+ + Archived issues +
+ +
diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts index eaf7f4b05f6..a1b2b06f333 100644 --- a/web/constants/empty-state.ts +++ b/web/constants/empty-state.ts @@ -244,9 +244,9 @@ export const EMPTY_ISSUE_STATE_DETAILS = { key: "archived", title: "No archived issues yet", description: - "Archived issues help you remove issues you completed or cancelled from focus. You can set automation to auto archive issues and find them here.", + "Archived issues help you remove issues you completed or canceled from focus. You can set automation to auto archive issues and find them here.", primaryButton: { - text: "Set Automation", + text: "Set automation", }, }, draft: { diff --git a/web/constants/project.ts b/web/constants/project.ts index 9e7bdee9ef5..6073e96be15 100644 --- a/web/constants/project.ts +++ b/web/constants/project.ts @@ -57,11 +57,11 @@ export const MONTHS = [ export const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; export const PROJECT_AUTOMATION_MONTHS = [ - { label: "1 Month", value: 1 }, - { label: "3 Months", value: 3 }, - { label: "6 Months", value: 6 }, - { label: "9 Months", value: 9 }, - { label: "12 Months", value: 12 }, + { label: "1 month", value: 1 }, + { label: "3 months", value: 3 }, + { label: "6 months", value: 6 }, + { label: "9 months", value: 9 }, + { label: "12 months", value: 12 }, ]; export const PROJECT_UNSPLASH_COVERS = [ diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx index c24c80a92d5..34019c02632 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx @@ -5,44 +5,28 @@ import { observer } from "mobx-react"; import { AppLayout } from "layouts/app-layout"; // contexts import { ArchivedIssueLayoutRoot } from "components/issues"; -// ui -import { ArchiveIcon } from "@plane/ui"; // components import { ProjectArchivedIssuesHeader } from "components/headers"; import { PageHead } from "components/core"; -// icons -import { X } from "lucide-react"; // types import { NextPageWithLayout } from "lib/types"; // hooks import { useProject } from "hooks/store"; const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => { + // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { projectId } = router.query; // store hooks const { getProjectById } = useProject(); // derived values const project = projectId ? getProjectById(projectId.toString()) : undefined; - const pageTitle = project?.name && `${project?.name} - Archived Issues`; + const pageTitle = project?.name && `${project?.name} - Archived issues`; return ( <> -
-
- -
- -
+ ); }); diff --git a/web/services/issue/issue_archive.service.ts b/web/services/issue/issue_archive.service.ts index 065f41d7eb4..aeac475a9a5 100644 --- a/web/services/issue/issue_archive.service.ts +++ b/web/services/issue/issue_archive.service.ts @@ -1,7 +1,8 @@ import { APIService } from "services/api.service"; -// type -import { API_BASE_URL } from "helpers/common.helper"; +// types import { TIssue } from "@plane/types"; +// constants +import { API_BASE_URL } from "helpers/common.helper"; export class IssueArchiveService extends APIService { constructor() { @@ -18,8 +19,16 @@ export class IssueArchiveService extends APIService { }); } + async archiveIssue(workspaceSlug: string, projectId: string, issueId: string): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/archive/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async unarchiveIssue(workspaceSlug: string, projectId: string, issueId: string): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/unarchive/${issueId}/`) + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/archive/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; @@ -32,7 +41,7 @@ export class IssueArchiveService extends APIService { issueId: string, queries?: any ): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issueId}/`, { + return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/archive/`, { params: queries, }) .then((response) => response?.data) diff --git a/web/store/issue/archived/issue.store.ts b/web/store/issue/archived/issue.store.ts index fa3a06f3782..8ff7d199aaa 100644 --- a/web/store/issue/archived/issue.store.ts +++ b/web/store/issue/archived/issue.store.ts @@ -70,7 +70,7 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues const archivedIssueIds = this.issues[projectId]; if (!archivedIssueIds) return undefined; - const _issues = this.rootIssueStore.issues.getIssuesByIds(archivedIssueIds); + const _issues = this.rootIssueStore.issues.getIssuesByIds(archivedIssueIds, "archived"); if (!_issues) return []; let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined; diff --git a/web/store/issue/cycle/issue.store.ts b/web/store/issue/cycle/issue.store.ts index 41731e134b6..61b280da9ea 100644 --- a/web/store/issue/cycle/issue.store.ts +++ b/web/store/issue/cycle/issue.store.ts @@ -48,6 +48,12 @@ export interface ICycleIssues { issueId: string, cycleId?: string | undefined ) => Promise; + archiveIssue: ( + workspaceSlug: string, + projectId: string, + issueId: string, + cycleId?: string | undefined + ) => Promise; quickAddIssue: ( workspaceSlug: string, projectId: string, @@ -100,6 +106,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { createIssue: action, updateIssue: action, removeIssue: action, + archiveIssue: action, quickAddIssue: action, addIssueToCycle: action, removeIssueFromCycle: action, @@ -127,7 +134,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { const cycleIssueIds = this.issues[cycleId]; if (!cycleIssueIds) return; - const _issues = this.rootIssueStore.issues.getIssuesByIds(cycleIssueIds); + const _issues = this.rootIssueStore.issues.getIssuesByIds(cycleIssueIds, "un-archived"); if (!_issues) return []; let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = []; @@ -237,6 +244,26 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues { } }; + archiveIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + cycleId: string | undefined = undefined + ) => { + try { + if (!cycleId) throw new Error("Cycle Id is required"); + + await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); + this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId); + + runInAction(() => { + pull(this.issues[cycleId], issueId); + }); + } catch (error) { + throw error; + } + }; + quickAddIssue = async ( workspaceSlug: string, projectId: string, diff --git a/web/store/issue/draft/issue.store.ts b/web/store/issue/draft/issue.store.ts index ee6d785ecbf..6181499a776 100644 --- a/web/store/issue/draft/issue.store.ts +++ b/web/store/issue/draft/issue.store.ts @@ -81,7 +81,7 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues { const draftIssueIds = this.issues[projectId]; if (!draftIssueIds) return undefined; - const _issues = this.rootIssueStore.issues.getIssuesByIds(draftIssueIds); + const _issues = this.rootIssueStore.issues.getIssuesByIds(draftIssueIds, "un-archived"); if (!_issues) return []; let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined; diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index 8bdb18dad4e..9903abcf754 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -18,7 +18,7 @@ export type IIssueStore = { removeIssue(issueId: string): void; // helper methods getIssueById(issueId: string): undefined | TIssue; - getIssuesByIds(issueIds: string[]): undefined | Record; // Record defines issue_id as key and TIssue as value + getIssuesByIds(issueIds: string[], type: "archived" | "un-archived"): undefined | Record; // Record defines issue_id as key and TIssue as value }; export class IssueStore implements IIssueStore { @@ -108,14 +108,15 @@ export class IssueStore implements IIssueStore { /** * @description This method will return the issues from the issuesMap * @param {string[]} issueIds + * @param {boolean} archivedIssues * @returns {Record | undefined} */ - getIssuesByIds = computedFn((issueIds: string[]) => { + getIssuesByIds = computedFn((issueIds: string[], archivedIssues: "archived" | "un-archived") => { if (!issueIds || issueIds.length <= 0 || isEmpty(this.issuesMap)) return undefined; const filteredIssues: { [key: string]: TIssue } = {}; Object.values(this.issuesMap).forEach((issue) => { - if (issueIds.includes(issue.id)) { - filteredIssues[issue.id] = issue; + if (archivedIssues === "archived" || issue.archived_at) { + if (issueIds.includes(issue.id)) filteredIssues[issue.id] = issue; } }); return isEmpty(filteredIssues) ? undefined : filteredIssues; diff --git a/web/store/issue/module/issue.store.ts b/web/store/issue/module/issue.store.ts index e9b96ac540e..9e6ad3f49f4 100644 --- a/web/store/issue/module/issue.store.ts +++ b/web/store/issue/module/issue.store.ts @@ -46,6 +46,12 @@ export interface IModuleIssues { issueId: string, moduleId?: string | undefined ) => Promise; + archiveIssue: ( + workspaceSlug: string, + projectId: string, + issueId: string, + moduleId?: string | undefined + ) => Promise; quickAddIssue: ( workspaceSlug: string, projectId: string, @@ -103,6 +109,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { createIssue: action, updateIssue: action, removeIssue: action, + archiveIssue: action, quickAddIssue: action, addIssuesToModule: action, removeIssuesFromModule: action, @@ -131,7 +138,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { const moduleIssueIds = this.issues[moduleId]; if (!moduleIssueIds) return; - const _issues = this.rootIssueStore.issues.getIssuesByIds(moduleIssueIds); + const _issues = this.rootIssueStore.issues.getIssuesByIds(moduleIssueIds, "un-archived"); if (!_issues) return []; let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = []; @@ -242,6 +249,26 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues { } }; + archiveIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + moduleId: string | undefined = undefined + ) => { + try { + if (!moduleId) throw new Error("Module Id is required"); + + await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); + this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + pull(this.issues[moduleId], issueId); + }); + } catch (error) { + throw error; + } + }; + quickAddIssue = async ( workspaceSlug: string, projectId: string, diff --git a/web/store/issue/profile/issue.store.ts b/web/store/issue/profile/issue.store.ts index 461928c646c..c39b33a80d1 100644 --- a/web/store/issue/profile/issue.store.ts +++ b/web/store/issue/profile/issue.store.ts @@ -1,5 +1,6 @@ import { action, observable, makeObservable, computed, runInAction } from "mobx"; import set from "lodash/set"; +import pull from "lodash/pull"; // base class import { IssueHelperStore } from "../helpers/issue-helper.store"; // services @@ -48,6 +49,12 @@ export interface IProfileIssues { issueId: string, userId?: string | undefined ) => Promise; + archiveIssue: ( + workspaceSlug: string, + projectId: string, + issueId: string, + userId?: string | undefined + ) => Promise; quickAddIssue: undefined; } @@ -77,6 +84,7 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues { createIssue: action, updateIssue: action, removeIssue: action, + archiveIssue: action, }); // root store this.rootIssueStore = _rootStore; @@ -104,7 +112,7 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues { if (!userIssueIds) return; - const _issues = this.rootStore.issues.getIssuesByIds(userIssueIds); + const _issues = this.rootStore.issues.getIssuesByIds(userIssueIds, "un-archived"); if (!_issues) return []; let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined; @@ -249,4 +257,24 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues { throw error; } }; + + archiveIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + userId: string | undefined = undefined + ) => { + if (!userId) return; + try { + await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); + + const uniqueViewId = `${workspaceSlug}_${this.currentView}`; + + runInAction(() => { + pull(this.issues[userId][uniqueViewId], issueId); + }); + } catch (error) { + throw error; + } + }; } diff --git a/web/store/issue/project-views/issue.store.ts b/web/store/issue/project-views/issue.store.ts index 8327ffcce96..b85465ec81f 100644 --- a/web/store/issue/project-views/issue.store.ts +++ b/web/store/issue/project-views/issue.store.ts @@ -1,5 +1,6 @@ import { action, observable, makeObservable, computed, runInAction } from "mobx"; import set from "lodash/set"; +import pull from "lodash/pull"; // base class import { IssueHelperStore } from "../helpers/issue-helper.store"; // services @@ -41,6 +42,12 @@ export interface IProjectViewIssues { issueId: string, viewId?: string | undefined ) => Promise; + archiveIssue: ( + workspaceSlug: string, + projectId: string, + issueId: string, + viewId?: string | undefined + ) => Promise; quickAddIssue: ( workspaceSlug: string, projectId: string, @@ -75,6 +82,7 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI createIssue: action, updateIssue: action, removeIssue: action, + archiveIssue: action, quickAddIssue: action, }); // root store @@ -98,7 +106,7 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI const viewIssueIds = this.issues[viewId]; if (!viewIssueIds) return; - const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds); + const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds, "un-archived"); if (!_issues) return []; let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = []; @@ -210,6 +218,26 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI } }; + archiveIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + viewId: string | undefined = undefined + ) => { + try { + if (!viewId) throw new Error("View Id is required"); + + await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); + + runInAction(() => { + pull(this.issues[viewId], issueId); + }); + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; + quickAddIssue = async ( workspaceSlug: string, projectId: string, diff --git a/web/store/issue/project/issue.store.ts b/web/store/issue/project/issue.store.ts index 76bf7bcc256..d5286bbcf80 100644 --- a/web/store/issue/project/issue.store.ts +++ b/web/store/issue/project/issue.store.ts @@ -6,7 +6,7 @@ import concat from "lodash/concat"; // base class import { IssueHelperStore } from "../helpers/issue-helper.store"; // services -import { IssueService } from "services/issue/issue.service"; +import { IssueService, IssueArchiveService } from "services/issue"; // types import { IIssueRootStore } from "../root.store"; import { TIssue, TGroupedIssues, TSubGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; @@ -23,6 +23,7 @@ export interface IProjectIssues { createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; quickAddIssue: (workspaceSlug: string, projectId: string, data: TIssue) => Promise; removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise; } @@ -40,6 +41,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { rootIssueStore: IIssueRootStore; // services issueService; + issueArchiveService; constructor(_rootStore: IIssueRootStore) { super(_rootStore); @@ -54,6 +56,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { createIssue: action, updateIssue: action, removeIssue: action, + archiveIssue: action, removeBulkIssues: action, quickAddIssue: action, }); @@ -61,6 +64,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { this.rootIssueStore = _rootStore; // services this.issueService = new IssueService(); + this.issueArchiveService = new IssueArchiveService(); } get groupedIssueIds() { @@ -78,7 +82,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { const projectIssueIds = this.issues[projectId]; if (!projectIssueIds) return; - const _issues = this.rootStore.issues.getIssuesByIds(projectIssueIds); + const _issues = this.rootStore.issues.getIssuesByIds(projectIssueIds, "un-archived"); if (!_issues) return []; let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = []; @@ -165,6 +169,18 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues { } }; + archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId); + + runInAction(() => { + pull(this.issues[projectId], issueId); + }); + } catch (error) { + throw error; + } + }; + quickAddIssue = async (workspaceSlug: string, projectId: string, data: TIssue) => { try { runInAction(() => { diff --git a/web/store/issue/workspace/issue.store.ts b/web/store/issue/workspace/issue.store.ts index e2b8418c7e0..707ed208d20 100644 --- a/web/store/issue/workspace/issue.store.ts +++ b/web/store/issue/workspace/issue.store.ts @@ -1,10 +1,11 @@ import { action, observable, makeObservable, computed, runInAction } from "mobx"; import set from "lodash/set"; +import pull from "lodash/pull"; // base class import { IssueHelperStore } from "../helpers/issue-helper.store"; // services import { WorkspaceService } from "services/workspace.service"; -import { IssueService } from "services/issue"; +import { IssueService, IssueArchiveService } from "services/issue"; // types import { IIssueRootStore } from "../root.store"; import { TIssue, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types"; @@ -37,6 +38,12 @@ export interface IWorkspaceIssues { issueId: string, viewId?: string | undefined ) => Promise; + archiveIssue: ( + workspaceSlug: string, + projectId: string, + issueId: string, + viewId?: string | undefined + ) => Promise; } export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssues { @@ -52,6 +59,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue // service workspaceService; issueService; + issueArchiveService; constructor(_rootStore: IIssueRootStore) { super(_rootStore); @@ -67,12 +75,14 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue createIssue: action, updateIssue: action, removeIssue: action, + archiveIssue: action, }); // root store this.rootIssueStore = _rootStore; // services this.workspaceService = new WorkspaceService(); this.issueService = new IssueService(); + this.issueArchiveService = new IssueArchiveService(); } get groupedIssueIds() { @@ -91,7 +101,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue if (!viewIssueIds) return { dataViewId: viewId, issueIds: undefined }; - const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds); + const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds, "un-archived"); if (!_issues) return { dataViewId: viewId, issueIds: [] }; let issueIds: TIssue | TUnGroupedIssues | undefined = undefined; @@ -196,4 +206,25 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue throw error; } }; + + archiveIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + viewId: string | undefined = undefined + ) => { + try { + if (!viewId) throw new Error("View id is required"); + + const uniqueViewId = `${workspaceSlug}_${viewId}`; + + await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId); + + runInAction(() => { + pull(this.issues[uniqueViewId], issueId); + }); + } catch (error) { + throw error; + } + }; } From 63c857e04193520121719dfefa4655ccde9fe535 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Fri, 23 Feb 2024 15:30:03 +0530 Subject: [PATCH 04/15] chore: peek overview archive button --- web/components/issues/archive-issue-modal.tsx | 2 - web/components/issues/issue-detail/root.tsx | 31 +++++++++++++- .../calendar/roots/cycle-root.tsx | 2 +- .../calendar/roots/module-root.tsx | 2 +- .../issue-layouts/kanban/roots/cycle-root.tsx | 2 +- .../kanban/roots/module-root.tsx | 2 +- .../issue-layouts/list/roots/cycle-root.tsx | 2 +- .../issue-layouts/list/roots/module-root.tsx | 2 +- .../quick-action-dropdowns/all-issue.tsx | 4 +- .../quick-action-dropdowns/cycle-issue.tsx | 2 +- .../quick-action-dropdowns/module-issue.tsx | 2 +- .../quick-action-dropdowns/project-issue.tsx | 4 +- .../spreadsheet/roots/cycle-root.tsx | 2 +- .../spreadsheet/roots/module-root.tsx | 2 +- .../issues/peek-overview/header.tsx | 27 +++++++++---- web/components/issues/peek-overview/root.tsx | 40 ++++++++++++++++--- web/components/issues/peek-overview/view.tsx | 17 ++++++-- web/constants/event-tracker.ts | 29 +++++++------- web/store/issue/issue-details/issue.store.ts | 4 ++ web/store/issue/issue-details/root.store.ts | 7 ++++ web/store/issue/issue.store.ts | 6 ++- 21 files changed, 141 insertions(+), 50 deletions(-) diff --git a/web/components/issues/archive-issue-modal.tsx b/web/components/issues/archive-issue-modal.tsx index 053929da917..6da71ed1639 100644 --- a/web/components/issues/archive-issue-modal.tsx +++ b/web/components/issues/archive-issue-modal.tsx @@ -38,9 +38,7 @@ export const ArchiveIssueModal: React.FC = (props) => { }; const handleArchiveIssue = async () => { - console.log("Outside"); if (!onSubmit) return; - console.log("Inside"); setIsArchiving(true); await onSubmit() diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index 6252fc03a7d..c6e23ff4637 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -16,7 +16,7 @@ import { TIssue } from "@plane/types"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; -import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker"; +import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "constants/event-tracker"; import { observer } from "mobx-react"; export type TIssueOperations = { @@ -29,6 +29,7 @@ export type TIssueOperations = { showToast?: boolean ) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + archive: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise; @@ -63,6 +64,7 @@ export const IssueDetailRoot: FC = observer((props) => { fetchIssue, updateIssue, removeIssue, + archiveIssue, addIssueToCycle, removeIssueFromCycle, addModulesToIssue, @@ -158,6 +160,32 @@ export const IssueDetailRoot: FC = observer((props) => { }); } }, + archive: async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + await archiveIssue(workspaceSlug, projectId, issueId); + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue archived successfully.", + }); + captureIssueEvent({ + eventName: ISSUE_ARCHIVED, + payload: { id: issueId, state: "SUCCESS", element: "Issue details page" }, + path: router.asPath, + }); + } catch (error) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be archived. Please try again.", + }); + captureIssueEvent({ + eventName: ISSUE_ARCHIVED, + payload: { id: issueId, state: "FAILED", element: "Issue details page" }, + path: router.asPath, + }); + } + }, addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); @@ -321,6 +349,7 @@ export const IssueDetailRoot: FC = observer((props) => { fetchIssue, updateIssue, removeIssue, + archiveIssue, removeArchivedIssue, addIssueToCycle, removeIssueFromCycle, diff --git a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx index 106e28cbf5f..4daf68b9f5f 100644 --- a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx @@ -35,7 +35,7 @@ export const CycleCalendarLayout: React.FC = observer(() => { }, [EIssueActions.ARCHIVE]: async (issue: TIssue) => { if (!workspaceSlug || !cycleId) return; - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); + await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); }, }), [issues, workspaceSlug, cycleId, projectId] diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index 1a62f740974..cb474d25ec6 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -36,7 +36,7 @@ export const ModuleCalendarLayout: React.FC = observer(() => { }, [EIssueActions.ARCHIVE]: async (issue: TIssue) => { if (!workspaceSlug || !moduleId) return; - await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); + await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, moduleId); }, }), [issues, workspaceSlug, moduleId] diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index 1a66541f3f4..f932db4a10a 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -42,7 +42,7 @@ export const CycleKanBanLayout: React.FC = observer(() => { [EIssueActions.ARCHIVE]: async (issue: TIssue) => { if (!workspaceSlug || !cycleId) return; - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); + await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); }, }), [issues, workspaceSlug, cycleId] diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index 5e8cd63c81f..07ad7eb83ca 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -41,7 +41,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => { [EIssueActions.ARCHIVE]: async (issue: TIssue) => { if (!workspaceSlug || !moduleId) return; - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); + await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); }, }), [issues, workspaceSlug, moduleId] diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index b282705e66c..977f18b0d67 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -41,7 +41,7 @@ export const CycleListLayout: React.FC = observer(() => { [EIssueActions.ARCHIVE]: async (issue: TIssue) => { if (!workspaceSlug || !cycleId) return; - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); + await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString()); }, }), [issues, workspaceSlug, cycleId] diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index 36e04a054d1..95c62d34cdb 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -40,7 +40,7 @@ export const ModuleListLayout: React.FC = observer(() => { [EIssueActions.ARCHIVE]: async (issue: TIssue) => { if (!workspaceSlug || !moduleId) return; - await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id); + await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString()); }, }), [issues, workspaceSlug, moduleId] diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 6c70c077026..8882f948a69 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -39,6 +39,8 @@ export const AllIssueQuickActions: React.FC = (props) => { // toast alert const { setToastAlert } = useToast(); + const isArchivingAllowed = handleArchive && !readOnly; + const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; const handleOpenInNewTab = () => window.open(`/${issueLink}}`, "_blank"); @@ -132,7 +134,7 @@ export const AllIssueQuickActions: React.FC = (props) => {
)} - {!readOnly && ( + {isArchivingAllowed && ( setArchiveIssueModal(true)}>
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index a6a500326f9..ee19d52021b 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -48,7 +48,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { } = useUser(); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; - const isArchivingAllowed = isEditingAllowed; + const isArchivingAllowed = handleArchive && isEditingAllowed; const isDeletingAllowed = isEditingAllowed; const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 8e28822b8f8..9a656b6a15e 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -48,7 +48,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { } = useUser(); const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; - const isArchivingAllowed = isEditingAllowed; + const isArchivingAllowed = handleArchive && isEditingAllowed; const isDeletingAllowed = isEditingAllowed; const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 5f780b5113c..d678b750943 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -45,7 +45,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; - const isArchivingAllowed = isEditingAllowed; + const isArchivingAllowed = handleArchive && isEditingAllowed; const isDeletingAllowed = isEditingAllowed; const { setToastAlert } = useToast(); @@ -73,8 +73,6 @@ export const ProjectIssueQuickActions: React.FC = (props) => const isDraftIssue = router?.asPath?.includes("draft-issues") || false; - console.log("handleArchive", handleArchive); - return ( <> { }, [EIssueActions.ARCHIVE]: async (issue: TIssue) => { if (!workspaceSlug || !cycleId) return; - issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); + issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, cycleId); }, }), [issues, workspaceSlug, cycleId] diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx index 7d011a364bb..af8abc80159 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx @@ -33,7 +33,7 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => { }, [EIssueActions.ARCHIVE]: async (issue: TIssue) => { if (!workspaceSlug || !moduleId) return; - issues.archiveIssue(workspaceSlug, issue.project_id, issue.id); + issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, moduleId); }, }), [issues, workspaceSlug, moduleId] diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx index 8b51c977e53..44c2c4d6df8 100644 --- a/web/components/issues/peek-overview/header.tsx +++ b/web/components/issues/peek-overview/header.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react"; -import { MoveRight, MoveDiagonal, Link2, Trash2 } from "lucide-react"; +import { MoveRight, MoveDiagonal, Link2, Trash2, Archive } from "lucide-react"; // ui -import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon } from "@plane/ui"; +import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Tooltip } from "@plane/ui"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; // hooks @@ -43,6 +43,7 @@ export type PeekOverviewHeaderProps = { isArchived: boolean; disabled: boolean; toggleDeleteIssueModal: (value: boolean) => void; + toggleArchiveIssueModal: (value: boolean) => void; isSubmitting: "submitting" | "submitted" | "saved"; }; @@ -57,6 +58,7 @@ export const IssuePeekOverviewHeader: FC = observer((pr disabled, removeRoutePeekId, toggleDeleteIssueModal, + toggleArchiveIssueModal, isSubmitting, } = props; // router @@ -138,13 +140,22 @@ export const IssuePeekOverviewHeader: FC = observer((pr {currentUser && !isArchived && ( )} - - {!disabled && ( - + + + + + {!disabled && ( + + + )}
diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index 76dec509416..61423b3d8c8 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -11,7 +11,7 @@ import { TIssue } from "@plane/types"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; -import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker"; +import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "constants/event-tracker"; interface IIssuePeekOverview { is_archived?: boolean; @@ -27,6 +27,7 @@ export type TIssuePeekOperations = { showToast?: boolean ) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + archive: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise; @@ -60,6 +61,7 @@ export const IssuePeekOverview: FC = observer((props) => { peekIssue, updateIssue, removeIssue, + archiveIssue, issue: { getIssueById, fetchIssue }, } = useIssueDetail(); const { addIssueToCycle, removeIssueFromCycle, addModulesToIssue, removeIssueFromModule, removeModulesFromIssue } = @@ -85,7 +87,7 @@ export const IssuePeekOverview: FC = observer((props) => { showToast: boolean = true ) => { try { - const response = await updateIssue(workspaceSlug, projectId, issueId, data); + await updateIssue(workspaceSlug, projectId, issueId, data); if (showToast) setToastAlert({ title: "Issue updated successfully", @@ -116,9 +118,8 @@ export const IssuePeekOverview: FC = observer((props) => { }, remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { - let response; - if (is_archived) response = await removeArchivedIssue(workspaceSlug, projectId, issueId); - else response = await removeIssue(workspaceSlug, projectId, issueId); + if (is_archived) await removeArchivedIssue(workspaceSlug, projectId, issueId); + await removeIssue(workspaceSlug, projectId, issueId); setToastAlert({ title: "Issue deleted successfully", type: "success", @@ -142,6 +143,34 @@ export const IssuePeekOverview: FC = observer((props) => { }); } }, + archive: async (workspaceSlug: string, projectId: string, issueId: string) => { + console.log("Archiving...", archiveIssue); + try { + await archiveIssue(workspaceSlug, projectId, issueId); + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue archived successfully.", + }); + captureIssueEvent({ + eventName: ISSUE_ARCHIVED, + payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, + path: router.asPath, + }); + } catch (error) { + console.log("error", error); + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be archived. Please try again.", + }); + captureIssueEvent({ + eventName: ISSUE_ARCHIVED, + payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, + path: router.asPath, + }); + } + }, addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); @@ -305,6 +334,7 @@ export const IssuePeekOverview: FC = observer((props) => { fetchIssue, updateIssue, removeIssue, + archiveIssue, removeArchivedIssue, addIssueToCycle, removeIssueFromCycle, diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index e69692eccca..1b75901bb2a 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -16,6 +16,7 @@ import { PeekOverviewIssueDetails, PeekOverviewProperties, TIssueOperations, + ArchiveIssueModal, } from "components/issues"; import { IssueActivity } from "../issue-detail/issue-activity"; // ui @@ -43,7 +44,9 @@ export const IssueView: FC = observer((props) => { setPeekIssue, isAnyModalOpen, isDeleteIssueModalOpen, + isArchiveIssueModalOpen, toggleDeleteIssueModal, + toggleArchiveIssueModal, issue: { getIssueById }, } = useIssueDetail(); const issue = getIssueById(issueId); @@ -58,6 +61,15 @@ export const IssueView: FC = observer((props) => { return ( <> + {issue && !is_archived && ( + toggleArchiveIssueModal(false)} + data={issue} + onSubmit={() => issueOperations.archive(workspaceSlug, projectId, issueId)} + /> + )} + {issue && !is_archived && ( = observer((props) => { {/* header */} { - setPeekMode(value); - }} + setPeekMode={(value) => setPeekMode(value)} removeRoutePeekId={removeRoutePeekId} toggleDeleteIssueModal={toggleDeleteIssueModal} + toggleArchiveIssueModal={toggleArchiveIssueModal} isArchived={is_archived} issueId={issueId} workspaceSlug={workspaceSlug} diff --git a/web/constants/event-tracker.ts b/web/constants/event-tracker.ts index a0bf0b5bbe8..835fcbf5d47 100644 --- a/web/constants/event-tracker.ts +++ b/web/constants/event-tracker.ts @@ -127,20 +127,18 @@ export const getIssueEventPayload = (props: IssueEventProps) => { return eventPayload; }; -export const getProjectStateEventPayload = (payload: any) => { - return { - workspace_id: payload.workspace_id, - project_id: payload.id, - state_id: payload.id, - created_at: payload.created_at, - updated_at: payload.updated_at, - group: payload.group, - color: payload.color, - default: payload.default, - state: payload.state, - element: payload.element, - }; -}; +export const getProjectStateEventPayload = (payload: any) => ({ + workspace_id: payload.workspace_id, + project_id: payload.id, + state_id: payload.id, + created_at: payload.created_at, + updated_at: payload.updated_at, + group: payload.group, + color: payload.color, + default: payload.default, + state: payload.state, + element: payload.element, +}); // Workspace crud Events export const WORKSPACE_CREATED = "Workspace created"; @@ -169,6 +167,7 @@ export const MODULE_LINK_DELETED = "Module link deleted"; export const ISSUE_CREATED = "Issue created"; export const ISSUE_UPDATED = "Issue updated"; export const ISSUE_DELETED = "Issue deleted"; +export const ISSUE_ARCHIVED = "Issue archived"; export const ISSUE_OPENED = "Issue opened"; // Project State Events export const STATE_CREATED = "State created"; @@ -218,7 +217,7 @@ export const NOTIFICATION_SNOOZED = "Notification snoozed"; export const NOTIFICATION_READ = "Notification marked read"; export const UNREAD_NOTIFICATIONS = "Unread notifications viewed"; export const NOTIFICATIONS_READ = "All notifications marked read"; -export const SNOOZED_NOTIFICATIONS= "Snoozed notifications viewed"; +export const SNOOZED_NOTIFICATIONS = "Snoozed notifications viewed"; export const ARCHIVED_NOTIFICATIONS = "Archived notifications viewed"; // Groups export const GROUP_WORKSPACE = "Workspace_metrics"; diff --git a/web/store/issue/issue-details/issue.store.ts b/web/store/issue/issue-details/issue.store.ts index 8731bf478bd..80e45350e53 100644 --- a/web/store/issue/issue-details/issue.store.ts +++ b/web/store/issue/issue-details/issue.store.ts @@ -11,6 +11,7 @@ export interface IIssueStoreActions { fetchIssue: (workspaceSlug: string, projectId: string, issueId: string, isArchived?: boolean) => Promise; updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise; @@ -116,6 +117,9 @@ export class IssueStore implements IIssueStore { removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); + archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => + this.rootIssueDetailStore.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId); + addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle( workspaceSlug, diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index 4c2d6add1e5..3b6b64693a1 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -47,6 +47,7 @@ export interface IIssueDetail isIssueLinkModalOpen: boolean; isParentIssueModalOpen: boolean; isDeleteIssueModalOpen: boolean; + isArchiveIssueModalOpen: boolean; isRelationModalOpen: TIssueRelationTypes | null; // computed isAnyModalOpen: boolean; @@ -55,6 +56,7 @@ export interface IIssueDetail toggleIssueLinkModal: (value: boolean) => void; toggleParentIssueModal: (value: boolean) => void; toggleDeleteIssueModal: (value: boolean) => void; + toggleArchiveIssueModal: (value: boolean) => void; toggleRelationModal: (value: TIssueRelationTypes | null) => void; // store rootIssueStore: IIssueRootStore; @@ -76,6 +78,7 @@ export class IssueDetail implements IIssueDetail { isIssueLinkModalOpen: boolean = false; isParentIssueModalOpen: boolean = false; isDeleteIssueModalOpen: boolean = false; + isArchiveIssueModalOpen: boolean = false; isRelationModalOpen: TIssueRelationTypes | null = null; // store rootIssueStore: IIssueRootStore; @@ -97,6 +100,7 @@ export class IssueDetail implements IIssueDetail { isIssueLinkModalOpen: observable.ref, isParentIssueModalOpen: observable.ref, isDeleteIssueModalOpen: observable.ref, + isArchiveIssueModalOpen: observable.ref, isRelationModalOpen: observable.ref, // computed isAnyModalOpen: computed, @@ -105,6 +109,7 @@ export class IssueDetail implements IIssueDetail { toggleIssueLinkModal: action, toggleParentIssueModal: action, toggleDeleteIssueModal: action, + toggleArchiveIssueModal: action, toggleRelationModal: action, }); @@ -128,6 +133,7 @@ export class IssueDetail implements IIssueDetail { this.isIssueLinkModalOpen || this.isParentIssueModalOpen || this.isDeleteIssueModalOpen || + this.isArchiveIssueModalOpen || Boolean(this.isRelationModalOpen) ); } @@ -137,6 +143,7 @@ export class IssueDetail implements IIssueDetail { toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value); toggleParentIssueModal = (value: boolean) => (this.isParentIssueModalOpen = value); toggleDeleteIssueModal = (value: boolean) => (this.isDeleteIssueModalOpen = value); + toggleArchiveIssueModal = (value: boolean) => (this.isArchiveIssueModalOpen = value); toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value); // issue diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index 9903abcf754..cbda505ff2a 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -111,11 +111,13 @@ export class IssueStore implements IIssueStore { * @param {boolean} archivedIssues * @returns {Record | undefined} */ - getIssuesByIds = computedFn((issueIds: string[], archivedIssues: "archived" | "un-archived") => { + getIssuesByIds = computedFn((issueIds: string[], type: "archived" | "un-archived") => { if (!issueIds || issueIds.length <= 0 || isEmpty(this.issuesMap)) return undefined; const filteredIssues: { [key: string]: TIssue } = {}; Object.values(this.issuesMap).forEach((issue) => { - if (archivedIssues === "archived" || issue.archived_at) { + // if type is archived then check archived_at is not null + // if type is un-archived then check archived_at is null + if ((type === "archived" && issue.archived_at) || (type === "un-archived" && !issue.archived_at)) { if (issueIds.includes(issue.id)) filteredIssues[issue.id] = issue; } }); From 79781b86a4999ed3fd5ee0b671245a7b99e055be Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 26 Feb 2024 12:43:33 +0530 Subject: [PATCH 05/15] chore: issue archive completed at state --- apiserver/plane/app/views/issue.py | 6 +++--- apiserver/plane/bgtasks/issue_activites_task.py | 10 ++++++++-- apiserver/plane/bgtasks/issue_automation_task.py | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index ebfdd4110b7..2c33d8f4b80 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -1633,14 +1633,14 @@ def archive(self, request, slug, project_id, pk=None): project_id=project_id, pk=pk, ) - if issue.state.group not in ["completed"]: + if issue.state.group not in ["completed", "cancelled"]: return Response( - {"error": "Can only archive a completed issue"}, + {"error": "Can only archive completed or cancelled state group issue"}, status=status.HTTP_400_BAD_REQUEST, ) issue_activity.delay( type="issue.activity.updated", - requested_data=json.dumps({"archived_at": str(timezone.now().date())}), + requested_data=json.dumps({"archived_at": str(timezone.now().date()), "automation": False}), actor_id=str(request.user.id), issue_id=str(issue.id), project_id=str(project_id), diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index b86ab5e783e..2a16ee911a8 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -483,17 +483,23 @@ def track_archive_at( ) ) else: + if requested_data.get("automation"): + comment = "Plane has archived the issue" + new_value = "archive" + else: + comment = "Actor has archived the issue" + new_value = "manual_archive" issue_activities.append( IssueActivity( issue_id=issue_id, project_id=project_id, workspace_id=workspace_id, - comment="Plane has archived the issue", + comment=comment, verb="updated", actor_id=actor_id, field="archived_at", old_value=None, - new_value="archive", + new_value=new_value, epoch=epoch, ) ) diff --git a/apiserver/plane/bgtasks/issue_automation_task.py b/apiserver/plane/bgtasks/issue_automation_task.py index 974a545fcdd..c6c4d75158c 100644 --- a/apiserver/plane/bgtasks/issue_automation_task.py +++ b/apiserver/plane/bgtasks/issue_automation_task.py @@ -79,7 +79,7 @@ def archive_old_issues(): issue_activity.delay( type="issue.activity.updated", requested_data=json.dumps( - {"archived_at": str(archive_at)} + {"archived_at": str(archive_at), "automation": True} ), actor_id=str(project.created_by_id), issue_id=issue.id, From 2a9481dd2ce97f50b90a172b61374e33aef8ca87 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 26 Feb 2024 15:50:03 +0530 Subject: [PATCH 06/15] chore: updated archiving icon and added archive option everywhere --- web/components/issues/archive-issue-modal.tsx | 6 +- .../issues/delete-archived-issue-modal.tsx | 139 ------------------ web/components/issues/delete-issue-modal.tsx | 20 +-- web/components/issues/index.ts | 1 - web/components/issues/issue-detail/root.tsx | 6 +- .../issues/issue-detail/sidebar.tsx | 88 +++++++---- .../calendar/base-calendar-root.tsx | 6 + .../issue-layouts/kanban/base-kanban-root.tsx | 4 + .../issue-layouts/list/base-list-root.tsx | 4 + .../issue-layouts/list/list-view-types.d.ts | 1 + .../list/roots/archived-issue-root.tsx | 5 + .../quick-action-dropdowns/all-issue.tsx | 25 ++-- .../quick-action-dropdowns/archived-issue.tsx | 55 ++++--- .../quick-action-dropdowns/cycle-issue.tsx | 33 +++-- .../quick-action-dropdowns/module-issue.tsx | 33 +++-- .../quick-action-dropdowns/project-issue.tsx | 28 ++-- .../spreadsheet/base-spreadsheet-root.tsx | 4 + web/components/issues/issue-layouts/types.ts | 1 + .../issues/peek-overview/header.tsx | 37 +++-- web/components/issues/peek-overview/root.tsx | 24 ++- web/components/issues/peek-overview/view.tsx | 7 +- .../archived-issues/[archivedIssueId].tsx | 25 ++-- web/services/issue/issue_archive.service.ts | 10 +- web/store/issue/archived/issue.store.ts | 25 ++-- web/store/issue/issue-details/root.store.ts | 2 + 25 files changed, 271 insertions(+), 318 deletions(-) delete mode 100644 web/components/issues/delete-archived-issue-modal.tsx diff --git a/web/components/issues/archive-issue-modal.tsx b/web/components/issues/archive-issue-modal.tsx index 6da71ed1639..2ec26a24361 100644 --- a/web/components/issues/archive-issue-modal.tsx +++ b/web/components/issues/archive-issue-modal.tsx @@ -40,14 +40,16 @@ export const ArchiveIssueModal: React.FC = (props) => { const handleArchiveIssue = async () => { if (!onSubmit) return; + console.log("onSubmit", onSubmit); + setIsArchiving(true); await onSubmit() .then(() => onClose()) .catch(() => setToastAlert({ - title: "Error", type: "error", - message: "Failed to delete issue", + title: "Error!", + message: "Issue could not be archived. Please try again.", }) ) .finally(() => setIsArchiving(false)); diff --git a/web/components/issues/delete-archived-issue-modal.tsx b/web/components/issues/delete-archived-issue-modal.tsx deleted file mode 100644 index 49d9e19ddee..00000000000 --- a/web/components/issues/delete-archived-issue-modal.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { useEffect, useState, Fragment } from "react"; -import { useRouter } from "next/router"; -import { observer } from "mobx-react-lite"; -import { Dialog, Transition } from "@headlessui/react"; -import { AlertTriangle } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; -import { useIssues, useProject } from "hooks/store"; -// ui -import { Button } from "@plane/ui"; -// types -import type { TIssue } from "@plane/types"; -import { EIssuesStoreType } from "constants/issue"; - -type Props = { - isOpen: boolean; - handleClose: () => void; - data: TIssue; - onSubmit?: () => Promise; -}; - -export const DeleteArchivedIssueModal: React.FC = observer((props) => { - const { data, isOpen, handleClose, onSubmit } = props; - - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { setToastAlert } = useToast(); - const { getProjectById } = useProject(); - - const { - issues: { removeIssue }, - } = useIssues(EIssuesStoreType.ARCHIVED); - - const [isDeleteLoading, setIsDeleteLoading] = useState(false); - - useEffect(() => { - setIsDeleteLoading(false); - }, [isOpen]); - - const onClose = () => { - setIsDeleteLoading(false); - handleClose(); - }; - - const handleIssueDelete = async () => { - if (!workspaceSlug) return; - - setIsDeleteLoading(true); - - await removeIssue(workspaceSlug.toString(), data.project_id, data.id) - .then(() => { - if (onSubmit) onSubmit(); - }) - .catch((err) => { - const error = err?.detail; - const errorString = Array.isArray(error) ? error[0] : error; - - setToastAlert({ - title: "Error", - type: "error", - message: errorString || "Something went wrong.", - }); - }) - .finally(() => { - setIsDeleteLoading(false); - onClose(); - }); - }; - - return ( - - - -
- - -
-
- - -
-
- - - -

Delete Archived Issue

-
-
- -

- Are you sure you want to delete issue{" "} - - {getProjectById(data?.project_id)?.identifier}-{data?.sequence_id} - - {""}? All of the data related to the archived issue will be permanently removed. This action - cannot be undone. -

-
-
- - -
-
-
-
-
-
-
-
- ); -}); diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index a063980c083..3a9c0653edf 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -23,14 +23,14 @@ export const DeleteIssueModal: React.FC = (props) => { const { issueMap } = useIssues(); - const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); const { setToastAlert } = useToast(); // hooks const { getProjectById } = useProject(); useEffect(() => { - setIsDeleteLoading(false); + setIsDeleting(false); }, [isOpen]); if (!dataId && !data) return null; @@ -38,12 +38,12 @@ export const DeleteIssueModal: React.FC = (props) => { const issue = data ? data : issueMap[dataId!]; const onClose = () => { - setIsDeleteLoading(false); + setIsDeleting(false); handleClose(); }; const handleIssueDelete = async () => { - setIsDeleteLoading(true); + setIsDeleting(true); if (onSubmit) await onSubmit() .then(() => { @@ -56,7 +56,7 @@ export const DeleteIssueModal: React.FC = (props) => { message: "Failed to delete issue", }); }) - .finally(() => setIsDeleteLoading(false)); + .finally(() => setIsDeleting(false)); }; return ( @@ -109,14 +109,8 @@ export const DeleteIssueModal: React.FC = (props) => { -
diff --git a/web/components/issues/index.ts b/web/components/issues/index.ts index 0750df3e3ae..9bb4e0419bd 100644 --- a/web/components/issues/index.ts +++ b/web/components/issues/index.ts @@ -21,4 +21,3 @@ export * from "./delete-draft-issue-modal"; // archived issue export * from "./archive-issue-modal"; -export * from "./delete-archived-issue-modal"; diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index c6e23ff4637..68d6bab4f97 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -29,7 +29,7 @@ export type TIssueOperations = { showToast?: boolean ) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; - archive: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise; @@ -379,7 +379,7 @@ export const IssueDetailRoot: FC = observer((props) => { /> ) : (
-
+
= observer((props) => { />
= observer((props) => { const { workspaceSlug, projectId, issueId, issueOperations, is_archived, is_editable } = props; + // states + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + const [isArchiving, setIsArchiving] = useState(false); // router const router = useRouter(); // store hooks @@ -66,8 +70,6 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { issue: { getIssueById }, } = useIssueDetail(); const { getStateById } = useProjectState(); - // states - const [deleteIssueModal, setDeleteIssueModal] = useState(false); const issue = getIssueById(issueId); if (!issue) return <>; @@ -83,8 +85,27 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { }); }; - const projectDetails = issue ? getProjectById(issue.project_id) : null; + const handleDeleteIssue = async () => { + await issueOperations.remove(workspaceSlug, projectId, issueId); + router.push(`/${workspaceSlug}/projects/${projectId}/issues`); + }; + + const handleArchiveIssue = async () => { + if (!issueOperations.archive) return; + setIsArchiving(true); + await issueOperations.archive(workspaceSlug, projectId, issueId).finally(() => setIsArchiving(false)); + router.push(`/${workspaceSlug}/projects/${projectId}/archived-issues`); + }; + // derived values + const projectDetails = getProjectById(issue.project_id); const stateDetails = getStateById(issue.state_id); + // auth + const isArchivingAllowed = + !is_archived && + issueOperations.archive && + is_editable && + !!stateDetails && + [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const minDate = issue.start_date ? new Date(issue.start_date) : null; minDate?.setDate(minDate.getDate()); @@ -99,37 +120,50 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { handleClose={() => setDeleteIssueModal(false)} isOpen={deleteIssueModal} data={issue} - onSubmit={async () => { - await issueOperations.remove(workspaceSlug, projectId, issueId); - router.push(`/${workspaceSlug}/projects/${projectId}/issues`); - }} + onSubmit={handleDeleteIssue} /> )}
-
+
{currentUser && !is_archived && ( )} - - - - {is_editable && ( - - )} +
+ + + + {isArchivingAllowed && ( + + + + )} + {is_editable && ( + + + + )} +
diff --git a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx index c95764da7d7..43f62e5bee4 100644 --- a/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx +++ b/web/components/issues/issue-layouts/calendar/base-calendar-root.tsx @@ -27,6 +27,7 @@ interface IBaseCalendarRoot { [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; + [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; }; viewId?: string; isCompletedCycle?: boolean; @@ -120,6 +121,11 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => { ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.ARCHIVE) : undefined } + handleRestore={ + issueActions[EIssueActions.RESTORE] + ? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.RESTORE) + : undefined + } readOnly={!isEditingAllowed || isCompletedCycle} /> )} diff --git a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx index e75fc91a95f..0d7a984b16b 100644 --- a/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -42,6 +42,7 @@ export interface IBaseKanBanLayout { [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; + [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; }; showLoader?: boolean; viewId?: string; @@ -192,6 +193,9 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas handleArchive={ issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined } + handleRestore={ + issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined + } readOnly={!isEditingAllowed || isCompletedCycle} /> ), diff --git a/web/components/issues/issue-layouts/list/base-list-root.tsx b/web/components/issues/issue-layouts/list/base-list-root.tsx index 10b4dbb952b..ffe9de66142 100644 --- a/web/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/components/issues/issue-layouts/list/base-list-root.tsx @@ -42,6 +42,7 @@ interface IBaseListRoot { [EIssueActions.UPDATE]?: (issue: TIssue) => Promise; [EIssueActions.REMOVE]?: (issue: TIssue) => Promise; [EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise; + [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; }; viewId?: string; storeType: TCreateModalStoreTypes; @@ -113,6 +114,9 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { handleArchive={ issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined } + handleRestore={ + issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined + } readOnly={!isEditingAllowed || isCompletedCycle} /> ), diff --git a/web/components/issues/issue-layouts/list/list-view-types.d.ts b/web/components/issues/issue-layouts/list/list-view-types.d.ts index 304872ffb60..f435d0639a3 100644 --- a/web/components/issues/issue-layouts/list/list-view-types.d.ts +++ b/web/components/issues/issue-layouts/list/list-view-types.d.ts @@ -6,6 +6,7 @@ export interface IQuickActionProps { handleUpdate?: (data: TIssue) => Promise; handleRemoveFromView?: () => Promise; handleArchive?: () => Promise; + handleRestore?: () => Promise; customActionButton?: React.ReactElement; portalElement?: HTMLDivElement | null; readOnly?: boolean; diff --git a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx index 2ba4ea7f5f7..6e70d00d029 100644 --- a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx @@ -24,6 +24,11 @@ export const ArchivedIssueListLayout: FC = observer(() => { await issues.removeIssue(workspaceSlug, projectId, issue.id); }, + [EIssueActions.RESTORE]: async (issue: TIssue) => { + if (!workspaceSlug || !projectId) return; + + await issues.restoreIssue(workspaceSlug, projectId, issue.id); + }, }), [issues, workspaceSlug, projectId] ); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 8882f948a69..3f0ac5ab8a1 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { CustomMenu } from "@plane/ui"; -import { Archive, Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; +import { ArchiveIcon, CustomMenu } from "@plane/ui"; +import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; import omit from "lodash/omit"; // hooks import useToast from "hooks/use-toast"; -import { useEventTracker } from "hooks/store"; +import { useEventTracker, useProjectState } from "hooks/store"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -15,6 +15,7 @@ import { TIssue } from "@plane/types"; import { IQuickActionProps } from "../list/list-view-types"; // constants import { EIssuesStoreType } from "constants/issue"; +import { STATE_GROUPS } from "constants/state"; export const AllIssueQuickActions: React.FC = (props) => { const { @@ -34,17 +35,23 @@ export const AllIssueQuickActions: React.FC = (props) => { // router const router = useRouter(); const { workspaceSlug } = router.query; - // hooks + // store hooks const { setTrackElement } = useEventTracker(); + const { getStateById } = useProjectState(); // toast alert const { setToastAlert } = useToast(); - - const isArchivingAllowed = handleArchive && !readOnly; + // derived values + const stateDetails = getStateById(issue.state_id); + // auth + const isArchivingAllowed = + handleArchive && + !readOnly && + !!stateDetails && + [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; - const handleOpenInNewTab = () => window.open(`/${issueLink}}`, "_blank"); - + const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => setToastAlert({ @@ -137,7 +144,7 @@ export const AllIssueQuickActions: React.FC = (props) => { {isArchivingAllowed && ( setArchiveIssueModal(true)}>
- + Archive
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index e331d718276..9bad920c891 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -1,12 +1,12 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { CustomMenu } from "@plane/ui"; -import { Link, Trash2 } from "lucide-react"; +import { ExternalLink, Link, RotateCcw, Trash2 } from "lucide-react"; // hooks import useToast from "hooks/use-toast"; import { useEventTracker, useIssues, useUser } from "hooks/store"; // components -import { DeleteArchivedIssueModal } from "components/issues"; +import { DeleteIssueModal } from "components/issues"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; // types @@ -15,40 +15,41 @@ import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; export const ArchivedIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, customActionButton, portalElement, readOnly = false } = props; + const { issue, handleDelete, handleRestore, customActionButton, portalElement, readOnly = false } = props; + // states + const [deleteIssueModal, setDeleteIssueModal] = useState(false); // router const router = useRouter(); const { workspaceSlug } = router.query; - // states - const [deleteIssueModal, setDeleteIssueModal] = useState(false); - // toast alert - const { setToastAlert } = useToast(); - // store hooks const { membership: { currentProjectRole }, } = useUser(); - - const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; - // store hooks const { setTrackElement } = useEventTracker(); const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); - + // derived values const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; + // auth + const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; + const isRestoringAllowed = handleRestore && isEditingAllowed; + // toast alert + const { setToastAlert } = useToast(); + + const issueLink = `${workspaceSlug}/projects/${issue.project_id}/archived-issues/${issue.id}`; - const handleCopyIssueLink = () => { - copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/archived-issues/${issue.id}`).then(() => + const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); + const handleCopyIssueLink = () => + copyUrlToClipboard(issueLink).then(() => setToastAlert({ type: "success", title: "Link copied", message: "Issue link copied to clipboard", }) ); - }; return ( <> - setDeleteIssueModal(false)} @@ -61,17 +62,27 @@ export const ArchivedIssueQuickActions: React.FC = (props) => closeOnSelect ellipsis > - { - handleCopyIssueLink(); - }} - > + {isRestoringAllowed && ( + +
+ + Restore +
+
+ )} + +
+ + Open in new tab +
+
+
Copy link
- {isEditingAllowed && !readOnly && ( + {isEditingAllowed && ( { setTrackElement(activeLayout); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index ee19d52021b..f2d226f3f45 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -1,11 +1,12 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { CustomMenu } from "@plane/ui"; -import { Archive, Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; +import { ArchiveIcon, CustomMenu } from "@plane/ui"; +import { observer } from "mobx-react"; +import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; import omit from "lodash/omit"; // hooks import useToast from "hooks/use-toast"; -import { useEventTracker, useIssues, useUser } from "hooks/store"; +import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -16,8 +17,9 @@ import { IQuickActionProps } from "../list/list-view-types"; // constants import { EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; +import { STATE_GROUPS } from "constants/state"; -export const CycleIssueQuickActions: React.FC = (props) => { +export const CycleIssueQuickActions: React.FC = observer((props) => { const { issue, handleDelete, @@ -39,23 +41,28 @@ export const CycleIssueQuickActions: React.FC = (props) => { // store hooks const { setTrackElement } = useEventTracker(); const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE); - // toast alert - const { setToastAlert } = useToast(); - - // store hooks const { membership: { currentProjectRole }, } = useUser(); - + const { getStateById } = useProjectState(); + // toast alert + const { setToastAlert } = useToast(); + // derived values + const stateDetails = getStateById(issue.state_id); + // auth const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; - const isArchivingAllowed = handleArchive && isEditingAllowed; + const isArchivingAllowed = + handleArchive && + isEditingAllowed && + !!stateDetails && + [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; - const handleOpenInNewTab = () => window.open(`/${issueLink}}`, "_blank"); + const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => @@ -164,7 +171,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { {isArchivingAllowed && ( setArchiveIssueModal(true)}>
- + Archive
@@ -185,4 +192,4 @@ export const CycleIssueQuickActions: React.FC = (props) => { ); -}; +}); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 9a656b6a15e..ad1da16f622 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -1,11 +1,12 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { CustomMenu } from "@plane/ui"; -import { Archive, Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; +import { ArchiveIcon, CustomMenu } from "@plane/ui"; +import { observer } from "mobx-react"; +import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react"; import omit from "lodash/omit"; // hooks import useToast from "hooks/use-toast"; -import { useIssues, useEventTracker, useUser } from "hooks/store"; +import { useIssues, useEventTracker, useUser, useProjectState } from "hooks/store"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; // helpers @@ -16,8 +17,9 @@ import { IQuickActionProps } from "../list/list-view-types"; // constants import { EIssuesStoreType } from "constants/issue"; import { EUserProjectRoles } from "constants/project"; +import { STATE_GROUPS } from "constants/state"; -export const ModuleIssueQuickActions: React.FC = (props) => { +export const ModuleIssueQuickActions: React.FC = observer((props) => { const { issue, handleDelete, @@ -39,23 +41,28 @@ export const ModuleIssueQuickActions: React.FC = (props) => { // store hooks const { setTrackElement } = useEventTracker(); const { issuesFilter } = useIssues(EIssuesStoreType.MODULE); - // toast alert - const { setToastAlert } = useToast(); - - // store hooks const { membership: { currentProjectRole }, } = useUser(); - + const { getStateById } = useProjectState(); + // toast alert + const { setToastAlert } = useToast(); + // derived values + const stateDetails = getStateById(issue.state_id); + // auth const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; - const isArchivingAllowed = handleArchive && isEditingAllowed; + const isArchivingAllowed = + handleArchive && + isEditingAllowed && + !!stateDetails && + [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; - const handleOpenInNewTab = () => window.open(`/${issueLink}}`, "_blank"); + const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => @@ -161,7 +168,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { {isArchivingAllowed && ( setArchiveIssueModal(true)}>
- + Archive
@@ -184,4 +191,4 @@ export const ModuleIssueQuickActions: React.FC = (props) => { ); -}; +}); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index d678b750943..61042f458c1 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -1,10 +1,11 @@ import { useState } from "react"; import { useRouter } from "next/router"; -import { CustomMenu } from "@plane/ui"; -import { Archive, Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; +import { ArchiveIcon, CustomMenu } from "@plane/ui"; +import { observer } from "mobx-react"; +import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; import omit from "lodash/omit"; // hooks -import { useEventTracker, useIssues, useUser } from "hooks/store"; +import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // components import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; @@ -16,8 +17,9 @@ import { IQuickActionProps } from "../list/list-view-types"; // constant import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; +import { STATE_GROUPS } from "constants/state"; -export const ProjectIssueQuickActions: React.FC = (props) => { +export const ProjectIssueQuickActions: React.FC = observer((props) => { const { issue, handleDelete, @@ -41,18 +43,24 @@ export const ProjectIssueQuickActions: React.FC = (props) => } = useUser(); const { setTrackElement } = useEventTracker(); const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - + const { getStateById } = useProjectState(); + // derived values const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; - + const stateDetails = getStateById(issue.state_id); + // auth const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; - const isArchivingAllowed = handleArchive && isEditingAllowed; + const isArchivingAllowed = + handleArchive && + isEditingAllowed && + !!stateDetails && + [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; const { setToastAlert } = useToast(); const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; - const handleOpenInNewTab = () => window.open(`/${issueLink}}`, "_blank"); + const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank"); const handleCopyIssueLink = () => copyUrlToClipboard(issueLink).then(() => @@ -149,7 +157,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => {isArchivingAllowed && ( setArchiveIssueModal(true)}>
- + Archive
@@ -170,4 +178,4 @@ export const ProjectIssueQuickActions: React.FC = (props) => ); -}; +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx index 4e8909e97b2..2f09b55d601 100644 --- a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -27,6 +27,7 @@ interface IBaseSpreadsheetRoot { [EIssueActions.UPDATE]?: (issue: TIssue) => void; [EIssueActions.REMOVE]?: (issue: TIssue) => void; [EIssueActions.ARCHIVE]?: (issue: TIssue) => void; + [EIssueActions.RESTORE]?: (issue: TIssue) => Promise; }; canEditPropertiesBasedOnProject?: (projectId: string) => boolean; isCompletedCycle?: boolean; @@ -107,6 +108,9 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { handleArchive={ issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined } + handleRestore={ + issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined + } portalElement={portalElement} readOnly={!isEditingAllowed || isCompletedCycle} /> diff --git a/web/components/issues/issue-layouts/types.ts b/web/components/issues/issue-layouts/types.ts index b943c159ce3..d1c3f4fd92a 100644 --- a/web/components/issues/issue-layouts/types.ts +++ b/web/components/issues/issue-layouts/types.ts @@ -3,4 +3,5 @@ export enum EIssueActions { DELETE = "delete", REMOVE = "remove", ARCHIVE = "archive", + RESTORE = "restore", } diff --git a/web/components/issues/peek-overview/header.tsx b/web/components/issues/peek-overview/header.tsx index 44c2c4d6df8..64cb48c0657 100644 --- a/web/components/issues/peek-overview/header.tsx +++ b/web/components/issues/peek-overview/header.tsx @@ -1,9 +1,9 @@ import { FC } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react"; -import { MoveRight, MoveDiagonal, Link2, Trash2, Archive } from "lucide-react"; +import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react"; // ui -import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Tooltip } from "@plane/ui"; +import { ArchiveIcon, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Tooltip } from "@plane/ui"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; // hooks @@ -70,12 +70,12 @@ export const IssuePeekOverviewHeader: FC = observer((pr // derived values const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode); + const issueLink = `${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${issueId}`; + const handleCopyText = (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); - copyUrlToClipboard( - `${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${issueId}` - ).then(() => { + copyUrlToClipboard(issueLink).then(() => { setToastAlert({ type: "success", title: "Link Copied!", @@ -83,14 +83,14 @@ export const IssuePeekOverviewHeader: FC = observer((pr }); }); }; - const redirectToIssueDetail = () => { - router.push({ - pathname: `/${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${issueId}`, - }); + router.push({ pathname: `/${issueLink}` }); removeRoutePeekId(); }; + const isArchivingAllowed = !isArchived && !disabled; + const isRestoringAllowed = isArchived && !disabled; + return (
= observer((pr - - - + {isArchivingAllowed && ( + + + + )} + {isRestoringAllowed && ( + + + + )} {!disabled && ( + + {isRestoring ? "Restoring" : "Restore"} +
)} {workspaceSlug && projectId && archivedIssueId && ( diff --git a/web/services/issue/issue_archive.service.ts b/web/services/issue/issue_archive.service.ts index aeac475a9a5..d2011977260 100644 --- a/web/services/issue/issue_archive.service.ts +++ b/web/services/issue/issue_archive.service.ts @@ -27,7 +27,7 @@ export class IssueArchiveService extends APIService { }); } - async unarchiveIssue(workspaceSlug: string, projectId: string, issueId: string): Promise { + async restoreIssue(workspaceSlug: string, projectId: string, issueId: string): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/archive/`) .then((response) => response?.data) .catch((error) => { @@ -49,12 +49,4 @@ export class IssueArchiveService extends APIService { throw error?.response?.data; }); } - - async deleteArchivedIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issuesId}/`) - .then((response) => response?.data) - .catch((error) => { - throw error?.response?.data; - }); - } } diff --git a/web/store/issue/archived/issue.store.ts b/web/store/issue/archived/issue.store.ts index 8ff7d199aaa..60d441e3175 100644 --- a/web/store/issue/archived/issue.store.ts +++ b/web/store/issue/archived/issue.store.ts @@ -1,5 +1,6 @@ import { action, observable, makeObservable, computed, runInAction } from "mobx"; import set from "lodash/set"; +import pull from "lodash/pull"; // base class import { IssueHelperStore } from "../helpers/issue-helper.store"; // services @@ -18,7 +19,7 @@ export interface IArchivedIssues { // actions fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; - removeIssueFromArchived: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; quickAddIssue: undefined; } @@ -48,7 +49,7 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues // action fetchIssues: action, removeIssue: action, - removeIssueFromArchived: action, + restoreIssue: action, }); // root store this.rootIssueStore = _rootStore; @@ -113,25 +114,21 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues try { await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); - const issueIndex = this.issues[projectId].findIndex((_issueId) => _issueId === issueId); - if (issueIndex >= 0) - runInAction(() => { - this.issues[projectId].splice(issueIndex, 1); - }); + runInAction(() => { + pull(this.issues[projectId], issueId); + }); } catch (error) { throw error; } }; - removeIssueFromArchived = async (workspaceSlug: string, projectId: string, issueId: string) => { + restoreIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { try { - const response = await this.archivedIssueService.unarchiveIssue(workspaceSlug, projectId, issueId); + const response = await this.archivedIssueService.restoreIssue(workspaceSlug, projectId, issueId); - const issueIndex = this.issues[projectId]?.findIndex((_issueId) => _issueId === issueId); - if (issueIndex && issueIndex >= 0) - runInAction(() => { - this.issues[projectId].splice(issueIndex, 1); - }); + runInAction(() => { + pull(this.issues[projectId], issueId); + }); return response; } catch (error) { diff --git a/web/store/issue/issue-details/root.store.ts b/web/store/issue/issue-details/root.store.ts index 3b6b64693a1..58a2bf6ce46 100644 --- a/web/store/issue/issue-details/root.store.ts +++ b/web/store/issue/issue-details/root.store.ts @@ -153,6 +153,8 @@ export class IssueDetail implements IIssueDetail { this.issue.updateIssue(workspaceSlug, projectId, issueId, data); removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => this.issue.removeIssue(workspaceSlug, projectId, issueId); + archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => + this.issue.archiveIssue(workspaceSlug, projectId, issueId); addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => this.issue.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => From ad5e311db38d5a4ff6b19d1bbdbe83de9f9d6c91 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 26 Feb 2024 16:51:20 +0530 Subject: [PATCH 07/15] chore: all issues quick actions dropdown --- packages/ui/src/dropdowns/custom-menu.tsx | 8 ++-- packages/ui/src/dropdowns/helper.tsx | 1 + web/components/issues/archive-issue-modal.tsx | 2 - .../quick-action-dropdowns/all-issue.tsx | 44 ++++++++++++------- .../quick-action-dropdowns/cycle-issue.tsx | 32 +++++++++----- .../quick-action-dropdowns/module-issue.tsx | 32 +++++++++----- .../quick-action-dropdowns/project-issue.tsx | 32 +++++++++----- .../roots/all-issue-layout-root.tsx | 13 +++++- .../issue-layouts/spreadsheet/issue-row.tsx | 8 ++-- 9 files changed, 115 insertions(+), 57 deletions(-) diff --git a/packages/ui/src/dropdowns/custom-menu.tsx b/packages/ui/src/dropdowns/custom-menu.tsx index 37aba932a59..cdfccbb4eda 100644 --- a/packages/ui/src/dropdowns/custom-menu.tsx +++ b/packages/ui/src/dropdowns/custom-menu.tsx @@ -177,17 +177,18 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => { }; const MenuItem: React.FC = (props) => { - const { children, onClick, className = "" } = props; + const { children, disabled = false, onClick, className } = props; return ( - + {({ active, close }) => ( diff --git a/packages/ui/src/dropdowns/helper.tsx b/packages/ui/src/dropdowns/helper.tsx index 930f332b9c0..f600499fe07 100644 --- a/packages/ui/src/dropdowns/helper.tsx +++ b/packages/ui/src/dropdowns/helper.tsx @@ -64,6 +64,7 @@ export type ICustomSearchSelectProps = IDropdownProps & export interface ICustomMenuItemProps { children: React.ReactNode; + disabled?: boolean; onClick?: (args?: any) => void; className?: string; } diff --git a/web/components/issues/archive-issue-modal.tsx b/web/components/issues/archive-issue-modal.tsx index 2ec26a24361..94c4c801a43 100644 --- a/web/components/issues/archive-issue-modal.tsx +++ b/web/components/issues/archive-issue-modal.tsx @@ -40,8 +40,6 @@ export const ArchiveIssueModal: React.FC = (props) => { const handleArchiveIssue = async () => { if (!onSubmit) return; - console.log("onSubmit", onSubmit); - setIsArchiving(true); await onSubmit() .then(() => onClose()) diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx index 3f0ac5ab8a1..1d0472454b8 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/all-issue.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { useRouter } from "next/router"; import { ArchiveIcon, CustomMenu } from "@plane/ui"; +import { observer } from "mobx-react"; import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react"; import omit from "lodash/omit"; // hooks @@ -17,7 +18,7 @@ import { IQuickActionProps } from "../list/list-view-types"; import { EIssuesStoreType } from "constants/issue"; import { STATE_GROUPS } from "constants/state"; -export const AllIssueQuickActions: React.FC = (props) => { +export const AllIssueQuickActions: React.FC = observer((props) => { const { issue, handleDelete, @@ -42,12 +43,11 @@ export const AllIssueQuickActions: React.FC = (props) => { const { setToastAlert } = useToast(); // derived values const stateDetails = getStateById(issue.state_id); + const isEditingAllowed = !readOnly; // auth - const isArchivingAllowed = - handleArchive && - !readOnly && - !!stateDetails && - [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); + const isArchivingAllowed = handleArchive && isEditingAllowed; + const isInArchivableGroup = + !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`; @@ -102,7 +102,7 @@ export const AllIssueQuickActions: React.FC = (props) => { closeOnSelect ellipsis > - {!readOnly && ( + {isEditingAllowed && ( { setTrackElement("Global issues"); @@ -128,7 +128,7 @@ export const AllIssueQuickActions: React.FC = (props) => { Copy link
- {!readOnly && ( + {isEditingAllowed && ( { setTrackElement("Global issues"); @@ -142,14 +142,28 @@ export const AllIssueQuickActions: React.FC = (props) => { )} {isArchivingAllowed && ( - setArchiveIssueModal(true)}> -
- - Archive -
+ setArchiveIssueModal(true)} disabled={!isInArchivableGroup}> + {isInArchivableGroup ? ( +
+ + Archive +
+ ) : ( +
+ +
+

Archive

+

+ Only completed or canceled +
+ issues can be archived +

+
+
+ )}
)} - {!readOnly && ( + {isEditingAllowed && ( { setTrackElement("Global issues"); @@ -165,4 +179,4 @@ export const AllIssueQuickActions: React.FC = (props) => { ); -}; +}); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index f2d226f3f45..3c0ed980007 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -51,11 +51,9 @@ export const CycleIssueQuickActions: React.FC = observer((pro const stateDetails = getStateById(issue.state_id); // auth const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; - const isArchivingAllowed = - handleArchive && - isEditingAllowed && - !!stateDetails && - [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); + const isArchivingAllowed = handleArchive && isEditingAllowed; + const isInArchivableGroup = + !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; @@ -169,11 +167,25 @@ export const CycleIssueQuickActions: React.FC = observer((pro )} {isArchivingAllowed && ( - setArchiveIssueModal(true)}> -
- - Archive -
+ setArchiveIssueModal(true)} disabled={!isInArchivableGroup}> + {isInArchivableGroup ? ( +
+ + Archive +
+ ) : ( +
+ +
+

Archive

+

+ Only completed or canceled +
+ issues can be archived +

+
+
+ )}
)} {isDeletingAllowed && ( diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index ad1da16f622..5532fa2a0dd 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -51,11 +51,9 @@ export const ModuleIssueQuickActions: React.FC = observer((pr const stateDetails = getStateById(issue.state_id); // auth const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; - const isArchivingAllowed = - handleArchive && - isEditingAllowed && - !!stateDetails && - [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); + const isArchivingAllowed = handleArchive && isEditingAllowed; + const isInArchivableGroup = + !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; @@ -166,11 +164,25 @@ export const ModuleIssueQuickActions: React.FC = observer((pr
)} {isArchivingAllowed && ( - setArchiveIssueModal(true)}> -
- - Archive -
+ setArchiveIssueModal(true)} disabled={!isInArchivableGroup}> + {isInArchivableGroup ? ( +
+ + Archive +
+ ) : ( +
+ +
+

Archive

+

+ Only completed or canceled +
+ issues can be archived +

+
+
+ )}
)} {isDeletingAllowed && ( diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 61042f458c1..f7e03eee1d0 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -49,11 +49,9 @@ export const ProjectIssueQuickActions: React.FC = observer((p const stateDetails = getStateById(issue.state_id); // auth const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly; - const isArchivingAllowed = - handleArchive && - isEditingAllowed && - !!stateDetails && - [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); + const isArchivingAllowed = handleArchive && isEditingAllowed; + const isInArchivableGroup = + !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const isDeletingAllowed = isEditingAllowed; const { setToastAlert } = useToast(); @@ -155,11 +153,25 @@ export const ProjectIssueQuickActions: React.FC = observer((p
)} {isArchivingAllowed && ( - setArchiveIssueModal(true)}> -
- - Archive -
+ setArchiveIssueModal(true)} disabled={!isInArchivableGroup}> + {isInArchivableGroup ? ( +
+ + Archive +
+ ) : ( +
+ +
+

Archive

+

+ Only completed or canceled +
+ issues can be archived +

+
+
+ )}
)} {isDeletingAllowed && ( diff --git a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx index 3ef5533c63a..ca1a04bc20a 100644 --- a/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/all-issue-layout-root.tsx @@ -34,7 +34,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { const { commandPalette: commandPaletteStore } = useApplication(); const { issuesFilter: { filters, fetchFilters, updateFilters }, - issues: { loader, groupedIssueIds, fetchIssues, updateIssue, removeIssue }, + issues: { loader, groupedIssueIds, fetchIssues, updateIssue, removeIssue, archiveIssue }, } = useIssues(EIssuesStoreType.GLOBAL); const { dataViewId, issueIds } = groupedIssueIds; @@ -133,6 +133,12 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { await removeIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString()); }, + [EIssueActions.ARCHIVE]: async (issue: TIssue) => { + const projectId = issue.project_id; + if (!workspaceSlug || !projectId || !globalViewId) return; + + await archiveIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString()); + }, }), // eslint-disable-next-line react-hooks/exhaustive-deps [updateIssue, removeIssue, workspaceSlug] @@ -142,6 +148,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { async (issue: TIssue, action: EIssueActions) => { if (action === EIssueActions.UPDATE) await issueActions[action]!(issue); if (action === EIssueActions.DELETE) await issueActions[action]!(issue); + if (action === EIssueActions.ARCHIVE) await issueActions[action]!(issue); }, // eslint-disable-next-line react-hooks/exhaustive-deps [] @@ -169,10 +176,12 @@ export const AllIssueLayoutRoot: React.FC = observer(() => { issue={issue} handleUpdate={async () => handleIssues({ ...issue }, EIssueActions.UPDATE)} handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)} + handleArchive={async () => handleIssues(issue, EIssueActions.ARCHIVE)} portalElement={portalElement} + readOnly={!canEditProperties(issue.project_id)} /> ), - [handleIssues] + [canEditProperties, handleIssues] ); const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER; diff --git a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx index b241a51686f..ec23d4e5599 100644 --- a/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/issue-row.tsx @@ -215,11 +215,9 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => { {getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id} - {canEditProperties(issueDetail.project_id) && ( - - )} +
{issueDetail.sub_issues_count > 0 && ( From 64b0227f899462b7351614f56a1024107f130fd1 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Mon, 26 Feb 2024 18:29:42 +0530 Subject: [PATCH 08/15] chore: archive and unarchive response --- apiserver/plane/app/views/issue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 977a3745889..0e9f2fd304d 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -1660,7 +1660,7 @@ def archive(self, request, slug, project_id, pk=None): issue.archived_at = timezone.now().date() issue.save() - return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) + return Response({"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK) def unarchive(self, request, slug, project_id, pk=None): @@ -1686,7 +1686,7 @@ def unarchive(self, request, slug, project_id, pk=None): issue.archived_at = None issue.save() - return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) + return Response(status=status.HTTP_204_NO_CONTENT) class IssueSubscriberViewSet(BaseViewSet): From fba242006d04c391afddf06f0aa9abb21123d129 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 26 Feb 2024 18:39:41 +0530 Subject: [PATCH 09/15] fix: archival mutation --- .../activity/actions/archived-at.tsx | 16 ++++- .../actions/helpers/activity-block.tsx | 5 +- .../activity/actions/helpers/issue-user.tsx | 25 +++++--- .../issue-detail/issue-activity/root.tsx | 2 +- web/components/issues/issue-detail/root.tsx | 2 +- .../issues/issue-detail/sidebar.tsx | 60 +++++++++++-------- .../issues/peek-overview/header.tsx | 38 +++++++++--- .../archived-issues/[archivedIssueId].tsx | 2 +- web/services/issue/issue_archive.service.ts | 8 ++- web/store/issue/archived/issue.store.ts | 3 + web/store/issue/project/issue.store.ts | 5 +- yarn.lock | 2 +- 12 files changed, 115 insertions(+), 53 deletions(-) diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/archived-at.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/archived-at.tsx index 55f07870ca7..e07d41c3c36 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/archived-at.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/archived-at.tsx @@ -1,10 +1,12 @@ import { FC } from "react"; import { observer } from "mobx-react"; -import { MessageSquare } from "lucide-react"; +import { RotateCcw } from "lucide-react"; // hooks import { useIssueDetail } from "hooks/store"; // components import { IssueActivityBlockComponent } from "./"; +// ui +import { ArchiveIcon } from "@plane/ui"; type TIssueArchivedAtActivity = { activityId: string; ends: "top" | "bottom" | undefined }; @@ -18,13 +20,21 @@ export const IssueArchivedAtActivity: FC = observer((p const activity = getActivityById(activityId); if (!activity) return <>; + return ( ); }); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx index c7b75340ba0..d868db2974b 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx @@ -14,10 +14,11 @@ type TIssueActivityBlockComponent = { activityId: string; ends: "top" | "bottom" | undefined; children: ReactNode; + automaticActivity?: boolean; }; export const IssueActivityBlockComponent: FC = (props) => { - const { icon, activityId, ends, children } = props; + const { icon, activityId, ends, children, automaticActivity = false } = props; // hooks const { activity: { getActivityById }, @@ -37,7 +38,7 @@ export const IssueActivityBlockComponent: FC = (pr {icon ? icon : }
- + {children} = (props) => { - const { activityId } = props; + const { activityId, automaticActivity = false } = props; // hooks const { activity: { getActivityById }, @@ -18,12 +18,19 @@ export const IssueUser: FC = (props) => { const activity = getActivityById(activityId); if (!activity) return <>; + return ( - - {activity.actor_detail?.display_name} - + <> + {automaticActivity ? ( + Plane + ) : ( + + {activity.actor_detail?.display_name} + + )} + ); }; diff --git a/web/components/issues/issue-detail/issue-activity/root.tsx b/web/components/issues/issue-detail/issue-activity/root.tsx index 42d856b1e3d..695b248de20 100644 --- a/web/components/issues/issue-detail/issue-activity/root.tsx +++ b/web/components/issues/issue-detail/issue-activity/root.tsx @@ -20,7 +20,7 @@ type TActivityTabs = "all" | "activity" | "comments"; const activityTabs: { key: TActivityTabs; title: string; icon: LucideIcon }[] = [ { key: "all", - title: "All Activity", + title: "All activity", icon: History, }, { diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index 68d6bab4f97..ca2eb6b7031 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -389,7 +389,7 @@ export const IssueDetailRoot: FC = observer((props) => { />
= observer((props) => { const { workspaceSlug, projectId, issueId, issueOperations, is_archived, is_editable } = props; // states const [deleteIssueModal, setDeleteIssueModal] = useState(false); - const [isArchiving, setIsArchiving] = useState(false); + const [archiveIssueModal, setArchiveIssueModal] = useState(false); // router const router = useRouter(); // store hooks @@ -92,20 +93,16 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { const handleArchiveIssue = async () => { if (!issueOperations.archive) return; - setIsArchiving(true); - await issueOperations.archive(workspaceSlug, projectId, issueId).finally(() => setIsArchiving(false)); - router.push(`/${workspaceSlug}/projects/${projectId}/archived-issues`); + await issueOperations.archive(workspaceSlug, projectId, issueId); + router.push(`/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}`); }; // derived values const projectDetails = getProjectById(issue.project_id); const stateDetails = getStateById(issue.state_id); // auth - const isArchivingAllowed = - !is_archived && - issueOperations.archive && - is_editable && - !!stateDetails && - [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); + const isArchivingAllowed = !is_archived && issueOperations.archive && is_editable; + const isInArchivableGroup = + !!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group); const minDate = issue.start_date ? new Date(issue.start_date) : null; minDate?.setDate(minDate.getDate()); @@ -115,15 +112,18 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { return ( <> - {workspaceSlug && projectId && issue && ( - setDeleteIssueModal(false)} - isOpen={deleteIssueModal} - data={issue} - onSubmit={handleDeleteIssue} - /> - )} - + setDeleteIssueModal(false)} + isOpen={deleteIssueModal} + data={issue} + onSubmit={handleDeleteIssue} + /> + setArchiveIssueModal(false)} + data={issue} + onSubmit={handleArchiveIssue} + />
@@ -134,19 +134,29 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { {isArchivingAllowed && ( - + @@ -156,7 +166,7 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { {currentMode && (
@@ -112,7 +123,7 @@ export const IssuePeekOverviewHeader: FC = observer((pr onChange={(val: any) => setPeekMode(val)} customButton={ } > @@ -146,9 +157,20 @@ export const IssuePeekOverviewHeader: FC = observer((pr {isArchivingAllowed && ( - - )} diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx index ab5841d5786..5d41cc54371 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx @@ -107,7 +107,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
-

This issue has been archived by Plane.

+

This issue has been archived.

@@ -161,6 +163,7 @@ export const IssuePeekOverviewHeader: FC = observer((pr tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"} > )} {!disabled && ( - diff --git a/web/components/issues/peek-overview/root.tsx b/web/components/issues/peek-overview/root.tsx index d0f4da17213..acff92d03e4 100644 --- a/web/components/issues/peek-overview/root.tsx +++ b/web/components/issues/peek-overview/root.tsx @@ -11,7 +11,7 @@ import { TIssue } from "@plane/types"; // constants import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; -import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "constants/event-tracker"; +import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "constants/event-tracker"; interface IIssuePeekOverview { is_archived?: boolean; @@ -28,6 +28,7 @@ export type TIssuePeekOperations = { ) => Promise; remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise; archive: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + restore: (workspaceSlug: string, projectId: string, issueId: string) => Promise; addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise; addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise; @@ -55,7 +56,7 @@ export const IssuePeekOverview: FC = observer((props) => { membership: { currentWorkspaceAllProjectsRole }, } = useUser(); const { - issues: { removeIssue: removeArchivedIssue }, + issues: { restoreIssue }, } = useIssues(EIssuesStoreType.ARCHIVED); const { peekIssue, @@ -118,8 +119,7 @@ export const IssuePeekOverview: FC = observer((props) => { }, remove: async (workspaceSlug: string, projectId: string, issueId: string) => { try { - if (is_archived) await removeArchivedIssue(workspaceSlug, projectId, issueId); - await removeIssue(workspaceSlug, projectId, issueId); + removeIssue(workspaceSlug, projectId, issueId); setToastAlert({ title: "Issue deleted successfully", type: "success", @@ -169,6 +169,32 @@ export const IssuePeekOverview: FC = observer((props) => { }); } }, + restore: async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + await restoreIssue(workspaceSlug, projectId, issueId); + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue restored successfully.", + }); + captureIssueEvent({ + eventName: ISSUE_RESTORED, + payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" }, + path: router.asPath, + }); + } catch (error) { + setToastAlert({ + type: "error", + title: "Error!", + message: "Issue could not be restored. Please try again.", + }); + captureIssueEvent({ + eventName: ISSUE_RESTORED, + payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" }, + path: router.asPath, + }); + } + }, addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { try { await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds); @@ -333,7 +359,7 @@ export const IssuePeekOverview: FC = observer((props) => { updateIssue, removeIssue, archiveIssue, - removeArchivedIssue, + restoreIssue, addIssueToCycle, removeIssueFromCycle, addModulesToIssue, @@ -370,7 +396,7 @@ export const IssuePeekOverview: FC = observer((props) => { issueId={peekIssue.issueId} isLoading={isLoading} is_archived={is_archived} - disabled={is_archived || !is_editable} + disabled={!is_editable} issueOperations={issueOperations} /> ); diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index ffb65bf3821..8cf9415d262 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -58,6 +58,12 @@ export const IssueView: FC = observer((props) => { const handleKeyDown = () => !isAnyModalOpen && removeRoutePeekId(); useKeypress("Escape", handleKeyDown); + const handleRestore = async () => { + if (!issueOperations.restore) return; + await issueOperations.restore(workspaceSlug, projectId, issueId); + removeRoutePeekId(); + }; + return ( <> {issue && !is_archived && ( @@ -113,6 +119,7 @@ export const IssueView: FC = observer((props) => { removeRoutePeekId={removeRoutePeekId} toggleDeleteIssueModal={toggleDeleteIssueModal} toggleArchiveIssueModal={toggleArchiveIssueModal} + handleRestoreIssue={handleRestore} isArchived={is_archived} issueId={issueId} workspaceSlug={workspaceSlug} @@ -136,7 +143,7 @@ export const IssueView: FC = observer((props) => { projectId={projectId} issueId={issueId} issueOperations={issueOperations} - disabled={disabled} + disabled={disabled || is_archived} isSubmitting={isSubmitting} setIsSubmitting={(value) => setIsSubmitting(value)} /> @@ -146,7 +153,7 @@ export const IssueView: FC = observer((props) => { projectId={projectId} issueId={issueId} issueOperations={issueOperations} - disabled={disabled} + disabled={disabled || is_archived} /> @@ -160,7 +167,7 @@ export const IssueView: FC = observer((props) => { projectId={projectId} issueId={issueId} issueOperations={issueOperations} - disabled={disabled} + disabled={disabled || is_archived} isSubmitting={isSubmitting} setIsSubmitting={(value) => setIsSubmitting(value)} /> @@ -178,7 +185,7 @@ export const IssueView: FC = observer((props) => { projectId={projectId} issueId={issueId} issueOperations={issueOperations} - disabled={disabled} + disabled={disabled || is_archived} />
diff --git a/web/constants/event-tracker.ts b/web/constants/event-tracker.ts index 835fcbf5d47..37a18a37db6 100644 --- a/web/constants/event-tracker.ts +++ b/web/constants/event-tracker.ts @@ -168,6 +168,7 @@ export const ISSUE_CREATED = "Issue created"; export const ISSUE_UPDATED = "Issue updated"; export const ISSUE_DELETED = "Issue deleted"; export const ISSUE_ARCHIVED = "Issue archived"; +export const ISSUE_RESTORED = "Issue restored"; export const ISSUE_OPENED = "Issue opened"; // Project State Events export const STATE_CREATED = "State created"; From 46507549b2bc63a104b9593ce97f4afc10d8cc1a Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 26 Feb 2024 20:09:58 +0530 Subject: [PATCH 11/15] chore: update notification content for archive/restore --- packages/types/src/notifications.d.ts | 22 +++---- .../notifications/notification-card.tsx | 59 ++++++++++--------- web/components/project/sidebar-list-item.tsx | 20 ++++--- 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/packages/types/src/notifications.d.ts b/packages/types/src/notifications.d.ts index 8033c19a94e..652e2776f51 100644 --- a/packages/types/src/notifications.d.ts +++ b/packages/types/src/notifications.d.ts @@ -12,27 +12,27 @@ export interface PaginatedUserNotification { } export interface IUserNotification { - id: string; - created_at: Date; - updated_at: Date; + archived_at: string | null; + created_at: string; + created_by: null; data: Data; entity_identifier: string; entity_name: string; - title: string; + id: string; message: null; message_html: string; message_stripped: null; - sender: string; + project: string; read_at: Date | null; - archived_at: Date | null; + receiver: string; + sender: string; snoozed_till: Date | null; - created_by: null; - updated_by: null; - workspace: string; - project: string; + title: string; triggered_by: string; triggered_by_details: IUserLite; - receiver: string; + updated_at: Date; + updated_by: null; + workspace: string; } export interface Data { diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx index d7ad61141cb..03d849a8219 100644 --- a/web/components/notifications/notification-card.tsx +++ b/web/components/notifications/notification-card.tsx @@ -49,7 +49,7 @@ export const NotificationCard: React.FC = (props) => { const router = useRouter(); const { workspaceSlug } = router.query; // states - const [showSnoozeOptions, setshowSnoozeOptions] = React.useState(false); + const [showSnoozeOptions, setShowSnoozeOptions] = React.useState(false); // toast alert const { setToastAlert } = useToast(); // refs @@ -105,7 +105,7 @@ export const NotificationCard: React.FC = (props) => { useEffect(() => { const handleClickOutside = (event: any) => { if (snoozeRef.current && !snoozeRef.current?.contains(event.target)) { - setshowSnoozeOptions(false); + setShowSnoozeOptions(false); } }; document.addEventListener("mousedown", handleClickOutside, true); @@ -116,6 +116,9 @@ export const NotificationCard: React.FC = (props) => { }; }, []); + const notificationField = notification.data.issue_activity.field; + const notificationTriggeredBy = notification.triggered_by_details; + if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null; return ( @@ -129,7 +132,7 @@ export const NotificationCard: React.FC = (props) => { closePopover(); }} href={`/${workspaceSlug}/projects/${notification.project}/${ - notification.data.issue_activity.field === "archived_at" ? "archived-issues" : "issues" + notificationField === "archived_at" ? "archived-issues" : "issues" }/${notification.data.issue.id}`} className={`group relative flex w-full cursor-pointer items-center gap-4 p-3 pl-6 ${ notification.read_at === null ? "bg-custom-primary-70/5" : "hover:bg-custom-background-200" @@ -139,10 +142,10 @@ export const NotificationCard: React.FC = (props) => { )}
- {notification.triggered_by_details.avatar && notification.triggered_by_details.avatar !== "" ? ( + {notificationTriggeredBy.avatar && notificationTriggeredBy.avatar !== "" ? (
Profile Image = (props) => { ) : (
- {notification.triggered_by_details.is_bot ? ( - notification.triggered_by_details.first_name?.[0]?.toUpperCase() - ) : notification.triggered_by_details.display_name?.[0] ? ( - notification.triggered_by_details.display_name?.[0]?.toUpperCase() + {notificationTriggeredBy.is_bot ? ( + notificationTriggeredBy.first_name?.[0]?.toUpperCase() + ) : notificationTriggeredBy.display_name?.[0] ? ( + notificationTriggeredBy.display_name?.[0]?.toUpperCase() ) : ( )} @@ -168,30 +171,32 @@ export const NotificationCard: React.FC = (props) => { {!notification.message ? (
- {notification.triggered_by_details.is_bot - ? notification.triggered_by_details.first_name - : notification.triggered_by_details.display_name}{" "} + {notificationTriggeredBy.is_bot + ? notificationTriggeredBy.first_name + : notificationTriggeredBy.display_name}{" "} - {notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.verb}{" "} - {notification.data.issue_activity.field === "comment" + {!["comment", "archived_at"].includes(notificationField) && notification.data.issue_activity.verb}{" "} + {notificationField === "comment" ? "commented" - : notification.data.issue_activity.field === "None" + : notificationField === "archived_at" + ? notification.data.issue_activity.new_value === "restore" + ? "restored the issue" + : "archived the issue" + : notificationField === "None" ? null - : replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "} - {notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.field !== "None" - ? "to" - : ""} + : replaceUnderscoreIfSnakeCase(notificationField)}{" "} + {!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""} {" "} - {notification.data.issue_activity.field !== "None" ? ( - notification.data.issue_activity.field !== "comment" ? ( - notification.data.issue_activity.field === "target_date" ? ( + {notificationField !== "None" ? ( + notificationField !== "comment" ? ( + notificationField === "target_date" ? ( renderFormattedDate(notification.data.issue_activity.new_value) - ) : notification.data.issue_activity.field === "attachment" ? ( + ) : notificationField === "attachment" ? ( "the issue" - ) : notification.data.issue_activity.field === "description" ? ( + ) : notificationField === "description" ? ( stripAndTruncateHTML(notification.data.issue_activity.new_value, 55) - ) : ( + ) : notificationField === "archived_at" ? null : ( notification.data.issue_activity.new_value ) ) : ( @@ -255,7 +260,7 @@ export const NotificationCard: React.FC = (props) => { onClick={(e) => { e.stopPropagation(); e.preventDefault(); - setshowSnoozeOptions(true); + setShowSnoozeOptions(true); }} className="flex gap-x-2 items-center p-1.5" > @@ -280,7 +285,7 @@ export const NotificationCard: React.FC = (props) => { onClick={(e) => { e.stopPropagation(); e.preventDefault(); - setshowSnoozeOptions(false); + setShowSnoozeOptions(false); snoozeOptionOnClick(item.value); }} > diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index c9b91a1f9f9..6465676cac2 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -270,19 +270,21 @@ export const ProjectSidebarListItem: React.FC = observer((props) => {
)} - - -
- - Archived issues -
- -
+ {!isViewerOrGuest && ( + + +
+ + Archived issues +
+ +
+ )}
- Draft Issues + Draft issues
From af94cef8ad1adf6eae210f56bcd46e79cfc4d7d4 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Mon, 26 Feb 2024 20:12:05 +0530 Subject: [PATCH 12/15] refactor: activity user name --- .../issue-activity/activity/actions/archived-at.tsx | 2 +- .../activity/actions/helpers/activity-block.tsx | 6 +++--- .../activity/actions/helpers/issue-user.tsx | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/archived-at.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/archived-at.tsx index e07d41c3c36..2335e4d3250 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/archived-at.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/archived-at.tsx @@ -32,7 +32,7 @@ export const IssueArchivedAtActivity: FC = observer((p } activityId={activityId} ends={ends} - automaticActivity={activity.new_value === "archive"} + customUserName={activity.new_value === "archive" ? "Plane" : undefined} > {activity.new_value === "restore" ? "restored the issue" : "archived the issue"}. diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx index d868db2974b..e209b4bbf23 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx @@ -14,11 +14,11 @@ type TIssueActivityBlockComponent = { activityId: string; ends: "top" | "bottom" | undefined; children: ReactNode; - automaticActivity?: boolean; + customUserName?: string; }; export const IssueActivityBlockComponent: FC = (props) => { - const { icon, activityId, ends, children, automaticActivity = false } = props; + const { icon, activityId, ends, children, customUserName } = props; // hooks const { activity: { getActivityById }, @@ -38,7 +38,7 @@ export const IssueActivityBlockComponent: FC = (pr {icon ? icon : }
- + {children} = (props) => { - const { activityId, automaticActivity = false } = props; + const { activityId, customUserName } = props; // hooks const { activity: { getActivityById }, @@ -21,8 +21,8 @@ export const IssueUser: FC = (props) => { return ( <> - {automaticActivity ? ( - Plane + {customUserName ? ( + {customUserName} ) : ( Date: Mon, 26 Feb 2024 20:21:54 +0530 Subject: [PATCH 13/15] fix: all issues mutation --- web/store/issue/workspace/issue.store.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/store/issue/workspace/issue.store.ts b/web/store/issue/workspace/issue.store.ts index 707ed208d20..b7fe43b3007 100644 --- a/web/store/issue/workspace/issue.store.ts +++ b/web/store/issue/workspace/issue.store.ts @@ -218,9 +218,12 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue const uniqueViewId = `${workspaceSlug}_${viewId}`; - await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId); + const response = await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId); runInAction(() => { + this.rootStore.issues.updateIssue(issueId, { + archived_at: response.archived_at, + }); pull(this.issues[uniqueViewId], issueId); }); } catch (error) { From ecb586d0524f0a34d75003edb62413705f65e87b Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 27 Feb 2024 12:42:46 +0530 Subject: [PATCH 14/15] fix: restore issue auth --- .../archived-issues/[archivedIssueId].tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx index 5d41cc54371..948a84a52e0 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react"; import useSWR from "swr"; // hooks import useToast from "hooks/use-toast"; -import { useIssueDetail, useIssues, useProject } from "hooks/store"; +import { useIssueDetail, useIssues, useProject, useUser } from "hooks/store"; // layouts import { AppLayout } from "layouts/app-layout"; // components @@ -19,6 +19,7 @@ import { RotateCcw } from "lucide-react"; import { NextPageWithLayout } from "lib/types"; // constants import { EIssuesStoreType } from "constants/issue"; +import { EUserProjectRoles } from "constants/project"; const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { // router @@ -36,6 +37,9 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { } = useIssues(EIssuesStoreType.ARCHIVED); const { setToastAlert } = useToast(); const { getProjectById } = useProject(); + const { + membership: { currentProjectRole }, + } = useUser(); const { isLoading } = useSWR( workspaceSlug && projectId && archivedIssueId @@ -46,9 +50,12 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { : null ); - const issue = getIssueById(archivedIssueId?.toString() || "") || undefined; - const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined; + // derived values + const issue = archivedIssueId ? getIssueById(archivedIssueId.toString()) : undefined; + const project = issue ? getProjectById(issue?.project_id) : undefined; const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined; + // auth + const canRestoreIssue = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER; if (!issue) return <>; @@ -103,7 +110,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => { ) : (
- {issue?.archived_at && ( + {issue?.archived_at && canRestoreIssue && (
From c3f88d3e1ba81466b9305807eb2f4e0bbce3c24e Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal Date: Tue, 27 Feb 2024 13:08:34 +0530 Subject: [PATCH 15/15] chore: close peek overview on archival --- web/components/issues/peek-overview/view.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/components/issues/peek-overview/view.tsx b/web/components/issues/peek-overview/view.tsx index 8cf9415d262..6dcfda0d699 100644 --- a/web/components/issues/peek-overview/view.tsx +++ b/web/components/issues/peek-overview/view.tsx @@ -73,6 +73,7 @@ export const IssueView: FC = observer((props) => { data={issue} onSubmit={async () => { if (issueOperations.archive) await issueOperations.archive(workspaceSlug, projectId, issueId); + removeRoutePeekId(); }} /> )}