forked from github/plane
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";
|
import { addSpaceIfCamelCase } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import RemirrorRichTextEditor from "components/rich-text-editor";
|
import RemirrorRichTextEditor from "components/rich-text-editor";
|
||||||
|
import { Icon } from "components/ui";
|
||||||
|
|
||||||
const activityDetails: {
|
const activityDetails: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
@ -105,6 +106,10 @@ const activityDetails: {
|
|||||||
message: "updated the attachment",
|
message: "updated the attachment",
|
||||||
icon: <PaperClipIcon className="h-3 w-3 text-custom-text-200 " aria-hidden="true" />,
|
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 }) => (
|
export const Feeds: React.FC<any> = ({ activities }) => (
|
||||||
@ -144,6 +149,11 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
action = `${activity.verb} the`;
|
action = `${activity.verb} the`;
|
||||||
} else if (activity.field === "link") {
|
} else if (activity.field === "link") {
|
||||||
action = `${activity.verb} the`;
|
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
|
// for values that are after the action clause
|
||||||
let value: any = activity.new_value ? activity.new_value : activity.old_value;
|
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 key={activity.id} className="mt-2">
|
||||||
<div className="relative flex items-start space-x-3">
|
<div className="relative flex items-start space-x-3">
|
||||||
<div className="relative px-1">
|
<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
|
<img
|
||||||
src={activity.actor_detail.avatar}
|
src={activity.actor_detail.avatar}
|
||||||
alt={activity.actor_detail.first_name}
|
alt={activity.actor_detail.first_name}
|
||||||
@ -296,14 +312,23 @@ export const Feeds: React.FC<any> = ({ activities }) => (
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1 py-3">
|
<div className="min-w-0 flex-1 py-3">
|
||||||
<div className="text-xs text-custom-text-200">
|
<div className="text-xs text-custom-text-200">
|
||||||
|
{activity.field === "archived_at" && activity.new_value !== "restore" ? (
|
||||||
|
<span className="text-gray font-medium">Plane</span>
|
||||||
|
) : (
|
||||||
<span className="text-gray font-medium">
|
<span className="text-gray font-medium">
|
||||||
{activity.actor_detail.first_name}
|
{activity.actor_detail.first_name}
|
||||||
{activity.actor_detail.is_bot
|
{activity.actor_detail.is_bot
|
||||||
? " Bot"
|
? " Bot"
|
||||||
: " " + activity.actor_detail.last_name}
|
: " " + activity.actor_detail.last_name}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
<span> {action} </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>
|
<span className="whitespace-nowrap">{timeAgo(activity.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,6 +53,7 @@ const issueViewOptions: { type: TIssueViewOptions; icon: any }[] = [
|
|||||||
export const IssuesFilterView: React.FC = () => {
|
export const IssuesFilterView: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, viewId } = router.query;
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
issueView,
|
issueView,
|
||||||
@ -78,6 +79,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{!isArchivedIssues && (
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
{issueViewOptions.map((option) => (
|
{issueViewOptions.map((option) => (
|
||||||
<button
|
<button
|
||||||
@ -94,6 +96,7 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<SelectFilters
|
<SelectFilters
|
||||||
filters={filters}
|
filters={filters}
|
||||||
onSelect={(option) => {
|
onSelect={(option) => {
|
||||||
|
@ -84,6 +84,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -207,11 +212,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
<ContextMenu.Item Icon={LinkIcon} onClick={handleCopyText}>
|
||||||
Copy issue link
|
Copy issue link
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
<a
|
<a href={singleIssuePath} target="_blank" rel="noreferrer noopener">
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
>
|
|
||||||
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
<ContextMenu.Item Icon={ArrowTopRightOnSquareIcon}>
|
||||||
Open issue in new tab
|
Open issue in new tab
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
@ -225,7 +226,7 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
setContextMenuPosition({ x: e.pageX, y: e.pageY });
|
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">
|
<div className="flex-grow cursor-pointer">
|
||||||
<a className="group relative flex items-center gap-2">
|
<a className="group relative flex items-center gap-2">
|
||||||
{properties.key && (
|
{properties.key && (
|
||||||
@ -247,7 +248,11 @@ export const SingleListIssue: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</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 && (
|
{properties.priority && (
|
||||||
<ViewPrioritySelect
|
<ViewPrioritySelect
|
||||||
issue={issue}
|
issue={issue}
|
||||||
|
@ -67,6 +67,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
|
||||||
|
|
||||||
@ -159,7 +160,9 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Disclosure.Button>
|
</Disclosure.Button>
|
||||||
{type === "issue" ? (
|
{isArchivedIssues ? (
|
||||||
|
""
|
||||||
|
) : type === "issue" ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||||
|
@ -11,7 +11,7 @@ import useEstimateOption from "hooks/use-estimate-option";
|
|||||||
// components
|
// components
|
||||||
import { CommentCard } from "components/issues/comment";
|
import { CommentCard } from "components/issues/comment";
|
||||||
// ui
|
// ui
|
||||||
import { Loader } from "components/ui";
|
import { Icon, Loader } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
@ -110,6 +110,10 @@ const activityDetails: {
|
|||||||
message: "updated the attachment",
|
message: "updated the attachment",
|
||||||
icon: <PaperClipIcon className="h-3 w-3 text-custom-text-200" aria-hidden="true" />,
|
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 = {
|
type Props = {
|
||||||
@ -253,6 +257,11 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
|||||||
activityItem.new_value && activityItem.new_value !== ""
|
activityItem.new_value && activityItem.new_value !== ""
|
||||||
? "set the module to"
|
? "set the module to"
|
||||||
: "removed the module";
|
: "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
|
// for values that are after the action clause
|
||||||
let value: any = activityItem.new_value ? activityItem.new_value : activityItem.old_value;
|
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="mt-1.5">
|
||||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 ring-white">
|
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 ring-white">
|
||||||
{activityItem.field ? (
|
{activityItem.field ? (
|
||||||
activityDetails[activityItem.field as keyof typeof activityDetails]
|
activityItem.new_value === "restore" ? (
|
||||||
?.icon
|
<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 &&
|
||||||
activityItem.actor_detail.avatar !== "" ? (
|
activityItem.actor_detail.avatar !== "" ? (
|
||||||
<img
|
<img
|
||||||
@ -369,17 +386,24 @@ export const IssueActivitySection: React.FC<Props> = ({ issueId, user }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1 py-3">
|
<div className="min-w-0 flex-1 py-3">
|
||||||
<div className="text-xs text-custom-text-200">
|
<div className="text-xs text-custom-text-200">
|
||||||
|
{activityItem.field === "archived_at" &&
|
||||||
|
activityItem.new_value !== "restore" ? (
|
||||||
|
<span className="text-gray font-medium">Plane</span>
|
||||||
|
) : (
|
||||||
<span className="text-gray font-medium">
|
<span className="text-gray font-medium">
|
||||||
{activityItem.actor_detail.first_name}
|
{activityItem.actor_detail.first_name}
|
||||||
{activityItem.actor_detail.is_bot
|
{activityItem.actor_detail.is_bot
|
||||||
? " Bot"
|
? " Bot"
|
||||||
: " " + activityItem.actor_detail.last_name}
|
: " " + activityItem.actor_detail.last_name}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
<span> {action} </span>
|
<span> {action} </span>
|
||||||
|
{activityItem.field !== "archived_at" && (
|
||||||
<span className="text-xs font-medium text-custom-text-100">
|
<span className="text-xs font-medium text-custom-text-100">
|
||||||
{" "}
|
{" "}
|
||||||
{value}{" "}
|
{value}{" "}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
<span className="whitespace-nowrap">
|
<span className="whitespace-nowrap">
|
||||||
{timeAgo(activityItem.created_at)}
|
{timeAgo(activityItem.created_at)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -17,7 +17,11 @@ import { IIssueAttachment } from "types";
|
|||||||
|
|
||||||
const maxFileSize = 5 * 1024 * 1024; // 5 MB
|
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 [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -74,7 +78,7 @@ export const IssueAttachmentUpload = () => {
|
|||||||
onDrop,
|
onDrop,
|
||||||
maxSize: maxFileSize,
|
maxSize: maxFileSize,
|
||||||
multiple: false,
|
multiple: false,
|
||||||
disabled: isLoading,
|
disabled: isLoading || disabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fileError =
|
const fileError =
|
||||||
@ -85,9 +89,9 @@ export const IssueAttachmentUpload = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...getRootProps()}
|
{...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"
|
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()} />
|
<input {...getInputProps()} />
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
|
@ -43,9 +43,10 @@ const defaultValues: Partial<IIssueComment> = {
|
|||||||
type Props = {
|
type Props = {
|
||||||
issueId: string;
|
issueId: string;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddComment: React.FC<Props> = ({ issueId, user }) => {
|
export const AddComment: React.FC<Props> = ({ issueId, user, disabled = false }) => {
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
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"}
|
{isSubmitting ? "Adding..." : "Comment"}
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,6 +23,7 @@ import type { IIssue, ICurrentUserResponse, ISubIssueResponse } from "types";
|
|||||||
import {
|
import {
|
||||||
CYCLE_ISSUES_WITH_PARAMS,
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
SUB_ISSUES,
|
SUB_ISSUES,
|
||||||
VIEW_ISSUES,
|
VIEW_ISSUES,
|
||||||
@ -40,6 +41,7 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const { issueView, params } = useIssuesView();
|
const { issueView, params } = useIssuesView();
|
||||||
const { params: calendarParams } = useCalendarIssuesView();
|
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 (
|
return (
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<Transition.Root show={isOpen} as={React.Fragment}>
|
||||||
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
<Dialog as="div" className="relative z-20" onClose={onClose}>
|
||||||
@ -177,7 +201,13 @@ export const DeleteIssueModal: React.FC<Props> = ({ isOpen, handleClose, data, u
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
<SecondaryButton onClick={onClose}>Cancel</SecondaryButton>
|
||||||
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
<DangerButton
|
||||||
|
onClick={() => {
|
||||||
|
if (isArchivedIssues) handleArchivedIssueDeletion();
|
||||||
|
else handleDeletion();
|
||||||
|
}}
|
||||||
|
loading={isDeleteLoading}
|
||||||
|
>
|
||||||
{isDeleteLoading ? "Deleting..." : "Delete Issue"}
|
{isDeleteLoading ? "Deleting..." : "Delete Issue"}
|
||||||
</DangerButton>
|
</DangerButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,11 +28,16 @@ import { SUB_ISSUES } from "constants/fetch-keys";
|
|||||||
type Props = {
|
type Props = {
|
||||||
issueDetails: IIssue;
|
issueDetails: IIssue;
|
||||||
submitChanges: (formData: Partial<IIssue>) => Promise<void>;
|
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 router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId, archivedIssueId } = router.query;
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
const { user } = useUserAuth();
|
||||||
const { memberRole } = useProjectMyMembership();
|
const { memberRole } = useProjectMyMembership();
|
||||||
@ -95,23 +100,30 @@ export const IssueMainContent: React.FC<Props> = ({ issueDetails, submitChanges
|
|||||||
<IssueDescriptionForm
|
<IssueDescriptionForm
|
||||||
issue={issueDetails}
|
issue={issueDetails}
|
||||||
handleFormSubmit={submitChanges}
|
handleFormSubmit={submitChanges}
|
||||||
isAllowed={memberRole.isMember || memberRole.isOwner}
|
isAllowed={memberRole.isMember || memberRole.isOwner || !nonEditable}
|
||||||
/>
|
/>
|
||||||
<div className="mt-2 space-y-2">
|
<div className="mt-2 space-y-2">
|
||||||
<SubIssuesList parentIssue={issueDetails} user={user} />
|
<SubIssuesList parentIssue={issueDetails} user={user} disabled={nonEditable} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-3 py-3">
|
<div className="flex flex-col gap-3 py-3">
|
||||||
<h3 className="text-lg">Attachments</h3>
|
<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">
|
<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 />
|
<IssueAttachments />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-5 pt-3">
|
<div className="space-y-5 pt-3">
|
||||||
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||||
<IssueActivitySection issueId={issueId as string} user={user} />
|
<IssueActivitySection
|
||||||
<AddComment issueId={issueId as string} user={user} />
|
issueId={(archivedIssueId as string) ?? (issueId as string)}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
<AddComment
|
||||||
|
issueId={(archivedIssueId as string) ?? (issueId as string)}
|
||||||
|
user={user}
|
||||||
|
disabled={nonEditable}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -20,9 +20,15 @@ type Props = {
|
|||||||
value: string[];
|
value: string[];
|
||||||
onChange: (val: string[]) => void;
|
onChange: (val: string[]) => void;
|
||||||
userAuth: UserAuth;
|
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 router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
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 (
|
return (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
|
@ -21,6 +21,7 @@ type Props = {
|
|||||||
submitChanges: (formData: Partial<IIssue>) => void;
|
submitChanges: (formData: Partial<IIssue>) => void;
|
||||||
watch: UseFormWatch<IIssue>;
|
watch: UseFormWatch<IIssue>;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarBlockedSelect: React.FC<Props> = ({
|
export const SidebarBlockedSelect: React.FC<Props> = ({
|
||||||
@ -28,6 +29,7 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
|||||||
submitChanges,
|
submitChanges,
|
||||||
watch,
|
watch,
|
||||||
userAuth,
|
userAuth,
|
||||||
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
||||||
|
|
||||||
@ -69,7 +71,7 @@ export const SidebarBlockedSelect: React.FC<Props> = ({
|
|||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -21,6 +21,7 @@ type Props = {
|
|||||||
submitChanges: (formData: Partial<IIssue>) => void;
|
submitChanges: (formData: Partial<IIssue>) => void;
|
||||||
watch: UseFormWatch<IIssue>;
|
watch: UseFormWatch<IIssue>;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarBlockerSelect: React.FC<Props> = ({
|
export const SidebarBlockerSelect: React.FC<Props> = ({
|
||||||
@ -28,6 +29,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
submitChanges,
|
submitChanges,
|
||||||
watch,
|
watch,
|
||||||
userAuth,
|
userAuth,
|
||||||
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
||||||
|
|
||||||
@ -69,7 +71,7 @@ export const SidebarBlockerSelect: React.FC<Props> = ({
|
|||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -22,12 +22,14 @@ type Props = {
|
|||||||
issueDetail: IIssue | undefined;
|
issueDetail: IIssue | undefined;
|
||||||
handleCycleChange: (cycle: ICycle) => void;
|
handleCycleChange: (cycle: ICycle) => void;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarCycleSelect: React.FC<Props> = ({
|
export const SidebarCycleSelect: React.FC<Props> = ({
|
||||||
issueDetail,
|
issueDetail,
|
||||||
handleCycleChange,
|
handleCycleChange,
|
||||||
userAuth,
|
userAuth,
|
||||||
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
@ -61,7 +63,7 @@ export const SidebarCycleSelect: React.FC<Props> = ({
|
|||||||
|
|
||||||
const issueCycle = issueDetail?.issue_cycle;
|
const issueCycle = issueDetail?.issue_cycle;
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
|
@ -13,10 +13,16 @@ type Props = {
|
|||||||
value: number | null;
|
value: number | null;
|
||||||
onChange: (val: number | null) => void;
|
onChange: (val: number | null) => void;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, userAuth }) => {
|
export const SidebarEstimateSelect: React.FC<Props> = ({
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
value,
|
||||||
|
onChange,
|
||||||
|
userAuth,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||||
|
|
||||||
const { isEstimateActive, estimatePoints } = useEstimateOption();
|
const { isEstimateActive, estimatePoints } = useEstimateOption();
|
||||||
|
|
||||||
@ -46,7 +52,7 @@ export const SidebarEstimateSelect: React.FC<Props> = ({ value, onChange, userAu
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
position="right"
|
position="right"
|
||||||
width="w-full"
|
width="w-full"
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed || disabled}
|
||||||
>
|
>
|
||||||
<CustomSelect.Option value={null}>
|
<CustomSelect.Option value={null}>
|
||||||
<>
|
<>
|
||||||
|
@ -21,12 +21,14 @@ type Props = {
|
|||||||
issueDetail: IIssue | undefined;
|
issueDetail: IIssue | undefined;
|
||||||
handleModuleChange: (module: IModule) => void;
|
handleModuleChange: (module: IModule) => void;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarModuleSelect: React.FC<Props> = ({
|
export const SidebarModuleSelect: React.FC<Props> = ({
|
||||||
issueDetail,
|
issueDetail,
|
||||||
handleModuleChange,
|
handleModuleChange,
|
||||||
userAuth,
|
userAuth,
|
||||||
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
@ -55,7 +57,7 @@ export const SidebarModuleSelect: React.FC<Props> = ({
|
|||||||
|
|
||||||
const issueModule = issueDetail?.issue_module;
|
const issueModule = issueDetail?.issue_module;
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
|
@ -23,6 +23,7 @@ type Props = {
|
|||||||
customDisplay: JSX.Element;
|
customDisplay: JSX.Element;
|
||||||
watch: UseFormWatch<IIssue>;
|
watch: UseFormWatch<IIssue>;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarParentSelect: React.FC<Props> = ({
|
export const SidebarParentSelect: React.FC<Props> = ({
|
||||||
@ -31,6 +32,7 @@ export const SidebarParentSelect: React.FC<Props> = ({
|
|||||||
customDisplay,
|
customDisplay,
|
||||||
watch,
|
watch,
|
||||||
userAuth,
|
userAuth,
|
||||||
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
||||||
|
|
||||||
@ -46,7 +48,7 @@ export const SidebarParentSelect: React.FC<Props> = ({
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
|
@ -14,10 +14,16 @@ type Props = {
|
|||||||
value: string | null;
|
value: string | null;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
userAuth: UserAuth;
|
userAuth: UserAuth;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarPrioritySelect: React.FC<Props> = ({ value, onChange, userAuth }) => {
|
export const SidebarPrioritySelect: React.FC<Props> = ({
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
value,
|
||||||
|
onChange,
|
||||||
|
userAuth,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
|
@ -23,9 +23,15 @@ type Props = {
|
|||||||
value: string;
|
value: string;
|
||||||
onChange: (val: string) => void;
|
onChange: (val: string) => void;
|
||||||
userAuth: UserAuth;
|
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 router = useRouter();
|
||||||
const { workspaceSlug, projectId, inboxIssueId } = router.query;
|
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 selectedState = states?.find((s) => s.id === value);
|
||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
|
@ -73,6 +73,7 @@ type Props = {
|
|||||||
| "delete"
|
| "delete"
|
||||||
| "all"
|
| "all"
|
||||||
)[];
|
)[];
|
||||||
|
nonEditable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<IIssueLabels> = {
|
const defaultValues: Partial<IIssueLabels> = {
|
||||||
@ -86,6 +87,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
issueDetail,
|
issueDetail,
|
||||||
watch: watchIssue,
|
watch: watchIssue,
|
||||||
fieldsToShow = ["all"],
|
fieldsToShow = ["all"],
|
||||||
|
nonEditable = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [createLabelForm, setCreateLabelForm] = useState(false);
|
const [createLabelForm, setCreateLabelForm] = useState(false);
|
||||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||||
@ -307,7 +309,8 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="divide-y-2 divide-custom-border-100">
|
|
||||||
|
<div className={`divide-y-2 divide-custom-border-100 ${nonEditable ? "opacity-60" : ""}`}>
|
||||||
{showFirstSection && (
|
{showFirstSection && (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||||
@ -319,6 +322,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={(val: string) => submitChanges({ state: val })}
|
onChange={(val: string) => submitChanges({ state: val })}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
|
disabled={nonEditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -332,6 +336,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={(val: string[]) => submitChanges({ assignees_list: val })}
|
onChange={(val: string[]) => submitChanges({ assignees_list: val })}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
|
disabled={nonEditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -345,6 +350,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={(val: string) => submitChanges({ priority: val })}
|
onChange={(val: string) => submitChanges({ priority: val })}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
|
disabled={nonEditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -358,6 +364,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={(val: number | null) => submitChanges({ estimate_point: val })}
|
onChange={(val: number | null) => submitChanges({ estimate_point: val })}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
|
disabled={nonEditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -389,6 +396,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
watch={watchIssue}
|
watch={watchIssue}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
|
disabled={nonEditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
||||||
@ -397,6 +405,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
submitChanges={submitChanges}
|
submitChanges={submitChanges}
|
||||||
watch={watchIssue}
|
watch={watchIssue}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
|
disabled={nonEditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
|
||||||
@ -405,6 +414,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
submitChanges={submitChanges}
|
submitChanges={submitChanges}
|
||||||
watch={watchIssue}
|
watch={watchIssue}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
|
disabled={nonEditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
||||||
@ -427,7 +437,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="bg-custom-background-90"
|
className="bg-custom-background-90"
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed || nonEditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -443,6 +453,7 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
issueDetail={issueDetail}
|
issueDetail={issueDetail}
|
||||||
handleCycleChange={handleCycleChange}
|
handleCycleChange={handleCycleChange}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
|
disabled={nonEditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && (
|
||||||
@ -450,13 +461,14 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
issueDetail={issueDetail}
|
issueDetail={issueDetail}
|
||||||
handleModuleChange={handleModuleChange}
|
handleModuleChange={handleModuleChange}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
|
disabled={nonEditable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
{(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 items-start justify-between">
|
||||||
<div className="flex basis-1/2 items-center gap-x-2 text-sm text-custom-text-200">
|
<div className="flex basis-1/2 items-center gap-x-2 text-sm text-custom-text-200">
|
||||||
<TagIcon className="h-4 w-4" />
|
<TagIcon className="h-4 w-4" />
|
||||||
@ -503,13 +515,13 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
onChange={(val: any) => submitChanges({ labels_list: val })}
|
onChange={(val: any) => submitChanges({ labels_list: val })}
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
multiple
|
multiple
|
||||||
disabled={isNotAllowed}
|
disabled={isNotAllowed || nonEditable}
|
||||||
>
|
>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Button
|
<Listbox.Button
|
||||||
className={`flex ${
|
className={`flex ${
|
||||||
isNotAllowed
|
isNotAllowed || nonEditable
|
||||||
? "cursor-not-allowed"
|
? "cursor-not-allowed"
|
||||||
: "cursor-pointer hover:bg-custom-background-90"
|
: "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`}
|
} 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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`flex ${
|
className={`flex ${
|
||||||
isNotAllowed
|
isNotAllowed || nonEditable
|
||||||
? "cursor-not-allowed"
|
? "cursor-not-allowed"
|
||||||
: "cursor-pointer hover:bg-custom-background-90"
|
: "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`}
|
} 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)}
|
onClick={() => setCreateLabelForm((prevData) => !prevData)}
|
||||||
|
disabled={nonEditable}
|
||||||
>
|
>
|
||||||
{createLabelForm ? (
|
{createLabelForm ? (
|
||||||
<>
|
<>
|
||||||
@ -709,14 +722,17 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
{(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">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h4>Links</h4>
|
<h4>Links</h4>
|
||||||
{!isNotAllowed && (
|
{!isNotAllowed && (
|
||||||
<button
|
<button
|
||||||
type="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)}
|
onClick={() => setLinkModal(true)}
|
||||||
|
disabled={nonEditable}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -28,9 +28,10 @@ import { PROJECT_ISSUES_LIST, SUB_ISSUES } from "constants/fetch-keys";
|
|||||||
type Props = {
|
type Props = {
|
||||||
parentIssue: IIssue;
|
parentIssue: IIssue;
|
||||||
user: ICurrentUserResponse | undefined;
|
user: ICurrentUserResponse | undefined;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SubIssuesList: FC<Props> = ({ parentIssue, user }) => {
|
export const SubIssuesList: FC<Props> = ({ parentIssue, user, disabled = false }) => {
|
||||||
// states
|
// states
|
||||||
const [createIssueModal, setCreateIssueModal] = useState(false);
|
const [createIssueModal, setCreateIssueModal] = useState(false);
|
||||||
const [subIssuesListModal, setSubIssuesListModal] = useState(false);
|
const [subIssuesListModal, setSubIssuesListModal] = useState(false);
|
||||||
@ -180,7 +181,7 @@ export const SubIssuesList: FC<Props> = ({ parentIssue, user }) => {
|
|||||||
|
|
||||||
const completionPercentage = (completedSubIssues / totalSubIssues) * 100;
|
const completionPercentage = (completedSubIssues / totalSubIssues) * 100;
|
||||||
|
|
||||||
const isNotAllowed = memberRole.isGuest || memberRole.isViewer;
|
const isNotAllowed = memberRole.isGuest || memberRole.isViewer || disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
// headless ui
|
// headless ui
|
||||||
import { Disclosure, Transition } from "@headlessui/react";
|
import { Disclosure, Transition } from "@headlessui/react";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu, Icon } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
@ -153,6 +153,18 @@ export const SingleSidebarProject: React.FC<Props> = ({
|
|||||||
<span>Copy project link</span>
|
<span>Copy project link</span>
|
||||||
</span>
|
</span>
|
||||||
</CustomMenu.MenuItem>
|
</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>
|
</CustomMenu>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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}`;
|
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) =>
|
export const PROJECT_ISSUES_DETAILS = (issueId: string) =>
|
||||||
`PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
|
`PROJECT_ISSUES_DETAILS_${issueId.toUpperCase()}`;
|
||||||
export const PROJECT_ISSUES_PROPERTIES = (projectId: string) =>
|
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 ISSUE_DETAILS = (issueId: string) => `ISSUE_DETAILS_${issueId.toUpperCase()}`;
|
||||||
export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`;
|
export const SUB_ISSUES = (issueId: string) => `SUB_ISSUES_${issueId.toUpperCase()}`;
|
||||||
export const ISSUE_ATTACHMENTS = (issueId: string) => `ISSUE_ATTACHMENTS_${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
|
// integrations
|
||||||
export const APP_INTEGRATIONS = "APP_INTEGRATIONS";
|
export const APP_INTEGRATIONS = "APP_INTEGRATIONS";
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
export const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||||
|
|
||||||
export const GROUP_CHOICES = {
|
export const GROUP_CHOICES = {
|
||||||
@ -27,3 +26,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 = [
|
||||||
|
{ 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 {
|
import {
|
||||||
CYCLE_ISSUES_WITH_PARAMS,
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
MODULE_ISSUES_WITH_PARAMS,
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_ARCHIVED_ISSUES_LIST_WITH_PARAMS,
|
||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
STATES_LIST,
|
STATES_LIST,
|
||||||
VIEW_ISSUES,
|
VIEW_ISSUES,
|
||||||
@ -44,6 +45,7 @@ const useIssuesView = () => {
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const params: any = {
|
const params: any = {
|
||||||
order_by: orderBy,
|
order_by: orderBy,
|
||||||
@ -73,6 +75,15 @@ const useIssuesView = () => {
|
|||||||
: null
|
: 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(
|
const { data: cycleIssues } = useSWR(
|
||||||
workspaceSlug && projectId && cycleId && params
|
workspaceSlug && projectId && cycleId && params
|
||||||
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
|
? CYCLE_ISSUES_WITH_PARAMS(cycleId as string, params)
|
||||||
@ -149,6 +160,8 @@ const useIssuesView = () => {
|
|||||||
? moduleIssues
|
? moduleIssues
|
||||||
: viewId
|
: viewId
|
||||||
? viewIssues
|
? viewIssues
|
||||||
|
: isArchivedIssues
|
||||||
|
? projectArchivedIssues
|
||||||
: projectIssues;
|
: projectIssues;
|
||||||
|
|
||||||
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
|
if (Array.isArray(issuesToGroup)) return { allIssues: issuesToGroup };
|
||||||
@ -161,10 +174,12 @@ const useIssuesView = () => {
|
|||||||
cycleIssues,
|
cycleIssues,
|
||||||
moduleIssues,
|
moduleIssues,
|
||||||
viewIssues,
|
viewIssues,
|
||||||
|
projectArchivedIssues,
|
||||||
groupByProperty,
|
groupByProperty,
|
||||||
cycleId,
|
cycleId,
|
||||||
moduleId,
|
moduleId,
|
||||||
viewId,
|
viewId,
|
||||||
|
isArchivedIssues,
|
||||||
emptyStatesObject,
|
emptyStatesObject,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -174,12 +189,12 @@ const useIssuesView = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
issueView,
|
issueView: isArchivedIssues ? "list" : issueView,
|
||||||
groupByProperty,
|
groupByProperty,
|
||||||
setGroupByProperty,
|
setGroupByProperty,
|
||||||
orderBy,
|
orderBy,
|
||||||
setOrderBy,
|
setOrderBy,
|
||||||
showEmptyGroups,
|
showEmptyGroups: isArchivedIssues ? false : showEmptyGroups,
|
||||||
setShowEmptyGroups,
|
setShowEmptyGroups,
|
||||||
calendarDateRange,
|
calendarDateRange,
|
||||||
setCalendarDateRange,
|
setCalendarDateRange,
|
||||||
|
@ -71,6 +71,10 @@ const SettingsNavbar: React.FC<Props> = ({ profilePage = false }) => {
|
|||||||
label: "Estimates",
|
label: "Estimates",
|
||||||
href: `/${workspaceSlug}/projects/${projectId}/settings/estimates`,
|
href: `/${workspaceSlug}/projects/${projectId}/settings/estimates`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Automations",
|
||||||
|
href: `/${workspaceSlug}/projects/${projectId}/settings/automations`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const profileLinks: Array<{
|
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;
|
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();
|
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 {
|
export interface IIssue {
|
||||||
|
archived_at: string;
|
||||||
assignees: string[];
|
assignees: string[];
|
||||||
assignee_details: IUser[];
|
assignee_details: IUser[];
|
||||||
assignees_list: string[];
|
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 "./";
|
} from "./";
|
||||||
|
|
||||||
export interface IProject {
|
export interface IProject {
|
||||||
|
archive_in: number;
|
||||||
|
close_in: number;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
cover_image: string | null;
|
cover_image: string | null;
|
||||||
@ -18,6 +20,7 @@ export interface IProject {
|
|||||||
page_view: boolean;
|
page_view: boolean;
|
||||||
inbox_view: boolean;
|
inbox_view: boolean;
|
||||||
default_assignee: IUser | string | null;
|
default_assignee: IUser | string | null;
|
||||||
|
default_state: string | null;
|
||||||
description: string;
|
description: string;
|
||||||
emoji: string | null;
|
emoji: string | null;
|
||||||
emoji_and_icon:
|
emoji_and_icon:
|
||||||
|
Loading…
Reference in New Issue
Block a user