mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: bulk delete and archive confirmation modals
This commit is contained in:
parent
5c328ff0b2
commit
51c0794a65
@ -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>
|
||||
|
@ -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={
|
||||
|
@ -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={
|
||||
|
@ -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={
|
||||
|
@ -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={
|
||||
|
@ -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={
|
||||
|
@ -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={
|
||||
|
@ -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
|
||||
/>
|
||||
);
|
||||
});
|
@ -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}`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
@ -1,2 +1,4 @@
|
||||
export * from "./bulk-archive-modal";
|
||||
export * from "./bulk-delete-modal";
|
||||
export * from "./properties";
|
||||
export * from "./root";
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -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={
|
||||
|
@ -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={
|
||||
|
@ -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={
|
||||
|
@ -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={
|
||||
|
@ -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={
|
||||
|
@ -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={
|
||||
|
@ -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={
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user