chore: bulk delete and archive confirmation modals

This commit is contained in:
Aaryan Khandelwal 2024-05-07 14:28:20 +05:30
parent 5c328ff0b2
commit 51c0794a65
20 changed files with 277 additions and 42 deletions

View File

@ -1,4 +1,4 @@
import { AlertTriangle, LucideIcon } from "lucide-react"; import { AlertTriangle, Info, LucideIcon } from "lucide-react";
// ui // ui
import { Button, TButtonVariant } from "@plane/ui"; import { Button, TButtonVariant } from "@plane/ui";
// components // components
@ -6,14 +6,14 @@ import { EModalPosition, EModalWidth, ModalCore } from "@/components/core";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
export type TModalVariant = "danger"; export type TModalVariant = "danger" | "primary";
type Props = { type Props = {
content: React.ReactNode | string; content: React.ReactNode | string;
handleClose: () => void; handleClose: () => void;
handleSubmit: () => Promise<void>; handleSubmit: () => Promise<void>;
hideIcon?: boolean; hideIcon?: boolean;
isDeleting: boolean; isSubmitting: boolean;
isOpen: boolean; isOpen: boolean;
position?: EModalPosition; position?: EModalPosition;
primaryButtonText?: { primaryButtonText?: {
@ -28,14 +28,17 @@ type Props = {
const VARIANT_ICONS: Record<TModalVariant, LucideIcon> = { const VARIANT_ICONS: Record<TModalVariant, LucideIcon> = {
danger: AlertTriangle, danger: AlertTriangle,
primary: Info,
}; };
const BUTTON_VARIANTS: Record<TModalVariant, TButtonVariant> = { const BUTTON_VARIANTS: Record<TModalVariant, TButtonVariant> = {
danger: "danger", danger: "danger",
primary: "primary",
}; };
const VARIANT_CLASSES: Record<TModalVariant, string> = { const VARIANT_CLASSES: Record<TModalVariant, string> = {
danger: "bg-red-500/20 text-red-500", danger: "bg-red-500/20 text-red-500",
primary: "bg-custom-primary-100/20 text-custom-primary-100",
}; };
export const AlertModalCore: React.FC<Props> = (props) => { export const AlertModalCore: React.FC<Props> = (props) => {
@ -44,7 +47,7 @@ export const AlertModalCore: React.FC<Props> = (props) => {
handleClose, handleClose,
handleSubmit, handleSubmit,
hideIcon = false, hideIcon = false,
isDeleting, isSubmitting,
isOpen, isOpen,
position = EModalPosition.CENTER, position = EModalPosition.CENTER,
primaryButtonText = { primaryButtonText = {
@ -81,8 +84,8 @@ export const AlertModalCore: React.FC<Props> = (props) => {
<Button variant="neutral-primary" size="sm" onClick={handleClose}> <Button variant="neutral-primary" size="sm" onClick={handleClose}>
{secondaryButtonText} {secondaryButtonText}
</Button> </Button>
<Button variant={BUTTON_VARIANTS[variant]} size="sm" tabIndex={1} onClick={handleSubmit} loading={isDeleting}> <Button variant={BUTTON_VARIANTS[variant]} size="sm" tabIndex={1} onClick={handleSubmit} loading={isSubmitting}>
{isDeleting ? primaryButtonText.loading : primaryButtonText.default} {isSubmitting ? primaryButtonText.loading : primaryButtonText.default}
</Button> </Button>
</div> </div>
</ModalCore> </ModalCore>

View File

@ -73,7 +73,7 @@ export const CycleDeleteModal: React.FC<ICycleDelete> = observer((props) => {
<AlertModalCore <AlertModalCore
handleClose={handleClose} handleClose={handleClose}
handleSubmit={formSubmit} handleSubmit={formSubmit}
isDeleting={loader} isSubmitting={loader}
isOpen={isOpen} isOpen={isOpen}
title="Delete Cycle" title="Delete Cycle"
content={ content={

View File

@ -64,7 +64,7 @@ export const DeleteEstimateModal: React.FC<Props> = observer((props) => {
<AlertModalCore <AlertModalCore
handleClose={onClose} handleClose={onClose}
handleSubmit={handleEstimateDelete} handleSubmit={handleEstimateDelete}
isDeleting={isDeleteLoading} isSubmitting={isDeleteLoading}
isOpen={isOpen} isOpen={isOpen}
title="Delete Estimate" title="Delete Estimate"
content={ content={

View File

@ -36,7 +36,7 @@ export const DeclineIssueModal: React.FC<Props> = (props) => {
<AlertModalCore <AlertModalCore
handleClose={handleClose} handleClose={handleClose}
handleSubmit={handleDecline} handleSubmit={handleDecline}
isDeleting={isDeclining} isSubmitting={isDeclining}
isOpen={isOpen} isOpen={isOpen}
title="Decline Issue" title="Decline Issue"
content={ content={

View File

@ -36,7 +36,7 @@ export const DeleteInboxIssueModal: React.FC<Props> = observer(({ isOpen, onClos
<AlertModalCore <AlertModalCore
handleClose={handleClose} handleClose={handleClose}
handleSubmit={handleDelete} handleSubmit={handleDelete}
isDeleting={isDeleting} isSubmitting={isDeleting}
isOpen={isOpen} isOpen={isOpen}
title="Delete Issue" title="Delete Issue"
content={ content={

View File

@ -35,7 +35,7 @@ export const IssueAttachmentDeleteModal: FC<Props> = (props) => {
<AlertModalCore <AlertModalCore
handleClose={handleClose} handleClose={handleClose}
handleSubmit={() => handleDeletion(data.id)} handleSubmit={() => handleDeletion(data.id)}
isDeleting={loader} isSubmitting={loader}
isOpen={isOpen} isOpen={isOpen}
title="Delete attachment" title="Delete attachment"
content={ content={

View File

@ -60,7 +60,7 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
<AlertModalCore <AlertModalCore
handleClose={onClose} handleClose={onClose}
handleSubmit={handleIssueDelete} handleSubmit={handleIssueDelete}
isDeleting={isDeleting} isSubmitting={isDeleting}
isOpen={isOpen} isOpen={isOpen}
title="Delete Issue" title="Delete Issue"
content={ content={

View File

@ -0,0 +1,79 @@
import { useState } from "react";
import { observer } from "mobx-react";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore, EModalPosition, EModalWidth } from "@/components/core";
// constants
import { EIssuesStoreType } from "@/constants/issue";
// hooks
import { useBulkIssueOperations, useIssues } from "@/hooks/store";
type Props = {
handleClose: () => void;
isOpen: boolean;
issueIds: string[];
projectId: string;
workspaceSlug: string;
};
export const BulkArchiveConfirmationModal: React.FC<Props> = observer((props) => {
const { handleClose, isOpen, issueIds, projectId, workspaceSlug } = props;
// states
const [isArchiving, setIsDeleting] = useState(false);
// store hooks
const {
issues: { archiveBulkIssues },
} = useIssues(EIssuesStoreType.PROJECT);
const { clearSelection } = useBulkIssueOperations();
const handleSubmit = async () => {
setIsDeleting(true);
await archiveBulkIssues(workspaceSlug, projectId, issueIds)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Issues archived successfully.",
});
clearSelection();
handleClose();
})
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Something went wrong. Please try again.",
})
)
.finally(() => setIsDeleting(false));
};
const issueVariant = issueIds.length > 1 ? "issues" : "issue";
return (
<AlertModalCore
handleClose={handleClose}
handleSubmit={handleSubmit}
isSubmitting={isArchiving}
isOpen={isOpen}
variant="primary"
position={EModalPosition.CENTER}
width={EModalWidth.XL}
title={`Archive ${issueVariant}`}
content={
<>
Are you sure you want to archive {issueIds.length} {issueVariant}? Sub issues of selected {issueVariant} will
also be archived. Once archived {issueIds.length > 1 ? "they" : "it"} can be restored later via the archives
section.
</>
}
primaryButtonText={{
loading: "Archiving",
default: `Archive ${issueVariant}`,
}}
hideIcon
/>
);
});

View File

@ -0,0 +1,78 @@
import { useState } from "react";
import { observer } from "mobx-react";
// ui
import { TOAST_TYPE, setToast } from "@plane/ui";
// components
import { AlertModalCore, EModalPosition, EModalWidth } from "@/components/core";
// constants
import { EIssuesStoreType } from "@/constants/issue";
// hooks
import { useBulkIssueOperations, useIssues } from "@/hooks/store";
type Props = {
handleClose: () => void;
isOpen: boolean;
issueIds: string[];
projectId: string;
workspaceSlug: string;
};
export const BulkDeleteConfirmationModal: React.FC<Props> = observer((props) => {
const { handleClose, isOpen, issueIds, projectId, workspaceSlug } = props;
// states
const [isDeleting, setIsDeleting] = useState(false);
// store hooks
const {
issues: { removeBulkIssues },
} = useIssues(EIssuesStoreType.PROJECT);
const { clearSelection } = useBulkIssueOperations();
const handleSubmit = async () => {
setIsDeleting(true);
await removeBulkIssues(workspaceSlug, projectId, issueIds)
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Issues deleted successfully.",
});
clearSelection();
handleClose();
})
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Something went wrong. Please try again.",
})
)
.finally(() => setIsDeleting(false));
};
const issueVariant = issueIds.length > 1 ? "issues" : "issue";
return (
<AlertModalCore
handleClose={handleClose}
handleSubmit={handleSubmit}
isSubmitting={isDeleting}
isOpen={isOpen}
variant="danger"
position={EModalPosition.CENTER}
width={EModalWidth.XL}
title={`Delete ${issueVariant}`}
content={
<>
Are you sure you want to delete {issueIds.length} {issueVariant}? Sub issues of selected {issueVariant} will
also be deleted. All of the data related to the {issueVariant} will be permanently removed. This action cannot
be undone.
</>
}
primaryButtonText={{
loading: "Deleting",
default: `Delete ${issueVariant}`,
}}
/>
);
});

View File

@ -1,2 +1,4 @@
export * from "./bulk-archive-modal";
export * from "./bulk-delete-modal";
export * from "./properties"; export * from "./properties";
export * from "./root"; export * from "./root";

View File

@ -1,33 +1,86 @@
import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { Trash2 } from "lucide-react"; import { Trash2 } from "lucide-react";
// ui // ui
import { ArchiveIcon, Tooltip } from "@plane/ui"; import { ArchiveIcon, Tooltip } from "@plane/ui";
// components // components
import { IssueBulkOperationsProperties } from "@/components/issues"; import {
BulkArchiveConfirmationModal,
BulkDeleteConfirmationModal,
IssueBulkOperationsProperties,
} from "@/components/issues";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useBulkIssueOperations } from "@/hooks/store";
export const IssueBulkOperationsRoot: React.FC<any> = observer((props) => { export const IssueBulkOperationsRoot = observer(() => {
const {} = props; // states
const [isBulkArchiveModalOpen, setIsBulkArchiveModalOpen] = useState(false);
const [isBulkDeleteModalOpen, setIsBulkDeleteModalOpen] = useState(false);
// router
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
// store hooks
const { issueIds } = useBulkIssueOperations();
return ( return (
<div className="h-full w-full bg-custom-background-100 border-t border-custom-border-200 py-4 px-3.5 flex items-center divide-x-[0.5px] divide-custom-border-200 text-custom-text-300"> <>
<div className="h-7 pr-3 text-sm flex items-center">2 selected</div> {workspaceSlug && projectId && (
<div className="h-7 px-3 flex items-center"> <BulkArchiveConfirmationModal
<Tooltip tooltipContent="Archive"> isOpen={isBulkArchiveModalOpen}
<button type="button" className="outline-none grid place-items-center"> handleClose={() => setIsBulkArchiveModalOpen(false)}
<ArchiveIcon className="size-4" /> issueIds={issueIds}
</button> projectId={projectId.toString()}
</Tooltip> workspaceSlug={workspaceSlug.toString()}
/>
)}
{workspaceSlug && projectId && (
<BulkDeleteConfirmationModal
isOpen={isBulkDeleteModalOpen}
handleClose={() => setIsBulkDeleteModalOpen(false)}
issueIds={issueIds}
projectId={projectId.toString()}
workspaceSlug={workspaceSlug.toString()}
/>
)}
<div className="h-full w-full bg-custom-background-100 border-t border-custom-border-200 py-4 px-3.5 flex items-center divide-x-[0.5px] divide-custom-border-200 text-custom-text-300">
<div className="h-7 pr-3 text-sm flex items-center">2 selected</div>
<div className="h-7 px-3 flex items-center">
<Tooltip tooltipContent="Archive">
<button
type="button"
className={cn("outline-none grid place-items-center", {
"cursor-not-allowed text-custom-text-400": issueIds.length === 0,
})}
onClick={() => {
if (issueIds.length > 0) setIsBulkArchiveModalOpen(true);
}}
>
<ArchiveIcon className="size-4" />
</button>
</Tooltip>
</div>
<div className="h-7 px-3 flex items-center">
<Tooltip tooltipContent="Delete">
<button
type="button"
className={cn("outline-none grid place-items-center", {
"cursor-not-allowed text-custom-text-400": issueIds.length === 0,
})}
onClick={() => {
if (issueIds.length > 0) setIsBulkDeleteModalOpen(true);
}}
>
<Trash2 className="size-4" />
</button>
</Tooltip>
</div>
<div className="h-7 pl-3 flex items-center gap-3">
<IssueBulkOperationsProperties />
</div>
</div> </div>
<div className="h-7 px-3 flex items-center"> </>
<Tooltip tooltipContent="Delete">
<button type="button" className="outline-none grid place-items-center">
<Trash2 className="size-4" />
</button>
</Tooltip>
</div>
<div className="h-7 pl-3 flex items-center gap-3">
<IssueBulkOperationsProperties />
</div>
</div>
); );
}); });

View File

@ -56,7 +56,7 @@ export const DeleteLabelModal: React.FC<Props> = observer((props) => {
<AlertModalCore <AlertModalCore
handleClose={handleClose} handleClose={handleClose}
handleSubmit={handleDeletion} handleSubmit={handleDeletion}
isDeleting={isDeleteLoading} isSubmitting={isDeleteLoading}
isOpen={isOpen} isOpen={isOpen}
title="Delete Label" title="Delete Label"
content={ content={

View File

@ -73,7 +73,7 @@ export const DeleteModuleModal: React.FC<Props> = observer((props) => {
<AlertModalCore <AlertModalCore
handleClose={handleClose} handleClose={handleClose}
handleSubmit={handleDeletion} handleSubmit={handleDeletion}
isDeleting={isDeleteLoading} isSubmitting={isDeleteLoading}
isOpen={isOpen} isOpen={isOpen}
title="Delete Module" title="Delete Module"
content={ content={

View File

@ -74,7 +74,7 @@ export const DeletePageModal: React.FC<TConfirmPageDeletionProps> = observer((pr
<AlertModalCore <AlertModalCore
handleClose={handleClose} handleClose={handleClose}
handleSubmit={handleDelete} handleSubmit={handleDelete}
isDeleting={isDeleting} isSubmitting={isDeleting}
isOpen={isOpen} isOpen={isOpen}
title="Delete Page" title="Delete Page"
content={ content={

View File

@ -81,7 +81,7 @@ export const DeleteStateModal: React.FC<Props> = observer((props) => {
<AlertModalCore <AlertModalCore
handleClose={handleClose} handleClose={handleClose}
handleSubmit={handleDeletion} handleSubmit={handleDeletion}
isDeleting={isDeleteLoading} isSubmitting={isDeleteLoading}
isOpen={isOpen} isOpen={isOpen}
title="Delete State" title="Delete State"
content={ content={

View File

@ -62,7 +62,7 @@ export const DeleteProjectViewModal: React.FC<Props> = observer((props) => {
<AlertModalCore <AlertModalCore
handleClose={handleClose} handleClose={handleClose}
handleSubmit={handleDeleteView} handleSubmit={handleDeleteView}
isDeleting={isDeleteLoading} isSubmitting={isDeleteLoading}
isOpen={isOpen} isOpen={isOpen}
title="Delete View" title="Delete View"
content={ content={

View File

@ -55,7 +55,7 @@ export const DeleteWebhookModal: FC<IDeleteWebhook> = (props) => {
<AlertModalCore <AlertModalCore
handleClose={handleClose} handleClose={handleClose}
handleSubmit={handleDelete} handleSubmit={handleDelete}
isDeleting={isDeleting} isSubmitting={isDeleting}
isOpen={isOpen} isOpen={isOpen}
title="Delete webhook" title="Delete webhook"
content={ content={

View File

@ -67,7 +67,7 @@ export const DeleteGlobalViewModal: React.FC<Props> = observer((props) => {
<AlertModalCore <AlertModalCore
handleClose={handleClose} handleClose={handleClose}
handleSubmit={handleDeletion} handleSubmit={handleDeletion}
isDeleting={isDeleteLoading} isSubmitting={isDeleteLoading}
isOpen={isOpen} isOpen={isOpen}
title="Delete View" title="Delete View"
content={ content={

View File

@ -266,7 +266,9 @@ export class IssueService extends APIService {
data: { data: {
issue_ids: string[]; issue_ids: string[];
} }
): Promise<any> { ): Promise<{
archived_at: string;
}> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-archive-issues/`, data) return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/bulk-archive-issues/`, data)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {

View File

@ -27,6 +27,7 @@ export interface IProjectIssues {
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>; archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
quickAddIssue: (workspaceSlug: string, projectId: string, data: TIssue) => Promise<TIssue>; quickAddIssue: (workspaceSlug: string, projectId: string, data: TIssue) => Promise<TIssue>;
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>; removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
archiveBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
} }
export class ProjectIssues extends IssueHelperStore implements IProjectIssues { export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
@ -59,6 +60,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
removeIssue: action, removeIssue: action,
archiveIssue: action, archiveIssue: action,
removeBulkIssues: action, removeBulkIssues: action,
archiveBulkIssues: action,
quickAddIssue: action, quickAddIssue: action,
}); });
// root store // root store
@ -260,4 +262,20 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
throw error; throw error;
} }
}; };
archiveBulkIssues = async (workspaceSlug: string, projectId: string, issueIds: string[]) => {
try {
const response = await this.issueService.bulkArchiveIssues(workspaceSlug, projectId, { issue_ids: issueIds });
runInAction(() => {
issueIds.forEach((issueId) => {
this.rootStore.issues.updateIssue(issueId, {
archived_at: response.archived_at,
});
});
});
} catch (error) {
throw error;
}
};
} }