diff --git a/web/components/modules/index.ts b/web/components/modules/index.ts
index 7bda973fa5a..957726e7d02 100644
--- a/web/components/modules/index.ts
+++ b/web/components/modules/index.ts
@@ -11,3 +11,7 @@ export * from "./sidebar";
export * from "./module-card-item";
export * from "./module-list-item";
export * from "./module-peek-overview";
+export * from "./quick-actions";
+
+// archived modules
+export * from "./archived-modules";
diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx
index b12b10fe834..ff228c82e60 100644
--- a/web/components/modules/module-card-item.tsx
+++ b/web/components/modules/module-card-item.tsx
@@ -1,19 +1,19 @@
-import React, { useState } from "react";
+import React from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { useRouter } from "next/router";
-import { Info, LinkIcon, Pencil, Star, Trash2 } from "lucide-react";
+// icons
+import { Info, Star } from "lucide-react";
// ui
-import { Avatar, AvatarGroup, CustomMenu, LayersIcon, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui";
+import { Avatar, AvatarGroup, LayersIcon, Tooltip, setPromiseToast } from "@plane/ui";
// components
-import { CreateUpdateModuleModal, DeleteModuleModal } from "@/components/modules";
+import { ModuleQuickActions } from "@/components/modules";
// constants
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker";
import { MODULE_STATUS } from "@/constants/module";
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { getDate, renderFormattedDate } from "@/helpers/date-time.helper";
-import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useEventTracker, useMember, useModule, useUser } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
@@ -24,9 +24,6 @@ type Props = {
export const ModuleCardItem: React.FC
= observer((props) => {
const { moduleId } = props;
- // states
- const [editModal, setEditModal] = useState(false);
- const [deleteModal, setDeleteModal] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -36,7 +33,7 @@ export const ModuleCardItem: React.FC = observer((props) => {
} = useUser();
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
const { getUserDetails } = useMember();
- const { setTrackElement, captureEvent } = useEventTracker();
+ const { captureEvent } = useEventTracker();
// derived values
const moduleDetails = getModuleById(moduleId);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
@@ -99,32 +96,6 @@ export const ModuleCardItem: React.FC = observer((props) => {
});
};
- const handleCopyText = (e: React.MouseEvent) => {
- e.stopPropagation();
- e.preventDefault();
- copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`).then(() => {
- setToast({
- type: TOAST_TYPE.SUCCESS,
- title: "Link Copied!",
- message: "Module link copied to clipboard.",
- });
- });
- };
-
- const handleEditModule = (e: React.MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
- setTrackElement("Modules page grid layout");
- setEditModal(true);
- };
-
- const handleDeleteModule = (e: React.MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
- setTrackElement("Modules page grid layout");
- setDeleteModal(true);
- };
-
const openModuleOverview = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
@@ -160,142 +131,112 @@ export const ModuleCardItem: React.FC = observer((props) => {
? !moduleTotalIssues || moduleTotalIssues === 0
? "0 Issue"
: moduleTotalIssues === moduleDetails.completed_issues
- ? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
- : `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues`
+ ? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
+ : `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues`
: "0 Issue";
return (
- <>
- {workspaceSlug && projectId && (
- setEditModal(false)}
- data={moduleDetails}
- projectId={projectId.toString()}
- workspaceSlug={workspaceSlug.toString()}
- />
- )}
- setDeleteModal(false)} />
-
-
-
-
-
- {moduleDetails.name}
-
-
- {moduleStatus && (
-
- {moduleStatus.label}
-
- )}
-
-
+
+
+
+
+
+ {moduleDetails.name}
+
+
+ {moduleStatus && (
+
+ {moduleStatus.label}
+
+ )}
+
+
-
-
-
-
- {issueCount ?? "0 Issue"}
-
- {moduleDetails.member_ids?.length > 0 && (
-
-
-
- {moduleDetails.member_ids.map((member_id) => {
- const member = getUserDetails(member_id);
- return ;
- })}
-
-
-
- )}
+
+
+
+
+ {issueCount ?? "0 Issue"}
+ {moduleDetails.member_ids?.length > 0 && (
+
+
+
+ {moduleDetails.member_ids.map((member_id) => {
+ const member = getUserDetails(member_id);
+ return ;
+ })}
+
+
+
+ )}
+
-
-
+
+
+
-
-
-
- {isDateValid ? (
- <>
-
- {renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
-
- >
- ) : (
- No due date
+
+
+
+
+ {isDateValid ? (
+ <>
+
+ {renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
+
+ >
+ ) : (
+
No due date
+ )}
+
+
+ {isEditingAllowed &&
+ (moduleDetails.is_favorite ? (
+
+ ) : (
+
+ ))}
+ {workspaceSlug && projectId && (
+
)}
-
-
- {isEditingAllowed &&
- (moduleDetails.is_favorite ? (
-
- ) : (
-
- ))}
-
-
- {isEditingAllowed && (
- <>
-
-
-
- Edit module
-
-
-
-
-
- Delete module
-
-
- >
- )}
-
-
-
- Copy module link
-
-
-
-
-
- >
+
+
);
});
diff --git a/web/components/modules/module-list-item.tsx b/web/components/modules/module-list-item.tsx
index c317b78d1b8..3fd630f2979 100644
--- a/web/components/modules/module-list-item.tsx
+++ b/web/components/modules/module-list-item.tsx
@@ -1,44 +1,30 @@
-import React, { useState } from "react";
+import React from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import { useRouter } from "next/router";
-import { Check, Info, LinkIcon, Pencil, Star, Trash2, User2 } from "lucide-react";
+// icons
+import { Check, Info, Star, User2 } from "lucide-react";
// ui
-import {
- Avatar,
- AvatarGroup,
- CircularProgressIndicator,
- CustomMenu,
- Tooltip,
- TOAST_TYPE,
- setToast,
- setPromiseToast,
-} from "@plane/ui";
-import { CreateUpdateModuleModal, DeleteModuleModal } from "@/components/modules";
-import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker";
-// helpers
+import { Avatar, AvatarGroup, CircularProgressIndicator, Tooltip, setPromiseToast } from "@plane/ui";
+// components
+import { ModuleQuickActions } from "@/components/modules";
// constants
+import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker";
import { MODULE_STATUS } from "@/constants/module";
import { EUserProjectRoles } from "@/constants/project";
+// helpers
import { getDate, renderFormattedDate } from "@/helpers/date-time.helper";
-import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useModule, useUser, useEventTracker, useMember } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
-// components
-// ui
-// helpers
-// constants
type Props = {
moduleId: string;
+ isArchived?: boolean;
};
export const ModuleListItem: React.FC = observer((props) => {
- const { moduleId } = props;
- // states
- const [editModal, setEditModal] = useState(false);
- const [deleteModal, setDeleteModal] = useState(false);
+ const { moduleId, isArchived = false } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@@ -48,7 +34,7 @@ export const ModuleListItem: React.FC = observer((props) => {
} = useUser();
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
const { getUserDetails } = useMember();
- const { setTrackElement, captureEvent } = useEventTracker();
+ const { captureEvent } = useEventTracker();
// derived values
const moduleDetails = getModuleById(moduleId);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
@@ -111,33 +97,7 @@ export const ModuleListItem: React.FC = observer((props) => {
});
};
- const handleCopyText = (e: React.MouseEvent) => {
- e.stopPropagation();
- e.preventDefault();
- copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`).then(() => {
- setToast({
- type: TOAST_TYPE.SUCCESS,
- title: "Link Copied!",
- message: "Module link copied to clipboard.",
- });
- });
- };
-
- const handleEditModule = (e: React.MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
- setTrackElement("Modules page list layout");
- setEditModal(true);
- };
-
- const handleDeleteModule = (e: React.MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
- setTrackElement("Modules page list layout");
- setDeleteModal(true);
- };
-
- const openModuleOverview = (e: React.MouseEvent) => {
+ const openModuleOverview = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
const { query } = router;
@@ -167,126 +127,105 @@ export const ModuleListItem: React.FC = observer((props) => {
const completedModuleCheck = moduleDetails.status === "completed";
return (
- <>
- {workspaceSlug && projectId && (
- setEditModal(false)}
- data={moduleDetails}
- projectId={projectId.toString()}
- workspaceSlug={workspaceSlug.toString()}
- />
- )}
- setDeleteModal(false)} />
-
-
-
-
-
-
-
- {completedModuleCheck ? (
- progress === 100 ? (
-
- ) : (
- {`!`}
- )
- ) : progress === 100 ? (
+ {
+ if (isArchived) {
+ openModuleOverview(e);
+ }
+ }}
+ >
+
+
+
+
+
+
+ {completedModuleCheck ? (
+ progress === 100 ? (
) : (
- {`${progress}%`}
- )}
-
-
-
- {moduleDetails.name}
-
-
-
-
-
- {moduleStatus && (
-
- {moduleStatus.label}
-
- )}
-
-
-
-
-
- {renderDate && (
-
- {renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
-
- )}
-
-
-
-
-
- {moduleDetails.member_ids.length > 0 ? (
-
- {moduleDetails.member_ids.map((member_id) => {
- const member = getUserDetails(member_id);
- return ;
- })}
-
+
{`!`}
+ )
+ ) : progress === 100 ? (
+
) : (
-
-
-
+
{`${progress}%`}
)}
-
+
+
+
+ {moduleDetails.name}
+
+
+
+
+ {moduleStatus && (
+
+ {moduleStatus.label}
+
+ )}
+
+
- {isEditingAllowed &&
- (moduleDetails.is_favorite ? (
-
- ) : (
-
- ))}
+
+
+ {renderDate && (
+
+ {renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"}
+
+ )}
+
-
- {isEditingAllowed && (
- <>
-
-
-
- Edit module
-
-
-
-
-
- Delete module
-
-
- >
- )}
-
-
-
- Copy module link
+
+
+
+ {moduleDetails.member_ids.length > 0 ? (
+
+ {moduleDetails.member_ids.map((member_id) => {
+ const member = getUserDetails(member_id);
+ return ;
+ })}
+
+ ) : (
+
+
-
-
-
+ )}
+
+
+
+ {isEditingAllowed &&
+ !isArchived &&
+ (moduleDetails.is_favorite ? (
+
+ ) : (
+
+ ))}
+ {workspaceSlug && projectId && (
+
+ )}
-
- >
+
+
);
});
diff --git a/web/components/modules/module-peek-overview.tsx b/web/components/modules/module-peek-overview.tsx
index f455a825d58..ba470a6499f 100644
--- a/web/components/modules/module-peek-overview.tsx
+++ b/web/components/modules/module-peek-overview.tsx
@@ -9,9 +9,10 @@ import { ModuleDetailsSidebar } from "./sidebar";
type Props = {
projectId: string;
workspaceSlug: string;
+ isArchived?: boolean;
};
-export const ModulePeekOverview: React.FC
= observer(({ projectId, workspaceSlug }) => {
+export const ModulePeekOverview: React.FC = observer(({ projectId, workspaceSlug, isArchived = false }) => {
// router
const router = useRouter();
const { peekModule } = router.query;
@@ -29,10 +30,10 @@ export const ModulePeekOverview: React.FC = observer(({ projectId, worksp
};
useEffect(() => {
- if (!peekModule) return;
+ if (!peekModule || isArchived) return;
fetchModuleDetails(workspaceSlug, projectId, peekModule.toString());
- }, [fetchModuleDetails, peekModule, projectId, workspaceSlug]);
+ }, [fetchModuleDetails, isArchived, peekModule, projectId, workspaceSlug]);
return (
<>
@@ -45,7 +46,11 @@ export const ModulePeekOverview: React.FC = observer(({ projectId, worksp
"0px 1px 4px 0px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(16, 24, 40, 0.06), 0px 1px 8px -1px rgba(16, 24, 40, 0.06)",
}}
>
-
+
)}
>
diff --git a/web/components/modules/quick-actions.tsx b/web/components/modules/quick-actions.tsx
new file mode 100644
index 00000000000..b7972d61240
--- /dev/null
+++ b/web/components/modules/quick-actions.tsx
@@ -0,0 +1,179 @@
+import { useState } from "react";
+import { observer } from "mobx-react";
+import { useRouter } from "next/router";
+// icons
+import { ArchiveRestoreIcon, LinkIcon, Pencil, Trash2 } from "lucide-react";
+// ui
+import { ArchiveIcon, CustomMenu, TOAST_TYPE, setToast } from "@plane/ui";
+// components
+import { ArchiveModuleModal, CreateUpdateModuleModal, DeleteModuleModal } from "@/components/modules";
+// constants
+import { EUserProjectRoles } from "@/constants/project";
+// helpers
+import { copyUrlToClipboard } from "@/helpers/string.helper";
+// hooks
+import { useModule, useEventTracker, useUser } from "@/hooks/store";
+
+type Props = {
+ moduleId: string;
+ projectId: string;
+ workspaceSlug: string;
+ isArchived?: boolean;
+};
+
+export const ModuleQuickActions: React.FC
= observer((props) => {
+ const { moduleId, projectId, workspaceSlug, isArchived } = props;
+ // router
+ const router = useRouter();
+ // states
+ const [editModal, setEditModal] = useState(false);
+ const [archiveModuleModal, setArchiveModuleModal] = useState(false);
+ const [deleteModal, setDeleteModal] = useState(false);
+ // store hooks
+ const { setTrackElement } = useEventTracker();
+ const {
+ membership: { currentWorkspaceAllProjectsRole },
+ } = useUser();
+ const { getModuleById, restoreModule } = useModule();
+ // derived values
+ const moduleDetails = getModuleById(moduleId);
+ // auth
+ const isEditingAllowed =
+ !!currentWorkspaceAllProjectsRole && currentWorkspaceAllProjectsRole[projectId] >= EUserProjectRoles.MEMBER;
+
+ const moduleState = moduleDetails?.status.toLocaleLowerCase();
+ const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState);
+
+ const handleCopyText = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ copyUrlToClipboard(`${workspaceSlug}/projects/${projectId}/modules/${moduleId}`).then(() => {
+ setToast({
+ type: TOAST_TYPE.SUCCESS,
+ title: "Link Copied!",
+ message: "Module link copied to clipboard.",
+ });
+ });
+ };
+
+ const handleEditModule = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setTrackElement("Modules page list layout");
+ setEditModal(true);
+ };
+
+ const handleArchiveModule = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setArchiveModuleModal(true);
+ };
+
+ const handleRestoreModule = async (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ await restoreModule(workspaceSlug, projectId, moduleId)
+ .then(() => {
+ setToast({
+ type: TOAST_TYPE.SUCCESS,
+ title: "Restore success",
+ message: "Your module can be found in project modules.",
+ });
+ router.push(`/${workspaceSlug}/projects/${projectId}/modules/${moduleId}`);
+ })
+ .catch(() =>
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Module could not be restored. Please try again.",
+ })
+ );
+ };
+
+ const handleDeleteModule = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setTrackElement("Modules page list layout");
+ setDeleteModal(true);
+ };
+
+ return (
+ <>
+ {moduleDetails && (
+
+
setEditModal(false)}
+ data={moduleDetails}
+ projectId={projectId}
+ workspaceSlug={workspaceSlug}
+ />
+ setArchiveModuleModal(false)}
+ />
+ setDeleteModal(false)} />
+
+ )}
+
+ {isEditingAllowed && !isArchived && (
+
+
+
+ Edit module
+
+
+ )}
+ {isEditingAllowed && !isArchived && (
+
+ {isInArchivableGroup ? (
+
+ ) : (
+
+
+
+
Archive module
+
+ Only completed or cancelled
module can be archived.
+
+
+
+ )}
+
+ )}
+ {isEditingAllowed && isArchived && (
+
+
+
+ Restore module
+
+
+ )}
+ {!isArchived && (
+
+
+
+ Copy module link
+
+
+ )}
+ {isEditingAllowed && (
+
+
+
+
+ Delete module
+
+
+
+ )}
+
+ >
+ );
+});
diff --git a/web/components/modules/sidebar.tsx b/web/components/modules/sidebar.tsx
index 4ed37d3449f..38618023dcc 100644
--- a/web/components/modules/sidebar.tsx
+++ b/web/components/modules/sidebar.tsx
@@ -4,6 +4,7 @@ import { useRouter } from "next/router";
import { Controller, useForm } from "react-hook-form";
import {
AlertCircle,
+ ArchiveRestoreIcon,
CalendarClock,
ChevronDown,
ChevronRight,
@@ -25,13 +26,14 @@ import {
UserGroupIcon,
TOAST_TYPE,
setToast,
+ ArchiveIcon,
TextArea,
} from "@plane/ui";
// components
import { LinkModal, LinksList, SidebarProgressStats } from "@/components/core";
import ProgressChart from "@/components/core/sidebar/progress-chart";
import { DateRangeDropdown, MemberDropdown } from "@/components/dropdowns";
-import { DeleteModuleModal } from "@/components/modules";
+import { ArchiveModuleModal, DeleteModuleModal } from "@/components/modules";
// constant
import {
MODULE_LINK_CREATED,
@@ -59,13 +61,15 @@ const defaultValues: Partial = {
type Props = {
moduleId: string;
handleClose: () => void;
+ isArchived?: boolean;
};
// TODO: refactor this component
export const ModuleDetailsSidebar: React.FC = observer((props) => {
- const { moduleId, handleClose } = props;
+ const { moduleId, handleClose, isArchived } = props;
// states
const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
+ const [archiveModuleModal, setArchiveModuleModal] = useState(false);
const [moduleLinkModal, setModuleLinkModal] = useState(false);
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState(null);
// router
@@ -75,10 +79,14 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
const {
membership: { currentProjectRole },
} = useUser();
- const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink } = useModule();
+ const { getModuleById, updateModuleDetails, createModuleLink, updateModuleLink, deleteModuleLink, restoreModule } =
+ useModule();
const { setTrackElement, captureModuleEvent, captureEvent } = useEventTracker();
const moduleDetails = getModuleById(moduleId);
+ const moduleState = moduleDetails?.status.toLocaleLowerCase();
+ const isInArchivableGroup = !!moduleState && ["completed", "cancelled"].includes(moduleState);
+
const { reset, control } = useForm({
defaultValues,
});
@@ -206,6 +214,30 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
});
};
+ const handleRestoreModule = async (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (!workspaceSlug || !projectId || !moduleId) return;
+
+ await restoreModule(workspaceSlug.toString(), projectId.toString(), moduleId)
+ .then(() => {
+ setToast({
+ type: TOAST_TYPE.SUCCESS,
+ title: "Restore success",
+ message: "Your module can be found in project modules.",
+ });
+ router.push(`/${workspaceSlug}/projects/${projectId}/modules/${moduleId}`);
+ })
+ .catch(() =>
+ setToast({
+ type: TOAST_TYPE.ERROR,
+ title: "Error!",
+ message: "Module could not be restored. Please try again.",
+ })
+ );
+ };
+
useEffect(() => {
if (moduleDetails)
reset({
@@ -262,8 +294,16 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
createIssueLink={handleCreateLink}
updateIssueLink={handleUpdateLink}
/>
+ {workspaceSlug && projectId && (
+ setArchiveModuleModal(false)}
+ />
+ )}
setModuleDeleteModal(false)} data={moduleDetails} />
-
<>
@@ -275,11 +315,41 @@ export const ModuleDetailsSidebar: React.FC
= observer((props) => {
-
+ {!isArchived && (
+
+ )}
{isEditingAllowed && (
+ {!isArchived && (
+ setArchiveModuleModal(true)} disabled={!isInArchivableGroup}>
+ {isInArchivableGroup ? (
+
+ ) : (
+
+
+
+
Archive module
+
+ Only completed or cancelled
module can be archived.
+
+
+
+ )}
+
+ )}
+ {isArchived && (
+
+
+
+ Restore module
+
+
+ )}
{
setTrackElement("Module peek-overview");
@@ -306,7 +376,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
customButton={
= observer((props) => {
onChange={(value: any) => {
submitChanges({ status: value });
}}
- disabled={!isEditingAllowed}
+ disabled={!isEditingAllowed || isArchived}
>
{MODULE_STATUS.map((status) => (
@@ -379,6 +449,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
from: "Start date",
to: "Target date",
}}
+ disabled={isArchived}
/>
);
}}
@@ -408,6 +479,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
multiple={false}
buttonVariant="background-with-text"
placeholder="Lead"
+ disabled={isArchived}
/>
)}
@@ -432,7 +504,7 @@ export const ModuleDetailsSidebar: React.FC
= observer((props) => {
projectId={projectId?.toString() ?? ""}
buttonVariant={value && value?.length > 0 ? "transparent-without-text" : "background-with-text"}
buttonClassName={value && value.length > 0 ? "hover:bg-transparent px-0" : ""}
- disabled={!isEditingAllowed}
+ disabled={!isEditingAllowed || isArchived}
/>
)}
@@ -556,7 +628,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
{currentProjectRole && moduleDetails.link_module && moduleDetails.link_module.length > 0 ? (
<>
- {isEditingAllowed && (
+ {isEditingAllowed && !isArchived && (
-
+ {isEditingAllowed && !isArchived && (
+
+ )}
)}
diff --git a/web/components/notifications/notification-card.tsx b/web/components/notifications/notification-card.tsx
index c7af0824cc0..4089e9108b8 100644
--- a/web/components/notifications/notification-card.tsx
+++ b/web/components/notifications/notification-card.tsx
@@ -16,12 +16,12 @@ import {
NOTIFICATION_SNOOZED,
} from "@/constants/event-tracker";
import { snoozeOptions } from "@/constants/notification";
+// helper
+import { calculateTimeAgo, renderFormattedTime, renderFormattedDate, getDate } from "@/helpers/date-time.helper";
+import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "@/helpers/string.helper";
// hooks
import { useEventTracker } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os";
-// helper
-import { calculateTimeAgo, renderFormattedTime, renderFormattedDate, getDate } from "helpers/date-time.helper";
-import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "helpers/string.helper";
type NotificationCardProps = {
@@ -137,8 +137,8 @@ export const NotificationCard: React.FC = (props) => {
closePopover();
}}
href={`/${workspaceSlug}/projects/${notification.project}/${
- notificationField === "archived_at" ? "archived-issues" : "issues"
- }/${notification.data.issue.id}`}
+ notificationField === "archived_at" ? "archives/" : ""
+ }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"
}`}
@@ -184,12 +184,12 @@ export const NotificationCard: React.FC = (props) => {
{notificationField === "comment"
? "commented"
: notificationField === "archived_at"
- ? notification.data.issue_activity.new_value === "restore"
- ? "restored the issue"
- : "archived the issue"
- : notificationField === "None"
- ? null
- : replaceUnderscoreIfSnakeCase(notificationField)}{" "}
+ ? notification.data.issue_activity.new_value === "restore"
+ ? "restored the issue"
+ : "archived the issue"
+ : notificationField === "None"
+ ? null
+ : replaceUnderscoreIfSnakeCase(notificationField)}{" "}
{!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""}
{" "}
diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx
index 9a50786792a..bb1d6ad17a2 100644
--- a/web/components/project/sidebar-list-item.tsx
+++ b/web/components/project/sidebar-list-item.tsx
@@ -282,13 +282,6 @@ export const ProjectSidebarListItem: React.FC = observer((props) => {
)}
-
-
-
- Copy project link
-
-
-
{/* publish project settings */}
{isAdmin && (
setPublishModal(true)}>
@@ -300,16 +293,6 @@ export const ProjectSidebarListItem: React.FC = observer((props) => {
)}
- {!isViewerOrGuest && (
-
-
-
-
-
- )}
@@ -318,6 +301,23 @@ export const ProjectSidebarListItem: React.FC
= observer((props) => {
+
+
+
+ Copy link
+
+
+
+ {!isViewerOrGuest && (
+
+
+
+
+
+ )}
diff --git a/web/constants/archives.ts b/web/constants/archives.ts
new file mode 100644
index 00000000000..9130a981a17
--- /dev/null
+++ b/web/constants/archives.ts
@@ -0,0 +1,50 @@
+// types
+import { IProject } from "@plane/types";
+// icons
+import { ContrastIcon, DiceIcon, LayersIcon } from "@plane/ui";
+
+export const ARCHIVES_TAB_LIST: {
+ key: string;
+ label: string;
+ shouldRender: (projectDetails: IProject) => boolean;
+}[] = [
+ {
+ key: "issues",
+ label: "Issues",
+ shouldRender: () => true,
+ },
+ {
+ key: "cycles",
+ label: "Cycles",
+ shouldRender: (projectDetails) => projectDetails.cycle_view,
+ },
+ {
+ key: "modules",
+ label: "Modules",
+ shouldRender: (projectDetails) => projectDetails.module_view,
+ },
+];
+
+export const PROJECT_ARCHIVES_BREADCRUMB_LIST: {
+ [key: string]: {
+ label: string;
+ href: string;
+ icon: React.FC
& { className?: string }>;
+ };
+} = {
+ issues: {
+ label: "Issues",
+ href: "/issues",
+ icon: LayersIcon,
+ },
+ cycles: {
+ label: "Cycles",
+ href: "/cycles",
+ icon: ContrastIcon,
+ },
+ modules: {
+ label: "Modules",
+ href: "/modules",
+ icon: DiceIcon,
+ },
+};
diff --git a/web/constants/empty-state.ts b/web/constants/empty-state.ts
index 587f58cee11..8be4a52e2ce 100644
--- a/web/constants/empty-state.ts
+++ b/web/constants/empty-state.ts
@@ -51,6 +51,7 @@ export enum EmptyStateType {
PROJECT_CYCLE_ACTIVE = "project-cycle-active",
PROJECT_CYCLE_ALL = "project-cycle-all",
PROJECT_CYCLE_COMPLETED_NO_ISSUES = "project-cycle-completed-no-issues",
+ PROJECT_ARCHIVED_NO_CYCLES = "project-archived-no-cycles",
PROJECT_EMPTY_FILTER = "project-empty-filter",
PROJECT_ARCHIVED_EMPTY_FILTER = "project-archived-empty-filter",
PROJECT_DRAFT_EMPTY_FILTER = "project-draft-empty-filter",
@@ -62,6 +63,7 @@ export enum EmptyStateType {
MEMBERS_EMPTY_SEARCH = "members-empty-search",
PROJECT_MODULE_ISSUES = "project-module-issues",
PROJECT_MODULE = "project-module",
+ PROJECT_ARCHIVED_NO_MODULES = "project-archived-no-modules",
PROJECT_VIEW = "project-view",
PROJECT_PAGE = "project-page",
PROJECT_PAGE_ALL = "project-page-all",
@@ -308,6 +310,12 @@ const emptyStateDetails = {
"No issues in the cycle. Issues are either transferred or hidden. To see hidden issues if any, update your display properties accordingly.",
path: "/empty-state/cycle/completed-no-issues",
},
+ [EmptyStateType.PROJECT_ARCHIVED_NO_CYCLES]: {
+ key: EmptyStateType.PROJECT_ARCHIVED_NO_CYCLES,
+ title: "No archived cycles yet",
+ description: "To tidy up your project, archive completed cycles. Find them here once archived.",
+ path: "/empty-state/archived/empty-cycles",
+ },
[EmptyStateType.PROJECT_CYCLE_ALL]: {
key: EmptyStateType.PROJECT_CYCLE_ALL,
title: "No cycles",
@@ -368,7 +376,7 @@ const emptyStateDetails = {
key: EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES,
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.",
+ "Manually or through automation, you can archive issues that are completed or cancelled. Find them here once archived.",
path: "/empty-state/archived/empty-issues",
primaryButton: {
text: "Set automation",
@@ -432,6 +440,12 @@ const emptyStateDetails = {
accessType: "project",
access: EUserProjectRoles.MEMBER,
},
+ [EmptyStateType.PROJECT_ARCHIVED_NO_MODULES]: {
+ key: EmptyStateType.PROJECT_ARCHIVED_NO_MODULES,
+ title: "No archived Modules yet",
+ description: "To tidy up your project, archive completed or cancelled modules. Find them here once archived.",
+ path: "/empty-state/archived/empty-modules",
+ },
// project views
[EmptyStateType.PROJECT_VIEW]: {
key: EmptyStateType.PROJECT_VIEW,
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archives/cycles/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archives/cycles/index.tsx
new file mode 100644
index 00000000000..bcc406fa087
--- /dev/null
+++ b/web/pages/[workspaceSlug]/projects/[projectId]/archives/cycles/index.tsx
@@ -0,0 +1,44 @@
+import { ReactElement } from "react";
+import { observer } from "mobx-react";
+import { useRouter } from "next/router";
+// components
+import { PageHead } from "@/components/core";
+import { ArchivedCycleLayoutRoot, ArchivedCyclesHeader } from "@/components/cycles";
+import { ProjectArchivesHeader } from "@/components/headers";
+// hooks
+import { useProject } from "@/hooks/store";
+// layouts
+import { AppLayout } from "@/layouts/app-layout";
+// types
+import { NextPageWithLayout } from "@/lib/types";
+
+const ProjectArchivedCyclesPage: NextPageWithLayout = observer(() => {
+ // router
+ const router = useRouter();
+ 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 cycles`;
+
+ return (
+ <>
+
+
+ >
+ );
+});
+
+ProjectArchivedCyclesPage.getLayout = function getLayout(page: ReactElement) {
+ return (
+ } withProjectWrapper>
+ {page}
+
+ );
+};
+
+export default ProjectArchivedCyclesPage;
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archives/issues/[archivedIssueId].tsx
similarity index 96%
rename from web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx
rename to web/pages/[workspaceSlug]/projects/[projectId]/archives/issues/[archivedIssueId].tsx
index 7d1b380f2a6..12b0c83917d 100644
--- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/[archivedIssueId].tsx
+++ b/web/pages/[workspaceSlug]/projects/[projectId]/archives/issues/[archivedIssueId].tsx
@@ -2,23 +2,23 @@ import { useState, ReactElement } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import useSWR from "swr";
-// hooks
-import { RotateCcw } from "lucide-react";
+// icons
+import { ArchiveRestoreIcon } from "lucide-react";
+// ui
import { ArchiveIcon, Button, Loader, TOAST_TYPE, setToast } from "@plane/ui";
+// components
import { PageHead } from "@/components/core";
import { ProjectArchivedIssueDetailsHeader } from "@/components/headers";
import { IssueDetailRoot } from "@/components/issues";
+// constants
import { EIssuesStoreType } from "@/constants/issue";
import { EUserProjectRoles } from "@/constants/project";
+// hooks
import { useIssueDetail, useIssues, useProject, useUser } from "@/hooks/store";
// layouts
import { AppLayout } from "@/layouts/app-layout";
-// components
-// ui
-// icons
// types
import { NextPageWithLayout } from "@/lib/types";
-// constants
const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
// router
@@ -112,7 +112,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
{issue?.archived_at && canRestoreIssue && (
-
+
This issue has been archived.
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archives/issues/index.tsx
similarity index 72%
rename from web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx
rename to web/pages/[workspaceSlug]/projects/[projectId]/archives/issues/index.tsx
index 5c2ca094509..b2c08e0a3e6 100644
--- a/web/pages/[workspaceSlug]/projects/[projectId]/archived-issues/index.tsx
+++ b/web/pages/[workspaceSlug]/projects/[projectId]/archives/issues/index.tsx
@@ -1,17 +1,16 @@
import { ReactElement } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
-// layouts
+// components
import { PageHead } from "@/components/core";
-import { ProjectArchivedIssuesHeader } from "@/components/headers";
-import { ArchivedIssueLayoutRoot } from "@/components/issues";
+import { ProjectArchivesHeader } from "@/components/headers";
+import { ArchivedIssueLayoutRoot, ArchivedIssuesHeader } from "@/components/issues";
+// hooks
import { useProject } from "@/hooks/store";
+// layouts
import { AppLayout } from "@/layouts/app-layout";
-// contexts
-// components
// types
import { NextPageWithLayout } from "@/lib/types";
-// hooks
const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => {
// router
@@ -26,14 +25,17 @@ const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => {
return (
<>
-
+
>
);
});
ProjectArchivedIssuesPage.getLayout = function getLayout(page: ReactElement) {
return (
- } withProjectWrapper>
+ } withProjectWrapper>
{page}
);
diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/archives/modules/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/archives/modules/index.tsx
new file mode 100644
index 00000000000..1e346098e67
--- /dev/null
+++ b/web/pages/[workspaceSlug]/projects/[projectId]/archives/modules/index.tsx
@@ -0,0 +1,44 @@
+import { ReactElement } from "react";
+import { observer } from "mobx-react";
+import { useRouter } from "next/router";
+// components
+import { PageHead } from "@/components/core";
+import { ProjectArchivesHeader } from "@/components/headers";
+import { ArchivedModuleLayoutRoot, ArchivedModulesHeader } from "@/components/modules";
+// hooks
+import { useProject } from "@/hooks/store";
+// layouts
+import { AppLayout } from "@/layouts/app-layout";
+// types
+import { NextPageWithLayout } from "@/lib/types";
+
+const ProjectArchivedModulesPage: NextPageWithLayout = observer(() => {
+ // router
+ const router = useRouter();
+ 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 modules`;
+
+ return (
+ <>
+
+
+ >
+ );
+});
+
+ProjectArchivedModulesPage.getLayout = function getLayout(page: ReactElement) {
+ return (
+ } withProjectWrapper>
+ {page}
+
+ );
+};
+
+export default ProjectArchivedModulesPage;
diff --git a/web/public/empty-state/archived/empty-cycles-dark.webp b/web/public/empty-state/archived/empty-cycles-dark.webp
new file mode 100644
index 00000000000..872fb2fca19
Binary files /dev/null and b/web/public/empty-state/archived/empty-cycles-dark.webp differ
diff --git a/web/public/empty-state/archived/empty-cycles-light.webp b/web/public/empty-state/archived/empty-cycles-light.webp
new file mode 100644
index 00000000000..2db1dc4b780
Binary files /dev/null and b/web/public/empty-state/archived/empty-cycles-light.webp differ
diff --git a/web/public/empty-state/archived/empty-issues-dark.webp b/web/public/empty-state/archived/empty-issues-dark.webp
index 264488cbf63..01abef1fee1 100644
Binary files a/web/public/empty-state/archived/empty-issues-dark.webp and b/web/public/empty-state/archived/empty-issues-dark.webp differ
diff --git a/web/public/empty-state/archived/empty-issues-light.webp b/web/public/empty-state/archived/empty-issues-light.webp
index 602f0b14f0f..f010155fc99 100644
Binary files a/web/public/empty-state/archived/empty-issues-light.webp and b/web/public/empty-state/archived/empty-issues-light.webp differ
diff --git a/web/public/empty-state/archived/empty-modules-dark.webp b/web/public/empty-state/archived/empty-modules-dark.webp
new file mode 100644
index 00000000000..e34bf373738
Binary files /dev/null and b/web/public/empty-state/archived/empty-modules-dark.webp differ
diff --git a/web/public/empty-state/archived/empty-modules-light.webp b/web/public/empty-state/archived/empty-modules-light.webp
new file mode 100644
index 00000000000..5caf2ea9039
Binary files /dev/null and b/web/public/empty-state/archived/empty-modules-light.webp differ
diff --git a/web/services/cycle_archive.service.ts b/web/services/cycle_archive.service.ts
new file mode 100644
index 00000000000..6ea3aeb40de
--- /dev/null
+++ b/web/services/cycle_archive.service.ts
@@ -0,0 +1,42 @@
+// type
+import { ICycle } from "@plane/types";
+// helpers
+import { API_BASE_URL } from "@/helpers/common.helper";
+// services
+import { APIService } from "@/services/api.service";
+
+export class CycleArchiveService extends APIService {
+ constructor() {
+ super(API_BASE_URL);
+ }
+
+ async getArchivedCycles(workspaceSlug: string, projectId: string): Promise {
+ return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-cycles/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async archiveCycle(
+ workspaceSlug: string,
+ projectId: string,
+ cycleId: string
+ ): Promise<{
+ archived_at: string;
+ }> {
+ return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/archive/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async restoreCycle(workspaceSlug: string, projectId: string, cycleId: string): Promise {
+ return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/archive/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+}
diff --git a/web/services/module_archive.service.ts b/web/services/module_archive.service.ts
new file mode 100644
index 00000000000..3b8f0582771
--- /dev/null
+++ b/web/services/module_archive.service.ts
@@ -0,0 +1,42 @@
+// type
+import { IModule } from "@plane/types";
+// helpers
+import { API_BASE_URL } from "@/helpers/common.helper";
+// services
+import { APIService } from "@/services/api.service";
+
+export class ModuleArchiveService extends APIService {
+ constructor() {
+ super(API_BASE_URL);
+ }
+
+ async getArchivedModules(workspaceSlug: string, projectId: string): Promise {
+ return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-modules/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async archiveModule(
+ workspaceSlug: string,
+ projectId: string,
+ moduleId: string
+ ): Promise<{
+ archived_at: string;
+ }> {
+ return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/archive/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async restoreModule(workspaceSlug: string, projectId: string, moduleId: string): Promise {
+ return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/archive/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+}
diff --git a/web/store/cycle.store.ts b/web/store/cycle.store.ts
index deaaf253061..ac90f9e5eed 100644
--- a/web/store/cycle.store.ts
+++ b/web/store/cycle.store.ts
@@ -3,17 +3,18 @@ import set from "lodash/set";
import sortBy from "lodash/sortBy";
import { action, computed, observable, makeObservable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
+// types
+import { ICycle, CycleDateCheckData } from "@plane/types";
// helpers
-import { getDate } from "@/helpers/date-time.helper";
import { orderCycles, shouldFilterCycle } from "@/helpers/cycle.helper";
+import { getDate } from "@/helpers/date-time.helper";
// services
import { CycleService } from "@/services/cycle.service";
+import { CycleArchiveService } from "@/services/cycle_archive.service";
import { IssueService } from "@/services/issue";
import { ProjectService } from "@/services/project";
-// mobx
+// store
import { RootStore } from "@/store/root.store";
-// types
-import { ICycle, CycleDateCheckData } from "@plane/types";
export interface ICycleStore {
// loaders
@@ -29,9 +30,11 @@ export interface ICycleStore {
currentProjectIncompleteCycleIds: string[] | null;
currentProjectDraftCycleIds: string[] | null;
currentProjectActiveCycleId: string | null;
+ currentProjectArchivedCycleIds: string[] | null;
// computed actions
getFilteredCycleIds: (projectId: string) => string[] | null;
getFilteredCompletedCycleIds: (projectId: string) => string[] | null;
+ getFilteredArchivedCycleIds: (projectId: string) => string[] | null;
getCycleById: (cycleId: string) => ICycle | null;
getCycleNameById: (cycleId: string) => string | undefined;
getActiveCycleById: (cycleId: string) => ICycle | null;
@@ -42,6 +45,7 @@ export interface ICycleStore {
fetchWorkspaceCycles: (workspaceSlug: string) => Promise;
fetchAllCycles: (workspaceSlug: string, projectId: string) => Promise;
fetchActiveCycle: (workspaceSlug: string, projectId: string) => Promise;
+ fetchArchivedCycles: (workspaceSlug: string, projectId: string) => Promise;
fetchCycleDetails: (workspaceSlug: string, projectId: string, cycleId: string) => Promise;
// crud
createCycle: (workspaceSlug: string, projectId: string, data: Partial) => Promise;
@@ -55,6 +59,9 @@ export interface ICycleStore {
// favorites
addCycleToFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise;
removeCycleFromFavorites: (workspaceSlug: string, projectId: string, cycleId: string) => Promise;
+ // archive
+ archiveCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise;
+ restoreCycle: (workspaceSlug: string, projectId: string, cycleId: string) => Promise;
}
export class CycleStore implements ICycleStore {
@@ -70,6 +77,7 @@ export class CycleStore implements ICycleStore {
projectService;
issueService;
cycleService;
+ cycleArchiveService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
@@ -85,22 +93,29 @@ export class CycleStore implements ICycleStore {
currentProjectIncompleteCycleIds: computed,
currentProjectDraftCycleIds: computed,
currentProjectActiveCycleId: computed,
+ currentProjectArchivedCycleIds: computed,
// actions
fetchWorkspaceCycles: action,
fetchAllCycles: action,
fetchActiveCycle: action,
+ fetchArchivedCycles: action,
fetchCycleDetails: action,
createCycle: action,
updateCycleDetails: action,
deleteCycle: action,
addCycleToFavorites: action,
removeCycleFromFavorites: action,
+ archiveCycle: action,
+ restoreCycle: action,
});
this.rootStore = _rootStore;
+
+ // services
this.projectService = new ProjectService();
this.issueService = new IssueService();
this.cycleService = new CycleService();
+ this.cycleArchiveService = new CycleArchiveService();
}
// computed
@@ -110,7 +125,7 @@ export class CycleStore implements ICycleStore {
get currentProjectCycleIds() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null;
- let allCycles = Object.values(this.cycleMap ?? {}).filter((c) => c?.project_id === projectId);
+ let allCycles = Object.values(this.cycleMap ?? {}).filter((c) => c?.project_id === projectId && !c?.archived_at);
allCycles = sortBy(allCycles, [(c) => c.sort_order]);
const allCycleIds = allCycles.map((c) => c.id);
return allCycleIds;
@@ -126,7 +141,7 @@ export class CycleStore implements ICycleStore {
const endDate = getDate(c.end_date);
const hasEndDatePassed = endDate && isPast(endDate);
const isEndDateToday = endDate && isToday(endDate);
- return c.project_id === projectId && hasEndDatePassed && !isEndDateToday;
+ return c.project_id === projectId && hasEndDatePassed && !isEndDateToday && !c?.archived_at;
});
completedCycles = sortBy(completedCycles, [(c) => c.sort_order]);
const completedCycleIds = completedCycles.map((c) => c.id);
@@ -142,7 +157,7 @@ export class CycleStore implements ICycleStore {
let upcomingCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const startDate = getDate(c.start_date);
const isStartDateUpcoming = startDate && isFuture(startDate);
- return c.project_id === projectId && isStartDateUpcoming;
+ return c.project_id === projectId && isStartDateUpcoming && !c?.archived_at;
});
upcomingCycles = sortBy(upcomingCycles, [(c) => c.sort_order]);
const upcomingCycleIds = upcomingCycles.map((c) => c.id);
@@ -158,7 +173,7 @@ export class CycleStore implements ICycleStore {
let incompleteCycles = Object.values(this.cycleMap ?? {}).filter((c) => {
const endDate = getDate(c.end_date);
const hasEndDatePassed = endDate && isPast(endDate);
- return c.project_id === projectId && !hasEndDatePassed;
+ return c.project_id === projectId && !hasEndDatePassed && !c?.archived_at;
});
incompleteCycles = sortBy(incompleteCycles, [(c) => c.sort_order]);
const incompleteCycleIds = incompleteCycles.map((c) => c.id);
@@ -172,7 +187,7 @@ export class CycleStore implements ICycleStore {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null;
let draftCycles = Object.values(this.cycleMap ?? {}).filter(
- (c) => c.project_id === projectId && !c.start_date && !c.end_date
+ (c) => c.project_id === projectId && !c.start_date && !c.end_date && !c?.archived_at
);
draftCycles = sortBy(draftCycles, [(c) => c.sort_order]);
const draftCycleIds = draftCycles.map((c) => c.id);
@@ -191,6 +206,20 @@ export class CycleStore implements ICycleStore {
return activeCycle || null;
}
+ /**
+ * returns all archived cycle ids for a project
+ */
+ get currentProjectArchivedCycleIds() {
+ const projectId = this.rootStore.app.router.projectId;
+ if (!projectId || !this.fetchedMap[projectId]) return null;
+ let archivedCycles = Object.values(this.cycleMap ?? {}).filter(
+ (c) => c.project_id === projectId && !!c.archived_at
+ );
+ archivedCycles = sortBy(archivedCycles, [(c) => c.sort_order]);
+ const archivedCycleIds = archivedCycles.map((c) => c.id);
+ return archivedCycleIds;
+ }
+
/**
* @description returns filtered cycle ids based on display filters and filters
* @param {TCycleDisplayFilters} displayFilters
@@ -204,6 +233,7 @@ export class CycleStore implements ICycleStore {
let cycles = Object.values(this.cycleMap ?? {}).filter(
(c) =>
c.project_id === projectId &&
+ !c.archived_at &&
c.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
shouldFilterCycle(c, filters ?? {})
);
@@ -225,6 +255,7 @@ export class CycleStore implements ICycleStore {
let cycles = Object.values(this.cycleMap ?? {}).filter(
(c) =>
c.project_id === projectId &&
+ !c.archived_at &&
c.status.toLowerCase() === "completed" &&
c.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
shouldFilterCycle(c, filters ?? {})
@@ -234,6 +265,27 @@ export class CycleStore implements ICycleStore {
return cycleIds;
});
+ /**
+ * @description returns filtered archived cycle ids based on display filters and filters
+ * @param {string} projectId
+ * @returns {string[] | null}
+ */
+ getFilteredArchivedCycleIds = computedFn((projectId: string) => {
+ const filters = this.rootStore.cycleFilter.getArchivedFiltersByProjectId(projectId);
+ const searchQuery = this.rootStore.cycleFilter.archivedCyclesSearchQuery;
+ if (!this.fetchedMap[projectId]) return null;
+ let cycles = Object.values(this.cycleMap ?? {}).filter(
+ (c) =>
+ c.project_id === projectId &&
+ !!c.archived_at &&
+ c.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
+ shouldFilterCycle(c, filters ?? {})
+ );
+ cycles = sortBy(cycles, [(c) => !c.start_date]);
+ const cycleIds = cycles.map((c) => c.id);
+ return cycleIds;
+ });
+
/**
* @description returns cycle details by cycle id
* @param cycleId
@@ -264,7 +316,7 @@ export class CycleStore implements ICycleStore {
getProjectCycleIds = computedFn((projectId: string): string[] | null => {
if (!this.fetchedMap[projectId]) return null;
- let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project_id === projectId);
+ let cycles = Object.values(this.cycleMap ?? {}).filter((c) => c.project_id === projectId && !c?.archived_at);
cycles = sortBy(cycles, [(c) => c.sort_order]);
const cycleIds = cycles.map((c) => c.id);
return cycleIds || null;
@@ -321,6 +373,31 @@ export class CycleStore implements ICycleStore {
}
};
+ /**
+ * @description fetches archived cycles for a project
+ * @param workspaceSlug
+ * @param projectId
+ * @returns
+ */
+ fetchArchivedCycles = async (workspaceSlug: string, projectId: string) => {
+ this.loader = true;
+ return await this.cycleArchiveService
+ .getArchivedCycles(workspaceSlug, projectId)
+ .then((response) => {
+ runInAction(() => {
+ response.forEach((cycle) => {
+ set(this.cycleMap, [cycle.id], cycle);
+ });
+ this.loader = false;
+ });
+ return response;
+ })
+ .catch(() => {
+ this.loader = false;
+ return undefined;
+ });
+ };
+
/**
* @description fetches active cycle for a project
* @param workspaceSlug
@@ -452,4 +529,48 @@ export class CycleStore implements ICycleStore {
throw error;
}
};
+
+ /**
+ * @description archives a cycle
+ * @param workspaceSlug
+ * @param projectId
+ * @param cycleId
+ * @returns
+ */
+ archiveCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => {
+ const cycleDetails = this.getCycleById(cycleId);
+ if (cycleDetails?.archived_at) return;
+ await this.cycleArchiveService
+ .archiveCycle(workspaceSlug, projectId, cycleId)
+ .then((response) => {
+ runInAction(() => {
+ set(this.cycleMap, [cycleId, "archived_at"], response.archived_at);
+ });
+ })
+ .catch((error) => {
+ console.error("Failed to archive cycle in cycle store", error);
+ });
+ };
+
+ /**
+ * @description restores a cycle
+ * @param workspaceSlug
+ * @param projectId
+ * @param cycleId
+ * @returns
+ */
+ restoreCycle = async (workspaceSlug: string, projectId: string, cycleId: string) => {
+ const cycleDetails = this.getCycleById(cycleId);
+ if (!cycleDetails?.archived_at) return;
+ await this.cycleArchiveService
+ .restoreCycle(workspaceSlug, projectId, cycleId)
+ .then(() => {
+ runInAction(() => {
+ set(this.cycleMap, [cycleId, "archived_at"], null);
+ });
+ })
+ .catch((error) => {
+ console.error("Failed to restore cycle in cycle store", error);
+ });
+ };
}
diff --git a/web/store/cycle_filter.store.ts b/web/store/cycle_filter.store.ts
index 2c57b5e7827..182ab0251dd 100644
--- a/web/store/cycle_filter.store.ts
+++ b/web/store/cycle_filter.store.ts
@@ -1,33 +1,39 @@
+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 { TCycleDisplayFilters, TCycleFilters, TCycleFiltersByState } from "@plane/types";
+// store
import { RootStore } from "@/store/root.store";
-import { TCycleDisplayFilters, TCycleFilters } from "@plane/types";
export interface ICycleFilterStore {
// observables
displayFilters: Record;
- filters: Record;
+ filters: Record;
searchQuery: string;
+ archivedCyclesSearchQuery: string;
// computed
currentProjectDisplayFilters: TCycleDisplayFilters | undefined;
currentProjectFilters: TCycleFilters | undefined;
+ currentProjectArchivedFilters: TCycleFilters | undefined;
// computed functions
getDisplayFiltersByProjectId: (projectId: string) => TCycleDisplayFilters | undefined;
getFiltersByProjectId: (projectId: string) => TCycleFilters | undefined;
+ getArchivedFiltersByProjectId: (projectId: string) => TCycleFilters | undefined;
// actions
updateDisplayFilters: (projectId: string, displayFilters: TCycleDisplayFilters) => void;
- updateFilters: (projectId: string, filters: TCycleFilters) => void;
+ updateFilters: (projectId: string, filters: TCycleFilters, state?: keyof TCycleFiltersByState) => void;
updateSearchQuery: (query: string) => void;
- clearAllFilters: (projectId: string) => void;
+ updateArchivedCyclesSearchQuery: (query: string) => void;
+ clearAllFilters: (projectId: string, state?: keyof TCycleFiltersByState) => void;
}
export class CycleFilterStore implements ICycleFilterStore {
// observables
displayFilters: Record = {};
- filters: Record = {};
+ filters: Record = {};
searchQuery: string = "";
+ archivedCyclesSearchQuery: string = "";
// root store
rootStore: RootStore;
@@ -37,13 +43,16 @@ export class CycleFilterStore implements ICycleFilterStore {
displayFilters: observable,
filters: observable,
searchQuery: observable.ref,
+ archivedCyclesSearchQuery: observable.ref,
// computed
currentProjectDisplayFilters: computed,
currentProjectFilters: computed,
+ currentProjectArchivedFilters: computed,
// actions
updateDisplayFilters: action,
updateFilters: action,
updateSearchQuery: action,
+ updateArchivedCyclesSearchQuery: action,
clearAllFilters: action,
});
// root store
@@ -73,7 +82,16 @@ export class CycleFilterStore implements ICycleFilterStore {
get currentProjectFilters() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return;
- return this.filters[projectId];
+ return this.filters[projectId]?.default ?? {};
+ }
+
+ /**
+ * @description get archived filters of the current project
+ */
+ get currentProjectArchivedFilters() {
+ const projectId = this.rootStore.app.router.projectId;
+ if (!projectId) return;
+ return this.filters[projectId].archived;
}
/**
@@ -86,7 +104,13 @@ export class CycleFilterStore implements ICycleFilterStore {
* @description get filters of a project by projectId
* @param {string} projectId
*/
- getFiltersByProjectId = computedFn((projectId: string) => this.filters[projectId]);
+ getFiltersByProjectId = computedFn((projectId: string) => this.filters[projectId]?.default ?? {});
+
+ /**
+ * @description get archived filters of a project by projectId
+ * @param {string} projectId
+ */
+ getArchivedFiltersByProjectId = computedFn((projectId: string) => this.filters[projectId].archived);
/**
* @description initialize display filters and filters of a project
@@ -99,7 +123,10 @@ export class CycleFilterStore implements ICycleFilterStore {
active_tab: displayFilters?.active_tab || "active",
layout: displayFilters?.layout || "list",
};
- this.filters[projectId] = this.filters[projectId] ?? {};
+ this.filters[projectId] = this.filters[projectId] ?? {
+ default: {},
+ archived: {},
+ };
});
};
@@ -121,10 +148,10 @@ export class CycleFilterStore implements ICycleFilterStore {
* @param {string} projectId
* @param {TCycleFilters} filters
*/
- updateFilters = (projectId: string, filters: TCycleFilters) => {
+ updateFilters = (projectId: string, filters: TCycleFilters, state: keyof TCycleFiltersByState = "default") => {
runInAction(() => {
Object.keys(filters).forEach((key) => {
- set(this.filters, [projectId, key], filters[key as keyof TCycleFilters]);
+ set(this.filters, [projectId, state, key], filters[key as keyof TCycleFilters]);
});
});
};
@@ -135,13 +162,19 @@ export class CycleFilterStore implements ICycleFilterStore {
*/
updateSearchQuery = (query: string) => (this.searchQuery = query);
+ /**
+ * @description update archived search query
+ * @param {string} query
+ */
+ updateArchivedCyclesSearchQuery = (query: string) => (this.archivedCyclesSearchQuery = query);
+
/**
* @description clear all filters of a project
* @param {string} projectId
*/
- clearAllFilters = (projectId: string) => {
+ clearAllFilters = (projectId: string, state: keyof TCycleFiltersByState = "default") => {
runInAction(() => {
- this.filters[projectId] = {};
+ this.filters[projectId][state] = {};
});
};
}
diff --git a/web/store/module.store.ts b/web/store/module.store.ts
index 7f24a9b5756..1a2896aa25a 100644
--- a/web/store/module.store.ts
+++ b/web/store/module.store.ts
@@ -2,14 +2,16 @@ import set from "lodash/set";
import sortBy from "lodash/sortBy";
import { action, computed, observable, makeObservable, runInAction } from "mobx";
import { computedFn } from "mobx-utils";
+// types
+import { IModule, ILinkDetails } from "@plane/types";
+// helpers
+import { orderModules, shouldFilterModule } from "@/helpers/module.helper";
// services
import { ModuleService } from "@/services/module.service";
+import { ModuleArchiveService } from "@/services/module_archive.service";
import { ProjectService } from "@/services/project";
-// helpers
-import { orderModules, shouldFilterModule } from "@/helpers/module.helper";
-// types
+// store
import { RootStore } from "@/store/root.store";
-import { IModule, ILinkDetails } from "@plane/types";
export interface IModuleStore {
//Loaders
@@ -19,8 +21,10 @@ export interface IModuleStore {
moduleMap: Record;
// computed
projectModuleIds: string[] | null;
+ projectArchivedModuleIds: string[] | null;
// computed actions
getFilteredModuleIds: (projectId: string) => string[] | null;
+ getFilteredArchivedModuleIds: (projectId: string) => string[] | null;
getModuleById: (moduleId: string) => IModule | null;
getModuleNameById: (moduleId: string) => string;
getProjectModuleIds: (projectId: string) => string[] | null;
@@ -28,6 +32,7 @@ export interface IModuleStore {
// fetch
fetchWorkspaceModules: (workspaceSlug: string) => Promise;
fetchModules: (workspaceSlug: string, projectId: string) => Promise;
+ fetchArchivedModules: (workspaceSlug: string, projectId: string) => Promise;
fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => Promise;
// crud
createModule: (workspaceSlug: string, projectId: string, data: Partial) => Promise;
@@ -55,6 +60,9 @@ export interface IModuleStore {
// favorites
addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise;
removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => Promise;
+ // archive
+ archiveModule: (workspaceSlug: string, projectId: string, moduleId: string) => Promise;
+ restoreModule: (workspaceSlug: string, projectId: string, moduleId: string) => Promise;
}
export class ModulesStore implements IModuleStore {
@@ -68,6 +76,7 @@ export class ModulesStore implements IModuleStore {
// services
projectService;
moduleService;
+ moduleArchiveService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
@@ -77,9 +86,11 @@ export class ModulesStore implements IModuleStore {
fetchedMap: observable,
// computed
projectModuleIds: computed,
+ projectArchivedModuleIds: computed,
// actions
fetchWorkspaceModules: action,
fetchModules: action,
+ fetchArchivedModules: action,
fetchModuleDetails: action,
createModule: action,
updateModuleDetails: action,
@@ -89,6 +100,8 @@ export class ModulesStore implements IModuleStore {
deleteModuleLink: action,
addModuleToFavorites: action,
removeModuleFromFavorites: action,
+ archiveModule: action,
+ restoreModule: action,
});
this.rootStore = _rootStore;
@@ -96,6 +109,7 @@ export class ModulesStore implements IModuleStore {
// services
this.projectService = new ProjectService();
this.moduleService = new ModuleService();
+ this.moduleArchiveService = new ModuleArchiveService();
}
// computed
@@ -105,12 +119,24 @@ export class ModulesStore implements IModuleStore {
get projectModuleIds() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId || !this.fetchedMap[projectId]) return null;
- let projectModules = Object.values(this.moduleMap).filter((m) => m.project_id === projectId);
+ let projectModules = Object.values(this.moduleMap).filter((m) => m.project_id === projectId && !m?.archived_at);
projectModules = sortBy(projectModules, [(m) => m.sort_order]);
const projectModuleIds = projectModules.map((m) => m.id);
return projectModuleIds || null;
}
+ /**
+ * get all archived module ids for the current project
+ */
+ get projectArchivedModuleIds() {
+ const projectId = this.rootStore.app.router.projectId;
+ if (!projectId || !this.fetchedMap[projectId]) return null;
+ let archivedModules = Object.values(this.moduleMap).filter((m) => m.project_id === projectId && !!m?.archived_at);
+ archivedModules = sortBy(archivedModules, [(m) => m.sort_order]);
+ const projectModuleIds = archivedModules.map((m) => m.id);
+ return projectModuleIds || null;
+ }
+
/**
* @description returns filtered module ids based on display filters and filters
* @param {TModuleDisplayFilters} displayFilters
@@ -125,6 +151,29 @@ export class ModulesStore implements IModuleStore {
let modules = Object.values(this.moduleMap ?? {}).filter(
(m) =>
m.project_id === projectId &&
+ !m.archived_at &&
+ m.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
+ shouldFilterModule(m, displayFilters ?? {}, filters ?? {})
+ );
+ modules = orderModules(modules, displayFilters?.order_by);
+ const moduleIds = modules.map((m) => m.id);
+ return moduleIds;
+ });
+
+ /**
+ * @description returns filtered archived module ids based on display filters and filters
+ * @param {string} projectId
+ * @returns {string[] | null}
+ */
+ getFilteredArchivedModuleIds = computedFn((projectId: string) => {
+ const displayFilters = this.rootStore.moduleFilter.getDisplayFiltersByProjectId(projectId);
+ const filters = this.rootStore.moduleFilter.getArchivedFiltersByProjectId(projectId);
+ const searchQuery = this.rootStore.moduleFilter.archivedModulesSearchQuery;
+ if (!this.fetchedMap[projectId]) return null;
+ let modules = Object.values(this.moduleMap ?? {}).filter(
+ (m) =>
+ m.project_id === projectId &&
+ !!m.archived_at &&
m.name.toLowerCase().includes(searchQuery.toLowerCase()) &&
shouldFilterModule(m, displayFilters ?? {}, filters ?? {})
);
@@ -154,7 +203,7 @@ export class ModulesStore implements IModuleStore {
getProjectModuleIds = computedFn((projectId: string) => {
if (!this.fetchedMap[projectId]) return null;
- let projectModules = Object.values(this.moduleMap).filter((m) => m.project_id === projectId);
+ let projectModules = Object.values(this.moduleMap).filter((m) => m.project_id === projectId && !m.archived_at);
projectModules = sortBy(projectModules, [(m) => m.sort_order]);
const projectModuleIds = projectModules.map((m) => m.id);
return projectModuleIds;
@@ -200,6 +249,31 @@ export class ModulesStore implements IModuleStore {
}
};
+ /**
+ * @description fetch all archived modules
+ * @param workspaceSlug
+ * @param projectId
+ * @returns IModule[]
+ */
+ fetchArchivedModules = async (workspaceSlug: string, projectId: string) => {
+ this.loader = true;
+ return await this.moduleArchiveService
+ .getArchivedModules(workspaceSlug, projectId)
+ .then((response) => {
+ runInAction(() => {
+ response.forEach((module) => {
+ set(this.moduleMap, [module.id], { ...this.moduleMap[module.id], ...module });
+ });
+ this.loader = false;
+ });
+ return response;
+ })
+ .catch(() => {
+ this.loader = false;
+ return undefined;
+ });
+ };
+
/**
* @description fetch module details
* @param workspaceSlug
@@ -386,4 +460,48 @@ export class ModulesStore implements IModuleStore {
});
}
};
+
+ /**
+ * @description archives a module
+ * @param workspaceSlug
+ * @param projectId
+ * @param moduleId
+ * @returns
+ */
+ archiveModule = async (workspaceSlug: string, projectId: string, moduleId: string) => {
+ const moduleDetails = this.getModuleById(moduleId);
+ if (moduleDetails?.archived_at) return;
+ await this.moduleArchiveService
+ .archiveModule(workspaceSlug, projectId, moduleId)
+ .then((response) => {
+ runInAction(() => {
+ set(this.moduleMap, [moduleId, "archived_at"], response.archived_at);
+ });
+ })
+ .catch((error) => {
+ console.error("Failed to archive module in module store", error);
+ });
+ };
+
+ /**
+ * @description restores a module
+ * @param workspaceSlug
+ * @param projectId
+ * @param moduleId
+ * @returns
+ */
+ restoreModule = async (workspaceSlug: string, projectId: string, moduleId: string) => {
+ const moduleDetails = this.getModuleById(moduleId);
+ if (!moduleDetails?.archived_at) return;
+ await this.moduleArchiveService
+ .restoreModule(workspaceSlug, projectId, moduleId)
+ .then(() => {
+ runInAction(() => {
+ set(this.moduleMap, [moduleId, "archived_at"], null);
+ });
+ })
+ .catch((error) => {
+ console.error("Failed to restore module in module store", error);
+ });
+ };
}
diff --git a/web/store/module_filter.store.ts b/web/store/module_filter.store.ts
index ae5d0713511..5b46e17321c 100644
--- a/web/store/module_filter.store.ts
+++ b/web/store/module_filter.store.ts
@@ -1,33 +1,39 @@
+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 { TModuleDisplayFilters, TModuleFilters, TModuleFiltersByState } from "@plane/types";
+// store
import { RootStore } from "@/store/root.store";
-import { TModuleDisplayFilters, TModuleFilters } from "@plane/types";
export interface IModuleFilterStore {
// observables
displayFilters: Record;
- filters: Record;
+ filters: Record;
searchQuery: string;
+ archivedModulesSearchQuery: string;
// computed
currentProjectDisplayFilters: TModuleDisplayFilters | undefined;
currentProjectFilters: TModuleFilters | undefined;
+ currentProjectArchivedFilters: TModuleFilters | undefined;
// computed functions
getDisplayFiltersByProjectId: (projectId: string) => TModuleDisplayFilters | undefined;
getFiltersByProjectId: (projectId: string) => TModuleFilters | undefined;
+ getArchivedFiltersByProjectId: (projectId: string) => TModuleFilters | undefined;
// actions
updateDisplayFilters: (projectId: string, displayFilters: TModuleDisplayFilters) => void;
- updateFilters: (projectId: string, filters: TModuleFilters) => void;
+ updateFilters: (projectId: string, filters: TModuleFilters, state?: keyof TModuleFiltersByState) => void;
updateSearchQuery: (query: string) => void;
- clearAllFilters: (projectId: string) => void;
+ updateArchivedModulesSearchQuery: (query: string) => void;
+ clearAllFilters: (projectId: string, state?: keyof TModuleFiltersByState) => void;
}
export class ModuleFilterStore implements IModuleFilterStore {
// observables
displayFilters: Record = {};
- filters: Record = {};
+ filters: Record = {};
searchQuery: string = "";
+ archivedModulesSearchQuery: string = "";
// root store
rootStore: RootStore;
@@ -37,13 +43,16 @@ export class ModuleFilterStore implements IModuleFilterStore {
displayFilters: observable,
filters: observable,
searchQuery: observable.ref,
+ archivedModulesSearchQuery: observable.ref,
// computed
currentProjectDisplayFilters: computed,
currentProjectFilters: computed,
+ currentProjectArchivedFilters: computed,
// actions
updateDisplayFilters: action,
updateFilters: action,
updateSearchQuery: action,
+ updateArchivedModulesSearchQuery: action,
clearAllFilters: action,
});
// root store
@@ -73,7 +82,16 @@ export class ModuleFilterStore implements IModuleFilterStore {
get currentProjectFilters() {
const projectId = this.rootStore.app.router.projectId;
if (!projectId) return;
- return this.filters[projectId];
+ return this.filters[projectId]?.default ?? {};
+ }
+
+ /**
+ * @description get archived filters of the current project
+ */
+ get currentProjectArchivedFilters() {
+ const projectId = this.rootStore.app.router.projectId;
+ if (!projectId) return;
+ return this.filters[projectId].archived;
}
/**
@@ -86,7 +104,13 @@ export class ModuleFilterStore implements IModuleFilterStore {
* @description get filters of a project by projectId
* @param {string} projectId
*/
- getFiltersByProjectId = computedFn((projectId: string) => this.filters[projectId]);
+ getFiltersByProjectId = computedFn((projectId: string) => this.filters[projectId]?.default ?? {});
+
+ /**
+ * @description get archived filters of a project by projectId
+ * @param {string} projectId
+ */
+ getArchivedFiltersByProjectId = computedFn((projectId: string) => this.filters[projectId].archived);
/**
* @description initialize display filters and filters of a project
@@ -100,7 +124,10 @@ export class ModuleFilterStore implements IModuleFilterStore {
layout: displayFilters?.layout || "list",
order_by: displayFilters?.order_by || "name",
};
- this.filters[projectId] = this.filters[projectId] ?? {};
+ this.filters[projectId] = this.filters[projectId] ?? {
+ default: {},
+ archived: {},
+ };
});
};
@@ -122,10 +149,10 @@ export class ModuleFilterStore implements IModuleFilterStore {
* @param {string} projectId
* @param {TModuleFilters} filters
*/
- updateFilters = (projectId: string, filters: TModuleFilters) => {
+ updateFilters = (projectId: string, filters: TModuleFilters, state: keyof TModuleFiltersByState = "default") => {
runInAction(() => {
Object.keys(filters).forEach((key) => {
- set(this.filters, [projectId, key], filters[key as keyof TModuleFilters]);
+ set(this.filters, [projectId, state, key], filters[key as keyof TModuleFilters]);
});
});
};
@@ -136,13 +163,19 @@ export class ModuleFilterStore implements IModuleFilterStore {
*/
updateSearchQuery = (query: string) => (this.searchQuery = query);
+ /**
+ * @description update archived search query
+ * @param {string} query
+ */
+ updateArchivedModulesSearchQuery = (query: string) => (this.archivedModulesSearchQuery = query);
+
/**
* @description clear all filters of a project
* @param {string} projectId
*/
- clearAllFilters = (projectId: string) => {
+ clearAllFilters = (projectId: string, state: keyof TModuleFiltersByState = "default") => {
runInAction(() => {
- this.filters[projectId] = {};
+ this.filters[projectId][state] = {};
});
};
}