mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
feat: auto-archive and auto-close (#1502)
* chore: issue archive services and types added * chore: project type and constant updated * feat: auto-close and auto-archive feature added * feat: implement rendering of archived issues * feat: implemented rendering of only list view for archived issues , chore: update types and services * feat: implemented archive issue detail page and unarchive issue functionality , chore: refactor code * feat: activity for issue archive and issue restore added * fix: redirection and delete fix * fix: merge conflict * fix: restore issue redirection fix * fix: disable modification of issue properties for archived issues, style: disable properties styling * fix: hide empty group, switch to list view on redirct to archived issues * fix: remove unnecessary header buttons for archived issue * fix: auto-close dropdown fix
This commit is contained in:
parent
275942a246
commit
c9cbca5ec8
96
apps/app/components/automation/auto-archive-automation.tsx
Normal file
96
apps/app/components/automation/auto-archive-automation.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
// component
|
||||
import { CustomSelect, ToggleSwitch } from "components/ui";
|
||||
import { SelectMonthModal } from "components/automation";
|
||||
// icons
|
||||
import { ChevronDownIcon } from "@heroicons/react/24/outline";
|
||||
// constants
|
||||
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
|
||||
type Props = {
|
||||
projectDetails: IProject | undefined;
|
||||
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||
};
|
||||
|
||||
export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => {
|
||||
const [monthModal, setmonthModal] = useState(false);
|
||||
|
||||
const initialValues: Partial<IProject> = { archive_in: 1 };
|
||||
return (
|
||||
<>
|
||||
<SelectMonthModal
|
||||
type="auto-archive"
|
||||
initialValues={initialValues}
|
||||
isOpen={monthModal}
|
||||
handleClose={() => setmonthModal(false)}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-brand-base bg-brand-base">
|
||||
<div className="flex items-center justify-between gap-x-8 gap-y-2 ">
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<h4 className="text-lg font-semibold">Auto-archive closed issues</h4>
|
||||
<p className="text-sm text-brand-secondary">
|
||||
Plane will automatically archive issues that have been completed or canceled for the
|
||||
configured time period
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
value={projectDetails?.archive_in !== 0}
|
||||
onChange={() => {
|
||||
if (projectDetails?.archive_in === 0) {
|
||||
handleChange({ archive_in: 1 });
|
||||
} else {
|
||||
handleChange({ archive_in: 0 });
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
{projectDetails?.archive_in !== 0 && (
|
||||
<div className="flex items-center justify-between gap-2 w-full">
|
||||
<div className="w-1/2 text-base font-medium">
|
||||
Auto-archive issues that are closed for
|
||||
</div>
|
||||
<div className="w-1/2 ">
|
||||
<CustomSelect
|
||||
value={projectDetails?.archive_in}
|
||||
customButton={
|
||||
<button className="flex w-full items-center justify-between gap-1 rounded-md border border-brand-base shadow-sm duration-300 text-brand-secondary hover:text-brand-base hover:bg-brand-surface-2 focus:outline-none px-3 py-2 text-sm text-left">
|
||||
{`${projectDetails?.archive_in} Months`}
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</button>
|
||||
}
|
||||
onChange={(val: number) => {
|
||||
handleChange({ archive_in: val });
|
||||
}}
|
||||
input
|
||||
width="w-full"
|
||||
>
|
||||
<>
|
||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||
<CustomSelect.Option key={month.label} value={month.value}>
|
||||
{month.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-custom-background-80"
|
||||
onClick={() => setmonthModal(true)}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-1 text-custom-text-200">
|
||||
<span>Customize Time Range</span>
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
</CustomSelect>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
193
apps/app/components/automation/auto-close-automation.tsx
Normal file
193
apps/app/components/automation/auto-close-automation.tsx
Normal file
@ -0,0 +1,193 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// component
|
||||
import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
|
||||
import { SelectMonthModal } from "components/automation";
|
||||
// icons
|
||||
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||
import { getStateGroupIcon } from "components/icons";
|
||||
// services
|
||||
import stateService from "services/state.service";
|
||||
// constants
|
||||
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
|
||||
import { STATES_LIST } from "constants/fetch-keys";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
// helper
|
||||
import { getStatesList } from "helpers/state.helper";
|
||||
|
||||
type Props = {
|
||||
projectDetails: IProject | undefined;
|
||||
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||
};
|
||||
|
||||
export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => {
|
||||
const [monthModal, setmonthModal] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { data: stateGroups } = useSWR(
|
||||
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
const states = getStatesList(stateGroups ?? {});
|
||||
|
||||
const options = states
|
||||
?.filter((state) => state.group === "cancelled")
|
||||
.map((state) => ({
|
||||
value: state.id,
|
||||
query: state.name,
|
||||
content: (
|
||||
<div className="flex items-center gap-2">
|
||||
{getStateGroupIcon(state.group, "16", "16", state.color)}
|
||||
{state.name}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const multipleOptions = options.length > 1;
|
||||
|
||||
const defaultState = stateGroups && stateGroups.cancelled ? stateGroups.cancelled[0].id : null;
|
||||
|
||||
const selectedOption = states?.find(
|
||||
(s) => s.id === projectDetails?.default_state ?? defaultState
|
||||
);
|
||||
const currentDefaultState = states.find((s) => s.id === defaultState);
|
||||
|
||||
const initialValues: Partial<IProject> = {
|
||||
close_in: 1,
|
||||
default_state: defaultState,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SelectMonthModal
|
||||
type="auto-close"
|
||||
initialValues={initialValues}
|
||||
isOpen={monthModal}
|
||||
handleClose={() => setmonthModal(false)}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-brand-base bg-brand-base">
|
||||
<div className="flex items-center justify-between gap-x-8 gap-y-2 ">
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<h4 className="text-lg font-semibold">Auto-close inactive issues</h4>
|
||||
<p className="text-sm text-brand-secondary">
|
||||
Plane will automatically close the issues that have not been updated for the
|
||||
configured time period.
|
||||
</p>
|
||||
</div>
|
||||
<ToggleSwitch
|
||||
value={projectDetails?.close_in !== 0}
|
||||
onChange={() => {
|
||||
if (projectDetails?.close_in === 0) {
|
||||
handleChange({ close_in: 1, default_state: defaultState });
|
||||
} else {
|
||||
handleChange({ close_in: 0, default_state: null });
|
||||
}
|
||||
}}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
{projectDetails?.close_in !== 0 && (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<div className="flex items-center justify-between gap-2 w-full">
|
||||
<div className="w-1/2 text-base font-medium">
|
||||
Auto-close issues that are inactive for
|
||||
</div>
|
||||
<div className="w-1/2 ">
|
||||
<CustomSelect
|
||||
value={projectDetails?.close_in}
|
||||
customButton={
|
||||
<button className="flex w-full items-center justify-between gap-1 rounded-md border border-brand-base shadow-sm duration-300 text-brand-secondary hover:text-brand-base hover:bg-brand-surface-2 focus:outline-none px-3 py-2 text-sm text-left">
|
||||
{`${projectDetails?.close_in} Months`}
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
</button>
|
||||
}
|
||||
onChange={(val: number) => {
|
||||
handleChange({ close_in: val });
|
||||
}}
|
||||
input
|
||||
width="w-full"
|
||||
>
|
||||
<>
|
||||
{PROJECT_AUTOMATION_MONTHS.map((month) => (
|
||||
<CustomSelect.Option key={month.label} value={month.value}>
|
||||
{month.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full select-none items-center rounded py-2 px-1 hover:bg-custom-background-80"
|
||||
onClick={() => setmonthModal(true)}
|
||||
>
|
||||
<span className="flex items-center justify-start gap-1 text-custom-text-200">
|
||||
<span>Customize Time Range</span>
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
</CustomSelect>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2 w-full">
|
||||
<div className="w-1/2 text-base font-medium">Auto-close Status</div>
|
||||
<div className="w-1/2 ">
|
||||
<CustomSearchSelect
|
||||
value={
|
||||
projectDetails?.default_state ? projectDetails?.default_state : defaultState
|
||||
}
|
||||
customButton={
|
||||
<button
|
||||
className={`flex w-full items-center justify-between gap-1 rounded-md border border-brand-base shadow-sm duration-300 text-brand-secondary hover:text-brand-base hover:bg-brand-surface-2 focus:outline-none px-3 py-2 text-sm text-left ${
|
||||
!multipleOptions ? "opacity-60" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedOption ? (
|
||||
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
|
||||
) : currentDefaultState ? (
|
||||
getStateGroupIcon(
|
||||
currentDefaultState.group,
|
||||
"16",
|
||||
"16",
|
||||
currentDefaultState.color
|
||||
)
|
||||
) : (
|
||||
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
)}
|
||||
{selectedOption?.name
|
||||
? selectedOption.name
|
||||
: currentDefaultState?.name ?? (
|
||||
<span className="text-custom-text-200">State</span>
|
||||
)}
|
||||
</div>
|
||||
{multipleOptions && (
|
||||
<ChevronDownIcon className="h-3 w-3" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
onChange={(val: string) => {
|
||||
handleChange({ default_state: val });
|
||||
}}
|
||||
options={options}
|
||||
disabled={!multipleOptions}
|
||||
dropdownWidth="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
3
apps/app/components/automation/index.ts
Normal file
3
apps/app/components/automation/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./auto-close-automation";
|
||||
export * from "./auto-archive-automation";
|
||||
export * from "./select-month-modal";
|
147
apps/app/components/automation/select-month-modal.tsx
Normal file
147
apps/app/components/automation/select-month-modal.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
|
||||
// types
|
||||
import type { IProject } from "types";
|
||||
|
||||
// types
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
type: "auto-close" | "auto-archive";
|
||||
initialValues: Partial<IProject>;
|
||||
handleClose: () => void;
|
||||
handleChange: (formData: Partial<IProject>) => Promise<void>;
|
||||
};
|
||||
|
||||
export const SelectMonthModal: React.FC<Props> = ({
|
||||
type,
|
||||
initialValues,
|
||||
isOpen,
|
||||
handleClose,
|
||||
handleChange,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
} = useForm<IProject>({
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
handleClose();
|
||||
reset(initialValues);
|
||||
};
|
||||
|
||||
const onSubmit = (formData: Partial<IProject>) => {
|
||||
if (!workspaceSlug && !projectId) return;
|
||||
handleChange(formData);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const inputSection = (name: string) => (
|
||||
<div className="relative flex flex-col gap-1 justify-center w-full">
|
||||
<Input
|
||||
type="number"
|
||||
id={name}
|
||||
name={name}
|
||||
placeholder="Enter Months"
|
||||
autoComplete="off"
|
||||
register={register}
|
||||
width="full"
|
||||
validations={{
|
||||
required: "Select a month between 1 and 12.",
|
||||
min: 1,
|
||||
max: 12,
|
||||
}}
|
||||
style={{ appearance: "none" }}
|
||||
/>
|
||||
<span className="absolute text-sm text-custom-text-200 top-2.5 right-8">Months</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-30" onClose={onClose}>
|
||||
<Transition.Child
|
||||
as={React.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-[#131313] bg-opacity-50 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={React.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 rounded-lg bg-custom-background-90 px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div>
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
||||
>
|
||||
Customize Time Range
|
||||
</Dialog.Title>
|
||||
<div className="mt-8 flex items-center gap-2">
|
||||
<div className="flex w-full flex-col gap-1 justify-center">
|
||||
{type === "auto-close" ? (
|
||||
<>
|
||||
{inputSection("close_in")}
|
||||
{errors.close_in && (
|
||||
<span className="text-sm px-1 text-red-500">
|
||||
Select a month between 1 and 12.
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{inputSection("archive_in")}
|
||||
{errors.archive_in && (
|
||||
<span className="text-sm px-1 text-red-500">
|
||||
Select a month between 1 and 12.
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||
{isSubmitting ? "Submitting..." : "Submit"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
@ -23,6 +23,7 @@ import { renderShortDateWithYearFormat, timeAgo } from "helpers/date-time.helper
|
||||
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||
// types
|
||||
import RemirrorRichTextEditor from "components/rich-text-editor";
|
||||
import { Icon } from "components/ui";
|
||||
|
||||
const activityDetails: {
|
||||
[key: string]: {
|
||||
@ -105,6 +106,10 @@ const activityDetails: {
|
||||
message: "updated the attachment",
|
||||
icon: <PaperClipIcon className="h-3 w-3 text-custom-text-200 " aria-hidden="true" />,
|
||||
},
|
||||
archived_at: {
|
||||
message: "archived",
|
||||
icon: <Icon iconName="archive" className="text-sm text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
};
|
||||
|
||||
export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
@ -144,6 +149,11 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
action = `${activity.verb} the`;
|
||||
} else if (activity.field === "link") {
|
||||
action = `${activity.verb} the`;
|
||||
} else if (activity.field === "archived_at") {
|
||||
action =
|
||||
activity.new_value && activity.new_value === "restore"
|
||||
? "restored the issue"
|
||||
: "archived the issue";
|
||||
}
|
||||
// for values that are after the action clause
|
||||
let value: any = activity.new_value ? activity.new_value : activity.old_value;
|
||||
@ -205,7 +215,13 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
<div key={activity.id} className="mt-2">
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
{activity.field ? (
|
||||
activity.new_value === "restore" ? (
|
||||
<Icon iconName="history" className="text-sm text-custom-text-200" />
|
||||
) : (
|
||||
activityDetails[activity.field as keyof typeof activityDetails]?.icon
|
||||
)
|
||||
) : activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.first_name}
|
||||
@ -296,14 +312,23 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-3">
|
||||
<div className="text-xs text-custom-text-200">
|
||||
<span className="text-gray font-medium">
|
||||
{activity.actor_detail.first_name}
|
||||
{activity.actor_detail.is_bot
|
||||
? " Bot"
|
||||
: " " + activity.actor_detail.last_name}
|
||||
</span>
|
||||
{activity.field === "archived_at" && activity.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : (
|
||||
<span className="text-gray font-medium">
|
||||
{activity.actor_detail.first_name}
|
||||
{activity.actor_detail.is_bot
|
||||
? " Bot"
|
||||
: " " + activity.actor_detail.last_name}
|
||||
</span>
|
||||
)}
|
||||
<span> {action} </span>
|
||||
<span className="text-xs font-medium text-custom-text-100"> {value} </span>
|
||||
{activity.field !== "archived_at" && (
|
||||
<span className="text-xs font-medium text-custom-text-100">
|
||||
{" "}
|
||||
{value}{" "}
|
||||
</span>
|
||||
)}
|
||||
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -53,6 +53,7 @@ const issueViewOptions: { type: TIssueViewOptions; icon: any }[] = [
|
||||
export const IssuesFilterView: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, viewId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const {
|
||||
issueView,
|
||||
@ -78,22 +79,24 @@ export const IssuesFilterView: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-x-1">
|
||||
{issueViewOptions.map((option) => (
|
||||
<button
|
||||
key={option.type}
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||
issueView === option.type
|
||||
? "bg-custom-sidebar-background-80"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => setIssueView(option.type)}
|
||||
>
|
||||
{option.icon}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{!isArchivedIssues && (
|
||||
<div className="flex items-center gap-x-1">
|
||||
{issueViewOptions.map((option) => (
|
||||
<button
|
||||
key={option.type}
|
||||
type="button"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none hover:bg-custom-sidebar-background-80 duration-300 ${
|
||||
issueView === option.type
|
||||
? "bg-custom-sidebar-background-80"
|
||||
: "text-custom-sidebar-text-200"
|
||||
}`}
|
||||
onClick={() => setIssueView(option.type)}
|
||||
>
|
||||
{option.icon}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<SelectFilters
|
||||
filters={filters}
|
||||
onSelect={(option) => {
|
||||
|
@ -84,6 +84,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -181,7 +182,11 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted;
|
||||
const singleIssuePath = isArchivedIssues
|
||||
? `/${workspaceSlug}/projects/${projectId}/archived-issues/${issue.id}`
|
||||
: `/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`;
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || isCompleted || isArchivedIssues;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -207,11 +212,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||
Copy issue link
|
||||
</ContextMenu.Item>
|
||||
<a
|
||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<a href={singleIssuePath} target="_blank" rel="noreferrer noopener">
|
||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||
Open issue in new tab
|
||||
</ContextMenu.Item>
|
||||
@ -225,7 +226,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
setContextMenuPosition({ x: e.pageX, y: e.pageY });
|
||||
}}
|
||||
>
|
||||
<Link href={`/${workspaceSlug}/projects/${issue?.project_detail?.id}/issues/${issue.id}`}>
|
||||
<Link href={singleIssuePath}>
|
||||
<div className="flex-grow cursor-pointer">
|
||||
<a className="group relative flex items-center gap-2">
|
||||
{properties.key && (
|
||||
@ -247,7 +248,11 @@ export const SingleListIssue: React.FC<Props> = ({
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="flex w-full flex-shrink flex-wrap items-center gap-2 text-xs sm:w-auto">
|
||||
<div
|
||||
className={`flex w-full flex-shrink flex-wrap items-center gap-2 text-xs sm:w-auto ${
|
||||
isArchivedIssues ? "opacity-60" : ""
|
||||
}`}
|
||||
>
|
||||
{properties.priority && (
|
||||
<ViewPrioritySelect
|
||||
issue={issue}
|
||||
|
@ -67,6 +67,7 @@ export const SingleList: React.FC<Props> = ({
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||
|
||||
@ -159,7 +160,9 @@ export const SingleList: React.FC<Props> = ({
|
||||
</span>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
{type === "issue" ? (
|
||||
{isArchivedIssues ? (
|
||||
""
|
||||
) : type === "issue" ? (
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||
|
@ -11,7 +11,7 @@ import useEstimateOption from "hooks/use-estimate-option";
|
||||
// components
|
||||
import { CommentCard } from "components/issues/comment";
|
||||
// ui
|
||||
import { Loader } from "components/ui";
|
||||
import { Icon, Loader } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
CalendarDaysIcon,
|
||||
@ -110,6 +110,10 @@ const activityDetails: {
|
||||
message: "updated the attachment",
|
||||
icon: <PaperClipIcon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
archived_at: {
|
||||
message: "archived",
|
||||
icon: <Icon iconName="archive" className="text-sm text-custom-text-200" aria-hidden="true" />,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {
|
||||
@ -253,6 +257,11 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
activityItem.new_value && activityItem.new_value !== ""
|
||||
? "set the module to"
|
||||
: "removed the module";
|
||||
} else if (activityItem.field === "archived_at") {
|
||||
action =
|
||||
activityItem.new_value && activityItem.new_value === "restore"
|
||||
? "restored the issue"
|
||||
: "archived the issue";
|
||||
}
|
||||
// for values that are after the action clause
|
||||
let value: any = activityItem.new_value ? activityItem.new_value : activityItem.old_value;
|
||||
@ -345,8 +354,16 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
<div className="mt-1.5">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 ring-white">
|
||||
{activityItem.field ? (
|
||||
activityDetails[activityItem.field as keyof typeof activityDetails]
|
||||
?.icon
|
||||
activityItem.new_value === "restore" ? (
|
||||
<Icon
|
||||
iconName="history"
|
||||
className="text-sm text-custom-text-200"
|
||||
/>
|
||||
) : (
|
||||
activityDetails[
|
||||
activityItem.field as keyof typeof activityDetails
|
||||
]?.icon
|
||||
)
|
||||
) : activityItem.actor_detail.avatar &&
|
||||
activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
@ -369,17 +386,24 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-3">
|
||||
<div className="text-xs text-custom-text-200">
|
||||
<span className="text-gray font-medium">
|
||||
{activityItem.actor_detail.first_name}
|
||||
{activityItem.actor_detail.is_bot
|
||||
? " Bot"
|
||||
: " " + activityItem.actor_detail.last_name}
|
||||
</span>
|
||||
{activityItem.field === "archived_at" &&
|
||||
activityItem.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : (
|
||||
<span className="text-gray font-medium">
|
||||
{activityItem.actor_detail.first_name}
|
||||
{activityItem.actor_detail.is_bot
|
||||
? " Bot"
|
||||
: " " + activityItem.actor_detail.last_name}
|
||||
</span>
|
||||
)}
|
||||
<span> {action} </span>
|
||||
<span className="text-xs font-medium text-custom-text-100">
|
||||
{" "}
|
||||
{value}{" "}
|
||||
</span>
|
||||
{activityItem.field !== "archived_at" && (
|
||||
<span className="text-xs font-medium text-custom-text-100">
|
||||
{" "}
|
||||
{value}{" "}
|
||||
</span>
|
||||
)}
|
||||
<span className="whitespace-nowrap">
|
||||
{timeAgo(activityItem.created_at)}
|
||||
</span>
|
||||
|
@ -17,7 +17,11 @@ import { IIssueAttachment } from "types";
|
||||
|
||||
const maxFileSize = 5 * 1024 * 1024; // 5 MB
|
||||
|
||||
export const IssueAttachmentUpload = () => {
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const IssueAttachmentUpload: React.FC<Props> = ({ disabled = false }) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
@ -74,7 +78,7 @@ export const IssueAttachmentUpload = () => {
|
||||
onDrop,
|
||||
maxSize: maxFileSize,
|
||||
multiple: false,
|
||||
disabled: isLoading,
|
||||
disabled: isLoading || disabled,
|
||||
});
|
||||
|
||||
const fileError =
|
||||
@ -85,9 +89,9 @@ export const IssueAttachmentUpload = () => {
|
||||
return (
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={`flex items-center justify-center h-[60px] cursor-pointer border-2 border-dashed text-custom-primary bg-custom-primary/5 text-xs rounded-md px-4 ${
|
||||
className={`flex items-center justify-center h-[60px] border-2 border-dashed text-custom-primary bg-custom-primary/5 text-xs rounded-md px-4 ${
|
||||
isDragActive ? "bg-custom-primary/10 border-custom-primary" : "border-custom-border-100"
|
||||
} ${isDragReject ? "bg-red-100" : ""}`}
|
||||
} ${isDragReject ? "bg-red-100" : ""} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<span className="flex items-center gap-2">
|
||||
|
@ -43,9 +43,10 @@ const defaultValues: Partial<IIssueComment> = {
|
||||
type Props = {
|
||||
issueId: string;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const AddComment: React.FC<Props> = ({ issueId, user }) => {
|
||||
export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false }) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
@ -111,7 +112,7 @@ export const AddComment: React.FC<Props> = ({ issueId, user }) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<SecondaryButton type="submit" disabled={isSubmitting} className="mt-2">
|
||||
<SecondaryButton type="submit" disabled={isSubmitting || disabled} className="mt-2">
|
||||
{isSubmitting ? "Adding..." : "Comment"}
|
||||
</SecondaryButton>
|
||||
</div>
|
||||
|
@ -23,6 +23,7 @@ import type { IIssue, ICurrentUserResponse, ISubIssueResponse } from "types";
|
||||
import {
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
SUB_ISSUES,
|
||||
VIEW_ISSUES,
|
||||
@ -40,6 +41,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const { issueView, params } = useIssuesView();
|
||||
const { params: calendarParams } = useCalendarIssuesView();
|
||||
@ -126,6 +128,28 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
||||
});
|
||||
};
|
||||
|
||||
const handleArchivedIssueDeletion = async () => {
|
||||
setIsDeleteLoading(true);
|
||||
if (!workspaceSlug || !projectId || !data) return;
|
||||
|
||||
await issueServices
|
||||
.deleteArchivedIssue(workspaceSlug as string, projectId as string, data.id)
|
||||
.then(() => {
|
||||
mutate(PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
|
||||
handleClose();
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Issue deleted successfully",
|
||||
});
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/archived-issues/`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
setIsDeleteLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||
@ -177,7 +201,13 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
||||
</span>
|
||||
<div className="flex justify-end gap-2">
|
||||
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
||||
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
||||
<DangerButton
|
||||
onClick={() => {
|
||||
if (isArchivedIssues) handleArchivedIssueDeletion();
|
||||
else handleDeletion();
|
||||
}}
|
||||
loading={isDeleteLoading}
|
||||
>
|
||||
{isDeleteLoading ? "Deleting..." : "Delete Issue"}
|
||||
</DangerButton>
|
||||
</div>
|
||||
|
@ -28,11 +28,16 @@ import { SUB_ISSUES } from "constants/fetch-keys";
|
||||
type Props = {
|
||||
issueDetails: IIssue;
|
||||
submitChanges: (formData: Partial<IIssue>) => Promise<void>;
|
||||
nonEditable?: boolean;
|
||||
};
|
||||
|
||||
export const IssueMainContent: React.FC<Props> = ({ issueDetails, submitChanges }) => {
|
||||
export const IssueMainContent: React.FC<Props> = ({
|
||||
issueDetails,
|
||||
submitChanges,
|
||||
nonEditable = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
const { workspaceSlug, projectId, issueId, archivedIssueId } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
const { memberRole } = useProjectMyMembership();
|
||||
@ -95,23 +100,30 @@ export const IssueMainContent: React.FC<Props> = ({ issueDetails, submitChanges
|
||||
<IssueDescriptionForm
|
||||
issue={issueDetails}
|
||||
handleFormSubmit={submitChanges}
|
||||
isAllowed={memberRole.isMember || memberRole.isOwner}
|
||||
isAllowed={memberRole.isMember || memberRole.isOwner || !nonEditable}
|
||||
/>
|
||||
<div className="mt-2 space-y-2">
|
||||
<SubIssuesList parentIssue={issueDetails} user={user} />
|
||||
<SubIssuesList parentIssue={issueDetails} user={user} disabled={nonEditable} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 py-3">
|
||||
<h3 className="text-lg">Attachments</h3>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<IssueAttachmentUpload />
|
||||
<IssueAttachmentUpload disabled={nonEditable} />
|
||||
<IssueAttachments />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-5 pt-3">
|
||||
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||
<IssueActivitySection issueId={issueId as string} user={user} />
|
||||
<AddComment issueId={issueId as string} user={user} />
|
||||
<IssueActivitySection
|
||||
issueId={(archivedIssueId as string) ?? (issueId as string)}
|
||||
user={user}
|
||||
/>
|
||||
<AddComment
|
||||
issueId={(archivedIssueId as string) ?? (issueId as string)}
|
||||
user={user}
|
||||
disabled={nonEditable}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -20,9 +20,15 @@ type Props = {
|
||||
value: string[];
|
||||
onChange: (val: string[]) => void;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarAssigneeSelect: React.FC<Props> = ({ value, onChange, userAuth }) => {
|
||||
export const SidebarAssigneeSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
@ -53,7 +59,7 @@ export const SidebarAssigneeSelect: React.FC<Props> = ({ value, onChange, userAu
|
||||
),
|
||||
}));
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
|
@ -21,6 +21,7 @@ type Props = {
|
||||
submitChanges: (formData: Partial<IIssue>) => void;
|
||||
watch: UseFormWatch<IIssue>;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarBlockedSelect: React.FC<Props> = ({
|
||||
@ -28,6 +29,7 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
||||
submitChanges,
|
||||
watch,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
||||
|
||||
@ -69,7 +71,7 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -21,6 +21,7 @@ type Props = {
|
||||
submitChanges: (formData: Partial<IIssue>) => void;
|
||||
watch: UseFormWatch<IIssue>;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarBlockerSelect: React.FC<Props> = ({
|
||||
@ -28,6 +29,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
||||
submitChanges,
|
||||
watch,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
||||
|
||||
@ -69,7 +71,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -22,12 +22,14 @@ type Props = {
|
||||
issueDetail: IIssue | undefined;
|
||||
handleCycleChange: (cycle: ICycle) => void;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarCycleSelect: React.FC<Props> = ({
|
||||
issueDetail,
|
||||
handleCycleChange,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
@ -61,7 +63,7 @@ export const SidebarCycleSelect: React.FC<Props> = ({
|
||||
|
||||
const issueCycle = issueDetail?.issue_cycle;
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
|
@ -13,10 +13,16 @@ type Props = {
|
||||
value: number | null;
|
||||
onChange: (val: number | null) => void;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, userAuth }) => {
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
export const SidebarEstimateSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
const { isEstimateActive, estimatePoints } = useEstimateOption();
|
||||
|
||||
@ -46,7 +52,7 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, userAu
|
||||
onChange={onChange}
|
||||
position="right"
|
||||
width="w-full"
|
||||
disabled={isNotAllowed}
|
||||
disabled={isNotAllowed || disabled}
|
||||
>
|
||||
<CustomSelect.Option value={null}>
|
||||
<>
|
||||
|
@ -21,12 +21,14 @@ type Props = {
|
||||
issueDetail: IIssue | undefined;
|
||||
handleModuleChange: (module: IModule) => void;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarModuleSelect: React.FC<Props> = ({
|
||||
issueDetail,
|
||||
handleModuleChange,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, issueId } = router.query;
|
||||
@ -55,7 +57,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({
|
||||
|
||||
const issueModule = issueDetail?.issue_module;
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
|
@ -23,6 +23,7 @@ type Props = {
|
||||
customDisplay: JSX.Element;
|
||||
watch: UseFormWatch<IIssue>;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarParentSelect: React.FC<Props> = ({
|
||||
@ -31,6 +32,7 @@ export const SidebarParentSelect: React.FC<Props> = ({
|
||||
customDisplay,
|
||||
watch,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
||||
|
||||
@ -46,7 +48,7 @@ export const SidebarParentSelect: React.FC<Props> = ({
|
||||
: null
|
||||
);
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
|
@ -14,10 +14,16 @@ type Props = {
|
||||
value: string | null;
|
||||
onChange: (val: string) => void;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, userAuth }) => {
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
export const SidebarPrioritySelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
|
@ -23,9 +23,15 @@ type Props = {
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
userAuth: UserAuth;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, userAuth }) => {
|
||||
export const SidebarStateSelect: React.FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
userAuth,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxIssueId } = router.query;
|
||||
|
||||
@ -39,7 +45,7 @@ export const SidebarStateSelect: React.FC<Props> = ({ value, onChange, userAuth
|
||||
|
||||
const selectedState = states?.find((s) => s.id === value);
|
||||
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
|
@ -73,6 +73,7 @@ type Props = {
|
||||
| "delete"
|
||||
| "all"
|
||||
)[];
|
||||
nonEditable?: boolean;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<IIssueLabels> = {
|
||||
@ -86,6 +87,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
issueDetail,
|
||||
watch: watchIssue,
|
||||
fieldsToShow = ["all"],
|
||||
nonEditable = false,
|
||||
}) => {
|
||||
const [createLabelForm, setCreateLabelForm] = useState(false);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
@ -307,7 +309,8 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y-2 divide-custom-border-100">
|
||||
|
||||
<div className={`divide-y-2 divide-custom-border-100 ${nonEditable ? "opacity-60" : ""}`}>
|
||||
{showFirstSection && (
|
||||
<div className="py-1">
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||
@ -319,6 +322,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
value={value}
|
||||
onChange={(val: string) => submitChanges({ state: val })}
|
||||
userAuth={memberRole}
|
||||
disabled={nonEditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -332,6 +336,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
value={value}
|
||||
onChange={(val: string[]) => submitChanges({ assignees_list: val })}
|
||||
userAuth={memberRole}
|
||||
disabled={nonEditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -345,6 +350,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
value={value}
|
||||
onChange={(val: string) => submitChanges({ priority: val })}
|
||||
userAuth={memberRole}
|
||||
disabled={nonEditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -358,6 +364,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
value={value}
|
||||
onChange={(val: number | null) => submitChanges({ estimate_point: val })}
|
||||
userAuth={memberRole}
|
||||
disabled={nonEditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -389,6 +396,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
}
|
||||
watch={watchIssue}
|
||||
userAuth={memberRole}
|
||||
disabled={nonEditable}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
||||
@ -397,6 +405,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
submitChanges={submitChanges}
|
||||
watch={watchIssue}
|
||||
userAuth={memberRole}
|
||||
disabled={nonEditable}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
|
||||
@ -405,6 +414,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
submitChanges={submitChanges}
|
||||
watch={watchIssue}
|
||||
userAuth={memberRole}
|
||||
disabled={nonEditable}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
||||
@ -427,7 +437,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
})
|
||||
}
|
||||
className="bg-custom-background-90"
|
||||
disabled={isNotAllowed}
|
||||
disabled={isNotAllowed || nonEditable}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@ -443,6 +453,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
issueDetail={issueDetail}
|
||||
handleCycleChange={handleCycleChange}
|
||||
userAuth={memberRole}
|
||||
disabled={nonEditable}
|
||||
/>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && (
|
||||
@ -450,13 +461,14 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
issueDetail={issueDetail}
|
||||
handleModuleChange={handleModuleChange}
|
||||
userAuth={memberRole}
|
||||
disabled={nonEditable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
||||
<div className="space-y-3 py-3">
|
||||
<div className={`space-y-3 py-3 ${nonEditable ? "opacity-60" : ""}`}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex basis-1/2 items-center gap-x-2 text-sm text-custom-text-200">
|
||||
<TagIcon className="h-4 w-4" />
|
||||
@ -503,13 +515,13 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
onChange={(val: any) => submitChanges({ labels_list: val })}
|
||||
className="flex-shrink-0"
|
||||
multiple
|
||||
disabled={isNotAllowed}
|
||||
disabled={isNotAllowed || nonEditable}
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<Listbox.Button
|
||||
className={`flex ${
|
||||
isNotAllowed
|
||||
isNotAllowed || nonEditable
|
||||
? "cursor-not-allowed"
|
||||
: "cursor-pointer hover:bg-custom-background-90"
|
||||
} items-center gap-2 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs text-custom-text-200`}
|
||||
@ -614,11 +626,12 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
<button
|
||||
type="button"
|
||||
className={`flex ${
|
||||
isNotAllowed
|
||||
isNotAllowed || nonEditable
|
||||
? "cursor-not-allowed"
|
||||
: "cursor-pointer hover:bg-custom-background-90"
|
||||
} items-center gap-1 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs text-custom-text-200`}
|
||||
onClick={() => setCreateLabelForm((prevData) => !prevData)}
|
||||
disabled={nonEditable}
|
||||
>
|
||||
{createLabelForm ? (
|
||||
<>
|
||||
@ -709,14 +722,17 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
||||
</div>
|
||||
)}
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
||||
<div className="min-h-[116px] py-1 text-xs">
|
||||
<div className={`min-h-[116px] py-1 text-xs ${nonEditable ? "opacity-60" : ""}`}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h4>Links</h4>
|
||||
{!isNotAllowed && (
|
||||
<button
|
||||
type="button"
|
||||
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90"
|
||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
|
||||
nonEditable ? "cursor-not-allowed" : "cursor-pointer"
|
||||
}`}
|
||||
onClick={() => setLinkModal(true)}
|
||||
disabled={nonEditable}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</button>
|
||||
|
@ -28,9 +28,10 @@ import { PROJECT_ISSUES_LIST, SUB_ISSUES } from "constants/fetch-keys";
|
||||
type Props = {
|
||||
parentIssue: IIssue;
|
||||
user: ICurrentUserResponse | undefined;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SubIssuesList: FC<Props> = ({ parentIssue, user }) => {
|
||||
export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }) => {
|
||||
// states
|
||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||
const [subIssuesListModal, setSubIssuesListModal] = useState(false);
|
||||
@ -180,7 +181,7 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user }) => {
|
||||
|
||||
const completionPercentage = (completedSubIssues / totalSubIssues) * 100;
|
||||
|
||||
const isNotAllowed = memberRole.isGuest || memberRole.isViewer;
|
||||
const isNotAllowed = memberRole.isGuest || memberRole.isViewer || disabled;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
// headless ui
|
||||
import { Disclosure, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { CustomMenu } from "components/ui";
|
||||
import { CustomMenu, Icon } from "components/ui";
|
||||
// icons
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
@ -153,6 +153,18 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
||||
<span>Copy project link</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
{project.archive_in > 0 && (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() =>
|
||||
router.push(`/${workspaceSlug}/projects/${project?.id}/archived-issues/`)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
<Icon iconName="archive" className="h-4 w-4" />
|
||||
<span>Archived Issues</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
|
@ -70,6 +70,13 @@ export const PROJECT_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any)
|
||||
|
||||
return `PROJECT_ISSUES_LIST_WITH_PARAMS_${projectId.toUpperCase()}_${paramsKey}`;
|
||||
};
|
||||
export const PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS = (projectId: string, params?: any) => {
|
||||
if (!params) return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}`;
|
||||
|
||||
const paramsKey = paramsToKey(params);
|
||||
|
||||
return `PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS${projectId.toUpperCase()}_${paramsKey}`;
|
||||
};
|
||||
export const PROJECT_ISSUES_DETAILS = (issueId: string) =>
|
||||
`PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
|
||||
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
|
||||
@ -155,6 +162,8 @@ export const INBOX_ISSUE_DETAILS = (inboxId: string, issueId: string) =>
|
||||
export const ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`;
|
||||
export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`;
|
||||
export const ISSUE_ATTACHMENTS = (issueId: string) => `ISSUE_ATTACHMENTS_${issueId.toUpperCase()}`;
|
||||
export const ARCHIVED_ISSUE_DETAILS = (issueId: string) =>
|
||||
`ARCHIVED_ISSUE_DETAILS_${issueId.toUpperCase()}`;
|
||||
|
||||
// integrations
|
||||
export const APP_INTEGRATIONS = "APP_INTEGRATIONS";
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||
|
||||
export const GROUP_CHOICES = {
|
||||
@ -27,3 +26,11 @@ export const MONTHS = [
|
||||
];
|
||||
|
||||
export const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
export const PROJECT_AUTOMATION_MONTHS = [
|
||||
{ label: "1 Months", value: 1 },
|
||||
{ label: "3 Months", value: 3 },
|
||||
{ label: "6 Months", value: 6 },
|
||||
{ label: "9 Months", value: 9 },
|
||||
{ label: "12 Months", value: 12 },
|
||||
];
|
||||
|
@ -19,6 +19,7 @@ import type { IIssue } from "types";
|
||||
import {
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
STATES_LIST,
|
||||
VIEW_ISSUES,
|
||||
@ -44,6 +45,7 @@ const useIssuesView = () => {
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||
|
||||
const params: any = {
|
||||
order_by: orderBy,
|
||||
@ -73,6 +75,15 @@ const useIssuesView = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: projectArchivedIssues } = useSWR(
|
||||
workspaceSlug && projectId
|
||||
? PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS(projectId as string, params)
|
||||
: null,
|
||||
workspaceSlug && projectId && params
|
||||
? () => issuesService.getArchivedIssues(workspaceSlug as string, projectId as string, params)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: cycleIssues } = useSWR(
|
||||
workspaceSlug && projectId && cycleId && params
|
||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
|
||||
@ -149,6 +160,8 @@ const useIssuesView = () => {
|
||||
? moduleIssues
|
||||
: viewId
|
||||
? viewIssues
|
||||
: isArchivedIssues
|
||||
? projectArchivedIssues
|
||||
: projectIssues;
|
||||
|
||||
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
|
||||
@ -161,10 +174,12 @@ const useIssuesView = () => {
|
||||
cycleIssues,
|
||||
moduleIssues,
|
||||
viewIssues,
|
||||
projectArchivedIssues,
|
||||
groupByProperty,
|
||||
cycleId,
|
||||
moduleId,
|
||||
viewId,
|
||||
isArchivedIssues,
|
||||
emptyStatesObject,
|
||||
]);
|
||||
|
||||
@ -174,12 +189,12 @@ const useIssuesView = () => {
|
||||
|
||||
return {
|
||||
groupedByIssues,
|
||||
issueView,
|
||||
issueView: isArchivedIssues ? "list" : issueView,
|
||||
groupByProperty,
|
||||
setGroupByProperty,
|
||||
orderBy,
|
||||
setOrderBy,
|
||||
showEmptyGroups,
|
||||
showEmptyGroups: isArchivedIssues ? false : showEmptyGroups,
|
||||
setShowEmptyGroups,
|
||||
calendarDateRange,
|
||||
setCalendarDateRange,
|
||||
|
@ -71,6 +71,10 @@ const SettingsNavbar: React.FC<Props> = ({ profilePage = false }) => {
|
||||
label: "Estimates",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/settings/estimates`,
|
||||
},
|
||||
{
|
||||
label: "Automations",
|
||||
href: `/${workspaceSlug}/projects/${projectId}/settings/automations`,
|
||||
},
|
||||
];
|
||||
|
||||
const profileLinks: Array<{
|
||||
|
@ -0,0 +1,211 @@
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
// react-hook-form
|
||||
import { useForm } from "react-hook-form";
|
||||
// services
|
||||
import issuesService from "services/issues.service";
|
||||
// hooks
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import useToast from "hooks/use-toast";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// components
|
||||
import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
|
||||
// ui
|
||||
import { Icon, Loader } from "components/ui";
|
||||
import { Breadcrumbs } from "components/breadcrumbs";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
const defaultValues = {
|
||||
name: "",
|
||||
description: "",
|
||||
description_html: "",
|
||||
estimate_point: null,
|
||||
state: "",
|
||||
assignees_list: [],
|
||||
priority: "low",
|
||||
target_date: new Date().toString(),
|
||||
issue_cycle: null,
|
||||
issue_module: null,
|
||||
labels_list: [],
|
||||
};
|
||||
|
||||
const ArchivedIssueDetailsPage: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, archivedIssueId } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { data: issueDetails, mutate: mutateIssueDetails } = useSWR<IIssue | undefined>(
|
||||
workspaceSlug && projectId && archivedIssueId ? ISSUE_DETAILS(archivedIssueId as string) : null,
|
||||
workspaceSlug && projectId && archivedIssueId
|
||||
? () =>
|
||||
issuesService.retrieveArchivedIssue(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
archivedIssueId as string
|
||||
)
|
||||
: null
|
||||
);
|
||||
|
||||
const { reset, control, watch } = useForm<IIssue>({
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const submitChanges = useCallback(
|
||||
async (formData: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !projectId || !archivedIssueId) return;
|
||||
|
||||
mutate<IIssue>(
|
||||
ISSUE_DETAILS(archivedIssueId as string),
|
||||
(prevData) => {
|
||||
if (!prevData) return prevData;
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
...formData,
|
||||
};
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
const payload: Partial<IIssue> = {
|
||||
...formData,
|
||||
};
|
||||
|
||||
await issuesService
|
||||
.patchIssue(
|
||||
workspaceSlug as string,
|
||||
projectId as string,
|
||||
archivedIssueId as string,
|
||||
payload,
|
||||
user
|
||||
)
|
||||
.then(() => {
|
||||
mutateIssueDetails();
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(archivedIssueId as string));
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
[workspaceSlug, archivedIssueId, projectId, mutateIssueDetails, user]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!issueDetails) return;
|
||||
|
||||
mutate(PROJECT_ISSUES_ACTIVITY(archivedIssueId as string));
|
||||
reset({
|
||||
...issueDetails,
|
||||
assignees_list:
|
||||
issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id),
|
||||
labels_list: issueDetails.labels_list ?? issueDetails.labels,
|
||||
labels: issueDetails.labels_list ?? issueDetails.labels,
|
||||
});
|
||||
}, [issueDetails, reset, archivedIssueId]);
|
||||
|
||||
const handleUnArchive = async () => {
|
||||
if (!workspaceSlug || !projectId || !archivedIssueId) return;
|
||||
|
||||
await issuesService
|
||||
.unArchivedIssue(workspaceSlug as string, projectId as string, archivedIssueId as string)
|
||||
.then(() => {
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue restored successfully.",
|
||||
});
|
||||
router.push(`/${workspaceSlug}/projects/${projectId}/issues/${archivedIssueId}`);
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ProjectAuthorizationWrapper
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
title={`${issueDetails?.project_detail.name ?? "Project"} Issues`}
|
||||
link={`/${workspaceSlug}/projects/${projectId as string}/issues`}
|
||||
/>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
title={`Issue ${issueDetails?.project_detail.identifier ?? "Project"}-${
|
||||
issueDetails?.sequence_id ?? "..."
|
||||
} Details`}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
{issueDetails && projectId ? (
|
||||
<div className="flex h-full">
|
||||
<div className="w-2/3 space-y-2 p-5">
|
||||
{issueDetails.archived_at && (
|
||||
<div className="flex items-center justify-between gap-2 px-2.5 py-2 text-sm border rounded-md text-custom-text-200 border-custom-border-200 bg-custom-background-90">
|
||||
<div className="flex gap-2 items-center">
|
||||
<Icon iconName="archive" className="" />
|
||||
<p>This issue has been archived by Plane.</p>
|
||||
</div>
|
||||
<button
|
||||
className="flex items-center gap-2 p-1.5 text-sm rounded-md border border-custom-border-200"
|
||||
onClick={handleUnArchive}
|
||||
>
|
||||
<Icon iconName="history" />
|
||||
<p>Restore Issue</p>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-5 divide-y-2 divide-custom-border-100 opacity-60">
|
||||
<IssueMainContent
|
||||
issueDetails={issueDetails}
|
||||
submitChanges={submitChanges}
|
||||
nonEditable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/3 space-y-5 border-l border-custom-border-100 p-5">
|
||||
<IssueDetailsSidebar
|
||||
control={control}
|
||||
issueDetail={issueDetails}
|
||||
submitChanges={submitChanges}
|
||||
watch={watch}
|
||||
nonEditable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="flex h-full gap-5 p-5">
|
||||
<div className="basis-2/3 space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="40%" />
|
||||
</div>
|
||||
<div className="basis-1/3 space-y-3">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</ProjectAuthorizationWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArchivedIssueDetailsPage;
|
@ -0,0 +1,77 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import useSWR from "swr";
|
||||
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// contexts
|
||||
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||
// helper
|
||||
import { truncateText } from "helpers/string.helper";
|
||||
// components
|
||||
import { IssuesFilterView, IssuesView } from "components/core";
|
||||
// ui
|
||||
import { Icon } from "components/ui";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// icons
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
// fetch-keys
|
||||
import { PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const ProjectArchivedIssues: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { showEmptyGroups, setShowEmptyGroups } = useIssuesView();
|
||||
|
||||
const { data: projectDetails } = useSWR(
|
||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
||||
workspaceSlug && projectId
|
||||
? () => projectService.getProject(workspaceSlug as string, projectId as string)
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<IssueViewContextProvider>
|
||||
<ProjectAuthorizationWrapper
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
||||
<BreadcrumbItem
|
||||
title={`${truncateText(projectDetails?.name ?? "Project", 12)} Archived Issues`}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
}
|
||||
right={
|
||||
<div className="flex items-center gap-2">
|
||||
<IssuesFilterView />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<div className="flex items-center ga-1 px-4 py-2.5 shadow-sm border-b border-custom-border-100">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
|
||||
className="flex items-center gap-1.5 rounded-full border border-custom-border-100 px-3 py-1.5 text-xs"
|
||||
>
|
||||
<Icon iconName="archive" className="text-base" />
|
||||
<span>Archived Issues</span>
|
||||
|
||||
<XMarkIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
<IssuesView />
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
</IssueViewContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProjectArchivedIssues;
|
@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { mutate } from "swr";
|
||||
|
||||
// services
|
||||
import projectService from "services/project.service";
|
||||
// layouts
|
||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
||||
// hooks
|
||||
import useUserAuth from "hooks/use-user-auth";
|
||||
import useProjectDetails from "hooks/use-project-details";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { SettingsHeader } from "components/project";
|
||||
import { AutoArchiveAutomation, AutoCloseAutomation } from "components/automation";
|
||||
// ui
|
||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||
// types
|
||||
import type { NextPage } from "next";
|
||||
import { IProject } from "types";
|
||||
// constant
|
||||
import { PROJECT_DETAILS } from "constants/fetch-keys";
|
||||
|
||||
const AutomationsSettings: NextPage = () => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { user } = useUserAuth();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { projectDetails } = useProjectDetails();
|
||||
|
||||
const handleChange = async (formData: Partial<IProject>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
|
||||
mutate<IProject>(
|
||||
PROJECT_DETAILS(projectId as string),
|
||||
(prevData) => ({ ...(prevData as IProject), ...formData }),
|
||||
false
|
||||
);
|
||||
|
||||
await projectService
|
||||
.updateProject(workspaceSlug as string, projectId as string, formData, user)
|
||||
.then(() => {
|
||||
mutate(PROJECT_DETAILS(projectId as string));
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Something went wrong. Please try again.",
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ProjectAuthorizationWrapper
|
||||
breadcrumbs={
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem
|
||||
title={`${projectDetails?.name ?? "Project"}`}
|
||||
link={`/${workspaceSlug}/projects/${projectDetails?.id}/issues`}
|
||||
/>
|
||||
<BreadcrumbItem title="Automations Settings" />
|
||||
</Breadcrumbs>
|
||||
}
|
||||
>
|
||||
<div className="p-8">
|
||||
<SettingsHeader />
|
||||
<section className="space-y-5">
|
||||
<AutoCloseAutomation projectDetails={projectDetails} handleChange={handleChange} />
|
||||
<AutoArchiveAutomation projectDetails={projectDetails} handleChange={handleChange} />
|
||||
</section>
|
||||
</div>
|
||||
</ProjectAuthorizationWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutomationsSettings;
|
@ -525,6 +525,52 @@ class ProjectIssuesServices extends APIService {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getArchivedIssues(workspaceSlug: string, projectId: string, queries?: any): Promise<any> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/`, {
|
||||
params: queries,
|
||||
})
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async unArchivedIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<any> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/unarchive/${issueId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async retrieveArchivedIssue(
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
issueId: string
|
||||
): Promise<any> {
|
||||
return this.get(
|
||||
`/api/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/${issueId}/`
|
||||
)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ProjectIssuesServices();
|
||||
|
1
apps/app/types/issues.d.ts
vendored
1
apps/app/types/issues.d.ts
vendored
@ -64,6 +64,7 @@ export interface IIssueLink {
|
||||
}
|
||||
|
||||
export interface IIssue {
|
||||
archived_at: string;
|
||||
assignees: string[];
|
||||
assignee_details: IUser[];
|
||||
assignees_list: string[];
|
||||
|
3
apps/app/types/projects.d.ts
vendored
3
apps/app/types/projects.d.ts
vendored
@ -9,6 +9,8 @@ import type {
|
||||
} from "./";
|
||||
|
||||
export interface IProject {
|
||||
archive_in: number;
|
||||
close_in: number;
|
||||
created_at: Date;
|
||||
created_by: string;
|
||||
cover_image: string | null;
|
||||
@ -18,6 +20,7 @@ export interface IProject {
|
||||
page_view: boolean;
|
||||
inbox_view: boolean;
|
||||
default_assignee: IUser | string | null;
|
||||
default_state: string | null;
|
||||
description: string;
|
||||
emoji: string | null;
|
||||
emoji_and_icon:
|
||||
|
Loading…
Reference in New Issue
Block a user