mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-419] feat: manual issue archival (#3801)
* fix: issue archive without automation * fix: unarchive issue endpoint change * chore: archiving logic implemented in the quick-actions dropdowns * chore: peek overview archive button * chore: issue archive completed at state * chore: updated archiving icon and added archive option everywhere * chore: all issues quick actions dropdown * chore: archive and unarchive response * fix: archival mutation * fix: restore issue from peek overview * chore: update notification content for archive/restore * refactor: activity user name * fix: all issues mutation * fix: restore issue auth * chore: close peek overview on archival --------- Co-authored-by: NarayanBavisetti <narayan3119@gmail.com> Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
parent
b1520783cf
commit
30cc923fdb
@ -259,23 +259,15 @@ urlpatterns = [
|
||||
name="project-issue-archive",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/archived-issues/<uuid:pk>/",
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:pk>/archive/",
|
||||
IssueArchiveViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"delete": "destroy",
|
||||
"post": "archive",
|
||||
"delete": "unarchive",
|
||||
}
|
||||
),
|
||||
name="project-issue-archive",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/unarchive/<uuid:pk>/",
|
||||
IssueArchiveViewSet.as_view(
|
||||
{
|
||||
"post": "unarchive",
|
||||
}
|
||||
),
|
||||
name="project-issue-archive",
|
||||
name="project-issue-archive-unarchive",
|
||||
),
|
||||
## End Issue Archives
|
||||
## Issue Relation
|
||||
|
@ -1647,6 +1647,36 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def archive(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.issue_objects.get(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
pk=pk,
|
||||
)
|
||||
if issue.state.group not in ["completed", "cancelled"]:
|
||||
return Response(
|
||||
{"error": "Can only archive completed or cancelled state group issue"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps({"archived_at": str(timezone.now().date()), "automation": False}),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||
),
|
||||
epoch=int(timezone.now().timestamp()),
|
||||
notification=True,
|
||||
origin=request.META.get("HTTP_ORIGIN"),
|
||||
)
|
||||
issue.archived_at = timezone.now().date()
|
||||
issue.save()
|
||||
|
||||
return Response({"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def unarchive(self, request, slug, project_id, pk=None):
|
||||
issue = Issue.objects.get(
|
||||
workspace__slug=slug,
|
||||
@ -1670,7 +1700,7 @@ class IssueArchiveViewSet(BaseViewSet):
|
||||
issue.archived_at = None
|
||||
issue.save()
|
||||
|
||||
return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK)
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class IssueSubscriberViewSet(BaseViewSet):
|
||||
|
@ -483,17 +483,23 @@ def track_archive_at(
|
||||
)
|
||||
)
|
||||
else:
|
||||
if requested_data.get("automation"):
|
||||
comment = "Plane has archived the issue"
|
||||
new_value = "archive"
|
||||
else:
|
||||
comment = "Actor has archived the issue"
|
||||
new_value = "manual_archive"
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
project_id=project_id,
|
||||
workspace_id=workspace_id,
|
||||
comment="Plane has archived the issue",
|
||||
comment=comment,
|
||||
verb="updated",
|
||||
actor_id=actor_id,
|
||||
field="archived_at",
|
||||
old_value=None,
|
||||
new_value="archive",
|
||||
new_value=new_value,
|
||||
epoch=epoch,
|
||||
)
|
||||
)
|
||||
|
@ -79,7 +79,7 @@ def archive_old_issues():
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=json.dumps(
|
||||
{"archived_at": str(archive_at)}
|
||||
{"archived_at": str(archive_at), "automation": True}
|
||||
),
|
||||
actor_id=str(project.created_by_id),
|
||||
issue_id=issue.id,
|
||||
|
24
packages/types/src/notifications.d.ts
vendored
24
packages/types/src/notifications.d.ts
vendored
@ -12,27 +12,27 @@ export interface PaginatedUserNotification {
|
||||
}
|
||||
|
||||
export interface IUserNotification {
|
||||
id: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
archived_at: string | null;
|
||||
created_at: string;
|
||||
created_by: null;
|
||||
data: Data;
|
||||
entity_identifier: string;
|
||||
entity_name: string;
|
||||
title: string;
|
||||
id: string;
|
||||
message: null;
|
||||
message_html: string;
|
||||
message_stripped: null;
|
||||
sender: string;
|
||||
read_at: Date | null;
|
||||
archived_at: Date | null;
|
||||
snoozed_till: Date | null;
|
||||
created_by: null;
|
||||
updated_by: null;
|
||||
workspace: string;
|
||||
project: string;
|
||||
read_at: Date | null;
|
||||
receiver: string;
|
||||
sender: string;
|
||||
snoozed_till: Date | null;
|
||||
title: string;
|
||||
triggered_by: string;
|
||||
triggered_by_details: IUserLite;
|
||||
receiver: string;
|
||||
updated_at: Date;
|
||||
updated_by: null;
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
|
@ -177,17 +177,18 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
||||
};
|
||||
|
||||
const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
|
||||
const { children, onClick, className = "" } = props;
|
||||
const { children, disabled = false, onClick, className } = props;
|
||||
|
||||
return (
|
||||
<Menu.Item as="div">
|
||||
<Menu.Item as="div" disabled={disabled}>
|
||||
{({ active, close }) => (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full select-none truncate rounded px-1 py-1.5 text-left text-custom-text-200",
|
||||
{
|
||||
"bg-custom-background-80": active,
|
||||
"bg-custom-background-80": active && !disabled,
|
||||
"text-custom-text-400": disabled,
|
||||
},
|
||||
className
|
||||
)}
|
||||
@ -195,6 +196,7 @@ const MenuItem: React.FC<ICustomMenuItemProps> = (props) => {
|
||||
close();
|
||||
onClick && onClick(e);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
@ -64,6 +64,7 @@ export type ICustomSearchSelectProps = IDropdownProps &
|
||||
|
||||
export interface ICustomMenuItemProps {
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
onClick?: (args?: any) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
||||
<div className="">
|
||||
<h4 className="text-sm font-medium">Auto-archive closed issues</h4>
|
||||
<p className="text-sm tracking-tight text-custom-text-200">
|
||||
Plane will auto archive issues that have been completed or cancelled.
|
||||
Plane will auto archive issues that have been completed or canceled.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -73,7 +73,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
||||
<CustomSelect
|
||||
value={currentProjectDetails?.archive_in}
|
||||
label={`${currentProjectDetails?.archive_in} ${
|
||||
currentProjectDetails?.archive_in === 1 ? "Month" : "Months"
|
||||
currentProjectDetails?.archive_in === 1 ? "month" : "months"
|
||||
}`}
|
||||
onChange={(val: number) => {
|
||||
handleChange({ archive_in: val });
|
||||
@ -93,7 +93,7 @@ export const AutoArchiveAutomation: React.FC<Props> = observer((props) => {
|
||||
className="flex w-full select-none items-center rounded px-1 py-1.5 text-sm text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => setmonthModal(true)}
|
||||
>
|
||||
Customise Time Range
|
||||
Customize time range
|
||||
</button>
|
||||
</>
|
||||
</CustomSelect>
|
||||
|
@ -74,7 +74,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
||||
<div className="">
|
||||
<h4 className="text-sm font-medium">Auto-close issues</h4>
|
||||
<p className="text-sm tracking-tight text-custom-text-200">
|
||||
Plane will automatically close issue that haven{"'"}t been completed or cancelled.
|
||||
Plane will automatically close issue that haven{"'"}t been completed or canceled.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -100,7 +100,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
||||
<CustomSelect
|
||||
value={currentProjectDetails?.close_in}
|
||||
label={`${currentProjectDetails?.close_in} ${
|
||||
currentProjectDetails?.close_in === 1 ? "Month" : "Months"
|
||||
currentProjectDetails?.close_in === 1 ? "month" : "months"
|
||||
}`}
|
||||
onChange={(val: number) => {
|
||||
handleChange({ close_in: val });
|
||||
@ -119,7 +119,7 @@ export const AutoCloseAutomation: React.FC<Props> = observer((props) => {
|
||||
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
|
||||
onClick={() => setmonthModal(true)}
|
||||
>
|
||||
Customize Time Range
|
||||
Customize time range
|
||||
</button>
|
||||
</>
|
||||
</CustomSelect>
|
||||
|
@ -72,7 +72,7 @@ export const SelectMonthModal: React.FC<Props> = ({ type, initialValues, isOpen,
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Customise Time Range
|
||||
Customize time range
|
||||
</Dialog.Title>
|
||||
<div className="mt-8 flex items-center gap-2">
|
||||
<div className="flex w-full flex-col justify-center gap-1">
|
||||
|
@ -147,7 +147,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
{BUTTON_VARIANTS_WITH_TEXT.includes(buttonVariant) && (
|
||||
<span className="flex-grow truncate">{value ? renderFormattedDate(value) : placeholder}</span>
|
||||
)}
|
||||
{isClearable && isDateSelected && (
|
||||
{isClearable && !disabled && isDateSelected && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
|
@ -71,7 +71,7 @@ export const ProjectArchivedIssueDetailsHeader: FC = observer(() => {
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
href={`/${workspaceSlug}/projects/${projectId}/archived-issues`}
|
||||
label="Archived Issues"
|
||||
label="Archived issues"
|
||||
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
|
||||
/>
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ export const ProjectArchivedIssuesHeader: FC = observer(() => {
|
||||
type="text"
|
||||
link={
|
||||
<BreadcrumbLink
|
||||
label="Archived Issues"
|
||||
label="Archived issues"
|
||||
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
|
||||
/>
|
||||
}
|
||||
|
106
web/components/issues/archive-issue-modal.tsx
Normal file
106
web/components/issues/archive-issue-modal.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import { useState, Fragment } from "react";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
import { useIssues } from "hooks/store/use-issues";
|
||||
import useToast from "hooks/use-toast";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
data?: TIssue;
|
||||
dataId?: string | null | undefined;
|
||||
handleClose: () => void;
|
||||
isOpen: boolean;
|
||||
onSubmit?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const ArchiveIssueModal: React.FC<Props> = (props) => {
|
||||
const { dataId, data, isOpen, handleClose, onSubmit } = props;
|
||||
// states
|
||||
const [isArchiving, setIsArchiving] = useState(false);
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { issueMap } = useIssues();
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
if (!dataId && !data) return null;
|
||||
|
||||
const issue = data ? data : issueMap[dataId!];
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
|
||||
const onClose = () => {
|
||||
setIsArchiving(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleArchiveIssue = async () => {
|
||||
if (!onSubmit) return;
|
||||
|
||||
setIsArchiving(true);
|
||||
await onSubmit()
|
||||
.then(() => onClose())
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be archived. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => setIsArchiving(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||
<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 issue {projectDetails?.identifier} {issue.sequence_id}
|
||||
</h3>
|
||||
<p className="text-sm text-custom-text-200 mt-3">
|
||||
Are you sure you want to archive the issue? All your archived issues can be restored later.
|
||||
</p>
|
||||
<div className="flex justify-end gap-2 mt-3">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" tabIndex={1} onClick={handleArchiveIssue} loading={isArchiving}>
|
||||
{isArchiving ? "Archiving" : "Archive"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
@ -1,139 +0,0 @@
|
||||
import { useEffect, useState, Fragment } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useIssues, useProject } from "hooks/store";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import type { TIssue } from "@plane/types";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
data: TIssue;
|
||||
onSubmit?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const DeleteArchivedIssueModal: React.FC<Props> = observer((props) => {
|
||||
const { data, isOpen, handleClose, onSubmit } = props;
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
const {
|
||||
issues: { removeIssue },
|
||||
} = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDeleteLoading(false);
|
||||
}, [isOpen]);
|
||||
|
||||
const onClose = () => {
|
||||
setIsDeleteLoading(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleIssueDelete = async () => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
await removeIssue(workspaceSlug.toString(), data.project_id, data.id)
|
||||
.then(() => {
|
||||
if (onSubmit) onSubmit();
|
||||
})
|
||||
.catch((err) => {
|
||||
const error = err?.detail;
|
||||
const errorString = Array.isArray(error) ? error[0] : error;
|
||||
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
type: "error",
|
||||
message: errorString || "Something went wrong.",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setIsDeleteLoading(false);
|
||||
onClose();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||
<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-2xl">
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
<div className="flex w-full items-center justify-start gap-6">
|
||||
<span className="place-items-center rounded-full bg-red-500/20 p-4">
|
||||
<AlertTriangle className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||
</span>
|
||||
<span className="flex items-center justify-start">
|
||||
<h3 className="text-xl font-medium 2xl:text-2xl">Delete Archived Issue</h3>
|
||||
</span>
|
||||
</div>
|
||||
<span>
|
||||
<p className="text-sm text-custom-text-200">
|
||||
Are you sure you want to delete issue{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">
|
||||
{getProjectById(data?.project_id)?.identifier}-{data?.sequence_id}
|
||||
</span>
|
||||
{""}? All of the data related to the archived issue will be permanently removed. This action
|
||||
cannot be undone.
|
||||
</p>
|
||||
</span>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
tabIndex={1}
|
||||
onClick={handleIssueDelete}
|
||||
loading={isDeleteLoading}
|
||||
>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete Issue"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
});
|
@ -23,14 +23,14 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
|
||||
const { issueMap } = useIssues();
|
||||
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
// hooks
|
||||
const { getProjectById } = useProject();
|
||||
|
||||
useEffect(() => {
|
||||
setIsDeleteLoading(false);
|
||||
setIsDeleting(false);
|
||||
}, [isOpen]);
|
||||
|
||||
if (!dataId && !data) return null;
|
||||
@ -38,12 +38,12 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
const issue = data ? data : issueMap[dataId!];
|
||||
|
||||
const onClose = () => {
|
||||
setIsDeleteLoading(false);
|
||||
setIsDeleting(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleIssueDelete = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
setIsDeleting(true);
|
||||
if (onSubmit)
|
||||
await onSubmit()
|
||||
.then(() => {
|
||||
@ -56,7 +56,7 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
message: "Failed to delete issue",
|
||||
});
|
||||
})
|
||||
.finally(() => setIsDeleteLoading(false));
|
||||
.finally(() => setIsDeleting(false));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -109,14 +109,8 @@ export const DeleteIssueModal: React.FC<Props> = (props) => {
|
||||
<Button variant="neutral-primary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
tabIndex={1}
|
||||
onClick={handleIssueDelete}
|
||||
loading={isDeleteLoading}
|
||||
>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete Issue"}
|
||||
<Button variant="danger" size="sm" tabIndex={1} onClick={handleIssueDelete} loading={isDeleting}>
|
||||
{isDeleting ? "Deleting" : "Delete"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,4 +15,4 @@ export * from "./issue-detail";
|
||||
export * from "./peek-overview";
|
||||
|
||||
// archived issue
|
||||
export * from "./delete-archived-issue-modal";
|
||||
export * from "./archive-issue-modal";
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { MessageSquare } from "lucide-react";
|
||||
import { RotateCcw } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import { IssueActivityBlockComponent } from "./";
|
||||
// ui
|
||||
import { ArchiveIcon } from "@plane/ui";
|
||||
|
||||
type TIssueArchivedAtActivity = { activityId: string; ends: "top" | "bottom" | undefined };
|
||||
|
||||
@ -18,13 +20,21 @@ export const IssueArchivedAtActivity: FC<TIssueArchivedAtActivity> = observer((p
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
|
||||
return (
|
||||
<IssueActivityBlockComponent
|
||||
icon={<MessageSquare size={14} color="#6b7280" aria-hidden="true" />}
|
||||
icon={
|
||||
activity.new_value === "restore" ? (
|
||||
<RotateCcw className="h-3.5 w-3.5" color="#6b7280" aria-hidden="true" />
|
||||
) : (
|
||||
<ArchiveIcon className="h-3.5 w-3.5" color="#6b7280" aria-hidden="true" />
|
||||
)
|
||||
}
|
||||
activityId={activityId}
|
||||
ends={ends}
|
||||
customUserName={activity.new_value === "archive" ? "Plane" : undefined}
|
||||
>
|
||||
{activity.new_value === "restore" ? `restored the issue` : `archived the issue`}.
|
||||
{activity.new_value === "restore" ? "restored the issue" : "archived the issue"}.
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
@ -14,10 +14,11 @@ type TIssueActivityBlockComponent = {
|
||||
activityId: string;
|
||||
ends: "top" | "bottom" | undefined;
|
||||
children: ReactNode;
|
||||
customUserName?: string;
|
||||
};
|
||||
|
||||
export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (props) => {
|
||||
const { icon, activityId, ends, children } = props;
|
||||
const { icon, activityId, ends, children, customUserName } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
@ -37,7 +38,7 @@ export const IssueActivityBlockComponent: FC<TIssueActivityBlockComponent> = (pr
|
||||
{icon ? icon : <Network className="w-3.5 h-3.5" />}
|
||||
</div>
|
||||
<div className="w-full text-custom-text-200">
|
||||
<IssueUser activityId={activityId} />
|
||||
<IssueUser activityId={activityId} customUserName={customUserName} />
|
||||
<span> {children} </span>
|
||||
<span>
|
||||
<Tooltip
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { FC } from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
// hooks
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// ui
|
||||
|
||||
type TIssueUser = {
|
||||
activityId: string;
|
||||
customUserName?: string;
|
||||
};
|
||||
|
||||
export const IssueUser: FC<TIssueUser> = (props) => {
|
||||
const { activityId } = props;
|
||||
const { activityId, customUserName } = props;
|
||||
// hooks
|
||||
const {
|
||||
activity: { getActivityById },
|
||||
@ -18,12 +18,19 @@ export const IssueUser: FC<TIssueUser> = (props) => {
|
||||
const activity = getActivityById(activityId);
|
||||
|
||||
if (!activity) return <></>;
|
||||
|
||||
return (
|
||||
<a
|
||||
<>
|
||||
{customUserName ? (
|
||||
<span className="text-custom-text-100 font-medium">{customUserName}</span>
|
||||
) : (
|
||||
<Link
|
||||
href={`/${activity?.workspace_detail?.slug}/profile/${activity?.actor_detail?.id}`}
|
||||
className="hover:underline text-custom-text-100 font-medium capitalize"
|
||||
className="hover:underline text-custom-text-100 font-medium"
|
||||
>
|
||||
{activity.actor_detail?.display_name}
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ type TActivityTabs = "all" | "activity" | "comments";
|
||||
const activityTabs: { key: TActivityTabs; title: string; icon: LucideIcon }[] = [
|
||||
{
|
||||
key: "all",
|
||||
title: "All Activity",
|
||||
title: "All activity",
|
||||
icon: History,
|
||||
},
|
||||
{
|
||||
|
@ -16,7 +16,7 @@ import { TIssue } from "@plane/types";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
|
||||
import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED } from "constants/event-tracker";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
export type TIssueOperations = {
|
||||
@ -29,6 +29,8 @@ export type TIssueOperations = {
|
||||
showToast?: boolean
|
||||
) => Promise<void>;
|
||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
archive?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
restore?: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
addIssueToCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssueFromCycle?: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<void>;
|
||||
@ -63,6 +65,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
fetchIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
archiveIssue,
|
||||
addIssueToCycle,
|
||||
removeIssueFromCycle,
|
||||
addModulesToIssue,
|
||||
@ -158,6 +161,32 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await archiveIssue(workspaceSlug, projectId, issueId);
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue archived successfully.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_ARCHIVED,
|
||||
payload: { id: issueId, state: "SUCCESS", element: "Issue details page" },
|
||||
path: router.asPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be archived. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_ARCHIVED,
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue details page" },
|
||||
path: router.asPath,
|
||||
});
|
||||
}
|
||||
},
|
||||
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||
try {
|
||||
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||
@ -321,6 +350,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
fetchIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
archiveIssue,
|
||||
removeArchivedIssue,
|
||||
addIssueToCycle,
|
||||
removeIssueFromCycle,
|
||||
@ -350,7 +380,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
/>
|
||||
) : (
|
||||
<div className="flex w-full h-full overflow-hidden">
|
||||
<div className="h-full w-full max-w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
|
||||
<div className="h-full w-full max-w-2/3 space-y-5 divide-y-2 divide-custom-border-200 overflow-y-auto p-5">
|
||||
<IssueMainContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
@ -360,7 +390,7 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = observer((props) => {
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="h-full w-full min-w-[300px] lg:min-w-80 xl:min-w-96 sm:w-1/2 md:w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5 fixed md:relative bg-custom-sidebar-background-100 right-0 z-[5]"
|
||||
className="h-full w-full min-w-[300px] lg:min-w-80 xl:min-w-96 sm:w-1/2 md:w-1/3 space-y-5 overflow-hidden border-l border-custom-border-200 py-5 fixed md:relative bg-custom-sidebar-background-100 right-0 z-[5]"
|
||||
style={themeStore.issueDetailSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
|
||||
>
|
||||
<IssueDetailsSidebar
|
||||
|
@ -25,11 +25,12 @@ import {
|
||||
IssueModuleSelect,
|
||||
IssueParentSelect,
|
||||
IssueLabel,
|
||||
ArchiveIssueModal,
|
||||
} from "components/issues";
|
||||
import { IssueSubscription } from "./subscription";
|
||||
import { DateDropdown, EstimateDropdown, PriorityDropdown, MemberDropdown, StateDropdown } from "components/dropdowns";
|
||||
// icons
|
||||
import { ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, UserGroupIcon } from "@plane/ui";
|
||||
import { ArchiveIcon, ContrastIcon, DiceIcon, DoubleCircleIcon, RelatedIcon, Tooltip, UserGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "helpers/string.helper";
|
||||
@ -37,6 +38,7 @@ import { cn } from "helpers/common.helper";
|
||||
import { shouldHighlightIssueDueDate } from "helpers/issue.helper";
|
||||
// types
|
||||
import type { TIssueOperations } from "./root";
|
||||
import { STATE_GROUPS } from "constants/state";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@ -49,6 +51,9 @@ type Props = {
|
||||
|
||||
export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, issueOperations, is_archived, is_editable } = props;
|
||||
// states
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
// store hooks
|
||||
@ -60,8 +65,6 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { getStateById } = useProjectState();
|
||||
// states
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
|
||||
const issue = getIssueById(issueId);
|
||||
if (!issue) return <></>;
|
||||
@ -77,8 +80,23 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const projectDetails = issue ? getProjectById(issue.project_id) : null;
|
||||
const handleDeleteIssue = async () => {
|
||||
await issueOperations.remove(workspaceSlug, projectId, issueId);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
|
||||
};
|
||||
|
||||
const handleArchiveIssue = async () => {
|
||||
if (!issueOperations.archive) return;
|
||||
await issueOperations.archive(workspaceSlug, projectId, issueId);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}`);
|
||||
};
|
||||
// derived values
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
// auth
|
||||
const isArchivingAllowed = !is_archived && issueOperations.archive && is_editable;
|
||||
const isInArchivableGroup =
|
||||
!!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
|
||||
|
||||
const minDate = issue.start_date ? new Date(issue.start_date) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
@ -88,44 +106,70 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspaceSlug && projectId && issue && (
|
||||
<DeleteIssueModal
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
isOpen={deleteIssueModal}
|
||||
data={issue}
|
||||
onSubmit={async () => {
|
||||
await issueOperations.remove(workspaceSlug, projectId, issueId);
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
|
||||
}}
|
||||
onSubmit={handleDeleteIssue}
|
||||
/>
|
||||
<ArchiveIssueModal
|
||||
isOpen={archiveIssueModal}
|
||||
handleClose={() => setArchiveIssueModal(false)}
|
||||
data={issue}
|
||||
onSubmit={handleArchiveIssue}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex h-full w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
||||
<div className="flex items-center justify-end px-5 pb-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{currentUser && !is_archived && (
|
||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
)}
|
||||
|
||||
<div className="flex items-center flex-wrap gap-2.5 text-custom-text-300">
|
||||
<Tooltip tooltipContent="Copy link">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-custom-border-200 p-2 shadow-sm duration-300 hover:bg-custom-background-90 focus:border-custom-primary focus:outline-none focus:ring-1 focus:ring-custom-primary"
|
||||
className="h-5 w-5 grid place-items-center hover:text-custom-text-200 rounded focus:outline-none focus:ring-2 focus:ring-custom-primary"
|
||||
onClick={handleCopyText}
|
||||
>
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{is_editable && (
|
||||
</Tooltip>
|
||||
{isArchivingAllowed && (
|
||||
<Tooltip
|
||||
tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-500/20 focus:outline-none"
|
||||
className={cn(
|
||||
"h-5 w-5 grid place-items-center rounded focus:outline-none focus:ring-2 focus:ring-custom-primary",
|
||||
{
|
||||
"hover:text-custom-text-200": isInArchivableGroup,
|
||||
"cursor-not-allowed text-custom-text-400": !isInArchivableGroup,
|
||||
}
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!isInArchivableGroup) return;
|
||||
setArchiveIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<ArchiveIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{is_editable && (
|
||||
<Tooltip tooltipContent="Delete">
|
||||
<button
|
||||
type="button"
|
||||
className="h-5 w-5 grid place-items-center hover:text-custom-text-200 rounded focus:outline-none focus:ring-2 focus:ring-custom-primary"
|
||||
onClick={() => setDeleteIssueModal(true)}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full overflow-y-auto px-6">
|
||||
<h5 className="text-sm font-medium mt-6">Properties</h5>
|
||||
|
@ -26,6 +26,8 @@ interface IBaseCalendarRoot {
|
||||
[EIssueActions.DELETE]: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.UPDATE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.REMOVE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.RESTORE]?: (issue: TIssue) => Promise<void>;
|
||||
};
|
||||
viewId?: string;
|
||||
isCompletedCycle?: boolean;
|
||||
@ -114,6 +116,16 @@ export const BaseCalendarRoot = observer((props: IBaseCalendarRoot) => {
|
||||
? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.REMOVE)
|
||||
: undefined
|
||||
}
|
||||
handleArchive={
|
||||
issueActions[EIssueActions.ARCHIVE]
|
||||
? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.ARCHIVE)
|
||||
: undefined
|
||||
}
|
||||
handleRestore={
|
||||
issueActions[EIssueActions.RESTORE]
|
||||
? async () => handleIssues(issue.target_date ?? "", issue, EIssueActions.RESTORE)
|
||||
: undefined
|
||||
}
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
/>
|
||||
)}
|
||||
|
@ -33,6 +33,10 @@ export const CycleCalendarLayout: React.FC = observer(() => {
|
||||
if (!workspaceSlug || !cycleId || !projectId) return;
|
||||
await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString());
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug, cycleId, projectId]
|
||||
);
|
||||
|
@ -34,6 +34,10 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
await issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, moduleId);
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug, moduleId]
|
||||
);
|
||||
|
@ -28,6 +28,11 @@ export const CalendarLayout: React.FC = observer(() => {
|
||||
|
||||
await issues.removeIssue(workspaceSlug.toString(), issue.project_id, issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id);
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug]
|
||||
);
|
||||
|
@ -16,6 +16,7 @@ export interface IViewCalendarLayout {
|
||||
[EIssueActions.DELETE]: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.UPDATE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.REMOVE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,8 @@ export interface IBaseKanBanLayout {
|
||||
[EIssueActions.DELETE]: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.UPDATE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.REMOVE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.RESTORE]?: (issue: TIssue) => Promise<void>;
|
||||
};
|
||||
showLoader?: boolean;
|
||||
viewId?: string;
|
||||
@ -188,6 +190,12 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined
|
||||
}
|
||||
handleArchive={
|
||||
issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined
|
||||
}
|
||||
handleRestore={
|
||||
issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined
|
||||
}
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
/>
|
||||
),
|
||||
|
@ -39,6 +39,11 @@ export const CycleKanBanLayout: React.FC = observer(() => {
|
||||
|
||||
await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString());
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug, cycleId]
|
||||
);
|
||||
|
@ -38,6 +38,11 @@ export const ModuleKanBanLayout: React.FC = observer(() => {
|
||||
|
||||
await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString());
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug, moduleId]
|
||||
);
|
||||
|
@ -35,6 +35,11 @@ export const ProfileIssuesKanBanLayout: React.FC = observer(() => {
|
||||
|
||||
await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
|
||||
await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, userId);
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug, userId]
|
||||
);
|
||||
|
@ -32,6 +32,11 @@ export const KanBanLayout: React.FC = observer(() => {
|
||||
|
||||
await issues.removeIssue(workspaceSlug, issue.project_id, issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id);
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug]
|
||||
);
|
||||
|
@ -17,6 +17,7 @@ export interface IViewKanBanLayout {
|
||||
[EIssueActions.DELETE]: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.UPDATE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.REMOVE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -41,6 +41,8 @@ interface IBaseListRoot {
|
||||
[EIssueActions.DELETE]: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.UPDATE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.REMOVE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.RESTORE]?: (issue: TIssue) => Promise<void>;
|
||||
};
|
||||
viewId?: string;
|
||||
storeType: TCreateModalStoreTypes;
|
||||
@ -109,6 +111,12 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined
|
||||
}
|
||||
handleArchive={
|
||||
issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined
|
||||
}
|
||||
handleRestore={
|
||||
issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined
|
||||
}
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
/>
|
||||
),
|
||||
|
@ -5,6 +5,8 @@ export interface IQuickActionProps {
|
||||
handleDelete: () => Promise<void>;
|
||||
handleUpdate?: (data: TIssue) => Promise<void>;
|
||||
handleRemoveFromView?: () => Promise<void>;
|
||||
handleArchive?: () => Promise<void>;
|
||||
handleRestore?: () => Promise<void>;
|
||||
customActionButton?: React.ReactElement;
|
||||
portalElement?: HTMLDivElement | null;
|
||||
readOnly?: boolean;
|
||||
|
@ -24,6 +24,11 @@ export const ArchivedIssueListLayout: FC = observer(() => {
|
||||
|
||||
await issues.removeIssue(workspaceSlug, projectId, issue.id);
|
||||
},
|
||||
[EIssueActions.RESTORE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
await issues.restoreIssue(workspaceSlug, projectId, issue.id);
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug, projectId]
|
||||
);
|
||||
|
@ -38,6 +38,11 @@ export const CycleListLayout: React.FC = observer(() => {
|
||||
|
||||
await issues.removeIssueFromCycle(workspaceSlug.toString(), issue.project_id, cycleId.toString(), issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
|
||||
await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, cycleId.toString());
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug, cycleId]
|
||||
);
|
||||
|
@ -37,6 +37,11 @@ export const ModuleListLayout: React.FC = observer(() => {
|
||||
|
||||
await issues.removeIssueFromModule(workspaceSlug.toString(), issue.project_id, moduleId.toString(), issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
|
||||
await issues.archiveIssue(workspaceSlug.toString(), issue.project_id, issue.id, moduleId.toString());
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug, moduleId]
|
||||
);
|
||||
|
@ -36,6 +36,11 @@ export const ProfileIssuesListLayout: FC = observer(() => {
|
||||
|
||||
await issues.removeIssue(workspaceSlug, issue.project_id, issue.id, userId);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !userId) return;
|
||||
|
||||
await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, userId);
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug, userId]
|
||||
);
|
||||
|
@ -33,6 +33,11 @@ export const ListLayout: FC = observer(() => {
|
||||
|
||||
await issues.removeIssue(workspaceSlug, projectId, issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
await issues.archiveIssue(workspaceSlug, projectId, issue.id);
|
||||
},
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[issues]
|
||||
|
@ -17,6 +17,7 @@ export interface IViewListLayout {
|
||||
[EIssueActions.DELETE]: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.UPDATE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.REMOVE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
||||
import { ArchiveIcon, CustomMenu } from "@plane/ui";
|
||||
import { observer } from "mobx-react";
|
||||
import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react";
|
||||
import omit from "lodash/omit";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useEventTracker } from "hooks/store";
|
||||
import { useEventTracker, useProjectState } from "hooks/store";
|
||||
// components
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
@ -15,30 +16,50 @@ import { TIssue } from "@plane/types";
|
||||
import { IQuickActionProps } from "../list/list-view-types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { STATE_GROUPS } from "constants/state";
|
||||
|
||||
export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, customActionButton, portalElement, readOnly = false } = props;
|
||||
export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const {
|
||||
issue,
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
handleArchive,
|
||||
customActionButton,
|
||||
portalElement,
|
||||
readOnly = false,
|
||||
} = props;
|
||||
// states
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// hooks
|
||||
// store hooks
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { getStateById } = useProjectState();
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// derived values
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
const isEditingAllowed = !readOnly;
|
||||
// auth
|
||||
const isArchivingAllowed = handleArchive && isEditingAllowed;
|
||||
const isInArchivableGroup =
|
||||
!!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
copyUrlToClipboard(`/${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||
const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`;
|
||||
|
||||
const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank");
|
||||
const handleCopyIssueLink = () =>
|
||||
copyUrlToClipboard(issueLink).then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied",
|
||||
message: "Issue link copied to clipboard",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
@ -50,6 +71,12 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ArchiveIssueModal
|
||||
data={issue}
|
||||
isOpen={archiveIssueModal}
|
||||
handleClose={() => setArchiveIssueModal(false)}
|
||||
onSubmit={handleArchive}
|
||||
/>
|
||||
<DeleteIssueModal
|
||||
data={issue}
|
||||
isOpen={deleteIssueModal}
|
||||
@ -75,18 +102,7 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
closeOnSelect
|
||||
ellipsis
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleCopyIssueLink();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className="h-3 w-3" />
|
||||
Copy link
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{!readOnly && (
|
||||
<>
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Global issues");
|
||||
@ -96,9 +112,23 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit issue
|
||||
Edit
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleOpenInNewTab}>
|
||||
<div className="flex items-center gap-2">
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Open in new tab
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyIssueLink}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className="h-3 w-3" />
|
||||
Copy link
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Global issues");
|
||||
@ -110,6 +140,30 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
Make a copy
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isArchivingAllowed && (
|
||||
<CustomMenu.MenuItem onClick={() => setArchiveIssueModal(true)} disabled={!isInArchivableGroup}>
|
||||
{isInArchivableGroup ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
Archive
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
<div className="-mt-1">
|
||||
<p>Archive</p>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
Only completed or canceled
|
||||
<br />
|
||||
issues can be archived
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement("Global issues");
|
||||
@ -118,12 +172,11 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete issue
|
||||
Delete
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { Link, Trash2 } from "lucide-react";
|
||||
import { ExternalLink, Link, RotateCcw, Trash2 } from "lucide-react";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
// components
|
||||
import { DeleteArchivedIssueModal } from "components/issues";
|
||||
import { DeleteIssueModal } from "components/issues";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
@ -15,40 +15,41 @@ import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
|
||||
export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, customActionButton, portalElement, readOnly = false } = props;
|
||||
const { issue, handleDelete, handleRestore, customActionButton, portalElement, readOnly = false } = props;
|
||||
// states
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// states
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
// store hooks
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
|
||||
// derived values
|
||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||
// auth
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly;
|
||||
const isRestoringAllowed = handleRestore && isEditingAllowed;
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/archived-issues/${issue.id}`).then(() =>
|
||||
const issueLink = `${workspaceSlug}/projects/${issue.project_id}/archived-issues/${issue.id}`;
|
||||
|
||||
const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank");
|
||||
const handleCopyIssueLink = () =>
|
||||
copyUrlToClipboard(issueLink).then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied",
|
||||
message: "Issue link copied to clipboard",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteArchivedIssueModal
|
||||
<DeleteIssueModal
|
||||
data={issue}
|
||||
isOpen={deleteIssueModal}
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
@ -61,17 +62,27 @@ export const ArchivedIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
closeOnSelect
|
||||
ellipsis
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleCopyIssueLink();
|
||||
}}
|
||||
>
|
||||
{isRestoringAllowed && (
|
||||
<CustomMenu.MenuItem onClick={handleRestore}>
|
||||
<div className="flex items-center gap-2">
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
Restore
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleOpenInNewTab}>
|
||||
<div className="flex items-center gap-2">
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Open in new tab
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyIssueLink}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className="h-3 w-3" />
|
||||
Copy link
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && !readOnly && (
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
||||
import { ArchiveIcon, CustomMenu } from "@plane/ui";
|
||||
import { observer } from "mobx-react";
|
||||
import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
||||
import omit from "lodash/omit";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store";
|
||||
// components
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
@ -16,13 +17,15 @@ import { IQuickActionProps } from "../list/list-view-types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { STATE_GROUPS } from "constants/state";
|
||||
|
||||
export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const {
|
||||
issue,
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
handleRemoveFromView,
|
||||
handleArchive,
|
||||
customActionButton,
|
||||
portalElement,
|
||||
readOnly = false,
|
||||
@ -31,33 +34,42 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, cycleId } = router.query;
|
||||
// store hooks
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
const { getStateById } = useProjectState();
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// derived values
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
// auth
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly;
|
||||
const isArchivingAllowed = handleArchive && isEditingAllowed;
|
||||
const isInArchivableGroup =
|
||||
!!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
|
||||
const isDeletingAllowed = isEditingAllowed;
|
||||
|
||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||
const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`;
|
||||
|
||||
const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank");
|
||||
|
||||
const handleCopyIssueLink = () =>
|
||||
copyUrlToClipboard(issueLink).then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied",
|
||||
message: "Issue link copied to clipboard",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
@ -69,6 +81,12 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ArchiveIssueModal
|
||||
data={issue}
|
||||
isOpen={archiveIssueModal}
|
||||
handleClose={() => setArchiveIssueModal(false)}
|
||||
onSubmit={handleArchive}
|
||||
/>
|
||||
<DeleteIssueModal
|
||||
data={issue}
|
||||
isOpen={deleteIssueModal}
|
||||
@ -94,18 +112,7 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
closeOnSelect
|
||||
ellipsis
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleCopyIssueLink();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className="h-3 w-3" />
|
||||
Copy link
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && !readOnly && (
|
||||
<>
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setIssueToEdit({
|
||||
@ -118,19 +125,23 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit issue
|
||||
Edit
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleRemoveFromView && handleRemoveFromView();
|
||||
}}
|
||||
>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleOpenInNewTab}>
|
||||
<div className="flex items-center gap-2">
|
||||
<XCircle className="h-3 w-3" />
|
||||
Remove from cycle
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Open in new tab
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyIssueLink}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className="h-3 w-3" />
|
||||
Copy link
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
@ -142,6 +153,42 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
Make a copy
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleRemoveFromView && handleRemoveFromView();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<XCircle className="h-3 w-3" />
|
||||
Remove from cycle
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isArchivingAllowed && (
|
||||
<CustomMenu.MenuItem onClick={() => setArchiveIssueModal(true)} disabled={!isInArchivableGroup}>
|
||||
{isInArchivableGroup ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
Archive
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
<div className="-mt-1">
|
||||
<p>Archive</p>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
Only completed or canceled
|
||||
<br />
|
||||
issues can be archived
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isDeletingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
@ -150,12 +197,11 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete issue
|
||||
Delete
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { Copy, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
||||
import { ArchiveIcon, CustomMenu } from "@plane/ui";
|
||||
import { observer } from "mobx-react";
|
||||
import { Copy, ExternalLink, Link, Pencil, Trash2, XCircle } from "lucide-react";
|
||||
import omit from "lodash/omit";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useIssues, useEventTracker, useUser } from "hooks/store";
|
||||
import { useIssues, useEventTracker, useUser, useProjectState } from "hooks/store";
|
||||
// components
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
@ -16,13 +17,15 @@ import { IQuickActionProps } from "../list/list-view-types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { STATE_GROUPS } from "constants/state";
|
||||
|
||||
export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const {
|
||||
issue,
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
handleRemoveFromView,
|
||||
handleArchive,
|
||||
customActionButton,
|
||||
portalElement,
|
||||
readOnly = false,
|
||||
@ -31,33 +34,42 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, moduleId } = router.query;
|
||||
// store hooks
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.MODULE);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
const { getStateById } = useProjectState();
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// derived values
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
// auth
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly;
|
||||
const isArchivingAllowed = handleArchive && isEditingAllowed;
|
||||
const isInArchivableGroup =
|
||||
!!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
|
||||
const isDeletingAllowed = isEditingAllowed;
|
||||
|
||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||
const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`;
|
||||
|
||||
const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank");
|
||||
|
||||
const handleCopyIssueLink = () =>
|
||||
copyUrlToClipboard(issueLink).then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied",
|
||||
message: "Issue link copied to clipboard",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
@ -69,6 +81,12 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ArchiveIssueModal
|
||||
data={issue}
|
||||
isOpen={archiveIssueModal}
|
||||
handleClose={() => setArchiveIssueModal(false)}
|
||||
onSubmit={handleArchive}
|
||||
/>
|
||||
<DeleteIssueModal
|
||||
data={issue}
|
||||
isOpen={deleteIssueModal}
|
||||
@ -94,18 +112,7 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
closeOnSelect
|
||||
ellipsis
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleCopyIssueLink();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className="h-3 w-3" />
|
||||
Copy link
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && !readOnly && (
|
||||
<>
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setIssueToEdit({ ...issue, module_ids: moduleId ? [moduleId.toString()] : [] });
|
||||
@ -115,19 +122,23 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit issue
|
||||
Edit
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleRemoveFromView && handleRemoveFromView();
|
||||
}}
|
||||
>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleOpenInNewTab}>
|
||||
<div className="flex items-center gap-2">
|
||||
<XCircle className="h-3 w-3" />
|
||||
Remove from module
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Open in new tab
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyIssueLink}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className="h-3 w-3" />
|
||||
Copy link
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
@ -139,6 +150,42 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
Make a copy
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleRemoveFromView && handleRemoveFromView();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<XCircle className="h-3 w-3" />
|
||||
Remove from module
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isArchivingAllowed && (
|
||||
<CustomMenu.MenuItem onClick={() => setArchiveIssueModal(true)} disabled={!isInArchivableGroup}>
|
||||
{isInArchivableGroup ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
Archive
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
<div className="-mt-1">
|
||||
<p>Archive</p>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
Only completed or canceled
|
||||
<br />
|
||||
issues can be archived
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isDeletingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
@ -149,12 +196,11 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete issue
|
||||
Delete
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { Copy, Link, Pencil, Trash2 } from "lucide-react";
|
||||
import { ArchiveIcon, CustomMenu } from "@plane/ui";
|
||||
import { observer } from "mobx-react";
|
||||
import { Copy, ExternalLink, Link, Pencil, Trash2 } from "lucide-react";
|
||||
import omit from "lodash/omit";
|
||||
// hooks
|
||||
import { useEventTracker, useIssues, useUser } from "hooks/store";
|
||||
import { useEventTracker, useIssues, useProjectState, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
import { ArchiveIssueModal, CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// types
|
||||
@ -16,9 +17,18 @@ import { IQuickActionProps } from "../list/list-view-types";
|
||||
// constant
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { STATE_GROUPS } from "constants/state";
|
||||
|
||||
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) => {
|
||||
const { issue, handleDelete, handleUpdate, customActionButton, portalElement, readOnly = false } = props;
|
||||
export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const {
|
||||
issue,
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
handleArchive,
|
||||
customActionButton,
|
||||
portalElement,
|
||||
readOnly = false,
|
||||
} = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
@ -26,28 +36,38 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
const [archiveIssueModal, setArchiveIssueModal] = useState(false);
|
||||
// store hooks
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
||||
|
||||
const { getStateById } = useProjectState();
|
||||
// derived values
|
||||
const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`;
|
||||
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
// auth
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER && !readOnly;
|
||||
const isArchivingAllowed = handleArchive && isEditingAllowed;
|
||||
const isInArchivableGroup =
|
||||
!!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
|
||||
const isDeletingAllowed = isEditingAllowed;
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const handleCopyIssueLink = () => {
|
||||
copyUrlToClipboard(`${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`).then(() =>
|
||||
const issueLink = `${workspaceSlug}/projects/${issue.project_id}/issues/${issue.id}`;
|
||||
|
||||
const handleOpenInNewTab = () => window.open(`/${issueLink}`, "_blank");
|
||||
|
||||
const handleCopyIssueLink = () =>
|
||||
copyUrlToClipboard(issueLink).then(() =>
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link copied",
|
||||
message: "Issue link copied to clipboard",
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const isDraftIssue = router?.asPath?.includes("draft-issues") || false;
|
||||
|
||||
@ -62,13 +82,18 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
|
||||
return (
|
||||
<>
|
||||
<ArchiveIssueModal
|
||||
data={issue}
|
||||
isOpen={archiveIssueModal}
|
||||
handleClose={() => setArchiveIssueModal(false)}
|
||||
onSubmit={handleArchive}
|
||||
/>
|
||||
<DeleteIssueModal
|
||||
data={issue}
|
||||
isOpen={deleteIssueModal}
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
onSubmit={handleDelete}
|
||||
/>
|
||||
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={createUpdateIssueModal}
|
||||
onClose={() => {
|
||||
@ -82,7 +107,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
storeType={EIssuesStoreType.PROJECT}
|
||||
isDraft={isDraftIssue}
|
||||
/>
|
||||
|
||||
<CustomMenu
|
||||
placement="bottom-start"
|
||||
customButton={customActionButton}
|
||||
@ -90,18 +114,7 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
closeOnSelect
|
||||
ellipsis
|
||||
>
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleCopyIssueLink();
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className="h-3 w-3" />
|
||||
Copy link
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && !readOnly && (
|
||||
<>
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
@ -111,9 +124,23 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit issue
|
||||
Edit
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
<CustomMenu.MenuItem onClick={handleOpenInNewTab}>
|
||||
<div className="flex items-center gap-2">
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
Open in new tab
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
<CustomMenu.MenuItem onClick={handleCopyIssueLink}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link className="h-3 w-3" />
|
||||
Copy link
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
{isEditingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
@ -125,6 +152,30 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
Make a copy
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isArchivingAllowed && (
|
||||
<CustomMenu.MenuItem onClick={() => setArchiveIssueModal(true)} disabled={!isInArchivableGroup}>
|
||||
{isInArchivableGroup ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
Archive
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-2">
|
||||
<ArchiveIcon className="h-3 w-3" />
|
||||
<div className="-mt-1">
|
||||
<p>Archive</p>
|
||||
<p className="text-xs text-custom-text-400">
|
||||
Only completed or canceled
|
||||
<br />
|
||||
issues can be archived
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isDeletingAllowed && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
setTrackElement(activeLayout);
|
||||
@ -133,12 +184,11 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = (props) =>
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete issue
|
||||
Delete
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
</>
|
||||
)}
|
||||
</CustomMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -34,7 +34,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
const { commandPalette: commandPaletteStore } = useApplication();
|
||||
const {
|
||||
issuesFilter: { filters, fetchFilters, updateFilters },
|
||||
issues: { loader, groupedIssueIds, fetchIssues, updateIssue, removeIssue },
|
||||
issues: { loader, groupedIssueIds, fetchIssues, updateIssue, removeIssue, archiveIssue },
|
||||
} = useIssues(EIssuesStoreType.GLOBAL);
|
||||
|
||||
const { dataViewId, issueIds } = groupedIssueIds;
|
||||
@ -138,6 +138,12 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
await removeIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString());
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
const projectId = issue.project_id;
|
||||
if (!workspaceSlug || !projectId || !globalViewId) return;
|
||||
|
||||
await archiveIssue(workspaceSlug.toString(), projectId, issue.id, globalViewId.toString());
|
||||
},
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[updateIssue, removeIssue, workspaceSlug]
|
||||
@ -147,6 +153,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
async (issue: TIssue, action: EIssueActions) => {
|
||||
if (action === EIssueActions.UPDATE) await issueActions[action]!(issue);
|
||||
if (action === EIssueActions.DELETE) await issueActions[action]!(issue);
|
||||
if (action === EIssueActions.ARCHIVE) await issueActions[action]!(issue);
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
@ -174,10 +181,12 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
issue={issue}
|
||||
handleUpdate={async () => handleIssues({ ...issue }, EIssueActions.UPDATE)}
|
||||
handleDelete={async () => handleIssues(issue, EIssueActions.DELETE)}
|
||||
handleArchive={async () => handleIssues(issue, EIssueActions.ARCHIVE)}
|
||||
portalElement={portalElement}
|
||||
readOnly={!canEditProperties(issue.project_id)}
|
||||
/>
|
||||
),
|
||||
[handleIssues]
|
||||
[canEditProperties, handleIssues]
|
||||
);
|
||||
|
||||
const isEditingAllowed = !!currentWorkspaceRole && currentWorkspaceRole >= EUserWorkspaceRoles.MEMBER;
|
||||
|
@ -26,6 +26,8 @@ interface IBaseSpreadsheetRoot {
|
||||
[EIssueActions.DELETE]: (issue: TIssue) => void;
|
||||
[EIssueActions.UPDATE]?: (issue: TIssue) => void;
|
||||
[EIssueActions.REMOVE]?: (issue: TIssue) => void;
|
||||
[EIssueActions.ARCHIVE]?: (issue: TIssue) => void;
|
||||
[EIssueActions.RESTORE]?: (issue: TIssue) => Promise<void>;
|
||||
};
|
||||
canEditPropertiesBasedOnProject?: (projectId: string) => boolean;
|
||||
isCompletedCycle?: boolean;
|
||||
@ -103,6 +105,12 @@ export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => {
|
||||
handleRemoveFromView={
|
||||
issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined
|
||||
}
|
||||
handleArchive={
|
||||
issueActions[EIssueActions.ARCHIVE] ? async () => handleIssues(issue, EIssueActions.ARCHIVE) : undefined
|
||||
}
|
||||
handleRestore={
|
||||
issueActions[EIssueActions.RESTORE] ? async () => handleIssues(issue, EIssueActions.RESTORE) : undefined
|
||||
}
|
||||
portalElement={portalElement}
|
||||
readOnly={!isEditingAllowed || isCompletedCycle}
|
||||
/>
|
||||
|
@ -216,11 +216,9 @@ const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
|
||||
{getProjectIdentifierById(issueDetail.project_id)}-{issueDetail.sequence_id}
|
||||
</span>
|
||||
|
||||
{canEditProperties(issueDetail.project_id) && (
|
||||
<div className={`absolute left-2.5 top-0 hidden group-hover:block ${isMenuActive ? "!block" : ""}`}>
|
||||
{quickActions(issueDetail, customActionButton, portalElement.current)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{issueDetail.sub_issues_count > 0 && (
|
||||
|
@ -32,6 +32,10 @@ export const CycleSpreadsheetLayout: React.FC = observer(() => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
issues.removeIssueFromCycle(workspaceSlug, issue.project_id, cycleId, issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !cycleId) return;
|
||||
issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, cycleId);
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug, cycleId]
|
||||
);
|
||||
|
@ -31,6 +31,10 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
issues.removeIssueFromModule(workspaceSlug, issue.project_id, moduleId, issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug || !moduleId) return;
|
||||
issues.archiveIssue(workspaceSlug, issue.project_id, issue.id, moduleId);
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug, moduleId]
|
||||
);
|
||||
|
@ -28,6 +28,11 @@ export const ProjectSpreadsheetLayout: React.FC = observer(() => {
|
||||
|
||||
await issues.removeIssue(workspaceSlug, issue.project_id, issue.id);
|
||||
},
|
||||
[EIssueActions.ARCHIVE]: async (issue: TIssue) => {
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
await issues.archiveIssue(workspaceSlug, issue.project_id, issue.id);
|
||||
},
|
||||
}),
|
||||
[issues, workspaceSlug]
|
||||
);
|
||||
|
@ -17,6 +17,7 @@ export interface IViewSpreadsheetLayout {
|
||||
[EIssueActions.DELETE]: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.UPDATE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.REMOVE]?: (issue: TIssue) => Promise<void>;
|
||||
[EIssueActions.ARCHIVE]?: (issue: TIssue) => Promise<void>;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2,4 +2,6 @@ export enum EIssueActions {
|
||||
UPDATE = "update",
|
||||
DELETE = "delete",
|
||||
REMOVE = "remove",
|
||||
ARCHIVE = "archive",
|
||||
RESTORE = "restore",
|
||||
}
|
||||
|
@ -1,17 +1,20 @@
|
||||
import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react";
|
||||
import { MoveRight, MoveDiagonal, Link2, Trash2 } from "lucide-react";
|
||||
import { MoveRight, MoveDiagonal, Link2, Trash2, RotateCcw } from "lucide-react";
|
||||
// ui
|
||||
import { CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon } from "@plane/ui";
|
||||
import { ArchiveIcon, CenterPanelIcon, CustomSelect, FullScreenPanelIcon, SidePanelIcon, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
// store hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import { useIssueDetail, useProjectState, useUser } from "hooks/store";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// components
|
||||
import { IssueSubscription, IssueUpdateStatus } from "components/issues";
|
||||
import { STATE_GROUPS } from "constants/state";
|
||||
|
||||
export type TPeekModes = "side-peek" | "modal" | "full-screen";
|
||||
|
||||
@ -43,6 +46,8 @@ export type PeekOverviewHeaderProps = {
|
||||
isArchived: boolean;
|
||||
disabled: boolean;
|
||||
toggleDeleteIssueModal: (value: boolean) => void;
|
||||
toggleArchiveIssueModal: (value: boolean) => void;
|
||||
handleRestoreIssue: () => void;
|
||||
isSubmitting: "submitting" | "submitted" | "saved";
|
||||
};
|
||||
|
||||
@ -57,23 +62,31 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
||||
disabled,
|
||||
removeRoutePeekId,
|
||||
toggleDeleteIssueModal,
|
||||
toggleArchiveIssueModal,
|
||||
handleRestoreIssue,
|
||||
isSubmitting,
|
||||
} = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const { getStateById } = useProjectState();
|
||||
// hooks
|
||||
const { setToastAlert } = useToast();
|
||||
// derived values
|
||||
const issueDetails = getIssueById(issueId);
|
||||
const stateDetails = issueDetails ? getStateById(issueDetails?.state_id) : undefined;
|
||||
const currentMode = PEEK_OPTIONS.find((m) => m.key === peekMode);
|
||||
|
||||
const issueLink = `${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${issueId}`;
|
||||
|
||||
const handleCopyText = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
copyUrlToClipboard(
|
||||
`${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${issueId}`
|
||||
).then(() => {
|
||||
copyUrlToClipboard(issueLink).then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Link Copied!",
|
||||
@ -81,13 +94,15 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const redirectToIssueDetail = () => {
|
||||
router.push({
|
||||
pathname: `/${workspaceSlug}/projects/${projectId}/${isArchived ? "archived-issues" : "issues"}/${issueId}`,
|
||||
});
|
||||
router.push({ pathname: `/${issueLink}` });
|
||||
removeRoutePeekId();
|
||||
};
|
||||
// auth
|
||||
const isArchivingAllowed = !isArchived && !disabled;
|
||||
const isInArchivableGroup =
|
||||
!!stateDetails && [STATE_GROUPS.completed.key, STATE_GROUPS.cancelled.key].includes(stateDetails?.group);
|
||||
const isRestoringAllowed = isArchived && !disabled;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -97,11 +112,11 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={removeRoutePeekId}>
|
||||
<MoveRight className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
<MoveRight className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
|
||||
<button onClick={redirectToIssueDetail}>
|
||||
<MoveDiagonal className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
<MoveDiagonal className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
{currentMode && (
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
@ -110,7 +125,7 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
||||
onChange={(val: any) => setPeekMode(val)}
|
||||
customButton={
|
||||
<button type="button" className="">
|
||||
<currentMode.icon className="h-4 w-4 text-custom-text-400 hover:text-custom-text-200" />
|
||||
<currentMode.icon className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
}
|
||||
>
|
||||
@ -138,13 +153,43 @@ export const IssuePeekOverviewHeader: FC<PeekOverviewHeaderProps> = observer((pr
|
||||
{currentUser && !isArchived && (
|
||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
)}
|
||||
<button onClick={handleCopyText}>
|
||||
<Tooltip tooltipContent="Copy link">
|
||||
<button type="button" onClick={handleCopyText}>
|
||||
<Link2 className="h-4 w-4 -rotate-45 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{isArchivingAllowed && (
|
||||
<Tooltip
|
||||
tooltipContent={isInArchivableGroup ? "Archive" : "Only completed or canceled issues can be archived"}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={cn("text-custom-text-300", {
|
||||
"hover:text-custom-text-200": isInArchivableGroup,
|
||||
"cursor-not-allowed text-custom-text-400": !isInArchivableGroup,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (!isInArchivableGroup) return;
|
||||
toggleArchiveIssueModal(true);
|
||||
}}
|
||||
>
|
||||
<ArchiveIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isRestoringAllowed && (
|
||||
<Tooltip tooltipContent="Restore">
|
||||
<button type="button" onClick={handleRestoreIssue}>
|
||||
<RotateCcw className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!disabled && (
|
||||
<button onClick={() => toggleDeleteIssueModal(true)}>
|
||||
<Tooltip tooltipContent="Delete">
|
||||
<button type="button" onClick={() => toggleDeleteIssueModal(true)}>
|
||||
<Trash2 className="h-4 w-4 text-custom-text-300 hover:text-custom-text-200" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FC, Fragment, useEffect, useState, useMemo } from "react";
|
||||
import { FC, useEffect, useState, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
@ -11,7 +11,7 @@ import { TIssue } from "@plane/types";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker";
|
||||
import { ISSUE_UPDATED, ISSUE_DELETED, ISSUE_ARCHIVED, ISSUE_RESTORED } from "constants/event-tracker";
|
||||
|
||||
interface IIssuePeekOverview {
|
||||
is_archived?: boolean;
|
||||
@ -28,6 +28,8 @@ export type TIssuePeekOperations = {
|
||||
showToast?: boolean
|
||||
) => Promise<void>;
|
||||
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
archive: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
restore: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||
addModulesToIssue?: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<void>;
|
||||
@ -55,12 +57,13 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
membership: { currentWorkspaceAllProjectsRole },
|
||||
} = useUser();
|
||||
const {
|
||||
issues: { removeIssue: removeArchivedIssue },
|
||||
issues: { restoreIssue },
|
||||
} = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
const {
|
||||
peekIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
archiveIssue,
|
||||
issue: { getIssueById, fetchIssue },
|
||||
} = useIssueDetail();
|
||||
const { addIssueToCycle, removeIssueFromCycle, addModulesToIssue, removeIssueFromModule, removeModulesFromIssue } =
|
||||
@ -91,7 +94,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
showToast: boolean = true
|
||||
) => {
|
||||
try {
|
||||
const response = await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
if (showToast)
|
||||
setToastAlert({
|
||||
title: "Issue updated successfully",
|
||||
@ -122,9 +125,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
},
|
||||
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
let response;
|
||||
if (is_archived) response = await removeArchivedIssue(workspaceSlug, projectId, issueId);
|
||||
else response = await removeIssue(workspaceSlug, projectId, issueId);
|
||||
removeIssue(workspaceSlug, projectId, issueId);
|
||||
setToastAlert({
|
||||
title: "Issue deleted successfully",
|
||||
type: "success",
|
||||
@ -148,6 +149,58 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
});
|
||||
}
|
||||
},
|
||||
archive: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await archiveIssue(workspaceSlug, projectId, issueId);
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue archived successfully.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_ARCHIVED,
|
||||
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
||||
path: router.asPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be archived. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_ARCHIVED,
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
||||
path: router.asPath,
|
||||
});
|
||||
}
|
||||
},
|
||||
restore: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
await restoreIssue(workspaceSlug, projectId, issueId);
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue restored successfully.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_RESTORED,
|
||||
payload: { id: issueId, state: "SUCCESS", element: "Issue peek-overview" },
|
||||
path: router.asPath,
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Issue could not be restored. Please try again.",
|
||||
});
|
||||
captureIssueEvent({
|
||||
eventName: ISSUE_RESTORED,
|
||||
payload: { id: issueId, state: "FAILED", element: "Issue peek-overview" },
|
||||
path: router.asPath,
|
||||
});
|
||||
}
|
||||
},
|
||||
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||
try {
|
||||
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||
@ -312,7 +365,8 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
fetchIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
removeArchivedIssue,
|
||||
archiveIssue,
|
||||
restoreIssue,
|
||||
addIssueToCycle,
|
||||
removeIssueFromCycle,
|
||||
addModulesToIssue,
|
||||
@ -343,16 +397,14 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
const isLoading = !issue || loader ? true : false;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<IssueView
|
||||
workspaceSlug={peekIssue.workspaceSlug}
|
||||
projectId={peekIssue.projectId}
|
||||
issueId={peekIssue.issueId}
|
||||
isLoading={isLoading}
|
||||
is_archived={is_archived}
|
||||
disabled={is_archived || !is_editable}
|
||||
disabled={!is_editable}
|
||||
issueOperations={issueOperations}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
|
@ -10,13 +10,13 @@ import useToast from "hooks/use-toast";
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
// components
|
||||
import {
|
||||
DeleteArchivedIssueModal,
|
||||
DeleteIssueModal,
|
||||
IssuePeekOverviewHeader,
|
||||
TPeekModes,
|
||||
PeekOverviewIssueDetails,
|
||||
PeekOverviewProperties,
|
||||
TIssueOperations,
|
||||
ArchiveIssueModal,
|
||||
} from "components/issues";
|
||||
import { IssueActivity } from "../issue-detail/issue-activity";
|
||||
// ui
|
||||
@ -44,7 +44,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
setPeekIssue,
|
||||
isAnyModalOpen,
|
||||
isDeleteIssueModalOpen,
|
||||
isArchiveIssueModalOpen,
|
||||
toggleDeleteIssueModal,
|
||||
toggleArchiveIssueModal,
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const issue = getIssueById(issueId);
|
||||
@ -69,8 +71,26 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
};
|
||||
useKeypress("Escape", handleKeyDown);
|
||||
|
||||
const handleRestore = async () => {
|
||||
if (!issueOperations.restore) return;
|
||||
await issueOperations.restore(workspaceSlug, projectId, issueId);
|
||||
removeRoutePeekId();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{issue && !is_archived && (
|
||||
<ArchiveIssueModal
|
||||
isOpen={isArchiveIssueModalOpen}
|
||||
handleClose={() => toggleArchiveIssueModal(false)}
|
||||
data={issue}
|
||||
onSubmit={async () => {
|
||||
if (issueOperations.archive) await issueOperations.archive(workspaceSlug, projectId, issueId);
|
||||
removeRoutePeekId();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{issue && !is_archived && (
|
||||
<DeleteIssueModal
|
||||
isOpen={isDeleteIssueModalOpen}
|
||||
@ -84,7 +104,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
)}
|
||||
|
||||
{issue && is_archived && (
|
||||
<DeleteArchivedIssueModal
|
||||
<DeleteIssueModal
|
||||
data={issue}
|
||||
isOpen={isDeleteIssueModalOpen}
|
||||
handleClose={() => toggleDeleteIssueModal(false)}
|
||||
@ -109,11 +129,11 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
{/* header */}
|
||||
<IssuePeekOverviewHeader
|
||||
peekMode={peekMode}
|
||||
setPeekMode={(value: TPeekModes) => {
|
||||
setPeekMode(value);
|
||||
}}
|
||||
setPeekMode={(value) => setPeekMode(value)}
|
||||
removeRoutePeekId={removeRoutePeekId}
|
||||
toggleDeleteIssueModal={toggleDeleteIssueModal}
|
||||
toggleArchiveIssueModal={toggleArchiveIssueModal}
|
||||
handleRestoreIssue={handleRestore}
|
||||
isArchived={is_archived}
|
||||
issueId={issueId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
@ -137,7 +157,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled}
|
||||
disabled={disabled || is_archived}
|
||||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
@ -147,7 +167,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled}
|
||||
disabled={disabled || is_archived}
|
||||
/>
|
||||
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
@ -161,7 +181,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled}
|
||||
disabled={disabled || is_archived}
|
||||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||
/>
|
||||
@ -179,7 +199,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={disabled}
|
||||
disabled={disabled || is_archived}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -49,7 +49,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// states
|
||||
const [showSnoozeOptions, setshowSnoozeOptions] = React.useState(false);
|
||||
const [showSnoozeOptions, setShowSnoozeOptions] = React.useState(false);
|
||||
// toast alert
|
||||
const { setToastAlert } = useToast();
|
||||
// refs
|
||||
@ -105,7 +105,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: any) => {
|
||||
if (snoozeRef.current && !snoozeRef.current?.contains(event.target)) {
|
||||
setshowSnoozeOptions(false);
|
||||
setShowSnoozeOptions(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside, true);
|
||||
@ -116,6 +116,9 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const notificationField = notification.data.issue_activity.field;
|
||||
const notificationTriggeredBy = notification.triggered_by_details;
|
||||
|
||||
if (isSnoozedTabOpen && new Date(notification.snoozed_till!) < new Date()) return null;
|
||||
|
||||
return (
|
||||
@ -129,7 +132,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
closePopover();
|
||||
}}
|
||||
href={`/${workspaceSlug}/projects/${notification.project}/${
|
||||
notification.data.issue_activity.field === "archived_at" ? "archived-issues" : "issues"
|
||||
notificationField === "archived_at" ? "archived-issues" : "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"
|
||||
@ -139,10 +142,10 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
<span className="absolute left-2 top-1/2 h-1.5 w-1.5 -translate-y-1/2 rounded-full bg-custom-primary-100" />
|
||||
)}
|
||||
<div className="relative h-12 w-12 rounded-full">
|
||||
{notification.triggered_by_details.avatar && notification.triggered_by_details.avatar !== "" ? (
|
||||
{notificationTriggeredBy.avatar && notificationTriggeredBy.avatar !== "" ? (
|
||||
<div className="h-12 w-12 rounded-full">
|
||||
<Image
|
||||
src={notification.triggered_by_details.avatar}
|
||||
src={notificationTriggeredBy.avatar}
|
||||
alt="Profile Image"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
@ -152,10 +155,10 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
) : (
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-custom-background-80">
|
||||
<span className="text-lg font-medium text-custom-text-100">
|
||||
{notification.triggered_by_details.is_bot ? (
|
||||
notification.triggered_by_details.first_name?.[0]?.toUpperCase()
|
||||
) : notification.triggered_by_details.display_name?.[0] ? (
|
||||
notification.triggered_by_details.display_name?.[0]?.toUpperCase()
|
||||
{notificationTriggeredBy.is_bot ? (
|
||||
notificationTriggeredBy.first_name?.[0]?.toUpperCase()
|
||||
) : notificationTriggeredBy.display_name?.[0] ? (
|
||||
notificationTriggeredBy.display_name?.[0]?.toUpperCase()
|
||||
) : (
|
||||
<User2 className="h-4 w-4" />
|
||||
)}
|
||||
@ -168,30 +171,32 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
{!notification.message ? (
|
||||
<div className="w-full break-words text-sm">
|
||||
<span className="font-semibold">
|
||||
{notification.triggered_by_details.is_bot
|
||||
? notification.triggered_by_details.first_name
|
||||
: notification.triggered_by_details.display_name}{" "}
|
||||
{notificationTriggeredBy.is_bot
|
||||
? notificationTriggeredBy.first_name
|
||||
: notificationTriggeredBy.display_name}{" "}
|
||||
</span>
|
||||
{notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.verb}{" "}
|
||||
{notification.data.issue_activity.field === "comment"
|
||||
{!["comment", "archived_at"].includes(notificationField) && notification.data.issue_activity.verb}{" "}
|
||||
{notificationField === "comment"
|
||||
? "commented"
|
||||
: notification.data.issue_activity.field === "None"
|
||||
: notificationField === "archived_at"
|
||||
? notification.data.issue_activity.new_value === "restore"
|
||||
? "restored the issue"
|
||||
: "archived the issue"
|
||||
: notificationField === "None"
|
||||
? null
|
||||
: replaceUnderscoreIfSnakeCase(notification.data.issue_activity.field)}{" "}
|
||||
{notification.data.issue_activity.field !== "comment" && notification.data.issue_activity.field !== "None"
|
||||
? "to"
|
||||
: ""}
|
||||
: replaceUnderscoreIfSnakeCase(notificationField)}{" "}
|
||||
{!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""}
|
||||
<span className="font-semibold">
|
||||
{" "}
|
||||
{notification.data.issue_activity.field !== "None" ? (
|
||||
notification.data.issue_activity.field !== "comment" ? (
|
||||
notification.data.issue_activity.field === "target_date" ? (
|
||||
{notificationField !== "None" ? (
|
||||
notificationField !== "comment" ? (
|
||||
notificationField === "target_date" ? (
|
||||
renderFormattedDate(notification.data.issue_activity.new_value)
|
||||
) : notification.data.issue_activity.field === "attachment" ? (
|
||||
) : notificationField === "attachment" ? (
|
||||
"the issue"
|
||||
) : notification.data.issue_activity.field === "description" ? (
|
||||
) : notificationField === "description" ? (
|
||||
stripAndTruncateHTML(notification.data.issue_activity.new_value, 55)
|
||||
) : (
|
||||
) : notificationField === "archived_at" ? null : (
|
||||
notification.data.issue_activity.new_value
|
||||
)
|
||||
) : (
|
||||
@ -255,7 +260,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setshowSnoozeOptions(true);
|
||||
setShowSnoozeOptions(true);
|
||||
}}
|
||||
className="flex gap-x-2 items-center p-1.5"
|
||||
>
|
||||
@ -280,7 +285,7 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setshowSnoozeOptions(false);
|
||||
setShowSnoozeOptions(false);
|
||||
snoozeOptionOnClick(item.value);
|
||||
}}
|
||||
>
|
||||
|
@ -84,7 +84,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
// store hooks
|
||||
const { theme: themeStore } = useApplication();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
const { currentProjectDetails, addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
|
||||
const { addProjectToFavorites, removeProjectFromFavorites, getProjectById } = useProject();
|
||||
const { getInboxesByProjectId, getInboxById } = useInbox();
|
||||
// states
|
||||
const [leaveProjectModalOpen, setLeaveProjectModal] = useState(false);
|
||||
@ -271,13 +271,12 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
|
||||
{project.archive_in > 0 && (
|
||||
{!isViewerOrGuest && (
|
||||
<CustomMenu.MenuItem>
|
||||
<Link href={`/${workspaceSlug}/projects/${project?.id}/archived-issues/`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<ArchiveIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
<span>Archived Issues</span>
|
||||
<span>Archived issues</span>
|
||||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
@ -286,7 +285,7 @@ export const ProjectSidebarListItem: React.FC<Props> = observer((props) => {
|
||||
<Link href={`/${workspaceSlug}/projects/${project?.id}/draft-issues/`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<PenSquare className="h-3.5 w-3.5 stroke-[1.5] text-custom-text-300" />
|
||||
<span>Draft Issues</span>
|
||||
<span>Draft issues</span>
|
||||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
|
@ -244,9 +244,9 @@ export const EMPTY_ISSUE_STATE_DETAILS = {
|
||||
key: "archived",
|
||||
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.",
|
||||
"Archived issues help you remove issues you completed or canceled from focus. You can set automation to auto archive issues and find them here.",
|
||||
primaryButton: {
|
||||
text: "Set Automation",
|
||||
text: "Set automation",
|
||||
},
|
||||
},
|
||||
draft: {
|
||||
|
@ -127,8 +127,7 @@ export const getIssueEventPayload = (props: IssueEventProps) => {
|
||||
return eventPayload;
|
||||
};
|
||||
|
||||
export const getProjectStateEventPayload = (payload: any) => {
|
||||
return {
|
||||
export const getProjectStateEventPayload = (payload: any) => ({
|
||||
workspace_id: payload.workspace_id,
|
||||
project_id: payload.id,
|
||||
state_id: payload.id,
|
||||
@ -139,8 +138,7 @@ export const getProjectStateEventPayload = (payload: any) => {
|
||||
default: payload.default,
|
||||
state: payload.state,
|
||||
element: payload.element,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
// Workspace crud Events
|
||||
export const WORKSPACE_CREATED = "Workspace created";
|
||||
@ -169,6 +167,8 @@ export const MODULE_LINK_DELETED = "Module link deleted";
|
||||
export const ISSUE_CREATED = "Issue created";
|
||||
export const ISSUE_UPDATED = "Issue updated";
|
||||
export const ISSUE_DELETED = "Issue deleted";
|
||||
export const ISSUE_ARCHIVED = "Issue archived";
|
||||
export const ISSUE_RESTORED = "Issue restored";
|
||||
export const ISSUE_OPENED = "Issue opened";
|
||||
// Project State Events
|
||||
export const STATE_CREATED = "State created";
|
||||
@ -218,7 +218,7 @@ export const NOTIFICATION_SNOOZED = "Notification snoozed";
|
||||
export const NOTIFICATION_READ = "Notification marked read";
|
||||
export const UNREAD_NOTIFICATIONS = "Unread notifications viewed";
|
||||
export const NOTIFICATIONS_READ = "All notifications marked read";
|
||||
export const SNOOZED_NOTIFICATIONS= "Snoozed notifications viewed";
|
||||
export const SNOOZED_NOTIFICATIONS = "Snoozed notifications viewed";
|
||||
export const ARCHIVED_NOTIFICATIONS = "Archived notifications viewed";
|
||||
// Groups
|
||||
export const GROUP_WORKSPACE = "Workspace_metrics";
|
||||
|
@ -57,11 +57,11 @@ export const MONTHS = [
|
||||
export const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
export const PROJECT_AUTOMATION_MONTHS = [
|
||||
{ label: "1 Month", value: 1 },
|
||||
{ label: "3 Months", value: 3 },
|
||||
{ label: "6 Months", value: 6 },
|
||||
{ label: "9 Months", value: 9 },
|
||||
{ label: "12 Months", value: 12 },
|
||||
{ label: "1 month", value: 1 },
|
||||
{ label: "3 months", value: 3 },
|
||||
{ label: "6 months", value: 6 },
|
||||
{ label: "9 months", value: 9 },
|
||||
{ label: "12 months", value: 12 },
|
||||
];
|
||||
|
||||
export const PROJECT_UNSPLASH_COVERS = [
|
||||
|
@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import { useIssueDetail, useIssues, useProject } from "hooks/store";
|
||||
import { useIssueDetail, useIssues, useProject, useUser } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// components
|
||||
@ -12,13 +12,14 @@ import { IssueDetailRoot } from "components/issues";
|
||||
import { ProjectArchivedIssueDetailsHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
// ui
|
||||
import { ArchiveIcon, Loader } from "@plane/ui";
|
||||
import { ArchiveIcon, Button, Loader } from "@plane/ui";
|
||||
// icons
|
||||
import { History } from "lucide-react";
|
||||
import { RotateCcw } from "lucide-react";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "constants/issue";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
|
||||
const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
@ -32,10 +33,13 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
const {
|
||||
issues: { removeIssueFromArchived },
|
||||
issues: { restoreIssue },
|
||||
} = useIssues(EIssuesStoreType.ARCHIVED);
|
||||
const { setToastAlert } = useToast();
|
||||
const { getProjectById } = useProject();
|
||||
const {
|
||||
membership: { currentProjectRole },
|
||||
} = useUser();
|
||||
|
||||
const { isLoading } = useSWR(
|
||||
workspaceSlug && projectId && archivedIssueId
|
||||
@ -46,18 +50,21 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||
: null
|
||||
);
|
||||
|
||||
const issue = getIssueById(archivedIssueId?.toString() || "") || undefined;
|
||||
const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined;
|
||||
// derived values
|
||||
const issue = archivedIssueId ? getIssueById(archivedIssueId.toString()) : undefined;
|
||||
const project = issue ? getProjectById(issue?.project_id) : undefined;
|
||||
const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined;
|
||||
// auth
|
||||
const canRestoreIssue = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
if (!issue) return <></>;
|
||||
|
||||
const handleUnArchive = async () => {
|
||||
const handleRestore = async () => {
|
||||
if (!workspaceSlug || !projectId || !archivedIssueId) return;
|
||||
|
||||
setIsRestoring(true);
|
||||
|
||||
await removeIssueFromArchived(workspaceSlug as string, projectId as string, archivedIssueId as string)
|
||||
await restoreIssue(workspaceSlug.toString(), projectId.toString(), archivedIssueId.toString())
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
@ -102,21 +109,22 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||
</Loader>
|
||||
) : (
|
||||
<div className="flex h-full overflow-hidden">
|
||||
<div className="h-full w-full space-y-2 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
|
||||
{issue?.archived_at && (
|
||||
<div className="h-full w-full space-y-3 divide-y-2 divide-custom-border-200 overflow-y-auto p-5">
|
||||
{issue?.archived_at && canRestoreIssue && (
|
||||
<div className="flex items-center justify-between gap-2 rounded-md border border-custom-border-200 bg-custom-background-90 px-2.5 py-2 text-sm text-custom-text-200">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArchiveIcon className="h-3.5 w-3.5" />
|
||||
<p>This issue has been archived by Plane.</p>
|
||||
<p>This issue has been archived.</p>
|
||||
</div>
|
||||
<button
|
||||
className="flex items-center gap-2 rounded-md border border-custom-border-200 p-1.5 text-sm"
|
||||
onClick={handleUnArchive}
|
||||
<Button
|
||||
className="flex items-center gap-1.5 rounded-md border border-custom-border-200 p-1.5 text-sm"
|
||||
onClick={handleRestore}
|
||||
disabled={isRestoring}
|
||||
variant="neutral-primary"
|
||||
>
|
||||
<History className="h-3.5 w-3.5" />
|
||||
<span>{isRestoring ? "Restoring..." : "Restore Issue"}</span>
|
||||
</button>
|
||||
<RotateCcw className="h-3 w-3" />
|
||||
<span>{isRestoring ? "Restoring" : "Restore"}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{workspaceSlug && projectId && archivedIssueId && (
|
||||
|
@ -5,44 +5,28 @@ import { observer } from "mobx-react";
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// contexts
|
||||
import { ArchivedIssueLayoutRoot } from "components/issues";
|
||||
// ui
|
||||
import { ArchiveIcon } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectArchivedIssuesHeader } from "components/headers";
|
||||
import { PageHead } from "components/core";
|
||||
// icons
|
||||
import { X } from "lucide-react";
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
// hooks
|
||||
import { useProject } from "hooks/store";
|
||||
|
||||
const ProjectArchivedIssuesPage: NextPageWithLayout = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
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 Issues`;
|
||||
const pageTitle = project?.name && `${project?.name} - Archived issues`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="gap-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
||||
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
|
||||
>
|
||||
<ArchiveIcon className="h-4 w-4" />
|
||||
<span>Archived Issues</span>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<ArchivedIssueLayoutRoot />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { APIService } from "services/api.service";
|
||||
// type
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
// constants
|
||||
import { API_BASE_URL } from "helpers/common.helper";
|
||||
|
||||
export class IssueArchiveService extends APIService {
|
||||
constructor() {
|
||||
@ -18,8 +19,22 @@ export class IssueArchiveService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async unarchiveIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/unarchive/${issueId}/`)
|
||||
async archiveIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string
|
||||
): Promise<{
|
||||
archived_at: string;
|
||||
}> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/archive/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async restoreIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/archive/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
@ -32,7 +47,7 @@ export class IssueArchiveService extends APIService {
|
||||
issueId: string,
|
||||
queries?: any
|
||||
): Promise<TIssue> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issueId}/`, {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/archive/`, {
|
||||
params: queries,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
@ -40,12 +55,4 @@ export class IssueArchiveService extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteArchivedIssue(workspaceSlug: string, projectId: string, issuesId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issuesId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { action, observable, makeObservable, computed, runInAction } from "mobx";
|
||||
import set from "lodash/set";
|
||||
import pull from "lodash/pull";
|
||||
// base class
|
||||
import { IssueHelperStore } from "../helpers/issue-helper.store";
|
||||
// services
|
||||
@ -18,7 +19,7 @@ export interface IArchivedIssues {
|
||||
// actions
|
||||
fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise<TIssue>;
|
||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
removeIssueFromArchived: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
restoreIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
quickAddIssue: undefined;
|
||||
}
|
||||
|
||||
@ -48,7 +49,7 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues
|
||||
// action
|
||||
fetchIssues: action,
|
||||
removeIssue: action,
|
||||
removeIssueFromArchived: action,
|
||||
restoreIssue: action,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueStore = _rootStore;
|
||||
@ -70,7 +71,7 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues
|
||||
const archivedIssueIds = this.issues[projectId];
|
||||
if (!archivedIssueIds) return undefined;
|
||||
|
||||
const _issues = this.rootIssueStore.issues.getIssuesByIds(archivedIssueIds);
|
||||
const _issues = this.rootIssueStore.issues.getIssuesByIds(archivedIssueIds, "archived");
|
||||
if (!_issues) return [];
|
||||
|
||||
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined;
|
||||
@ -113,24 +114,23 @@ export class ArchivedIssues extends IssueHelperStore implements IArchivedIssues
|
||||
try {
|
||||
await this.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
const issueIndex = this.issues[projectId].findIndex((_issueId) => _issueId === issueId);
|
||||
if (issueIndex >= 0)
|
||||
runInAction(() => {
|
||||
this.issues[projectId].splice(issueIndex, 1);
|
||||
pull(this.issues[projectId], issueId);
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
removeIssueFromArchived = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
restoreIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
const response = await this.archivedIssueService.unarchiveIssue(workspaceSlug, projectId, issueId);
|
||||
const response = await this.archivedIssueService.restoreIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
const issueIndex = this.issues[projectId]?.findIndex((_issueId) => _issueId === issueId);
|
||||
if (issueIndex && issueIndex >= 0)
|
||||
runInAction(() => {
|
||||
this.issues[projectId].splice(issueIndex, 1);
|
||||
this.rootStore.issues.updateIssue(issueId, {
|
||||
archived_at: null,
|
||||
});
|
||||
pull(this.issues[projectId], issueId);
|
||||
});
|
||||
|
||||
return response;
|
||||
|
@ -48,6 +48,12 @@ export interface ICycleIssues {
|
||||
issueId: string,
|
||||
cycleId?: string | undefined
|
||||
) => Promise<void>;
|
||||
archiveIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
cycleId?: string | undefined
|
||||
) => Promise<void>;
|
||||
quickAddIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@ -100,6 +106,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
|
||||
createIssue: action,
|
||||
updateIssue: action,
|
||||
removeIssue: action,
|
||||
archiveIssue: action,
|
||||
quickAddIssue: action,
|
||||
addIssueToCycle: action,
|
||||
removeIssueFromCycle: action,
|
||||
@ -127,7 +134,7 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
|
||||
const cycleIssueIds = this.issues[cycleId];
|
||||
if (!cycleIssueIds) return;
|
||||
|
||||
const _issues = this.rootIssueStore.issues.getIssuesByIds(cycleIssueIds);
|
||||
const _issues = this.rootIssueStore.issues.getIssuesByIds(cycleIssueIds, "un-archived");
|
||||
if (!_issues) return [];
|
||||
|
||||
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = [];
|
||||
@ -237,6 +244,26 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
|
||||
}
|
||||
};
|
||||
|
||||
archiveIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
cycleId: string | undefined = undefined
|
||||
) => {
|
||||
try {
|
||||
if (!cycleId) throw new Error("Cycle Id is required");
|
||||
|
||||
await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
this.rootIssueStore.rootStore.cycle.fetchCycleDetails(workspaceSlug, projectId, cycleId);
|
||||
|
||||
runInAction(() => {
|
||||
pull(this.issues[cycleId], issueId);
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
quickAddIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
|
@ -81,7 +81,7 @@ export class DraftIssues extends IssueHelperStore implements IDraftIssues {
|
||||
const draftIssueIds = this.issues[projectId];
|
||||
if (!draftIssueIds) return undefined;
|
||||
|
||||
const _issues = this.rootIssueStore.issues.getIssuesByIds(draftIssueIds);
|
||||
const _issues = this.rootIssueStore.issues.getIssuesByIds(draftIssueIds, "un-archived");
|
||||
if (!_issues) return [];
|
||||
|
||||
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined;
|
||||
|
@ -16,6 +16,7 @@ export interface IIssueStoreActions {
|
||||
) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<TIssue>;
|
||||
addModulesToIssue: (workspaceSlug: string, projectId: string, issueId: string, moduleIds: string[]) => Promise<any>;
|
||||
@ -156,6 +157,9 @@ export class IssueStore implements IIssueStore {
|
||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.rootIssueDetailStore.rootIssueStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.rootIssueDetailStore.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||
await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle(
|
||||
workspaceSlug,
|
||||
|
@ -47,6 +47,7 @@ export interface IIssueDetail
|
||||
isIssueLinkModalOpen: boolean;
|
||||
isParentIssueModalOpen: boolean;
|
||||
isDeleteIssueModalOpen: boolean;
|
||||
isArchiveIssueModalOpen: boolean;
|
||||
isRelationModalOpen: TIssueRelationTypes | null;
|
||||
// computed
|
||||
isAnyModalOpen: boolean;
|
||||
@ -55,6 +56,7 @@ export interface IIssueDetail
|
||||
toggleIssueLinkModal: (value: boolean) => void;
|
||||
toggleParentIssueModal: (value: boolean) => void;
|
||||
toggleDeleteIssueModal: (value: boolean) => void;
|
||||
toggleArchiveIssueModal: (value: boolean) => void;
|
||||
toggleRelationModal: (value: TIssueRelationTypes | null) => void;
|
||||
// store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
@ -76,6 +78,7 @@ export class IssueDetail implements IIssueDetail {
|
||||
isIssueLinkModalOpen: boolean = false;
|
||||
isParentIssueModalOpen: boolean = false;
|
||||
isDeleteIssueModalOpen: boolean = false;
|
||||
isArchiveIssueModalOpen: boolean = false;
|
||||
isRelationModalOpen: TIssueRelationTypes | null = null;
|
||||
// store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
@ -97,6 +100,7 @@ export class IssueDetail implements IIssueDetail {
|
||||
isIssueLinkModalOpen: observable.ref,
|
||||
isParentIssueModalOpen: observable.ref,
|
||||
isDeleteIssueModalOpen: observable.ref,
|
||||
isArchiveIssueModalOpen: observable.ref,
|
||||
isRelationModalOpen: observable.ref,
|
||||
// computed
|
||||
isAnyModalOpen: computed,
|
||||
@ -105,6 +109,7 @@ export class IssueDetail implements IIssueDetail {
|
||||
toggleIssueLinkModal: action,
|
||||
toggleParentIssueModal: action,
|
||||
toggleDeleteIssueModal: action,
|
||||
toggleArchiveIssueModal: action,
|
||||
toggleRelationModal: action,
|
||||
});
|
||||
|
||||
@ -128,6 +133,7 @@ export class IssueDetail implements IIssueDetail {
|
||||
this.isIssueLinkModalOpen ||
|
||||
this.isParentIssueModalOpen ||
|
||||
this.isDeleteIssueModalOpen ||
|
||||
this.isArchiveIssueModalOpen ||
|
||||
Boolean(this.isRelationModalOpen)
|
||||
);
|
||||
}
|
||||
@ -137,6 +143,7 @@ export class IssueDetail implements IIssueDetail {
|
||||
toggleIssueLinkModal = (value: boolean) => (this.isIssueLinkModalOpen = value);
|
||||
toggleParentIssueModal = (value: boolean) => (this.isParentIssueModalOpen = value);
|
||||
toggleDeleteIssueModal = (value: boolean) => (this.isDeleteIssueModalOpen = value);
|
||||
toggleArchiveIssueModal = (value: boolean) => (this.isArchiveIssueModalOpen = value);
|
||||
toggleRelationModal = (value: TIssueRelationTypes | null) => (this.isRelationModalOpen = value);
|
||||
|
||||
// issue
|
||||
@ -150,6 +157,8 @@ export class IssueDetail implements IIssueDetail {
|
||||
this.issue.updateIssue(workspaceSlug, projectId, issueId, data);
|
||||
removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.issue.removeIssue(workspaceSlug, projectId, issueId);
|
||||
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
this.issue.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) =>
|
||||
this.issue.addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) =>
|
||||
|
@ -18,7 +18,7 @@ export type IIssueStore = {
|
||||
removeIssue(issueId: string): void;
|
||||
// helper methods
|
||||
getIssueById(issueId: string): undefined | TIssue;
|
||||
getIssuesByIds(issueIds: string[]): undefined | Record<string, TIssue>; // Record defines issue_id as key and TIssue as value
|
||||
getIssuesByIds(issueIds: string[], type: "archived" | "un-archived"): undefined | Record<string, TIssue>; // Record defines issue_id as key and TIssue as value
|
||||
};
|
||||
|
||||
export class IssueStore implements IIssueStore {
|
||||
@ -108,14 +108,17 @@ export class IssueStore implements IIssueStore {
|
||||
/**
|
||||
* @description This method will return the issues from the issuesMap
|
||||
* @param {string[]} issueIds
|
||||
* @param {boolean} archivedIssues
|
||||
* @returns {Record<string, TIssue> | undefined}
|
||||
*/
|
||||
getIssuesByIds = computedFn((issueIds: string[]) => {
|
||||
getIssuesByIds = computedFn((issueIds: string[], type: "archived" | "un-archived") => {
|
||||
if (!issueIds || issueIds.length <= 0 || isEmpty(this.issuesMap)) return undefined;
|
||||
const filteredIssues: { [key: string]: TIssue } = {};
|
||||
Object.values(this.issuesMap).forEach((issue) => {
|
||||
if (issueIds.includes(issue.id)) {
|
||||
filteredIssues[issue.id] = issue;
|
||||
// if type is archived then check archived_at is not null
|
||||
// if type is un-archived then check archived_at is null
|
||||
if ((type === "archived" && issue.archived_at) || (type === "un-archived" && !issue.archived_at)) {
|
||||
if (issueIds.includes(issue.id)) filteredIssues[issue.id] = issue;
|
||||
}
|
||||
});
|
||||
return isEmpty(filteredIssues) ? undefined : filteredIssues;
|
||||
|
@ -46,6 +46,12 @@ export interface IModuleIssues {
|
||||
issueId: string,
|
||||
moduleId?: string | undefined
|
||||
) => Promise<void>;
|
||||
archiveIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
moduleId?: string | undefined
|
||||
) => Promise<void>;
|
||||
quickAddIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@ -103,6 +109,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
||||
createIssue: action,
|
||||
updateIssue: action,
|
||||
removeIssue: action,
|
||||
archiveIssue: action,
|
||||
quickAddIssue: action,
|
||||
addIssuesToModule: action,
|
||||
removeIssuesFromModule: action,
|
||||
@ -131,7 +138,7 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
||||
const moduleIssueIds = this.issues[moduleId];
|
||||
if (!moduleIssueIds) return;
|
||||
|
||||
const _issues = this.rootIssueStore.issues.getIssuesByIds(moduleIssueIds);
|
||||
const _issues = this.rootIssueStore.issues.getIssuesByIds(moduleIssueIds, "un-archived");
|
||||
if (!_issues) return [];
|
||||
|
||||
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = [];
|
||||
@ -242,6 +249,26 @@ export class ModuleIssues extends IssueHelperStore implements IModuleIssues {
|
||||
}
|
||||
};
|
||||
|
||||
archiveIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
moduleId: string | undefined = undefined
|
||||
) => {
|
||||
try {
|
||||
if (!moduleId) throw new Error("Module Id is required");
|
||||
|
||||
await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
this.rootIssueStore.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
|
||||
|
||||
runInAction(() => {
|
||||
pull(this.issues[moduleId], issueId);
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
quickAddIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { action, observable, makeObservable, computed, runInAction } from "mobx";
|
||||
import set from "lodash/set";
|
||||
import pull from "lodash/pull";
|
||||
// base class
|
||||
import { IssueHelperStore } from "../helpers/issue-helper.store";
|
||||
// services
|
||||
@ -48,6 +49,12 @@ export interface IProfileIssues {
|
||||
issueId: string,
|
||||
userId?: string | undefined
|
||||
) => Promise<void>;
|
||||
archiveIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
userId?: string | undefined
|
||||
) => Promise<void>;
|
||||
quickAddIssue: undefined;
|
||||
}
|
||||
|
||||
@ -77,6 +84,7 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues {
|
||||
createIssue: action,
|
||||
updateIssue: action,
|
||||
removeIssue: action,
|
||||
archiveIssue: action,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueStore = _rootStore;
|
||||
@ -104,7 +112,7 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues {
|
||||
|
||||
if (!userIssueIds) return;
|
||||
|
||||
const _issues = this.rootStore.issues.getIssuesByIds(userIssueIds);
|
||||
const _issues = this.rootStore.issues.getIssuesByIds(userIssueIds, "un-archived");
|
||||
if (!_issues) return [];
|
||||
|
||||
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues | undefined = undefined;
|
||||
@ -249,4 +257,24 @@ export class ProfileIssues extends IssueHelperStore implements IProfileIssues {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
archiveIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
userId: string | undefined = undefined
|
||||
) => {
|
||||
if (!userId) return;
|
||||
try {
|
||||
await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
const uniqueViewId = `${workspaceSlug}_${this.currentView}`;
|
||||
|
||||
runInAction(() => {
|
||||
pull(this.issues[userId][uniqueViewId], issueId);
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { action, observable, makeObservable, computed, runInAction } from "mobx";
|
||||
import set from "lodash/set";
|
||||
import pull from "lodash/pull";
|
||||
// base class
|
||||
import { IssueHelperStore } from "../helpers/issue-helper.store";
|
||||
// services
|
||||
@ -41,6 +42,12 @@ export interface IProjectViewIssues {
|
||||
issueId: string,
|
||||
viewId?: string | undefined
|
||||
) => Promise<void>;
|
||||
archiveIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
viewId?: string | undefined
|
||||
) => Promise<void>;
|
||||
quickAddIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@ -75,6 +82,7 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI
|
||||
createIssue: action,
|
||||
updateIssue: action,
|
||||
removeIssue: action,
|
||||
archiveIssue: action,
|
||||
quickAddIssue: action,
|
||||
});
|
||||
// root store
|
||||
@ -98,7 +106,7 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI
|
||||
const viewIssueIds = this.issues[viewId];
|
||||
if (!viewIssueIds) return;
|
||||
|
||||
const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds);
|
||||
const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds, "un-archived");
|
||||
if (!_issues) return [];
|
||||
|
||||
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = [];
|
||||
@ -210,6 +218,26 @@ export class ProjectViewIssues extends IssueHelperStore implements IProjectViewI
|
||||
}
|
||||
};
|
||||
|
||||
archiveIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
viewId: string | undefined = undefined
|
||||
) => {
|
||||
try {
|
||||
if (!viewId) throw new Error("View Id is required");
|
||||
|
||||
await this.rootIssueStore.projectIssues.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
pull(this.issues[viewId], issueId);
|
||||
});
|
||||
} catch (error) {
|
||||
this.fetchIssues(workspaceSlug, projectId, "mutation");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
quickAddIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
|
@ -6,7 +6,7 @@ import concat from "lodash/concat";
|
||||
// base class
|
||||
import { IssueHelperStore } from "../helpers/issue-helper.store";
|
||||
// services
|
||||
import { IssueService } from "services/issue/issue.service";
|
||||
import { IssueService, IssueArchiveService } from "services/issue";
|
||||
// types
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
import { TIssue, TGroupedIssues, TSubGroupedIssues, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types";
|
||||
@ -23,6 +23,7 @@ export interface IProjectIssues {
|
||||
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
|
||||
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||
removeIssue: (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>;
|
||||
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
|
||||
}
|
||||
@ -40,6 +41,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
|
||||
rootIssueStore: IIssueRootStore;
|
||||
// services
|
||||
issueService;
|
||||
issueArchiveService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
super(_rootStore);
|
||||
@ -54,6 +56,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
|
||||
createIssue: action,
|
||||
updateIssue: action,
|
||||
removeIssue: action,
|
||||
archiveIssue: action,
|
||||
removeBulkIssues: action,
|
||||
quickAddIssue: action,
|
||||
});
|
||||
@ -61,6 +64,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
|
||||
this.rootIssueStore = _rootStore;
|
||||
// services
|
||||
this.issueService = new IssueService();
|
||||
this.issueArchiveService = new IssueArchiveService();
|
||||
}
|
||||
|
||||
get groupedIssueIds() {
|
||||
@ -78,7 +82,7 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
|
||||
const projectIssueIds = this.issues[projectId];
|
||||
if (!projectIssueIds) return;
|
||||
|
||||
const _issues = this.rootStore.issues.getIssuesByIds(projectIssueIds);
|
||||
const _issues = this.rootStore.issues.getIssuesByIds(projectIssueIds, "un-archived");
|
||||
if (!_issues) return [];
|
||||
|
||||
let issues: TGroupedIssues | TSubGroupedIssues | TUnGroupedIssues = [];
|
||||
@ -165,6 +169,21 @@ export class ProjectIssues extends IssueHelperStore implements IProjectIssues {
|
||||
}
|
||||
};
|
||||
|
||||
archiveIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
try {
|
||||
const response = await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
this.rootStore.issues.updateIssue(issueId, {
|
||||
archived_at: response.archived_at,
|
||||
});
|
||||
pull(this.issues[projectId], issueId);
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
quickAddIssue = async (workspaceSlug: string, projectId: string, data: TIssue) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { action, observable, makeObservable, computed, runInAction } from "mobx";
|
||||
import set from "lodash/set";
|
||||
import pull from "lodash/pull";
|
||||
// base class
|
||||
import { IssueHelperStore } from "../helpers/issue-helper.store";
|
||||
// services
|
||||
import { WorkspaceService } from "services/workspace.service";
|
||||
import { IssueService } from "services/issue";
|
||||
import { IssueService, IssueArchiveService } from "services/issue";
|
||||
// types
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
import { TIssue, TLoader, TUnGroupedIssues, ViewFlags } from "@plane/types";
|
||||
@ -37,6 +38,12 @@ export interface IWorkspaceIssues {
|
||||
issueId: string,
|
||||
viewId?: string | undefined
|
||||
) => Promise<void>;
|
||||
archiveIssue: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
viewId?: string | undefined
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssues {
|
||||
@ -52,6 +59,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
|
||||
// service
|
||||
workspaceService;
|
||||
issueService;
|
||||
issueArchiveService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
super(_rootStore);
|
||||
@ -67,12 +75,14 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
|
||||
createIssue: action,
|
||||
updateIssue: action,
|
||||
removeIssue: action,
|
||||
archiveIssue: action,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueStore = _rootStore;
|
||||
// services
|
||||
this.workspaceService = new WorkspaceService();
|
||||
this.issueService = new IssueService();
|
||||
this.issueArchiveService = new IssueArchiveService();
|
||||
}
|
||||
|
||||
get groupedIssueIds() {
|
||||
@ -91,7 +101,7 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
|
||||
|
||||
if (!viewIssueIds) return { dataViewId: viewId, issueIds: undefined };
|
||||
|
||||
const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds);
|
||||
const _issues = this.rootStore.issues.getIssuesByIds(viewIssueIds, "un-archived");
|
||||
if (!_issues) return { dataViewId: viewId, issueIds: [] };
|
||||
|
||||
let issueIds: TIssue | TUnGroupedIssues | undefined = undefined;
|
||||
@ -196,4 +206,28 @@ export class WorkspaceIssues extends IssueHelperStore implements IWorkspaceIssue
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
archiveIssue = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string,
|
||||
viewId: string | undefined = undefined
|
||||
) => {
|
||||
try {
|
||||
if (!viewId) throw new Error("View id is required");
|
||||
|
||||
const uniqueViewId = `${workspaceSlug}_${viewId}`;
|
||||
|
||||
const response = await this.issueArchiveService.archiveIssue(workspaceSlug, projectId, issueId);
|
||||
|
||||
runInAction(() => {
|
||||
this.rootStore.issues.updateIssue(issueId, {
|
||||
archived_at: response.archived_at,
|
||||
});
|
||||
pull(this.issues[uniqueViewId], issueId);
|
||||
});
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user