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