[WEB-447] feat: projects archive. (#4014)
* dev: project archive response * feat: projects archive. * dev: response changes for cycle and module * chore: status message changed * chore: update clear all applied display filters logic. * style: archived project card UI update. * chore: archive/ restore taost message update. * fix: clear all applied display filter logic. * chore: project empty state update to handle archived projects. * chore: minor typo fix in cycles and modules archive. * chore: close cycle/ module overview sidebar if it's already open when clicked on overview button. * chore: optimize current workspace applied display filter logic. * chore: update all `archived_at` type from `Date` to `string`. --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
9642b761b7
commit
231fd52992
31 changed files with 749 additions and 162 deletions
|
|
@ -31,7 +31,7 @@ export const ArchiveCycleModal: React.FC<Props> = (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> = (props) => {
|
|||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" tabIndex={1} onClick={handleArchiveIssue} loading={isArchiving}>
|
||||
<Button size="sm" tabIndex={1} onClick={handleArchiveCycle} loading={isArchiving}>
|
||||
{isArchiving ? "Archiving" : "Archive"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = 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<HTMLButtonElement>) => {
|
||||
|
|
@ -134,10 +134,18 @@ export const CyclesBoardCard: FC<ICyclesBoardCard> = 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;
|
||||
|
|
|
|||
|
|
@ -106,10 +106,18 @@ export const CyclesListItem: FC<TCyclesListItem> = 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);
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ export const ArchiveModuleModal: React.FC<Props> = (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> = (props) => {
|
|||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" tabIndex={1} onClick={handleArchiveIssue} loading={isArchiving}>
|
||||
<Button size="sm" tabIndex={1} onClick={handleArchiveModule} loading={isArchiving}>
|
||||
{isArchiving ? "Archiving" : "Archive"}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -101,10 +101,18 @@ export const ModuleCardItem: React.FC<Props> = 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;
|
||||
|
|
|
|||
|
|
@ -102,10 +102,18 @@ export const ModuleListItem: React.FC<Props> = 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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * from "./access";
|
||||
export * from "./date";
|
||||
export * from "./members";
|
||||
export * from "./project-display-filters";
|
||||
export * from "./root";
|
||||
|
|
|
|||
|
|
@ -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<Props> = observer((props) => {
|
||||
const { handleRemove, values, editable } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
{values.map((key) => {
|
||||
const filterLabel = PROJECT_DISPLAY_FILTER_OPTIONS.find((s) => s.key === key)?.label;
|
||||
return (
|
||||
<div key={key} className="flex items-center gap-1 rounded p-1 text-xs bg-custom-background-80">
|
||||
{filterLabel}
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center text-custom-text-300 hover:text-custom-text-200"
|
||||
onClick={() => handleRemove(key)}
|
||||
>
|
||||
<X size={10} strokeWidth={2} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
@ -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> = (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 (
|
||||
<div className="flex items-start justify-between gap-1.5">
|
||||
<div className="flex flex-wrap items-stretch gap-2 bg-custom-background-100">
|
||||
{/* Applied filters */}
|
||||
{Object.entries(appliedFilters).map(([key, value]) => {
|
||||
const filterKey = key as keyof TProjectFilters;
|
||||
|
||||
|
|
@ -85,6 +95,22 @@ export const ProjectAppliedFiltersList: React.FC<Props> = (props) => {
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
{/* Applied display filters */}
|
||||
{appliedDisplayFilters.length > 0 && (
|
||||
<div
|
||||
key="project_display_filters"
|
||||
className="flex flex-wrap items-center gap-2 rounded-md border border-custom-border-200 px-2 py-1 capitalize"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-xs text-custom-text-300">Projects</span>
|
||||
<AppliedProjectDisplayFilters
|
||||
editable={isEditingAllowed}
|
||||
values={appliedDisplayFilters}
|
||||
handleRemove={(key) => handleRemoveDisplayFilter(key)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ export const ProjectCardList = observer(() => {
|
|||
const { commandPalette: commandPaletteStore } = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { workspaceProjectIds, filteredProjectIds, getProjectById } = useProject();
|
||||
const { searchQuery } = useProjectFilter();
|
||||
const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter();
|
||||
|
||||
if (workspaceProjectIds?.length === 0)
|
||||
if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects)
|
||||
return (
|
||||
<EmptyState
|
||||
type={EmptyStateType.WORKSPACE_PROJECTS}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,13 @@ import React, { useState } from "react";
|
|||
import { observer } from "mobx-react-lite";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Check, LinkIcon, Lock, Pencil, Star } from "lucide-react";
|
||||
import { ArchiveRestoreIcon, Check, LinkIcon, Lock, Pencil, Star, Trash2 } from "lucide-react";
|
||||
import { cn } from "@plane/editor-core";
|
||||
import type { IProject } from "@plane/types";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, Button, Tooltip, TOAST_TYPE, setToast, setPromiseToast } from "@plane/ui";
|
||||
// components
|
||||
import { DeleteProjectModal, JoinProjectModal, ProjectLogo } from "@/components/project";
|
||||
import { ArchiveRestoreProjectModal, DeleteProjectModal, JoinProjectModal, ProjectLogo } from "@/components/project";
|
||||
// helpers
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
|
|
@ -28,6 +29,7 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
|
|||
// states
|
||||
const [deleteProjectModalOpen, setDeleteProjectModal] = useState(false);
|
||||
const [joinProjectModalOpen, setJoinProjectModal] = useState(false);
|
||||
const [restoreProject, setRestoreProject] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
|
@ -41,6 +43,8 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
|
|||
// auth
|
||||
const isOwner = project.member_role === EUserProjectRoles.ADMIN;
|
||||
const isMember = project.member_role === EUserProjectRoles.MEMBER;
|
||||
// archive
|
||||
const isArchived = !!project.archived_at;
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!workspaceSlug) return;
|
||||
|
|
@ -102,13 +106,23 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
|
|||
handleClose={() => setJoinProjectModal(false)}
|
||||
/>
|
||||
)}
|
||||
{/* Restore project modal */}
|
||||
{workspaceSlug && project && (
|
||||
<ArchiveRestoreProjectModal
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={project.id}
|
||||
isOpen={restoreProject}
|
||||
onClose={() => setRestoreProject(false)}
|
||||
archive={false}
|
||||
/>
|
||||
)}
|
||||
<Link
|
||||
href={`/${workspaceSlug}/projects/${project.id}/issues`}
|
||||
onClick={(e) => {
|
||||
if (!project.is_member) {
|
||||
if (!project.is_member || isArchived) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setJoinProjectModal(true);
|
||||
if (!isArchived) setJoinProjectModal(true);
|
||||
}
|
||||
}}
|
||||
className="flex flex-col rounded border border-custom-border-200 bg-custom-background-100"
|
||||
|
|
@ -140,96 +154,137 @@ export const ProjectCard: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full flex-shrink-0 items-center gap-2">
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded bg-white/10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleCopyText();
|
||||
}}
|
||||
>
|
||||
<LinkIcon className="h-3 w-3 text-white" />
|
||||
</button>
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded bg-white/10"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (project.is_favorite) handleRemoveFromFavorites();
|
||||
else handleAddToFavorites();
|
||||
}}
|
||||
>
|
||||
<Star
|
||||
className={`h-3 w-3 ${project.is_favorite ? "fill-amber-400 text-transparent" : "text-white"} `}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{!isArchived && (
|
||||
<div className="flex h-full flex-shrink-0 items-center gap-2">
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded bg-white/10"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleCopyText();
|
||||
}}
|
||||
>
|
||||
<LinkIcon className="h-3 w-3 text-white" />
|
||||
</button>
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded bg-white/10"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (project.is_favorite) handleRemoveFromFavorites();
|
||||
else handleAddToFavorites();
|
||||
}}
|
||||
>
|
||||
<Star
|
||||
className={`h-3 w-3 ${project.is_favorite ? "fill-amber-400 text-transparent" : "text-white"} `}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-[104px] w-full flex-col justify-between rounded-b p-4">
|
||||
<div
|
||||
className={cn("flex h-[104px] w-full flex-col justify-between rounded-b p-4", {
|
||||
"opacity-90": isArchived,
|
||||
})}
|
||||
>
|
||||
<p className="line-clamp-2 break-words text-sm text-custom-text-300">
|
||||
{project.description && project.description.trim() !== ""
|
||||
? project.description
|
||||
: `Created on ${renderFormattedDate(project.created_at)}`}
|
||||
</p>
|
||||
<div className="item-center flex justify-between">
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipHeading="Members"
|
||||
tooltipContent={
|
||||
project.members && project.members.length > 0 ? `${project.members.length} Members` : "No Member"
|
||||
}
|
||||
position="top"
|
||||
>
|
||||
{projectMembersIds && projectMembersIds.length > 0 ? (
|
||||
<div className="flex cursor-pointer items-center gap-2 text-custom-text-200">
|
||||
<AvatarGroup showTooltip={false}>
|
||||
{projectMembersIds.map((memberId) => {
|
||||
const member = project.members?.find((m) => m.member_id === memberId);
|
||||
|
||||
if (!member) return null;
|
||||
|
||||
return <Avatar key={member.id} name={member.member__display_name} src={member.member__avatar} />;
|
||||
})}
|
||||
</AvatarGroup>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Tooltip
|
||||
isMobile={isMobile}
|
||||
tooltipHeading="Members"
|
||||
tooltipContent={
|
||||
project.members && project.members.length > 0 ? `${project.members.length} Members` : "No Member"
|
||||
}
|
||||
position="top"
|
||||
>
|
||||
{projectMembersIds && projectMembersIds.length > 0 ? (
|
||||
<div className="flex cursor-pointer items-center gap-2 text-custom-text-200">
|
||||
<AvatarGroup showTooltip={false}>
|
||||
{projectMembersIds.map((memberId) => {
|
||||
const member = project.members?.find((m) => m.member_id === memberId);
|
||||
if (!member) return null;
|
||||
return (
|
||||
<Avatar key={member.id} name={member.member__display_name} src={member.member__avatar} />
|
||||
);
|
||||
})}
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm italic text-custom-text-400">No Member Yet</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
{isArchived && <div className="text-xs text-custom-text-400 font-medium">Archived</div>}
|
||||
</div>
|
||||
{isArchived ? (
|
||||
isOwner && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div
|
||||
className="flex items-center justify-center text-xs text-custom-text-400 font-medium hover:text-custom-text-200"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setRestoreProject(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<ArchiveRestoreIcon className="h-3.5 w-3.5" />
|
||||
Restore
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="flex items-center justify-center text-xs text-custom-text-400 font-medium hover:text-custom-text-200"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDeleteProjectModal(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-sm italic text-custom-text-400">No Member Yet</span>
|
||||
)}
|
||||
</Tooltip>
|
||||
{project.is_member &&
|
||||
(isOwner || isMember ? (
|
||||
<Link
|
||||
className="flex items-center justify-center rounded p-1 text-custom-text-400 hover:bg-custom-background-80 hover:text-custom-text-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
href={`/${workspaceSlug}/projects/${project.id}/settings`}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-custom-text-400 text-sm">
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
Joined
|
||||
</span>
|
||||
))}
|
||||
{!project.is_member && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="link-primary"
|
||||
className="!p-0 font-semibold"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setJoinProjectModal(true);
|
||||
}}
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{project.is_member &&
|
||||
(isOwner || isMember ? (
|
||||
<Link
|
||||
className="flex items-center justify-center rounded p-1 text-custom-text-400 hover:bg-custom-background-80 hover:text-custom-text-200"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
href={`/${workspaceSlug}/projects/${project.id}/settings`}
|
||||
>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Link>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-custom-text-400 text-sm">
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
Joined
|
||||
</span>
|
||||
))}
|
||||
{!project.is_member && (
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="link-primary"
|
||||
className="!p-0 font-semibold"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setJoinProjectModal(true);
|
||||
}}
|
||||
>
|
||||
Join
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -51,6 +51,15 @@ export const ProjectFiltersSelection: React.FC<Props> = observer((props) => {
|
|||
}
|
||||
title="My projects"
|
||||
/>
|
||||
<FilterOption
|
||||
isChecked={!!displayFilters.archived_projects}
|
||||
onClick={() =>
|
||||
handleDisplayFiltersUpdate({
|
||||
archived_projects: !displayFilters.archived_projects,
|
||||
})
|
||||
}
|
||||
title="Archived"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* access */}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
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> = (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);
|
||||
if (!projectDetails) return null;
|
||||
|
||||
const handleClose = () => {
|
||||
setIsLoading(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleArchiveProject = async () => {
|
||||
setIsLoading(true);
|
||||
await archiveProject(workspaceSlug, projectId)
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Archive success",
|
||||
message: `${projectDetails.name} has been archived successfully`,
|
||||
});
|
||||
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: `You can find ${projectDetails.name} in your 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));
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-full sm:max-w-lg">
|
||||
<div className="px-5 py-4">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">
|
||||
{archive ? "Archive" : "Restore"} {projectDetails.name}
|
||||
</h3>
|
||||
<p className="mt-3 text-sm text-custom-text-200">
|
||||
{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?"}
|
||||
</p>
|
||||
<div className="mt-3 flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
tabIndex={1}
|
||||
onClick={archive ? handleArchiveProject : handleRestoreProject}
|
||||
loading={isLoading}
|
||||
>
|
||||
{archive ? (isLoading ? "Archiving" : "Archive") : isLoading ? "Restoring" : "Restore"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./selection";
|
||||
export * from "./archive-restore-modal";
|
||||
|
|
@ -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<IArchiveProject> = (props) => {
|
||||
const { projectDetails, handleArchive } = props;
|
||||
|
||||
return (
|
||||
<Disclosure as="div" className="border-t border-custom-border-100 py-4">
|
||||
{({ open }) => (
|
||||
<div className="w-full">
|
||||
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between">
|
||||
<span className="text-xl tracking-tight">Archive project</span>
|
||||
{open ? <ChevronUp className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
|
||||
</Disclosure.Button>
|
||||
<Transition
|
||||
show={open}
|
||||
enter="transition duration-100 ease-out"
|
||||
enterFrom="transform opacity-0"
|
||||
enterTo="transform opacity-100"
|
||||
leave="transition duration-75 ease-out"
|
||||
leaveFrom="transform opacity-100"
|
||||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-8 pt-4">
|
||||
<span className="text-sm tracking-tight">
|
||||
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.
|
||||
</span>
|
||||
<div>
|
||||
{projectDetails ? (
|
||||
<div>
|
||||
<Button variant="outline-danger" onClick={handleArchive}>
|
||||
Archive project
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="mt-2 w-full">
|
||||
<Loader.Item height="38px" width="144px" />
|
||||
</Loader>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
|
|
@ -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";
|
||||
import { IProject } from "@plane/types";
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
// icons
|
||||
// types
|
||||
import { IProject } from "@plane/types";
|
||||
// ui
|
||||
import { Button, Loader } from "@plane/ui";
|
||||
|
||||
export interface IDeleteProjectSection {
|
||||
projectDetails: IProject;
|
||||
|
|
@ -17,12 +15,12 @@ export const DeleteProjectSection: React.FC<IDeleteProjectSection> = (props) =>
|
|||
const { projectDetails, handleDelete } = props;
|
||||
|
||||
return (
|
||||
<Disclosure as="div" className="border-t border-custom-border-100">
|
||||
<Disclosure as="div" className="border-t border-custom-border-100 py-4">
|
||||
{({ open }) => (
|
||||
<div className="w-full">
|
||||
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between py-4">
|
||||
<Disclosure.Button as="button" type="button" className="flex w-full items-center justify-between">
|
||||
<span className="text-xl tracking-tight">Delete Project</span>
|
||||
{open ? <ChevronUp className="h-5 w-5" /> : <ChevronDown className="h-5 w-5" />}
|
||||
{open ? <ChevronUp className="h-5 w-5" /> : <ChevronRight className="h-5 w-5" />}
|
||||
</Disclosure.Button>
|
||||
|
||||
<Transition
|
||||
|
|
@ -35,7 +33,7 @@ export const DeleteProjectSection: React.FC<IDeleteProjectSection> = (props) =>
|
|||
leaveTo="transform opacity-0"
|
||||
>
|
||||
<Disclosure.Panel>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-8 pt-4">
|
||||
<span className="text-sm tracking-tight">
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./delete-project-section";
|
||||
export * from "./features-list";
|
||||
export * from "./archive-project";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue