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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,33 +1,86 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { useRouter } from "next/router";
import { Trash2 } from "lucide-react";
// ui
import { ArchiveIcon, Tooltip } from "@plane/ui";
// 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) => {
const {} = props;
export const IssueBulkOperationsRoot = observer(() => {
// 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 (
<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="outline-none grid place-items-center">
<ArchiveIcon className="size-4" />
</button>
</Tooltip>
<>
{workspaceSlug && projectId && (
<BulkArchiveConfirmationModal
isOpen={isBulkArchiveModalOpen}
handleClose={() => setIsBulkArchiveModalOpen(false)}
issueIds={issueIds}
projectId={projectId.toString()}
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 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
handleClose={handleClose}
handleSubmit={handleDeletion}
isDeleting={isDeleteLoading}
isSubmitting={isDeleteLoading}
isOpen={isOpen}
title="Delete Label"
content={

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,6 +27,7 @@ export interface IProjectIssues {
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
quickAddIssue: (workspaceSlug: string, projectId: string, data: TIssue) => Promise<TIssue>;
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 {
@ -59,6 +60,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
removeIssue: action,
archiveIssue: action,
removeBulkIssues: action,
archiveBulkIssues: action,
quickAddIssue: action,
});
// root store
@ -260,4 +262,20 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
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;
}
};
}