From ed71b5309742583605c393a4f835bf0352d5d9b2 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 21 Mar 2024 02:02:03 +0530 Subject: [PATCH 01/13] dev: project archive response --- apiserver/plane/app/views/project/base.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index b2f9f56e9f0..1672cd47ca0 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -372,7 +372,7 @@ def partial_update(self, request, slug, pk=None): return Response( {"error": "Archived projects cannot be updated"}, status=status.HTTP_400_BAD_REQUEST, - ) + ) serializer = ProjectSerializer( project, @@ -433,11 +433,15 @@ class ProjectArchiveUnarchiveEndpoint(BaseAPIView): permission_classes = [ ProjectBasePermission, ] + def post(self, request, slug, project_id): project = Project.objects.get(pk=project_id, workspace__slug=slug) project.archived_at = timezone.now() project.save() - return Response(status=status.HTTP_204_NO_CONTENT) + return Response( + {"archived_at": str(project.archived_at)}, + status=status.HTTP_200_OK, + ) def delete(self, request, slug, project_id): project = Project.objects.get(pk=project_id, workspace__slug=slug) From 7f8f491ae41db18d918e3377ae11768b7f1daf0e Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 21 Mar 2024 03:42:07 +0530 Subject: [PATCH 02/13] feat: projects archive. --- .../types/src/project/project_filters.d.ts | 5 + packages/types/src/project/projects.d.ts | 1 + .../project/applied-filters/index.ts | 1 + .../project-display-filters.tsx | 39 ++++ .../project/applied-filters/root.tsx | 40 +++- web/components/project/card-list.tsx | 4 +- web/components/project/card.tsx | 174 ++++++++++++------ .../project/dropdowns/filters/root.tsx | 9 + .../archive-project/archive-restore-modal.tsx | 136 ++++++++++++++ .../settings/archive-project/index.tsx | 2 + .../settings/archive-project/selection.tsx | 60 ++++++ .../settings/delete-project-section.tsx | 16 +- web/components/project/settings/index.ts | 1 + web/constants/project.ts | 16 +- web/helpers/project.helper.ts | 8 +- .../projects/[projectId]/settings/index.tsx | 42 +++-- web/pages/[workspaceSlug]/projects/index.tsx | 36 +++- web/services/project/index.ts | 1 + .../project/project-archive.service.ts | 31 ++++ web/store/project/project.store.ts | 102 +++++++++- web/store/project/project_filter.store.ts | 36 +++- 21 files changed, 651 insertions(+), 109 deletions(-) create mode 100644 web/components/project/applied-filters/project-display-filters.tsx create mode 100644 web/components/project/settings/archive-project/archive-restore-modal.tsx create mode 100644 web/components/project/settings/archive-project/index.tsx create mode 100644 web/components/project/settings/archive-project/selection.tsx create mode 100644 web/services/project/project-archive.service.ts diff --git a/packages/types/src/project/project_filters.d.ts b/packages/types/src/project/project_filters.d.ts index 02ad09ee1bd..77da7365fb8 100644 --- a/packages/types/src/project/project_filters.d.ts +++ b/packages/types/src/project/project_filters.d.ts @@ -9,9 +9,14 @@ export type TProjectOrderByOptions = export type TProjectDisplayFilters = { my_projects?: boolean; + archived_projects?: boolean; order_by?: TProjectOrderByOptions; }; +export type TProjectAppliedDisplayFilterKeys = + | "my_projects" + | "archived_projects"; + export type TProjectFilters = { access?: string[] | null; lead?: string[] | null; diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts index 86384b401cc..a8c32882e2c 100644 --- a/packages/types/src/project/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -23,6 +23,7 @@ export type TProjectLogoProps = { export interface IProject { archive_in: number; + archived_at: Date | null; archived_issues: number; archived_sub_issues: number; close_in: number; diff --git a/web/components/project/applied-filters/index.ts b/web/components/project/applied-filters/index.ts index 818aa613403..85bcda446c2 100644 --- a/web/components/project/applied-filters/index.ts +++ b/web/components/project/applied-filters/index.ts @@ -1,4 +1,5 @@ export * from "./access"; export * from "./date"; export * from "./members"; +export * from "./project-display-filters"; export * from "./root"; diff --git a/web/components/project/applied-filters/project-display-filters.tsx b/web/components/project/applied-filters/project-display-filters.tsx new file mode 100644 index 00000000000..0c8af70970a --- /dev/null +++ b/web/components/project/applied-filters/project-display-filters.tsx @@ -0,0 +1,39 @@ +import { observer } from "mobx-react-lite"; +// icons +import { X } from "lucide-react"; +// types +import { TProjectAppliedDisplayFilterKeys } from "@plane/types"; +// constants +import { PROJECT_DISPLAY_FILTER_OPTIONS } from "@/constants/project"; + +type Props = { + handleRemove: (key: TProjectAppliedDisplayFilterKeys) => void; + values: TProjectAppliedDisplayFilterKeys[]; + editable: boolean | undefined; +}; + +export const AppliedProjectDisplayFilters: React.FC = observer((props) => { + const { handleRemove, values, editable } = props; + + return ( + <> + {values.map((key) => { + const filterLabel = PROJECT_DISPLAY_FILTER_OPTIONS.find((s) => s.key === key)?.label; + return ( +
+ {filterLabel} + {editable && ( + + )} +
+ ); + })} + + ); +}); diff --git a/web/components/project/applied-filters/root.tsx b/web/components/project/applied-filters/root.tsx index 66a2513c4e5..7c4381989a6 100644 --- a/web/components/project/applied-filters/root.tsx +++ b/web/components/project/applied-filters/root.tsx @@ -1,17 +1,24 @@ import { X } from "lucide-react"; -import { TProjectFilters } from "@plane/types"; -// components -import { Tooltip } from "@plane/ui"; -import { AppliedAccessFilters, AppliedDateFilters, AppliedMembersFilters } from "@/components/project"; +// types +import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types"; // ui +import { Tooltip } from "@plane/ui"; +// components +import { + AppliedAccessFilters, + AppliedDateFilters, + AppliedMembersFilters, + AppliedProjectDisplayFilters, +} from "@/components/project"; // helpers import { replaceUnderscoreIfSnakeCase } from "@/helpers/string.helper"; -// types type Props = { appliedFilters: TProjectFilters; + appliedDisplayFilters: TProjectAppliedDisplayFilterKeys[]; handleClearAllFilters: () => void; handleRemoveFilter: (key: keyof TProjectFilters, value: string | null) => void; + handleRemoveDisplayFilter: (key: TProjectAppliedDisplayFilterKeys) => void; alwaysAllowEditing?: boolean; filteredProjects: number; totalProjects: number; @@ -23,21 +30,24 @@ const DATE_FILTERS = ["created_at"]; export const ProjectAppliedFiltersList: React.FC = (props) => { const { appliedFilters, + appliedDisplayFilters, handleClearAllFilters, handleRemoveFilter, + handleRemoveDisplayFilter, alwaysAllowEditing, filteredProjects, totalProjects, } = props; - if (!appliedFilters) return null; - if (Object.keys(appliedFilters).length === 0) return null; + if (!appliedFilters && !appliedDisplayFilters) return null; + if (Object.keys(appliedFilters).length === 0 && appliedDisplayFilters.length === 0) return null; const isEditingAllowed = alwaysAllowEditing; return (
+ {/* Applied filters */} {Object.entries(appliedFilters).map(([key, value]) => { const filterKey = key as keyof TProjectFilters; @@ -85,6 +95,22 @@ export const ProjectAppliedFiltersList: React.FC = (props) => {
); })} + {/* Applied display filters */} + {appliedDisplayFilters.length > 0 && ( +
+
+ Projects + handleRemoveDisplayFilter(key)} + /> +
+
+ )} {isEditingAllowed && (
-
- - -
+ {!isArchived && ( +
+ + +
+ )} -
+

{project.description && project.description.trim() !== "" ? project.description @@ -199,37 +219,69 @@ export const ProjectCard: React.FC = observer((props) => { No Member Yet )} - {project.is_member && - (isOwner || isMember ? ( - { - e.stopPropagation(); - }} - href={`/${workspaceSlug}/projects/${project.id}/settings`} - > - - - ) : ( - - - Joined - - ))} - {!project.is_member && ( -

- -
+ {isArchived ? ( + isOwner && ( +
+
{ + e.preventDefault(); + e.stopPropagation(); + setRestoreProject(true); + }} + > +
+ + Restore +
+
+
{ + e.preventDefault(); + e.stopPropagation(); + setDeleteProjectModal(true); + }} + > + +
+
+ ) + ) : ( + <> + {project.is_member && + (isOwner || isMember ? ( + { + e.stopPropagation(); + }} + href={`/${workspaceSlug}/projects/${project.id}/settings`} + > + + + ) : ( + + + Joined + + ))} + {!project.is_member && ( +
+ +
+ )} + )}
diff --git a/web/components/project/dropdowns/filters/root.tsx b/web/components/project/dropdowns/filters/root.tsx index a7e50cec14d..12008eecd2d 100644 --- a/web/components/project/dropdowns/filters/root.tsx +++ b/web/components/project/dropdowns/filters/root.tsx @@ -51,6 +51,15 @@ export const ProjectFiltersSelection: React.FC = observer((props) => { } title="My projects" /> + + handleDisplayFiltersUpdate({ + archived_projects: !displayFilters.archived_projects, + }) + } + title="Archived" + /> {/* access */} diff --git a/web/components/project/settings/archive-project/archive-restore-modal.tsx b/web/components/project/settings/archive-project/archive-restore-modal.tsx new file mode 100644 index 00000000000..3e2ebf6c8ff --- /dev/null +++ b/web/components/project/settings/archive-project/archive-restore-modal.tsx @@ -0,0 +1,136 @@ +import { useState, Fragment } from "react"; +import { useRouter } from "next/router"; +import { Dialog, Transition } from "@headlessui/react"; +// ui +import { Button, TOAST_TYPE, setToast } from "@plane/ui"; +// hooks +import { useProject } from "@/hooks/store"; + +type Props = { + workspaceSlug: string; + projectId: string; + isOpen: boolean; + onClose: () => void; + archive: boolean; +}; + +export const ArchiveRestoreProjectModal: React.FC = (props) => { + const { workspaceSlug, projectId, isOpen, onClose, archive } = props; + // router + const router = useRouter(); + // states + const [isLoading, setIsLoading] = useState(false); + // store hooks + const { getProjectById, archiveProject, restoreProject } = useProject(); + + const projectDetails = getProjectById(projectId); + + const handleClose = () => { + setIsLoading(false); + onClose(); + }; + + const handleArchiveProject = async () => { + setIsLoading(true); + await archiveProject(workspaceSlug, projectId) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Archive success", + message: "Your archives can be found using archived project filter.", + }); + onClose(); + router.push(`/${workspaceSlug}/projects/`); + }) + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Project could not be archived. Please try again.", + }) + ) + .finally(() => setIsLoading(false)); + }; + + const handleRestoreProject = async () => { + setIsLoading(true); + await restoreProject(workspaceSlug, projectId) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Restore success", + message: "Your project can be found in projects.", + }); + onClose(); + router.push(`/${workspaceSlug}/projects/`); + }) + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Project could not be restored. Please try again.", + }) + ) + .finally(() => setIsLoading(false)); + }; + + if (!projectDetails) return null; + + return ( + + + +
+ + +
+
+ + +
+

+ {archive ? "Archive" : "Restore"} {projectDetails.name} +

+

+ {archive + ? "This project and its issues, cycles, modules, and pages will be archived. Its issues won’t appear in search. Only project admins can restore the project." + : "Restoring a project will activate it and make it visible to all members of the project. Are you sure you want to continue?"} +

+
+ + +
+
+
+
+
+
+
+
+ ); +}; diff --git a/web/components/project/settings/archive-project/index.tsx b/web/components/project/settings/archive-project/index.tsx new file mode 100644 index 00000000000..23da8dcb2f4 --- /dev/null +++ b/web/components/project/settings/archive-project/index.tsx @@ -0,0 +1,2 @@ +export * from "./selection"; +export * from "./archive-restore-modal"; diff --git a/web/components/project/settings/archive-project/selection.tsx b/web/components/project/settings/archive-project/selection.tsx new file mode 100644 index 00000000000..14fb4305300 --- /dev/null +++ b/web/components/project/settings/archive-project/selection.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { ChevronRight, ChevronUp } from "lucide-react"; +import { Disclosure, Transition } from "@headlessui/react"; +// types +import { IProject } from "@plane/types"; +// ui +import { Button, Loader } from "@plane/ui"; + +export interface IArchiveProject { + projectDetails: IProject; + handleArchive: () => void; +} + +export const ArchiveProjectSelection: React.FC = (props) => { + const { projectDetails, handleArchive } = props; + + return ( + + {({ open }) => ( +
+ + Archive project + {open ? : } + + + +
+ + Archiving a project will unlist your project from your side navigation although you will still be able + to access it from your projects page. You can restore the project or delete it whenever you want. + +
+ {projectDetails ? ( +
+ +
+ ) : ( + + + + )} +
+
+
+
+
+ )} +
+ ); +}; diff --git a/web/components/project/settings/delete-project-section.tsx b/web/components/project/settings/delete-project-section.tsx index 991b2920771..690f6743269 100644 --- a/web/components/project/settings/delete-project-section.tsx +++ b/web/components/project/settings/delete-project-section.tsx @@ -1,12 +1,10 @@ import React from "react"; - -// ui -import { ChevronDown, ChevronUp } from "lucide-react"; +import { ChevronRight, ChevronUp } from "lucide-react"; import { Disclosure, Transition } from "@headlessui/react"; +// types import { IProject } from "@plane/types"; +// ui import { Button, Loader } from "@plane/ui"; -// icons -// types export interface IDeleteProjectSection { projectDetails: IProject; @@ -17,12 +15,12 @@ export const DeleteProjectSection: React.FC = (props) => const { projectDetails, handleDelete } = props; return ( - + {({ open }) => (
- + Delete Project - {open ? : } + {open ? : } = (props) => leaveTo="transform opacity-0" > -
+
The danger zone of the project delete page is a critical area that requires careful consideration and attention. When deleting a project, all of the data and resources within that project will be diff --git a/web/components/project/settings/index.ts b/web/components/project/settings/index.ts index 0bf79ec17cc..0f8e9aa6d22 100644 --- a/web/components/project/settings/index.ts +++ b/web/components/project/settings/index.ts @@ -1,2 +1,3 @@ export * from "./delete-project-section"; export * from "./features-list"; +export * from "./archive-project"; diff --git a/web/constants/project.ts b/web/constants/project.ts index 6393a6a6c6b..c4ef817fdbf 100644 --- a/web/constants/project.ts +++ b/web/constants/project.ts @@ -1,9 +1,9 @@ // icons import { Globe2, Lock, LucideIcon } from "lucide-react"; +import { TProjectAppliedDisplayFilterKeys, TProjectOrderByOptions } from "@plane/types"; import { SettingIcon } from "@/components/icons"; // types import { Props } from "@/components/icons/types"; -import { TProjectOrderByOptions } from "@plane/types"; export enum EUserProjectRoles { GUEST = 5, @@ -162,3 +162,17 @@ export const PROJECT_ORDER_BY_OPTIONS: { label: "Number of members", }, ]; + +export const PROJECT_DISPLAY_FILTER_OPTIONS: { + key: TProjectAppliedDisplayFilterKeys; + label: string; +}[] = [ + { + key: "my_projects", + label: "My projects", + }, + { + key: "archived_projects", + label: "Archived", + }, +]; diff --git a/web/helpers/project.helper.ts b/web/helpers/project.helper.ts index b4c461bb4e3..f479eb25c22 100644 --- a/web/helpers/project.helper.ts +++ b/web/helpers/project.helper.ts @@ -1,11 +1,11 @@ import sortBy from "lodash/sortBy"; -// helpers -import { satisfiesDateFilter } from "@/helpers/filter.helper"; -import { getDate } from "@/helpers/date-time.helper"; // types import { IProject, TProjectDisplayFilters, TProjectFilters, TProjectOrderByOptions } from "@plane/types"; // constants import { EUserProjectRoles } from "@/constants/project"; +// helpers +import { getDate } from "@/helpers/date-time.helper"; +import { satisfiesDateFilter } from "@/helpers/filter.helper"; /** * Updates the sort order of the project. @@ -93,6 +93,8 @@ export const shouldFilterProject = ( } }); if (displayFilters.my_projects && !project.is_member) fallsInFilters = false; + if (displayFilters.archived_projects && !project.archived_at) fallsInFilters = false; + if (project.archived_at) fallsInFilters = displayFilters.archived_projects ? fallsInFilters : false; return fallsInFilters; }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx index f099a97f784..946d9b9cb3a 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/settings/index.tsx @@ -2,26 +2,29 @@ import { useState, ReactElement } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import useSWR from "swr"; -// hooks +// components import { PageHead } from "@/components/core"; import { ProjectSettingHeader } from "@/components/headers"; import { + ArchiveRestoreProjectModal, + ArchiveProjectSelection, DeleteProjectModal, DeleteProjectSection, ProjectDetailsForm, ProjectDetailsFormLoader, } from "@/components/project"; +// hooks import { useProject } from "@/hooks/store"; // layouts import { AppLayout } from "@/layouts/app-layout"; import { ProjectSettingLayout } from "@/layouts/settings-layout"; -// components // types import { NextPageWithLayout } from "@/lib/types"; const GeneralSettingsPage: NextPageWithLayout = observer(() => { // states const [selectProject, setSelectedProject] = useState(null); + const [archiveProject, setArchiveProject] = useState(false); // router const router = useRouter(); const { workspaceSlug, projectId } = router.query; @@ -42,12 +45,21 @@ const GeneralSettingsPage: NextPageWithLayout = observer(() => { return ( <> - {currentProjectDetails && ( - setSelectedProject(null)} - /> + {currentProjectDetails && workspaceSlug && projectId && ( + <> + setArchiveProject(false)} + archive + /> + setSelectedProject(null)} + /> + )}
@@ -63,10 +75,16 @@ const GeneralSettingsPage: NextPageWithLayout = observer(() => { )} {isAdmin && ( - setSelectedProject(currentProjectDetails.id ?? null)} - /> + <> + setArchiveProject(true)} + /> + setSelectedProject(currentProjectDetails.id ?? null)} + /> + )}
diff --git a/web/pages/[workspaceSlug]/projects/index.tsx b/web/pages/[workspaceSlug]/projects/index.tsx index 1e153b68835..5db5daa34cf 100644 --- a/web/pages/[workspaceSlug]/projects/index.tsx +++ b/web/pages/[workspaceSlug]/projects/index.tsx @@ -1,6 +1,6 @@ import { ReactElement, useCallback } from "react"; import { observer } from "mobx-react"; -import { TProjectFilters } from "@plane/types"; +import { TProjectAppliedDisplayFilterKeys, TProjectFilters } from "@plane/types"; // components import { PageHead } from "@/components/core"; import { ProjectsHeader } from "@/components/headers"; @@ -19,8 +19,15 @@ const ProjectsPage: NextPageWithLayout = observer(() => { router: { workspaceSlug }, } = useApplication(); const { currentWorkspace } = useWorkspace(); - const { workspaceProjectIds, filteredProjectIds } = useProject(); - const { currentWorkspaceFilters, clearAllFilters, updateFilters } = useProjectFilter(); + const { totalProjectIds, filteredProjectIds } = useProject(); + const { + currentWorkspaceFilters, + currentWorkspaceAppliedDisplayFilters, + clearAllFilters, + clearAllAppliedDisplayFilters, + updateFilters, + updateDisplayFilters, + } = useProjectFilter(); // derived values const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Projects` : undefined; @@ -37,18 +44,35 @@ const ProjectsPage: NextPageWithLayout = observer(() => { [currentWorkspaceFilters, updateFilters, workspaceSlug] ); + const handleRemoveDisplayFilter = useCallback( + (key: TProjectAppliedDisplayFilterKeys) => { + if (!workspaceSlug) return; + updateDisplayFilters(workspaceSlug.toString(), { [key]: false }); + }, + [updateDisplayFilters, workspaceSlug] + ); + + const handleClearAllFilters = useCallback(() => { + if (!workspaceSlug) return; + clearAllFilters(workspaceSlug.toString()); + clearAllAppliedDisplayFilters(workspaceSlug.toString()); + }, [clearAllFilters, clearAllAppliedDisplayFilters, workspaceSlug]); + return ( <>
- {calculateTotalFilters(currentWorkspaceFilters ?? {}) !== 0 && ( + {(calculateTotalFilters(currentWorkspaceFilters ?? {}) !== 0 || + currentWorkspaceAppliedDisplayFilters?.length !== 0) && (
clearAllFilters(`${workspaceSlug}`)} + appliedDisplayFilters={currentWorkspaceAppliedDisplayFilters ?? []} + handleClearAllFilters={handleClearAllFilters} handleRemoveFilter={handleRemoveFilter} + handleRemoveDisplayFilter={handleRemoveDisplayFilter} filteredProjects={filteredProjectIds?.length ?? 0} - totalProjects={workspaceProjectIds?.length ?? 0} + totalProjects={totalProjectIds?.length ?? 0} alwaysAllowEditing />
diff --git a/web/services/project/index.ts b/web/services/project/index.ts index 18cf1200aa3..d131ceb6b54 100644 --- a/web/services/project/index.ts +++ b/web/services/project/index.ts @@ -4,3 +4,4 @@ export * from "./project-export.service"; export * from "./project-member.service"; export * from "./project-state.service"; export * from "./project-publish.service"; +export * from "./project-archive.service"; diff --git a/web/services/project/project-archive.service.ts b/web/services/project/project-archive.service.ts new file mode 100644 index 00000000000..5fdca54b627 --- /dev/null +++ b/web/services/project/project-archive.service.ts @@ -0,0 +1,31 @@ +// helpers +import { API_BASE_URL } from "@/helpers/common.helper"; +// services +import { APIService } from "@/services/api.service"; + +export class ProjectArchiveService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async archiveProject( + workspaceSlug: string, + projectId: string + ): Promise<{ + archived_at: string; + }> { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archive/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async restoreProject(workspaceSlug: string, projectId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archive/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } +} diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index dfd35ac5982..a12039ba67e 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -3,12 +3,15 @@ import sortBy from "lodash/sortBy"; import { observable, action, computed, makeObservable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types -import { IssueLabelService, IssueService } from "@/services/issue"; -import { ProjectService, ProjectStateService } from "@/services/project"; import { IProject } from "@plane/types"; -import { RootStore } from "../root.store"; +// helpers import { orderProjects, shouldFilterProject } from "@/helpers/project.helper"; // services +import { IssueLabelService, IssueService } from "@/services/issue"; +import { ProjectService, ProjectStateService, ProjectArchiveService } from "@/services/project"; +// store +import { RootStore } from "../root.store"; + export interface IProjectStore { // observables projectMap: { @@ -17,6 +20,8 @@ export interface IProjectStore { // computed filteredProjectIds: string[] | undefined; workspaceProjectIds: string[] | undefined; + archivedProjectIds: string[] | undefined; + totalProjectIds: string[] | undefined; joinedProjectIds: string[]; favoriteProjectIds: string[]; currentProjectDetails: IProject | undefined; @@ -35,6 +40,9 @@ export interface IProjectStore { createProject: (workspaceSlug: string, data: Partial) => Promise; updateProject: (workspaceSlug: string, projectId: string, data: Partial) => Promise; deleteProject: (workspaceSlug: string, projectId: string) => Promise; + // archive actions + archiveProject: (workspaceSlug: string, projectId: string) => Promise; + restoreProject: (workspaceSlug: string, projectId: string) => Promise; } export class ProjectStore implements IProjectStore { @@ -46,6 +54,7 @@ export class ProjectStore implements IProjectStore { rootStore: RootStore; // service projectService; + projectArchiveService; issueLabelService; issueService; stateService; @@ -57,6 +66,8 @@ export class ProjectStore implements IProjectStore { // computed filteredProjectIds: computed, workspaceProjectIds: computed, + archivedProjectIds: computed, + totalProjectIds: computed, currentProjectDetails: computed, joinedProjectIds: computed, favoriteProjectIds: computed, @@ -76,6 +87,7 @@ export class ProjectStore implements IProjectStore { this.rootStore = _rootStore; // services this.projectService = new ProjectService(); + this.projectArchiveService = new ProjectArchiveService(); this.issueService = new IssueService(); this.issueLabelService = new IssueLabelService(); this.stateService = new ProjectStateService(); @@ -109,11 +121,42 @@ export class ProjectStore implements IProjectStore { get workspaceProjectIds() { const workspaceDetails = this.rootStore.workspaceRoot.currentWorkspace; if (!workspaceDetails) return; - const workspaceProjects = Object.values(this.projectMap).filter((p) => p.workspace === workspaceDetails.id); + const workspaceProjects = Object.values(this.projectMap).filter( + (p) => p.workspace === workspaceDetails.id && !p.archived_at + ); const projectIds = workspaceProjects.map((p) => p.id); return projectIds ?? null; } + /** + * Returns archived project IDs belong to current workspace. + */ + get archivedProjectIds() { + const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace; + if (!currentWorkspace) return; + + let projects = Object.values(this.projectMap ?? {}); + projects = sortBy(projects, "archived_at"); + + const projectIds = projects + .filter((project) => project.workspace === currentWorkspace.id && !!project.archived_at) + .map((project) => project.id); + return projectIds; + } + + /** + * Returns total project IDs belong to the current workspace + */ + // workspaceProjectIds + archivedProjectIds + get totalProjectIds() { + const currentWorkspace = this.rootStore.workspaceRoot.currentWorkspace; + if (!currentWorkspace) return; + + const workspaceProjects = this.workspaceProjectIds ?? []; + const archivedProjects = this.archivedProjectIds ?? []; + return [...workspaceProjects, ...archivedProjects]; + } + /** * Returns current project details */ @@ -133,7 +176,7 @@ export class ProjectStore implements IProjectStore { projects = sortBy(projects, "sort_order"); const projectIds = projects - .filter((project) => project.workspace === currentWorkspace.id && project.is_member) + .filter((project) => project.workspace === currentWorkspace.id && project.is_member && !project.archived_at) .map((project) => project.id); return projectIds; } @@ -149,7 +192,10 @@ export class ProjectStore implements IProjectStore { projects = sortBy(projects, "created_at"); const projectIds = projects - .filter((project) => project.workspace === currentWorkspace.id && project.is_member && project.is_favorite) + .filter( + (project) => + project.workspace === currentWorkspace.id && project.is_member && project.is_favorite && !project.archived_at + ) .map((project) => project.id); return projectIds; } @@ -348,4 +394,48 @@ export class ProjectStore implements IProjectStore { this.fetchProjects(workspaceSlug); } }; + + /** + * Archives a project from specific workspace and updates it in the store + * @param workspaceSlug + * @param projectId + * @returns Promise + */ + archiveProject = async (workspaceSlug: string, projectId: string) => { + await this.projectArchiveService + .archiveProject(workspaceSlug, projectId) + .then((response) => { + runInAction(() => { + set(this.projectMap, [projectId, "archived_at"], response.archived_at); + }); + }) + .catch((error) => { + console.log("Failed to archive project from project store"); + this.fetchProjects(workspaceSlug); + this.fetchProjectDetails(workspaceSlug, projectId); + throw error; + }); + }; + + /** + * Restores a project from specific workspace and updates it in the store + * @param workspaceSlug + * @param projectId + * @returns Promise + */ + restoreProject = async (workspaceSlug: string, projectId: string) => { + await this.projectArchiveService + .restoreProject(workspaceSlug, projectId) + .then(() => { + runInAction(() => { + set(this.projectMap, [projectId, "archived_at"], null); + }); + }) + .catch((error) => { + console.log("Failed to restore project from project store"); + this.fetchProjects(workspaceSlug); + this.fetchProjectDetails(workspaceSlug, projectId); + throw error; + }); + }; } diff --git a/web/store/project/project_filter.store.ts b/web/store/project/project_filter.store.ts index 7d6aff96f54..a357d6e6b81 100644 --- a/web/store/project/project_filter.store.ts +++ b/web/store/project/project_filter.store.ts @@ -1,9 +1,10 @@ +import set from "lodash/set"; import { action, computed, observable, makeObservable, runInAction, reaction } from "mobx"; import { computedFn } from "mobx-utils"; -import set from "lodash/set"; // types +import { TProjectDisplayFilters, TProjectFilters, TProjectAppliedDisplayFilterKeys } from "@plane/types"; +// store import { RootStore } from "@/store/root.store"; -import { TProjectDisplayFilters, TProjectFilters } from "@plane/types"; export interface IProjectFilterStore { // observables @@ -12,6 +13,7 @@ export interface IProjectFilterStore { searchQuery: string; // computed currentWorkspaceDisplayFilters: TProjectDisplayFilters | undefined; + currentWorkspaceAppliedDisplayFilters: TProjectAppliedDisplayFilterKeys[] | undefined; currentWorkspaceFilters: TProjectFilters | undefined; // computed functions getDisplayFiltersByWorkspaceSlug: (workspaceSlug: string) => TProjectDisplayFilters | undefined; @@ -21,6 +23,7 @@ export interface IProjectFilterStore { updateFilters: (workspaceSlug: string, filters: TProjectFilters) => void; updateSearchQuery: (query: string) => void; clearAllFilters: (workspaceSlug: string) => void; + clearAllAppliedDisplayFilters: (workspaceSlug: string) => void; } export class ProjectFilterStore implements IProjectFilterStore { @@ -39,12 +42,14 @@ export class ProjectFilterStore implements IProjectFilterStore { searchQuery: observable.ref, // computed currentWorkspaceDisplayFilters: computed, + currentWorkspaceAppliedDisplayFilters: computed, currentWorkspaceFilters: computed, // actions updateDisplayFilters: action, updateFilters: action, updateSearchQuery: action, clearAllFilters: action, + clearAllAppliedDisplayFilters: action, }); // root store this.rootStore = _rootStore; @@ -67,6 +72,19 @@ export class ProjectFilterStore implements IProjectFilterStore { return this.displayFilters[workspaceSlug]; } + /** + * @description get project state applied display filter of the current workspace + */ + // TODO: Figure out a better approach for this + get currentWorkspaceAppliedDisplayFilters() { + const workspaceSlug = this.rootStore.app.router.workspaceSlug; + if (!workspaceSlug) return; + const displayFilters = this.displayFilters[workspaceSlug]; + return Object.keys(displayFilters) + .filter((key): key is TProjectAppliedDisplayFilterKeys => ["my_projects", "archived_projects"].includes(key)) + .filter((key) => !!displayFilters[key as keyof TProjectDisplayFilters]); + } + /** * @description get filters of the current workspace */ @@ -143,4 +161,18 @@ export class ProjectFilterStore implements IProjectFilterStore { this.filters[workspaceSlug] = {}; }); }; + + /** + * @description clear project display filters of a workspace + * @param {string} workspaceSlug + */ + clearAllAppliedDisplayFilters = (workspaceSlug: string) => { + runInAction(() => { + set(this.displayFilters, [workspaceSlug], { + ...this.displayFilters[workspaceSlug], + my_projects: false, + archived_projects: false, + }); + }); + }; } From 828c30d63bbc3a7c06b29e03028b02e35db3e037 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 21 Mar 2024 13:23:48 +0530 Subject: [PATCH 03/13] dev: response changes for cycle and module --- apiserver/plane/app/views/cycle/base.py | 5 ++++- apiserver/plane/app/views/module/base.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 6809efbe6a4..e1aba63ffe5 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -878,7 +878,10 @@ def post(self, request, slug, project_id, cycle_id): ) cycle.archived_at = timezone.now() cycle.save() - return Response(status=status.HTTP_204_NO_CONTENT) + return Response( + {"archived_at": str(cycle.archived_at)}, + status=status.HTTP_204_NO_CONTENT, + ) def delete(self, request, slug, project_id, cycle_id): cycle = Cycle.objects.get( diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 39dbcb751ec..8818fd24bf8 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -621,7 +621,7 @@ def get(self, request, slug, project_id): "backlog_issues", "created_at", "updated_at", - "archived_at" + "archived_at", ) return Response(modules, status=status.HTTP_200_OK) @@ -631,7 +631,10 @@ def post(self, request, slug, project_id, module_id): ) module.archived_at = timezone.now() module.save() - return Response(status=status.HTTP_204_NO_CONTENT) + return Response( + {"archived_at": str(module.archived_at)}, + status=status.HTTP_204_NO_CONTENT, + ) def delete(self, request, slug, project_id, module_id): module = Module.objects.get( From f122bb129602958c8a0f7d47329e0bfe43f832e2 Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Thu, 21 Mar 2024 13:27:06 +0530 Subject: [PATCH 04/13] chore: status message changed --- apiserver/plane/app/views/cycle/base.py | 2 +- apiserver/plane/app/views/module/base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index e1aba63ffe5..58719e373d2 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -880,7 +880,7 @@ def post(self, request, slug, project_id, cycle_id): cycle.save() return Response( {"archived_at": str(cycle.archived_at)}, - status=status.HTTP_204_NO_CONTENT, + status=status.HTTP_200_OK, ) def delete(self, request, slug, project_id, cycle_id): diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 8818fd24bf8..61bb16e40ef 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -633,7 +633,7 @@ def post(self, request, slug, project_id, module_id): module.save() return Response( {"archived_at": str(module.archived_at)}, - status=status.HTTP_204_NO_CONTENT, + status=status.HTTP_200_OK, ) def delete(self, request, slug, project_id, module_id): From faab03dd738e859513c991fb0832c90bd1ac03a8 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 21 Mar 2024 13:39:50 +0530 Subject: [PATCH 05/13] chore: update clear all applied display filters logic. --- web/store/project/project_filter.store.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/store/project/project_filter.store.ts b/web/store/project/project_filter.store.ts index a357d6e6b81..40fcf42e8ac 100644 --- a/web/store/project/project_filter.store.ts +++ b/web/store/project/project_filter.store.ts @@ -74,6 +74,7 @@ export class ProjectFilterStore implements IProjectFilterStore { /** * @description get project state applied display filter of the current workspace + * @returns {TProjectAppliedDisplayFilterKeys[] | undefined} // An array of keys of applied display filters */ // TODO: Figure out a better approach for this get currentWorkspaceAppliedDisplayFilters() { @@ -168,10 +169,9 @@ export class ProjectFilterStore implements IProjectFilterStore { */ clearAllAppliedDisplayFilters = (workspaceSlug: string) => { runInAction(() => { - set(this.displayFilters, [workspaceSlug], { - ...this.displayFilters[workspaceSlug], - my_projects: false, - archived_projects: false, + if (!this.currentWorkspaceAppliedDisplayFilters) return; + Object.keys(this.currentWorkspaceAppliedDisplayFilters).forEach((key) => { + set(this.displayFilters, [workspaceSlug, key], false); }); }); }; From daf7e0d5167fb1467c6eddbb4b19803bdf2df314 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 21 Mar 2024 13:53:58 +0530 Subject: [PATCH 06/13] style: archived project card UI update. --- web/components/project/card.tsx | 55 +++++++++++++++++---------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/web/components/project/card.tsx b/web/components/project/card.tsx index 4895e258ec3..81813f3d0f2 100644 --- a/web/components/project/card.tsx +++ b/web/components/project/card.tsx @@ -195,35 +195,38 @@ export const ProjectCard: React.FC = observer((props) => { : `Created on ${renderFormattedDate(project.created_at)}`}

- 0 ? `${project.members.length} Members` : "No Member" - } - position="top" - > - {projectMembersIds && projectMembersIds.length > 0 ? ( -
- - {projectMembersIds.map((memberId) => { - const member = project.members?.find((m) => m.member_id === memberId); - - if (!member) return null; - - return ; - })} - -
- ) : ( - No Member Yet - )} -
+
+ 0 ? `${project.members.length} Members` : "No Member" + } + position="top" + > + {projectMembersIds && projectMembersIds.length > 0 ? ( +
+ + {projectMembersIds.map((memberId) => { + const member = project.members?.find((m) => m.member_id === memberId); + if (!member) return null; + return ( + + ); + })} + +
+ ) : ( + No Member Yet + )} +
+ {isArchived &&
Archived
} +
{isArchived ? ( isOwner && (
{ e.preventDefault(); e.stopPropagation(); @@ -236,7 +239,7 @@ export const ProjectCard: React.FC = observer((props) => {
{ e.preventDefault(); e.stopPropagation(); From 258139443fe00701be6f49da58e9231abc5c2e59 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 21 Mar 2024 13:54:38 +0530 Subject: [PATCH 07/13] chore: archive/ restore taost message update. --- .../settings/archive-project/archive-restore-modal.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web/components/project/settings/archive-project/archive-restore-modal.tsx b/web/components/project/settings/archive-project/archive-restore-modal.tsx index 3e2ebf6c8ff..88361895c9a 100644 --- a/web/components/project/settings/archive-project/archive-restore-modal.tsx +++ b/web/components/project/settings/archive-project/archive-restore-modal.tsx @@ -24,6 +24,7 @@ export const ArchiveRestoreProjectModal: React.FC = (props) => { const { getProjectById, archiveProject, restoreProject } = useProject(); const projectDetails = getProjectById(projectId); + if (!projectDetails) return null; const handleClose = () => { setIsLoading(false); @@ -37,7 +38,7 @@ export const ArchiveRestoreProjectModal: React.FC = (props) => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Archive success", - message: "Your archives can be found using archived project filter.", + message: `${projectDetails.name} has been archived successfully`, }); onClose(); router.push(`/${workspaceSlug}/projects/`); @@ -59,7 +60,7 @@ export const ArchiveRestoreProjectModal: React.FC = (props) => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Restore success", - message: "Your project can be found in projects.", + message: `You can find ${projectDetails.name} in your projects.`, }); onClose(); router.push(`/${workspaceSlug}/projects/`); @@ -74,8 +75,6 @@ export const ArchiveRestoreProjectModal: React.FC = (props) => { .finally(() => setIsLoading(false)); }; - if (!projectDetails) return null; - return ( From 37d0b8752f4f4cc0d57ec4366abb550fe8b3d0eb Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 21 Mar 2024 13:59:47 +0530 Subject: [PATCH 08/13] fix: clear all applied display filter logic. --- web/store/project/project_filter.store.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/store/project/project_filter.store.ts b/web/store/project/project_filter.store.ts index 40fcf42e8ac..68653ca947a 100644 --- a/web/store/project/project_filter.store.ts +++ b/web/store/project/project_filter.store.ts @@ -170,7 +170,7 @@ export class ProjectFilterStore implements IProjectFilterStore { clearAllAppliedDisplayFilters = (workspaceSlug: string) => { runInAction(() => { if (!this.currentWorkspaceAppliedDisplayFilters) return; - Object.keys(this.currentWorkspaceAppliedDisplayFilters).forEach((key) => { + this.currentWorkspaceAppliedDisplayFilters.forEach((key) => { set(this.displayFilters, [workspaceSlug, key], false); }); }); From e0354453fce94fc8dc48b733efa1a15b0ed70b21 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 21 Mar 2024 14:15:22 +0530 Subject: [PATCH 09/13] chore: project empty state update to handle archived projects. --- web/components/project/card-list.tsx | 6 +++--- web/constants/empty-state.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/components/project/card-list.tsx b/web/components/project/card-list.tsx index 43ad760247f..4d19eace6ce 100644 --- a/web/components/project/card-list.tsx +++ b/web/components/project/card-list.tsx @@ -16,10 +16,10 @@ export const ProjectCardList = observer(() => { // store hooks const { commandPalette: commandPaletteStore } = useApplication(); const { setTrackElement } = useEventTracker(); - const { totalProjectIds, filteredProjectIds, getProjectById } = useProject(); - const { searchQuery } = useProjectFilter(); + const { workspaceProjectIds, filteredProjectIds, getProjectById } = useProject(); + const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter(); - if (totalProjectIds?.length === 0) + if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects) return ( Date: Thu, 21 Mar 2024 14:16:24 +0530 Subject: [PATCH 10/13] chore: minor typo fix in cycles and modules archive. --- packages/types/src/cycle/cycle.d.ts | 2 +- packages/types/src/module/modules.d.ts | 2 +- web/components/cycles/archived-cycles/modal.tsx | 4 ++-- web/components/modules/archived-modules/modal.tsx | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/types/src/cycle/cycle.d.ts b/packages/types/src/cycle/cycle.d.ts index 30724706b4a..32c928373a5 100644 --- a/packages/types/src/cycle/cycle.d.ts +++ b/packages/types/src/cycle/cycle.d.ts @@ -31,7 +31,7 @@ export interface ICycle { unstarted_issues: number; updated_at: Date; updated_by: string; - archived_at: string | null; + archived_at: Date | null; assignee_ids: string[]; view_props: { filters: IIssueFilterOptions; diff --git a/packages/types/src/module/modules.d.ts b/packages/types/src/module/modules.d.ts index 7ba2c3b4181..b00d50d87c5 100644 --- a/packages/types/src/module/modules.d.ts +++ b/packages/types/src/module/modules.d.ts @@ -39,7 +39,7 @@ export interface IModule { unstarted_issues: number; updated_at: Date; updated_by: string; - archived_at: string | null; + archived_at: Date | null; view_props: { filters: IIssueFilterOptions; }; diff --git a/web/components/cycles/archived-cycles/modal.tsx b/web/components/cycles/archived-cycles/modal.tsx index a9b421351bf..6e0ddef35bb 100644 --- a/web/components/cycles/archived-cycles/modal.tsx +++ b/web/components/cycles/archived-cycles/modal.tsx @@ -31,7 +31,7 @@ export const ArchiveCycleModal: React.FC = (props) => { handleClose(); }; - const handleArchiveIssue = async () => { + const handleArchiveCycle = async () => { setIsArchiving(true); await archiveCycle(workspaceSlug, projectId, cycleId) .then(() => { @@ -89,7 +89,7 @@ export const ArchiveCycleModal: React.FC = (props) => { -
diff --git a/web/components/modules/archived-modules/modal.tsx b/web/components/modules/archived-modules/modal.tsx index f34aff26040..f922e0ba9c9 100644 --- a/web/components/modules/archived-modules/modal.tsx +++ b/web/components/modules/archived-modules/modal.tsx @@ -31,7 +31,7 @@ export const ArchiveModuleModal: React.FC = (props) => { handleClose(); }; - const handleArchiveIssue = async () => { + const handleArchiveModule = async () => { setIsArchiving(true); await archiveModule(workspaceSlug, projectId, moduleId) .then(() => { @@ -89,7 +89,7 @@ export const ArchiveModuleModal: React.FC = (props) => { -
From 206e043b5f0c5c8e573ec92fd726d8e042661274 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 21 Mar 2024 15:03:15 +0530 Subject: [PATCH 11/13] chore: close cycle/ module overview sidebar if it's already open when clicked on overview button. --- .../cycles/board/cycles-board-card.tsx | 20 +++++++++++++------ .../cycles/list/cycles-list-item.tsx | 16 +++++++++++---- web/components/modules/module-card-item.tsx | 16 +++++++++++---- web/components/modules/module-list-item.tsx | 16 +++++++++++---- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/web/components/cycles/board/cycles-board-card.tsx b/web/components/cycles/board/cycles-board-card.tsx index 8426fe3134c..34d395db46b 100644 --- a/web/components/cycles/board/cycles-board-card.tsx +++ b/web/components/cycles/board/cycles-board-card.tsx @@ -69,8 +69,8 @@ export const CyclesBoardCard: FC = observer((props) => { ? cycleTotalIssues === 0 ? "0 Issue" : cycleTotalIssues === cycleDetails.completed_issues - ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` - : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` + ? `${cycleTotalIssues} Issue${cycleTotalIssues > 1 ? "s" : ""}` + : `${cycleDetails.completed_issues}/${cycleTotalIssues} Issues` : "0 Issue"; const handleAddToFavorites = (e: MouseEvent) => { @@ -134,10 +134,18 @@ export const CyclesBoardCard: FC = observer((props) => { e.preventDefault(); e.stopPropagation(); - router.push({ - pathname: router.pathname, - query: { ...query, peekCycle: cycleId }, - }); + if (query.peekCycle) { + delete query.peekCycle; + router.push({ + pathname: router.pathname, + query: { ...query }, + }); + } else { + router.push({ + pathname: router.pathname, + query: { ...query, peekCycle: cycleId }, + }); + } }; const daysLeft = findHowManyDaysLeft(cycleDetails.end_date) ?? 0; diff --git a/web/components/cycles/list/cycles-list-item.tsx b/web/components/cycles/list/cycles-list-item.tsx index a418f9b0475..c9c3992f2e2 100644 --- a/web/components/cycles/list/cycles-list-item.tsx +++ b/web/components/cycles/list/cycles-list-item.tsx @@ -106,10 +106,18 @@ export const CyclesListItem: FC = observer((props) => { e.preventDefault(); e.stopPropagation(); - router.push({ - pathname: router.pathname, - query: { ...query, peekCycle: cycleId }, - }); + if (query.peekCycle) { + delete query.peekCycle; + router.push({ + pathname: router.pathname, + query: { ...query }, + }); + } else { + router.push({ + pathname: router.pathname, + query: { ...query, peekCycle: cycleId }, + }); + } }; const cycleDetails = getCycleById(cycleId); diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index ff228c82e60..5f040fdd69d 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -101,10 +101,18 @@ export const ModuleCardItem: React.FC = observer((props) => { e.preventDefault(); const { query } = router; - router.push({ - pathname: router.pathname, - query: { ...query, peekModule: moduleId }, - }); + if (query.peekModule) { + delete query.peekModule; + router.push({ + pathname: router.pathname, + query: { ...query }, + }); + } else { + router.push({ + pathname: router.pathname, + query: { ...query, peekModule: moduleId }, + }); + } }; if (!moduleDetails) return null; diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx index 3fd630f2979..2354ac81c52 100644 --- a/web/components/modules/module-list-item.tsx +++ b/web/components/modules/module-list-item.tsx @@ -102,10 +102,18 @@ export const ModuleListItem: React.FC = observer((props) => { e.preventDefault(); const { query } = router; - router.push({ - pathname: router.pathname, - query: { ...query, peekModule: moduleId }, - }); + if (query.peekModule) { + delete query.peekModule; + router.push({ + pathname: router.pathname, + query: { ...query }, + }); + } else { + router.push({ + pathname: router.pathname, + query: { ...query, peekModule: moduleId }, + }); + } }; if (!moduleDetails) return null; From 1934ac90e8b3178d369985728b2832bd7438166c Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 21 Mar 2024 15:21:50 +0530 Subject: [PATCH 12/13] chore: optimize current workspace applied display filter logic. --- web/store/project/project_filter.store.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/store/project/project_filter.store.ts b/web/store/project/project_filter.store.ts index 68653ca947a..94d5f70736c 100644 --- a/web/store/project/project_filter.store.ts +++ b/web/store/project/project_filter.store.ts @@ -81,9 +81,10 @@ export class ProjectFilterStore implements IProjectFilterStore { const workspaceSlug = this.rootStore.app.router.workspaceSlug; if (!workspaceSlug) return; const displayFilters = this.displayFilters[workspaceSlug]; - return Object.keys(displayFilters) - .filter((key): key is TProjectAppliedDisplayFilterKeys => ["my_projects", "archived_projects"].includes(key)) - .filter((key) => !!displayFilters[key as keyof TProjectDisplayFilters]); + return Object.keys(displayFilters).filter( + (key): key is TProjectAppliedDisplayFilterKeys => + ["my_projects", "archived_projects"].includes(key) && !!displayFilters[key as keyof TProjectDisplayFilters] + ); } /** From a2eedf6a53cbde8f43bb52cd8348ff8fdf066da6 Mon Sep 17 00:00:00 2001 From: Prateek Shourya Date: Thu, 21 Mar 2024 15:24:10 +0530 Subject: [PATCH 13/13] chore: update all `archived_at` type from `Date` to `string`. --- packages/types/src/cycle/cycle.d.ts | 2 +- packages/types/src/module/modules.d.ts | 2 +- packages/types/src/project/projects.d.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/types/src/cycle/cycle.d.ts b/packages/types/src/cycle/cycle.d.ts index 32c928373a5..30724706b4a 100644 --- a/packages/types/src/cycle/cycle.d.ts +++ b/packages/types/src/cycle/cycle.d.ts @@ -31,7 +31,7 @@ export interface ICycle { unstarted_issues: number; updated_at: Date; updated_by: string; - archived_at: Date | null; + archived_at: string | null; assignee_ids: string[]; view_props: { filters: IIssueFilterOptions; diff --git a/packages/types/src/module/modules.d.ts b/packages/types/src/module/modules.d.ts index b00d50d87c5..7ba2c3b4181 100644 --- a/packages/types/src/module/modules.d.ts +++ b/packages/types/src/module/modules.d.ts @@ -39,7 +39,7 @@ export interface IModule { unstarted_issues: number; updated_at: Date; updated_by: string; - archived_at: Date | null; + archived_at: string | null; view_props: { filters: IIssueFilterOptions; }; diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts index a8c32882e2c..157ecb16e5e 100644 --- a/packages/types/src/project/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -23,7 +23,7 @@ export type TProjectLogoProps = { export interface IProject { archive_in: number; - archived_at: Date | null; + archived_at: string | null; archived_issues: number; archived_sub_issues: number; close_in: number;