forked from github/plane
Compare commits
27 Commits
preview
...
refactor/d
Author | SHA1 | Date | |
---|---|---|---|
|
b8c3abdc80 | ||
|
b0413a50f6 | ||
|
aade1a72b7 | ||
|
3d2d2befaf | ||
|
c57c50aea8 | ||
|
50dd42aa8d | ||
|
131f076010 | ||
|
bc492fa94f | ||
|
2891ed1c3a | ||
|
a434b1008d | ||
|
f489f97aa8 | ||
|
24b82e518c | ||
|
f9586ede31 | ||
|
855c65bc87 | ||
|
6aaf9642bb | ||
|
faefb61d2f | ||
|
0c4e197940 | ||
|
e3947254c8 | ||
|
b9e7fd93ae | ||
|
308e6781a1 | ||
|
adda14e8bf | ||
|
82e65c44a4 | ||
|
e9a79b368b | ||
|
55b9fbffd7 | ||
|
af9aec6769 | ||
|
bc6a983d1e | ||
|
b85c93b0e5 |
@ -4,8 +4,6 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
|
||||||
import { IssueDraftService } from "services/issue";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// icons
|
// icons
|
||||||
@ -19,14 +17,16 @@ type Props = {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
data: IIssue | null;
|
data: IIssue | null;
|
||||||
onSubmit?: () => Promise<void> | void;
|
onSuccess?: () => Promise<void> | void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueDraftService = new IssueDraftService();
|
|
||||||
|
|
||||||
export const DeleteDraftIssueModal: React.FC<Props> = observer((props) => {
|
export const DeleteDraftIssueModal: React.FC<Props> = observer((props) => {
|
||||||
const { isOpen, handleClose, data, onSubmit } = props;
|
const { isOpen, handleClose, data, onSuccess } = props;
|
||||||
|
|
||||||
|
// store
|
||||||
|
const { draftIssues: draftIssueStore } = useMobxStore();
|
||||||
|
|
||||||
|
// states
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const { user: userStore } = useMobxStore();
|
const { user: userStore } = useMobxStore();
|
||||||
@ -50,30 +50,28 @@ export const DeleteDraftIssueModal: React.FC<Props> = observer((props) => {
|
|||||||
if (!workspaceSlug || !data || !user) return;
|
if (!workspaceSlug || !data || !user) return;
|
||||||
|
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
|
await draftIssueStore
|
||||||
await issueDraftService
|
|
||||||
.deleteDraftIssue(workspaceSlug as string, data.project, data.id)
|
.deleteDraftIssue(workspaceSlug as string, data.project, data.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setIsDeleteLoading(false);
|
|
||||||
handleClose();
|
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Success",
|
title: "Success",
|
||||||
message: "Draft Issue deleted successfully",
|
message: "Draft Issue deleted successfully",
|
||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
console.log(error);
|
|
||||||
handleClose();
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
message: "Something went wrong",
|
message: "Something went wrong",
|
||||||
type: "error",
|
type: "error",
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
handleClose();
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
});
|
});
|
||||||
if (onSubmit) await onSubmit();
|
|
||||||
|
if (onSuccess) await onSuccess();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
@ -22,7 +22,6 @@ import {
|
|||||||
import { CreateStateModal } from "components/states";
|
import { CreateStateModal } from "components/states";
|
||||||
import { CreateLabelModal } from "components/labels";
|
import { CreateLabelModal } from "components/labels";
|
||||||
// ui
|
// ui
|
||||||
import {} from "components/ui";
|
|
||||||
import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui";
|
import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui";
|
||||||
// icons
|
// icons
|
||||||
import { Sparkle, X } from "lucide-react";
|
import { Sparkle, X } from "lucide-react";
|
||||||
@ -62,9 +61,8 @@ interface IssueFormProps {
|
|||||||
formData: Partial<IIssue>,
|
formData: Partial<IIssue>,
|
||||||
action?: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue"
|
action?: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue"
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
data?: Partial<IIssue> | null;
|
initialData?: Partial<IIssue> | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
prePopulatedData?: Partial<IIssue> | null;
|
|
||||||
projectId: string;
|
projectId: string;
|
||||||
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
|
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
|
||||||
createMore: boolean;
|
createMore: boolean;
|
||||||
@ -92,9 +90,8 @@ interface IssueFormProps {
|
|||||||
export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
handleFormSubmit,
|
handleFormSubmit,
|
||||||
data,
|
initialData,
|
||||||
isOpen,
|
isOpen,
|
||||||
prePopulatedData,
|
|
||||||
projectId,
|
projectId,
|
||||||
setActiveProject,
|
setActiveProject,
|
||||||
createMore,
|
createMore,
|
||||||
@ -125,39 +122,38 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
const editorSuggestions = useEditorSuggestions();
|
const editorSuggestions = useEditorSuggestions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting, isDirty },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
watch,
|
watch,
|
||||||
control,
|
control,
|
||||||
getValues,
|
getValues,
|
||||||
setValue,
|
setValue,
|
||||||
setFocus,
|
|
||||||
} = useForm<IIssue>({
|
} = useForm<IIssue>({
|
||||||
defaultValues: prePopulatedData ?? defaultValues,
|
defaultValues: initialData ?? defaultValues,
|
||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
const issueName = watch("name");
|
const issueName = watch("name");
|
||||||
|
|
||||||
const payload: Partial<IIssue> = {
|
const payload: Partial<IIssue> = {
|
||||||
name: watch("name"),
|
name: getValues("name"),
|
||||||
description: watch("description"),
|
description: getValues("description"),
|
||||||
description_html: watch("description_html"),
|
description_html: getValues("description_html"),
|
||||||
state: watch("state"),
|
state: getValues("state"),
|
||||||
priority: watch("priority"),
|
priority: getValues("priority"),
|
||||||
assignees: watch("assignees"),
|
assignees: getValues("assignees"),
|
||||||
labels: watch("labels"),
|
labels: getValues("labels"),
|
||||||
start_date: watch("start_date"),
|
start_date: getValues("start_date"),
|
||||||
target_date: watch("target_date"),
|
target_date: getValues("target_date"),
|
||||||
project: watch("project"),
|
project: getValues("project"),
|
||||||
parent: watch("parent"),
|
parent: getValues("parent"),
|
||||||
cycle: watch("cycle"),
|
cycle: getValues("cycle"),
|
||||||
module: watch("module"),
|
module: getValues("module"),
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen || data) return;
|
if (!isOpen) return;
|
||||||
|
|
||||||
setLocalStorageValue(
|
setLocalStorageValue(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
@ -165,26 +161,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [JSON.stringify(payload), isOpen, data]);
|
}, [JSON.stringify(payload), isOpen]);
|
||||||
|
|
||||||
// const onClose = () => {
|
|
||||||
// handleClose();
|
|
||||||
// };
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen || data) return;
|
|
||||||
|
|
||||||
setLocalStorageValue(
|
|
||||||
JSON.stringify({
|
|
||||||
...payload,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [JSON.stringify(payload), isOpen, data]);
|
|
||||||
|
|
||||||
// const onClose = () => {
|
|
||||||
// handleClose();
|
|
||||||
// };
|
|
||||||
|
|
||||||
const handleCreateUpdateIssue = async (
|
const handleCreateUpdateIssue = async (
|
||||||
formData: Partial<IIssue>,
|
formData: Partial<IIssue>,
|
||||||
@ -192,9 +169,8 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
) => {
|
) => {
|
||||||
await handleFormSubmit(
|
await handleFormSubmit(
|
||||||
{
|
{
|
||||||
...(data ?? {}),
|
...(initialData ?? {}),
|
||||||
...formData,
|
...formData,
|
||||||
is_draft: action === "createDraft" || action === "updateDraft",
|
|
||||||
},
|
},
|
||||||
action
|
action
|
||||||
);
|
);
|
||||||
@ -270,14 +246,14 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFocus("name");
|
// if form is dirty, don't reset
|
||||||
|
if (isDirty) return;
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
...defaultValues,
|
...defaultValues,
|
||||||
...(prePopulatedData ?? {}),
|
...(initialData ?? {}),
|
||||||
...(data ?? {}),
|
|
||||||
});
|
});
|
||||||
}, [setFocus, prePopulatedData, reset, data]);
|
}, [reset, initialData, isDirty]);
|
||||||
|
|
||||||
// update projectId in form when projectId changes
|
// update projectId in form when projectId changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -311,7 +287,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit((formData) =>
|
onSubmit={handleSubmit((formData) =>
|
||||||
handleCreateUpdateIssue(formData, data ? "convertToNewIssue" : "createDraft")
|
handleCreateUpdateIssue(formData, initialData?.id ? "convertToNewIssue" : "createDraft")
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
@ -379,6 +355,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
type="text"
|
type="text"
|
||||||
|
tabIndex={1}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -606,7 +583,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
variant="neutral-primary"
|
variant="neutral-primary"
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
onClick={handleSubmit((formData) =>
|
onClick={handleSubmit((formData) =>
|
||||||
handleCreateUpdateIssue(formData, data?.id ? "updateDraft" : "createDraft")
|
handleCreateUpdateIssue(formData, initialData?.id ? "updateDraft" : "createDraft")
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Saving..." : "Save Draft"}
|
{isSubmitting ? "Saving..." : "Save Draft"}
|
||||||
@ -615,7 +592,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
onClick={handleSubmit((formData) =>
|
onClick={handleSubmit((formData) =>
|
||||||
handleCreateUpdateIssue(formData, data ? "convertToNewIssue" : "createNewIssue")
|
handleCreateUpdateIssue(formData, initialData?.id ? "convertToNewIssue" : "createNewIssue")
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Saving..." : "Add Issue"}
|
{isSubmitting ? "Saving..." : "Add Issue"}
|
@ -6,24 +6,25 @@ import { Dialog, Transition } from "@headlessui/react";
|
|||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
// services
|
||||||
import { IssueService, IssueDraftService } from "services/issue";
|
import { IssueService } from "services/issue";
|
||||||
import { ModuleService } from "services/module.service";
|
import { ModuleService } from "services/module.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
import { DraftIssueForm } from "components/issues";
|
import { DraftIssueForm } from "components/draft-issues";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_PRIORITIES, ISSUE_STATE_GROUPS, getValueFromObject } from "constants/issue";
|
||||||
// types
|
// types
|
||||||
import type { IIssue } from "types";
|
import type { IIssue } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUES_DETAILS, USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys";
|
import { SUB_ISSUES } from "constants/fetch-keys";
|
||||||
|
|
||||||
interface IssuesModalProps {
|
interface IssuesModalProps {
|
||||||
data?: IIssue | null;
|
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
isUpdatingSingleIssue?: boolean;
|
isUpdatingSingleIssue?: boolean;
|
||||||
prePopulateData?: Partial<IIssue>;
|
initialData?: Partial<IIssue>;
|
||||||
fieldsToShow?: (
|
fieldsToShow?: (
|
||||||
| "project"
|
| "project"
|
||||||
| "name"
|
| "name"
|
||||||
@ -43,19 +44,10 @@ interface IssuesModalProps {
|
|||||||
|
|
||||||
// services
|
// services
|
||||||
const issueService = new IssueService();
|
const issueService = new IssueService();
|
||||||
const issueDraftService = new IssueDraftService();
|
|
||||||
const moduleService = new ModuleService();
|
const moduleService = new ModuleService();
|
||||||
|
|
||||||
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
||||||
const {
|
const { handleClose, isOpen, initialData, fieldsToShow = ["all"], onSubmit } = props;
|
||||||
data,
|
|
||||||
handleClose,
|
|
||||||
isOpen,
|
|
||||||
isUpdatingSingleIssue = false,
|
|
||||||
prePopulateData: prePopulateDataProps,
|
|
||||||
fieldsToShow = ["all"],
|
|
||||||
onSubmit,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [createMore, setCreateMore] = useState(false);
|
const [createMore, setCreateMore] = useState(false);
|
||||||
@ -65,7 +57,14 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
const { project: projectStore, user: userStore } = useMobxStore();
|
const {
|
||||||
|
project: projectStore,
|
||||||
|
draftIssues: draftIssueStore,
|
||||||
|
draftIssueFilters: draftIssueFilterStore,
|
||||||
|
issueDetail: issueDetailStore,
|
||||||
|
issue: issueStore,
|
||||||
|
user: userStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
const user = userStore.currentUser;
|
const user = userStore.currentUser;
|
||||||
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
|
||||||
@ -85,62 +84,30 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPreloadedData(prePopulateDataProps ?? {});
|
setPreloadedData(initialData ?? {});
|
||||||
|
|
||||||
if (cycleId && !prePopulateDataProps?.cycle) {
|
if (cycleId && !initialData?.cycle) {
|
||||||
setPreloadedData((prevData) => ({
|
setPreloadedData((prevData) => ({
|
||||||
...(prevData ?? {}),
|
...(prevData ?? {}),
|
||||||
...prePopulateDataProps,
|
...initialData,
|
||||||
cycle: cycleId.toString(),
|
cycle: cycleId.toString(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (moduleId && !prePopulateDataProps?.module) {
|
if (moduleId && !initialData?.module) {
|
||||||
setPreloadedData((prevData) => ({
|
setPreloadedData((prevData) => ({
|
||||||
...(prevData ?? {}),
|
...(prevData ?? {}),
|
||||||
...prePopulateDataProps,
|
...initialData,
|
||||||
module: moduleId.toString(),
|
module: moduleId.toString(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (
|
if ((router.asPath.includes("my-issues") || router.asPath.includes("assigned")) && !initialData?.assignees) {
|
||||||
(router.asPath.includes("my-issues") || router.asPath.includes("assigned")) &&
|
|
||||||
!prePopulateDataProps?.assignees
|
|
||||||
) {
|
|
||||||
setPreloadedData((prevData) => ({
|
setPreloadedData((prevData) => ({
|
||||||
...(prevData ?? {}),
|
...(prevData ?? {}),
|
||||||
...prePopulateDataProps,
|
...initialData,
|
||||||
assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""],
|
assignees: initialData?.assignees ?? [user?.id ?? ""],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]);
|
}, [initialData, cycleId, moduleId, router.asPath, user?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPreloadedData(prePopulateDataProps ?? {});
|
|
||||||
|
|
||||||
if (cycleId && !prePopulateDataProps?.cycle) {
|
|
||||||
setPreloadedData((prevData) => ({
|
|
||||||
...(prevData ?? {}),
|
|
||||||
...prePopulateDataProps,
|
|
||||||
cycle: cycleId.toString(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (moduleId && !prePopulateDataProps?.module) {
|
|
||||||
setPreloadedData((prevData) => ({
|
|
||||||
...(prevData ?? {}),
|
|
||||||
...prePopulateDataProps,
|
|
||||||
module: moduleId.toString(),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
(router.asPath.includes("my-issues") || router.asPath.includes("assigned")) &&
|
|
||||||
!prePopulateDataProps?.assignees
|
|
||||||
) {
|
|
||||||
setPreloadedData((prevData) => ({
|
|
||||||
...(prevData ?? {}),
|
|
||||||
...prePopulateDataProps,
|
|
||||||
assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""],
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// if modal is closed, reset active project to null
|
// if modal is closed, reset active project to null
|
||||||
@ -152,7 +119,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
|
|
||||||
// if data is present, set active project to the project of the
|
// if data is present, set active project to the project of the
|
||||||
// issue. This has more priority than the project in the url.
|
// issue. This has more priority than the project in the url.
|
||||||
if (data && data.project) return setActiveProject(data.project);
|
if (initialData && initialData.project) return setActiveProject(initialData.project);
|
||||||
|
|
||||||
if (prePopulateData && prePopulateData.project && !activeProject) return setActiveProject(prePopulateData.project);
|
if (prePopulateData && prePopulateData.project && !activeProject) return setActiveProject(prePopulateData.project);
|
||||||
|
|
||||||
@ -162,21 +129,21 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
// in the url. This has the least priority.
|
// in the url. This has the least priority.
|
||||||
if (projects && projects.length > 0 && !activeProject)
|
if (projects && projects.length > 0 && !activeProject)
|
||||||
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
|
||||||
}, [activeProject, data, projectId, projects, isOpen, prePopulateData]);
|
}, [activeProject, initialData, projectId, projects, isOpen, prePopulateData]);
|
||||||
|
|
||||||
const createDraftIssue = async (payload: Partial<IIssue>) => {
|
const createDraftIssue = async (payload: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !activeProject || !user) return;
|
if (!workspaceSlug || !activeProject || !user) return;
|
||||||
|
|
||||||
await issueDraftService
|
await draftIssueStore
|
||||||
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload)
|
.createDraftIssue(workspaceSlug.toString(), activeProject, payload)
|
||||||
.then(async () => {
|
.then(() => {
|
||||||
|
draftIssueStore.fetchIssues(workspaceSlug.toString(), activeProject);
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Issue created successfully.",
|
message: "Issue created successfully.",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug.toString()));
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -190,24 +157,34 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateDraftIssue = async (payload: Partial<IIssue>) => {
|
const updateDraftIssue = async (payload: Partial<IIssue>) => {
|
||||||
if (!user) return;
|
if (!user || !workspaceSlug || !activeProject) return;
|
||||||
|
|
||||||
await issueDraftService
|
await draftIssueStore
|
||||||
.updateDraftIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload)
|
.updateDraftIssue(workspaceSlug.toString(), activeProject, payload as IIssue)
|
||||||
.then((res) => {
|
.then((response) => {
|
||||||
if (isUpdatingSingleIssue) {
|
if (!createMore) onClose();
|
||||||
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
|
||||||
} else {
|
const userDisplayFilters = draftIssueFilterStore?.userDisplayFilters;
|
||||||
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
|
const groupBy = userDisplayFilters?.group_by || null;
|
||||||
|
|
||||||
|
let groupById: null | string = null;
|
||||||
|
|
||||||
|
if (groupBy === "priority") {
|
||||||
|
groupById = getValueFromObject(ISSUE_PRIORITIES, "key") as string;
|
||||||
|
} else if (groupBy === "labels") {
|
||||||
|
groupById = getValueFromObject(projectStore?.projectLabels ?? [], "id") as string;
|
||||||
|
} else if (groupBy === "state_detail.group") {
|
||||||
|
groupById = getValueFromObject(ISSUE_STATE_GROUPS, "key") as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
draftIssueStore.updateIssueStructure(groupById, null, response);
|
||||||
|
draftIssueStore.fetchIssues(workspaceSlug.toString(), activeProject);
|
||||||
|
|
||||||
if (!payload.is_draft) {
|
if (!payload.is_draft) {
|
||||||
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
if (payload.cycle && payload.cycle !== "") addIssueToCycle(response.id, payload.cycle);
|
||||||
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
|
if (payload.module && payload.module !== "") addIssueToModule(response.id, payload.module);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!createMore) onClose();
|
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -254,9 +231,11 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
const createIssue = async (payload: Partial<IIssue>) => {
|
const createIssue = async (payload: Partial<IIssue>) => {
|
||||||
if (!workspaceSlug || !activeProject || !user) return;
|
if (!workspaceSlug || !activeProject || !user) return;
|
||||||
|
|
||||||
await issueService
|
await issueDetailStore
|
||||||
.createIssue(workspaceSlug.toString(), activeProject, payload, user)
|
.createIssue(workspaceSlug.toString(), activeProject, payload)
|
||||||
.then(async (res) => {
|
.then(async (res) => {
|
||||||
|
issueStore.fetchIssues(workspaceSlug.toString(), activeProject);
|
||||||
|
|
||||||
if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle);
|
if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle);
|
||||||
if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module);
|
if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module);
|
||||||
|
|
||||||
@ -266,10 +245,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
message: "Issue created successfully.",
|
message: "Issue created successfully.",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createMore) onClose();
|
|
||||||
|
|
||||||
if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string));
|
|
||||||
|
|
||||||
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -279,6 +254,19 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
message: "Issue could not be created. Please try again.",
|
message: "Issue could not be created. Please try again.",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!createMore) onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDraftToIssue = async (payload: Partial<IIssue>) => {
|
||||||
|
if (!workspaceSlug || !activeProject) return;
|
||||||
|
await draftIssueStore.convertDraftIssueToIssue(workspaceSlug.toString(), activeProject, payload?.id!).then(() => {
|
||||||
|
draftIssueStore.fetchIssues(workspaceSlug.toString(), activeProject);
|
||||||
|
|
||||||
|
// adding to cycle or/and module if payload is available for the same
|
||||||
|
if (payload.cycle && payload.cycle !== "") addIssueToCycle(payload?.id!, payload.cycle);
|
||||||
|
if (payload.module && payload.module !== "") addIssueToModule(payload?.id!, payload.module);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (
|
const handleFormSubmit = async (
|
||||||
@ -294,8 +282,9 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (action === "createDraft") await createDraftIssue(payload);
|
if (action === "createDraft") await createDraftIssue(payload);
|
||||||
else if (action === "updateDraft" || action === "convertToNewIssue") await updateDraftIssue(payload);
|
else if (action === "updateDraft") await updateDraftIssue(payload);
|
||||||
else if (action === "createNewIssue") await createIssue(payload);
|
else if (action === "createNewIssue") await createIssue(payload);
|
||||||
|
else if (action === "convertToNewIssue") await convertDraftToIssue(payload);
|
||||||
|
|
||||||
clearDraftIssueLocalStorage();
|
clearDraftIssueLocalStorage();
|
||||||
|
|
||||||
@ -335,15 +324,14 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
|||||||
<DraftIssueForm
|
<DraftIssueForm
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
handleFormSubmit={handleFormSubmit}
|
handleFormSubmit={handleFormSubmit}
|
||||||
prePopulatedData={prePopulateData}
|
initialData={initialData}
|
||||||
data={data}
|
|
||||||
createMore={createMore}
|
createMore={createMore}
|
||||||
setCreateMore={setCreateMore}
|
setCreateMore={setCreateMore}
|
||||||
handleClose={onClose}
|
handleClose={onClose}
|
||||||
handleDiscard={onDiscard}
|
handleDiscard={onDiscard}
|
||||||
projectId={activeProject ?? ""}
|
projectId={activeProject ?? ""}
|
||||||
setActiveProject={setActiveProject}
|
setActiveProject={setActiveProject}
|
||||||
status={data ? true : false}
|
status={initialData?.id ? true : false}
|
||||||
user={user ?? undefined}
|
user={user ?? undefined}
|
||||||
fieldsToShow={fieldsToShow}
|
fieldsToShow={fieldsToShow}
|
||||||
/>
|
/>
|
4
web/components/draft-issues/index.ts
Normal file
4
web/components/draft-issues/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./draft-issue-form";
|
||||||
|
export * from "./draft-issue-modal";
|
||||||
|
export * from "./delete-draft-issue-modal";
|
||||||
|
export * from "./confirm-issue-discard";
|
@ -19,3 +19,4 @@ export * from "./project-archived-issue-details";
|
|||||||
export * from "./project-archived-issues";
|
export * from "./project-archived-issues";
|
||||||
export * from "./project-issue-details";
|
export * from "./project-issue-details";
|
||||||
export * from "./user-profile";
|
export * from "./user-profile";
|
||||||
|
export * from "./project-draft-issues";
|
||||||
|
@ -3,18 +3,73 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs, LayersIcon } from "@plane/ui";
|
import { Breadcrumbs, LayersIcon } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||||
// helper
|
// helper
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
|
|
||||||
export const ProjectDraftIssueHeader: FC = observer(() => {
|
export const ProjectDraftIssueHeader: FC = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { project: projectStore } = useMobxStore();
|
const { project: projectStore, draftIssueFilters: draftIssueFiltersStore } = useMobxStore();
|
||||||
const { currentProjectDetails } = projectStore;
|
const { currentProjectDetails } = projectStore;
|
||||||
|
|
||||||
|
const activeLayout = draftIssueFiltersStore.userDisplayFilters.layout;
|
||||||
|
|
||||||
|
const handleLayoutChange = (layout: TIssueLayouts) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
display_filters: {
|
||||||
|
layout,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFiltersUpdate = (key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
const newValues = draftIssueFiltersStore.userFilters?.[key] ?? [];
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((val) => {
|
||||||
|
if (!newValues.includes(val)) newValues.push(val);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (draftIssueFiltersStore.userFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
|
else newValues.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
filters: {
|
||||||
|
[key]: newValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisplayFiltersUpdate = (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
display_filters: {
|
||||||
|
...updatedDisplayFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDisplayPropertiesUpdate = (property: Partial<IIssueDisplayProperties>) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
draftIssueFiltersStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
|
||||||
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
|
||||||
@ -45,6 +100,38 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* filter/layout/display options */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<LayoutSelection
|
||||||
|
layouts={["list", "kanban"]}
|
||||||
|
onChange={(layout) => handleLayoutChange(layout)}
|
||||||
|
selectedLayout={activeLayout}
|
||||||
|
/>
|
||||||
|
<FiltersDropdown title="Filters" placement="bottom-end">
|
||||||
|
<FilterSelection
|
||||||
|
filters={draftIssueFiltersStore.userFilters}
|
||||||
|
handleFiltersUpdate={handleFiltersUpdate}
|
||||||
|
layoutDisplayFiltersOptions={
|
||||||
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.draft_issues[activeLayout] : undefined
|
||||||
|
}
|
||||||
|
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
|
||||||
|
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||||
|
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
|
||||||
|
/>
|
||||||
|
</FiltersDropdown>
|
||||||
|
<FiltersDropdown title="Display" placement="bottom-end">
|
||||||
|
<DisplayFiltersSelection
|
||||||
|
displayFilters={draftIssueFiltersStore.userDisplayFilters}
|
||||||
|
displayProperties={draftIssueFiltersStore.userDisplayProperties}
|
||||||
|
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||||
|
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||||
|
layoutDisplayFiltersOptions={
|
||||||
|
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.draft_issues[activeLayout] : undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FiltersDropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -135,6 +135,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
const payload: Partial<IIssue> = {
|
const payload: Partial<IIssue> = {
|
||||||
name: getValues("name"),
|
name: getValues("name"),
|
||||||
description: getValues("description"),
|
description: getValues("description"),
|
||||||
|
description_html: getValues("description_html"),
|
||||||
state: getValues("state"),
|
state: getValues("state"),
|
||||||
priority: getValues("priority"),
|
priority: getValues("priority"),
|
||||||
assignees: getValues("assignees"),
|
assignees: getValues("assignees"),
|
||||||
|
@ -14,12 +14,6 @@ export * from "./sidebar";
|
|||||||
export * from "./label";
|
export * from "./label";
|
||||||
export * from "./issue-reaction";
|
export * from "./issue-reaction";
|
||||||
export * from "./peek-overview";
|
export * from "./peek-overview";
|
||||||
export * from "./confirm-issue-discard";
|
|
||||||
|
|
||||||
// draft issue
|
|
||||||
export * from "./draft-issue-form";
|
|
||||||
export * from "./draft-issue-modal";
|
|
||||||
export * from "./delete-draft-issue-modal";
|
|
||||||
|
|
||||||
// archived issue
|
// archived issue
|
||||||
export * from "./delete-archived-issue-modal";
|
export * from "./delete-archived-issue-modal";
|
||||||
|
@ -4,3 +4,4 @@ export * from "./module-root";
|
|||||||
export * from "./project-view-root";
|
export * from "./project-view-root";
|
||||||
export * from "./project-root";
|
export * from "./project-root";
|
||||||
export * from "./archived-issue";
|
export * from "./archived-issue";
|
||||||
|
export * from "./project-draft-issue";
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { AppliedFiltersList } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssueFilterOptions } from "types";
|
||||||
|
|
||||||
|
export const ProjectDraftIssueAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { project: projectStore, draftIssueFilters: draftIssueFiltersStore } = useMobxStore();
|
||||||
|
|
||||||
|
const userFilters = draftIssueFiltersStore.userFilters;
|
||||||
|
|
||||||
|
// filters whose value not null or empty array
|
||||||
|
const appliedFilters: IIssueFilterOptions = {};
|
||||||
|
Object.entries(userFilters).forEach(([key, value]) => {
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
if (Array.isArray(value) && value.length === 0) return;
|
||||||
|
|
||||||
|
appliedFilters[key as keyof IIssueFilterOptions] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
// remove all values of the key if value is null
|
||||||
|
if (!value) {
|
||||||
|
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
filters: {
|
||||||
|
[key]: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the passed value from the key
|
||||||
|
let newValues = draftIssueFiltersStore.userFilters?.[key] ?? [];
|
||||||
|
newValues = newValues.filter((val) => val !== value);
|
||||||
|
|
||||||
|
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
filters: {
|
||||||
|
[key]: newValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAllFilters = () => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
const newFilters: IIssueFilterOptions = {};
|
||||||
|
Object.keys(userFilters).forEach((key) => {
|
||||||
|
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
|
||||||
|
filters: { ...newFilters },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// return if no filters are applied
|
||||||
|
if (Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<AppliedFiltersList
|
||||||
|
appliedFilters={appliedFilters}
|
||||||
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
|
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
||||||
|
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||||
|
states={projectStore.states?.[projectId?.toString() ?? ""]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -3,3 +3,4 @@ export * from "./module-root";
|
|||||||
export * from "./profile-issues-root";
|
export * from "./profile-issues-root";
|
||||||
export * from "./project-root";
|
export * from "./project-root";
|
||||||
export * from "./project-view-root";
|
export * from "./project-view-root";
|
||||||
|
export * from "./project-draft-issue-root";
|
||||||
|
@ -0,0 +1,152 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { DragDropContext } from "@hello-pangea/dnd";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { KanBanSwimLanes } from "../swimlanes";
|
||||||
|
import { KanBan } from "../default";
|
||||||
|
import { DraftIssueQuickActions } from "components/issues";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
|
||||||
|
export const DraftIssueKanBanLayout: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
project: projectStore,
|
||||||
|
draftIssueFilters: draftIssueFiltersStore,
|
||||||
|
issueKanBanView: issueKanBanViewStore,
|
||||||
|
draftIssues: draftIssuesStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
// derived values
|
||||||
|
const issues = draftIssuesStore.getDraftIssues;
|
||||||
|
const displayProperties = draftIssueFiltersStore?.userDisplayProperties;
|
||||||
|
const userDisplayFilters = draftIssueFiltersStore?.userDisplayFilters;
|
||||||
|
const group_by: string | null = userDisplayFilters?.group_by || null;
|
||||||
|
const order_by: string | null = draftIssueFiltersStore?.userDisplayFilters?.order_by || null;
|
||||||
|
const showEmptyGroup = userDisplayFilters?.show_empty_groups || false;
|
||||||
|
const sub_group_by: string | null = userDisplayFilters?.sub_group_by || null;
|
||||||
|
|
||||||
|
const currentKanBanView = "default";
|
||||||
|
|
||||||
|
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const onDragStart = () => {
|
||||||
|
setIsDragStarted(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (result: any) => {
|
||||||
|
setIsDragStarted(false);
|
||||||
|
|
||||||
|
if (!result) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
result.destination &&
|
||||||
|
result.source &&
|
||||||
|
result.destination.droppableId === result.source.droppableId &&
|
||||||
|
result.destination.index === result.source.index
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
currentKanBanView === "default"
|
||||||
|
? issueKanBanViewStore?.handleDragDrop(result.source, result.destination)
|
||||||
|
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIssues = useCallback(
|
||||||
|
(
|
||||||
|
sub_group_by: string | null,
|
||||||
|
group_by: string | null,
|
||||||
|
issue: IIssue,
|
||||||
|
action: "update" | "delete" | "convertToIssue"
|
||||||
|
) => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
|
if (action === "update") {
|
||||||
|
draftIssuesStore.updateDraftIssue(workspaceSlug.toString(), issue.project, issue);
|
||||||
|
draftIssuesStore.updateIssueStructure(group_by, sub_group_by, issue);
|
||||||
|
}
|
||||||
|
if (action === "delete") draftIssuesStore.deleteDraftIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||||
|
if (action === "convertToIssue")
|
||||||
|
draftIssuesStore.convertDraftIssueToIssue(workspaceSlug.toString(), issue.project, issue.id);
|
||||||
|
draftIssuesStore.fetchIssues(workspaceSlug.toString(), issue.project);
|
||||||
|
},
|
||||||
|
[draftIssuesStore, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
||||||
|
issueKanBanViewStore.handleKanBanToggle(toggle, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const states = projectStore?.projectStates || null;
|
||||||
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
|
const labels = projectStore?.projectLabels || null;
|
||||||
|
const members = projectStore?.projectMembers || null;
|
||||||
|
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||||
|
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||||
|
const orderBy = draftIssueFiltersStore?.userDisplayFilters?.order_by || null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
|
||||||
|
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
||||||
|
{currentKanBanView === "default" ? (
|
||||||
|
<KanBan
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
order_by={order_by}
|
||||||
|
handleIssues={handleIssues}
|
||||||
|
quickActions={(sub_group_by, group_by, issue) => (
|
||||||
|
<DraftIssueQuickActions
|
||||||
|
issue={issue}
|
||||||
|
handleUpdate={(issue: any, action: any) => handleIssues(sub_group_by, group_by, issue, action)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||||
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
|
states={states}
|
||||||
|
stateGroups={stateGroups}
|
||||||
|
priorities={priorities}
|
||||||
|
labels={labels}
|
||||||
|
members={members?.map((m) => m.member) ?? null}
|
||||||
|
projects={projects}
|
||||||
|
showEmptyGroup={showEmptyGroup}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<KanBanSwimLanes
|
||||||
|
issues={issues}
|
||||||
|
sub_group_by={sub_group_by}
|
||||||
|
group_by={group_by}
|
||||||
|
order_by={orderBy}
|
||||||
|
handleIssues={handleIssues}
|
||||||
|
quickActions={(sub_group_by, group_by, issue) => (
|
||||||
|
<DraftIssueQuickActions
|
||||||
|
issue={issue}
|
||||||
|
handleUpdate={(issue: any, action: any) => handleIssues(sub_group_by, group_by, issue, action)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
|
||||||
|
handleKanBanToggle={handleKanBanToggle}
|
||||||
|
states={states}
|
||||||
|
stateGroups={stateGroups}
|
||||||
|
priorities={priorities}
|
||||||
|
labels={labels}
|
||||||
|
members={members?.map((m) => m.member) ?? null}
|
||||||
|
projects={projects}
|
||||||
|
showEmptyGroup={showEmptyGroup}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DragDropContext>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -3,4 +3,5 @@ export * from "./module-root";
|
|||||||
export * from "./profile-issues-root";
|
export * from "./profile-issues-root";
|
||||||
export * from "./project-root";
|
export * from "./project-root";
|
||||||
export * from "./project-view-root";
|
export * from "./project-view-root";
|
||||||
|
export * from "./project-draft-issue-root";
|
||||||
export * from "./archived-issue-root";
|
export * from "./archived-issue-root";
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
import { FC, useCallback } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// hooks
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { List } from "../default";
|
||||||
|
import { DraftIssueQuickActions } from "components/issues";
|
||||||
|
// helpers
|
||||||
|
import { orderArrayBy } from "helpers/array.helper";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
// constants
|
||||||
|
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
|
||||||
|
|
||||||
|
export const DraftIssueListLayout: FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
project: projectStore,
|
||||||
|
draftIssues: draftIssuesStore,
|
||||||
|
draftIssueFilters: draftIssueFiltersStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const issues = draftIssuesStore.getDraftIssues;
|
||||||
|
|
||||||
|
const group_by: string | null = draftIssueFiltersStore?.userDisplayFilters?.group_by || null;
|
||||||
|
|
||||||
|
const display_properties = draftIssueFiltersStore?.userDisplayProperties || null;
|
||||||
|
|
||||||
|
const handleIssues = useCallback(
|
||||||
|
(group_by: string | null, issue: IIssue, action: "update" | "delete" | "convertToIssue") => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
if (action === "update") {
|
||||||
|
draftIssuesStore.updateDraftIssue(workspaceSlug.toString(), projectId.toString(), issue);
|
||||||
|
draftIssuesStore.updateIssueStructure(group_by, null, issue);
|
||||||
|
} else if (action === "delete") {
|
||||||
|
draftIssuesStore.deleteDraftIssue(workspaceSlug.toString(), projectId.toString(), issue.id);
|
||||||
|
} else if (action === "convertToIssue") {
|
||||||
|
draftIssuesStore.convertDraftIssueToIssue(workspaceSlug.toString(), projectId.toString(), issue.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[workspaceSlug, projectId, draftIssuesStore]
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null;
|
||||||
|
|
||||||
|
const states = projectStore?.projectStates || null;
|
||||||
|
const priorities = ISSUE_PRIORITIES || null;
|
||||||
|
const labels = projectStore?.projectLabels || null;
|
||||||
|
const members = projectStore?.projectMembers || null;
|
||||||
|
const stateGroups = ISSUE_STATE_GROUPS || null;
|
||||||
|
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
|
||||||
|
const estimates =
|
||||||
|
projectDetails?.estimate !== null
|
||||||
|
? projectStore.projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-full bg-custom-background-90">
|
||||||
|
<List
|
||||||
|
issues={issues}
|
||||||
|
group_by={group_by}
|
||||||
|
handleIssues={handleIssues}
|
||||||
|
quickActions={(group_by, issue) => (
|
||||||
|
<DraftIssueQuickActions
|
||||||
|
issue={issue}
|
||||||
|
handleUpdate={(issue: any, action: any) => handleIssues(group_by, issue, action)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
displayProperties={display_properties}
|
||||||
|
states={states}
|
||||||
|
stateGroups={stateGroups}
|
||||||
|
priorities={priorities}
|
||||||
|
labels={labels}
|
||||||
|
members={members?.map((m) => m.member) ?? null}
|
||||||
|
projects={projects}
|
||||||
|
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,74 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { CustomMenu } from "@plane/ui";
|
||||||
|
import { Copy, Pencil, Trash2 } from "lucide-react";
|
||||||
|
// components
|
||||||
|
import { CreateUpdateDraftIssueModal, DeleteDraftIssueModal } from "components/draft-issues";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
issue: IIssue;
|
||||||
|
handleUpdate: (data: IIssue, action: any) => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DraftIssueQuickActions: React.FC<Props> = (props) => {
|
||||||
|
const { issue, handleUpdate } = props;
|
||||||
|
|
||||||
|
// states
|
||||||
|
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||||
|
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(null);
|
||||||
|
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteDraftIssueModal data={issue} isOpen={deleteIssueModal} handleClose={() => setDeleteIssueModal(false)} />
|
||||||
|
<CreateUpdateDraftIssueModal
|
||||||
|
isOpen={createUpdateIssueModal}
|
||||||
|
handleClose={() => {
|
||||||
|
setCreateUpdateIssueModal(false);
|
||||||
|
setIssueToEdit(null);
|
||||||
|
}}
|
||||||
|
initialData={issueToEdit ? { ...issueToEdit } : { ...issue, name: issue?.name, description: issue.description }}
|
||||||
|
/>
|
||||||
|
<CustomMenu ellipsis>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIssueToEdit(issue);
|
||||||
|
setCreateUpdateIssueModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Pencil className="h-3 w-3" />
|
||||||
|
Edit draft issue
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleUpdate(issue, "convertToIssue");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Copy className="h-3 w-3" />
|
||||||
|
Convert to issue
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeleteIssueModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
Delete draft issue
|
||||||
|
</div>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
export * from "./cycle-issue";
|
export * from "./cycle-issue";
|
||||||
export * from "./module-issue";
|
export * from "./module-issue";
|
||||||
export * from "./project-issue";
|
export * from "./project-issue";
|
||||||
|
export * from "./draft-issue";
|
||||||
export * from "./archived-issue";
|
export * from "./archived-issue";
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import useSWR from "swr";
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { DraftIssueListLayout, DraftIssueKanBanLayout, ProjectDraftIssueAppliedFiltersRoot } from "components/issues";
|
||||||
|
|
||||||
|
export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { draftIssues: draftIssuesStore, draftIssueFilters: draftIssueFiltersStore } = useMobxStore();
|
||||||
|
|
||||||
|
useSWR(workspaceSlug && projectId ? `PROJECT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
|
||||||
|
if (workspaceSlug && projectId) {
|
||||||
|
await draftIssueFiltersStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
|
||||||
|
await draftIssuesStore.fetchIssues(workspaceSlug.toString(), projectId.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeLayout = draftIssueFiltersStore.userDisplayFilters.layout;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full h-full flex flex-col overflow-hidden">
|
||||||
|
<ProjectDraftIssueAppliedFiltersRoot />
|
||||||
|
<div className="w-full h-full overflow-auto">
|
||||||
|
{activeLayout === "list" ? (
|
||||||
|
<DraftIssueListLayout />
|
||||||
|
) : activeLayout === "kanban" ? (
|
||||||
|
<DraftIssueKanBanLayout />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -3,4 +3,5 @@ export * from "./global-view-layout-root";
|
|||||||
export * from "./module-layout-root";
|
export * from "./module-layout-root";
|
||||||
export * from "./project-layout-root";
|
export * from "./project-layout-root";
|
||||||
export * from "./project-view-layout-root";
|
export * from "./project-view-layout-root";
|
||||||
|
export * from "./draft-issue-layout-root";
|
||||||
export * from "./archived-issue-layout-root";
|
export * from "./archived-issue-layout-root";
|
||||||
|
@ -5,17 +5,16 @@ import { mutate } from "swr";
|
|||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
|
||||||
import { IssueDraftService } from "services/issue";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
import { IssueForm, ConfirmIssueDiscard } from "components/issues";
|
import { IssueForm } from "components/issues";
|
||||||
|
import { ConfirmIssueDiscard } from "components/draft-issues";
|
||||||
// types
|
// types
|
||||||
import type { IIssue } from "types";
|
import type { IIssue } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys";
|
import { SUB_ISSUES } from "constants/fetch-keys";
|
||||||
|
|
||||||
export interface IssuesModalProps {
|
export interface IssuesModalProps {
|
||||||
data?: IIssue | null;
|
data?: IIssue | null;
|
||||||
@ -41,8 +40,6 @@ export interface IssuesModalProps {
|
|||||||
onSubmit?: (data: Partial<IIssue>) => Promise<void>;
|
onSubmit?: (data: Partial<IIssue>) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const issueDraftService = new IssueDraftService();
|
|
||||||
|
|
||||||
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
||||||
const { data, handleClose, isOpen, prePopulateData: prePopulateDataProps, fieldsToShow = ["all"], onSubmit } = props;
|
const { data, handleClose, isOpen, prePopulateData: prePopulateDataProps, fieldsToShow = ["all"], onSubmit } = props;
|
||||||
|
|
||||||
@ -63,6 +60,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
cycleIssue: cycleIssueStore,
|
cycleIssue: cycleIssueStore,
|
||||||
moduleIssue: moduleIssueStore,
|
moduleIssue: moduleIssueStore,
|
||||||
user: userStore,
|
user: userStore,
|
||||||
|
draftIssues: draftIssuesStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const user = userStore.currentUser;
|
const user = userStore.currentUser;
|
||||||
@ -217,9 +215,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
...formDirtyState,
|
...formDirtyState,
|
||||||
};
|
};
|
||||||
|
|
||||||
await issueDraftService
|
await draftIssuesStore
|
||||||
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload)
|
.createDraftIssue(workspaceSlug.toString(), activeProject, payload)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
draftIssuesStore.fetchIssues(workspaceSlug.toString(), activeProject);
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
@ -230,10 +230,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
|
|||||||
setActiveProject(null);
|
setActiveProject(null);
|
||||||
setFormDirtyState(null);
|
setFormDirtyState(null);
|
||||||
setShowConfirmDiscard(false);
|
setShowConfirmDiscard(false);
|
||||||
|
|
||||||
if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string));
|
|
||||||
|
|
||||||
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
|
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
|
@ -5,7 +5,7 @@ import { ChevronDown, PenSquare, Search } from "lucide-react";
|
|||||||
// hooks
|
// hooks
|
||||||
import useLocalStorage from "hooks/use-local-storage";
|
import useLocalStorage from "hooks/use-local-storage";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateDraftIssueModal } from "components/issues";
|
import { CreateUpdateDraftIssueModal } from "components/draft-issues";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ export const WorkspaceSidebarQuickAction = () => {
|
|||||||
<CreateUpdateDraftIssueModal
|
<CreateUpdateDraftIssueModal
|
||||||
isOpen={isDraftIssueModalOpen}
|
isOpen={isDraftIssueModalOpen}
|
||||||
handleClose={() => setIsDraftIssueModalOpen(false)}
|
handleClose={() => setIsDraftIssueModalOpen(false)}
|
||||||
prePopulateData={storedValue ? JSON.parse(storedValue) : {}}
|
initialData={storedValue ? JSON.parse(storedValue) : {}}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
localStorage.removeItem("draftedIssue");
|
localStorage.removeItem("draftedIssue");
|
||||||
clearValue();
|
clearValue();
|
||||||
|
@ -283,29 +283,29 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
|
|||||||
},
|
},
|
||||||
draft_issues: {
|
draft_issues: {
|
||||||
list: {
|
list: {
|
||||||
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
filters: ["priority", "state", "assignees", "created_by", "labels", "start_date", "target_date"],
|
||||||
display_properties: true,
|
display_properties: true,
|
||||||
display_filters: {
|
display_filters: {
|
||||||
group_by: ["state_detail.group", "priority", "project", "labels", null],
|
group_by: ["state", "priority", "labels", "assignees", "created_by", null],
|
||||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
|
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
|
||||||
type: [null, "active", "backlog"],
|
type: [null, "active", "backlog"],
|
||||||
},
|
},
|
||||||
extra_options: {
|
extra_options: {
|
||||||
access: true,
|
access: true,
|
||||||
values: ["show_empty_groups"],
|
values: ["show_empty_groups", "sub_issue"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
kanban: {
|
kanban: {
|
||||||
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
|
filters: ["priority", "state", "assignees", "created_by", "labels", "start_date", "target_date"],
|
||||||
display_properties: true,
|
display_properties: true,
|
||||||
display_filters: {
|
display_filters: {
|
||||||
group_by: ["state_detail.group", "priority", "project", "labels", null],
|
group_by: ["state", "priority", "labels", "assignees", "created_by"],
|
||||||
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
|
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
|
||||||
type: [null, "active", "backlog"],
|
type: [null, "active", "backlog"],
|
||||||
},
|
},
|
||||||
extra_options: {
|
extra_options: {
|
||||||
access: true,
|
access: true,
|
||||||
values: ["show_empty_groups"],
|
values: ["show_empty_groups", "sub_issue"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,7 @@ import { useRouter } from "next/router";
|
|||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// contexts
|
// contexts
|
||||||
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||||
|
import { DraftIssueLayoutRoot } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { ProjectDraftIssueHeader } from "components/headers";
|
import { ProjectDraftIssueHeader } from "components/headers";
|
||||||
// icons
|
// icons
|
||||||
@ -29,6 +30,9 @@ const ProjectDraftIssuesPage: NextPageWithLayout = () => {
|
|||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<DraftIssueLayoutRoot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { APIService } from "services/api.service";
|
import { APIService } from "services/api.service";
|
||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
export class IssueDraftService extends APIService {
|
export class IssueDraftService extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDraftIssues(workspaceSlug: string, projectId: string, params?: any): Promise<any> {
|
async getDraftIssues(workspaceSlug: string, projectId: string, params?: any): Promise<IIssue> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, {
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, {
|
||||||
params,
|
params,
|
||||||
})
|
})
|
@ -1,10 +1,10 @@
|
|||||||
import { observable, action, computed, makeObservable, runInAction } from "mobx";
|
import { observable, action, computed, makeObservable, runInAction, autorun } from "mobx";
|
||||||
// store
|
// store
|
||||||
import { RootStore } from "../root";
|
import { RootStore } from "../root";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
// services
|
// services
|
||||||
import { IssueService } from "services/issue";
|
import { IssueDraftService } from "services/issue";
|
||||||
import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers";
|
import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers";
|
||||||
import {
|
import {
|
||||||
IIssueGroupWithSubGroupsStructure,
|
IIssueGroupWithSubGroupsStructure,
|
||||||
@ -13,29 +13,11 @@ import {
|
|||||||
IIssueUnGroupedStructure,
|
IIssueUnGroupedStructure,
|
||||||
} from "store/issue";
|
} from "store/issue";
|
||||||
|
|
||||||
export interface IDraftIssueStore {
|
export interface IIssueDraftStore {
|
||||||
loader: boolean;
|
loader: boolean;
|
||||||
error: any | null;
|
error: any | null;
|
||||||
// issues
|
|
||||||
issues: {
|
|
||||||
[project_id: string]: {
|
|
||||||
grouped: IIssueGroupedStructure;
|
|
||||||
groupWithSubGroups: IIssueGroupWithSubGroupsStructure;
|
|
||||||
ungrouped: IIssueUnGroupedStructure;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
// computed
|
|
||||||
getIssueType: IIssueType | null;
|
|
||||||
getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null;
|
|
||||||
// action
|
|
||||||
fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>;
|
|
||||||
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DraftIssueStore implements IDraftIssueStore {
|
draftIssues: {
|
||||||
loader: boolean = false;
|
|
||||||
error: any | null = null;
|
|
||||||
issues: {
|
|
||||||
[project_id: string]: {
|
[project_id: string]: {
|
||||||
grouped: {
|
grouped: {
|
||||||
[group_id: string]: IIssue[];
|
[group_id: string]: IIssue[];
|
||||||
@ -47,9 +29,31 @@ export class DraftIssueStore implements IDraftIssueStore {
|
|||||||
};
|
};
|
||||||
ungrouped: IIssue[];
|
ungrouped: IIssue[];
|
||||||
};
|
};
|
||||||
} = {};
|
};
|
||||||
|
rootStore: RootStore;
|
||||||
|
|
||||||
|
// computed
|
||||||
|
getIssueType: IIssueType | null;
|
||||||
|
getDraftIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null;
|
||||||
|
|
||||||
|
// actions
|
||||||
|
fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>;
|
||||||
|
createDraftIssue: (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => Promise<any>;
|
||||||
|
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
|
||||||
|
deleteDraftIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>;
|
||||||
|
updateDraftIssue: (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => Promise<any>;
|
||||||
|
convertDraftIssueToIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>;
|
||||||
|
|
||||||
// service
|
// service
|
||||||
issueService;
|
draftIssueService: IssueDraftService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class IssueDraftStore implements IIssueDraftStore {
|
||||||
|
loader: boolean = false;
|
||||||
|
error: any | null = null;
|
||||||
|
draftIssues: IIssueDraftStore["draftIssues"] = {};
|
||||||
|
// service
|
||||||
|
draftIssueService: IssueDraftService;
|
||||||
rootStore;
|
rootStore;
|
||||||
|
|
||||||
constructor(_rootStore: RootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
@ -57,24 +61,42 @@ export class DraftIssueStore implements IDraftIssueStore {
|
|||||||
// observable
|
// observable
|
||||||
loader: observable.ref,
|
loader: observable.ref,
|
||||||
error: observable.ref,
|
error: observable.ref,
|
||||||
issues: observable.ref,
|
draftIssues: observable.ref,
|
||||||
// computed
|
// computed
|
||||||
getIssueType: computed,
|
getIssueType: computed,
|
||||||
getIssues: computed,
|
getDraftIssues: computed,
|
||||||
// actions
|
// actions
|
||||||
fetchIssues: action,
|
fetchIssues: action,
|
||||||
|
createDraftIssue: action,
|
||||||
updateIssueStructure: action,
|
updateIssueStructure: action,
|
||||||
|
deleteDraftIssue: action,
|
||||||
|
updateDraftIssue: action,
|
||||||
|
convertDraftIssueToIssue: action,
|
||||||
});
|
});
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
this.issueService = new IssueService();
|
this.draftIssueService = new IssueDraftService();
|
||||||
|
|
||||||
|
autorun(() => {
|
||||||
|
const workspaceSlug = this.rootStore.workspace.workspaceSlug;
|
||||||
|
const projectId = this.rootStore.project.projectId;
|
||||||
|
|
||||||
|
if (
|
||||||
|
workspaceSlug &&
|
||||||
|
projectId &&
|
||||||
|
this.rootStore.draftIssueFilters.userFilters &&
|
||||||
|
this.rootStore.draftIssueFilters.userDisplayFilters &&
|
||||||
|
this.rootStore.draftIssueFilters.appliedFilters
|
||||||
|
)
|
||||||
|
this.fetchIssues(workspaceSlug, projectId);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get getIssueType() {
|
get getIssueType() {
|
||||||
const groupedLayouts = ["kanban", "list", "calendar"];
|
const groupedLayouts = ["kanban", "list", "calendar"];
|
||||||
const ungroupedLayouts = ["spreadsheet", "gantt_chart"];
|
const ungroupedLayouts = ["spreadsheet", "gantt_chart"];
|
||||||
|
|
||||||
const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null;
|
const issueLayout = this.rootStore?.draftIssueFilters?.userDisplayFilters?.layout || null;
|
||||||
const issueSubGroup = this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by || null;
|
const issueSubGroup = this.rootStore?.draftIssueFilters?.userDisplayFilters?.sub_group_by || null;
|
||||||
if (!issueLayout) return null;
|
if (!issueLayout) return null;
|
||||||
|
|
||||||
const _issueState = groupedLayouts.includes(issueLayout)
|
const _issueState = groupedLayouts.includes(issueLayout)
|
||||||
@ -88,46 +110,135 @@ export class DraftIssueStore implements IDraftIssueStore {
|
|||||||
return _issueState || null;
|
return _issueState || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get getIssues() {
|
get getDraftIssues() {
|
||||||
const projectId: string | null = this.rootStore?.project?.projectId;
|
|
||||||
const issueType = this.getIssueType;
|
const issueType = this.getIssueType;
|
||||||
|
const projectId = this.rootStore?.project?.projectId;
|
||||||
|
|
||||||
if (!projectId || !issueType) return null;
|
if (!projectId || !issueType) return null;
|
||||||
|
|
||||||
return this.issues?.[projectId]?.[issueType] || null;
|
return this.draftIssues?.[projectId]?.[issueType] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchIssues = async (workspaceSlug: string, projectId: string) => {
|
||||||
|
try {
|
||||||
|
this.loader = true;
|
||||||
|
this.error = null;
|
||||||
|
|
||||||
|
this.rootStore.workspace.setWorkspaceSlug(workspaceSlug);
|
||||||
|
this.rootStore.project.setProjectId(projectId);
|
||||||
|
|
||||||
|
const params = this.rootStore?.draftIssueFilters?.appliedFilters;
|
||||||
|
const issueResponse = await this.draftIssueService.getDraftIssues(workspaceSlug, projectId, params);
|
||||||
|
|
||||||
|
const issueType = this.getIssueType;
|
||||||
|
if (issueType != null) {
|
||||||
|
const _issues = {
|
||||||
|
...this.draftIssues,
|
||||||
|
[projectId]: {
|
||||||
|
...this.draftIssues[projectId],
|
||||||
|
[issueType]: issueResponse,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = null;
|
||||||
|
this.draftIssues = _issues;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return issueResponse;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error: Fetching error in issues", error);
|
||||||
|
this.loader = false;
|
||||||
|
this.error = error;
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createDraftIssue = async (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => {
|
||||||
|
const originalIssues = { ...this.draftIssues };
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = true;
|
||||||
|
this.error = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await this.draftIssueService.createDraftIssue(workspaceSlug, projectId, issueForm);
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Creating issue error", error);
|
||||||
|
// reverting back to original issues in case of error
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = error;
|
||||||
|
this.draftIssues = originalIssues;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
|
||||||
const projectId: string | null = issue?.project;
|
const projectId: string | null = issue?.project;
|
||||||
const issueType = this.getIssueType;
|
const issueType = this.getIssueType;
|
||||||
|
|
||||||
if (!projectId || !issueType) return null;
|
if (!projectId || !issueType) return null;
|
||||||
|
|
||||||
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
|
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
|
||||||
this.getIssues;
|
this.getDraftIssues;
|
||||||
if (!issues) return null;
|
if (!issues) return null;
|
||||||
|
|
||||||
if (issueType === "grouped" && group_id) {
|
if (issueType === "grouped" && group_id) {
|
||||||
issues = issues as IIssueGroupedStructure;
|
issues = issues as IIssueGroupedStructure;
|
||||||
issues = {
|
const currentIssue = issues?.[group_id]?.find((i: IIssue) => i?.id === issue?.id);
|
||||||
...issues,
|
|
||||||
[group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
|
// if issue is already present in the list then update it
|
||||||
};
|
if (currentIssue)
|
||||||
|
issues = {
|
||||||
|
...issues,
|
||||||
|
[group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
|
||||||
|
};
|
||||||
|
// if issue is not present in the list then append it
|
||||||
|
else issues = { ...issues, [group_id]: [...issues[group_id], issue] };
|
||||||
}
|
}
|
||||||
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
|
if (issueType === "groupWithSubGroups" && group_id && sub_group_id) {
|
||||||
issues = issues as IIssueGroupWithSubGroupsStructure;
|
issues = issues as IIssueGroupWithSubGroupsStructure;
|
||||||
issues = {
|
const currentIssue = issues?.[sub_group_id]?.[group_id]?.find((i: IIssue) => i?.id === issue?.id);
|
||||||
...issues,
|
|
||||||
[sub_group_id]: {
|
// if issue is already present in the list then update it
|
||||||
...issues[sub_group_id],
|
if (currentIssue)
|
||||||
[group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
|
issues = {
|
||||||
},
|
...issues,
|
||||||
};
|
[sub_group_id]: {
|
||||||
|
...issues[sub_group_id],
|
||||||
|
[group_id]: issues[sub_group_id][group_id].map((i: IIssue) =>
|
||||||
|
i?.id === issue?.id ? { ...i, ...issue } : i
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// if issue is not present in the list then append it
|
||||||
|
else
|
||||||
|
issues = {
|
||||||
|
...issues,
|
||||||
|
[sub_group_id]: {
|
||||||
|
...issues[sub_group_id],
|
||||||
|
[group_id]: [...issues[sub_group_id][group_id], issue],
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (issueType === "ungrouped") {
|
if (issueType === "ungrouped") {
|
||||||
issues = issues as IIssueUnGroupedStructure;
|
issues = (issues || []) as IIssueUnGroupedStructure;
|
||||||
issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i));
|
const currentIssue = issues?.find((i: IIssue) => i?.id === issue?.id);
|
||||||
|
if (currentIssue) issues = issues?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i));
|
||||||
|
else issues = [...issues, issue];
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || "";
|
const orderBy = this.rootStore?.draftIssueFilters?.userDisplayFilters?.order_by || "";
|
||||||
if (orderBy === "-created_at") {
|
if (orderBy === "-created_at") {
|
||||||
issues = sortArrayByDate(issues as any, "created_at");
|
issues = sortArrayByDate(issues as any, "created_at");
|
||||||
}
|
}
|
||||||
@ -142,43 +253,72 @@ export class DraftIssueStore implements IDraftIssueStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } };
|
this.draftIssues = { ...this.draftIssues, [projectId]: { ...this.draftIssues[projectId], [issueType]: issues } };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchIssues = async (workspaceSlug: string, projectId: string) => {
|
updateDraftIssue = async (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => {
|
||||||
try {
|
const originalIssues = { ...this.draftIssues };
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
});
|
||||||
|
|
||||||
this.rootStore.workspace.setWorkspaceSlug(workspaceSlug);
|
try {
|
||||||
this.rootStore.project.setProjectId(projectId);
|
const response = await this.draftIssueService.updateDraftIssue(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
issueForm?.id!,
|
||||||
|
issueForm
|
||||||
|
);
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = null;
|
||||||
|
});
|
||||||
|
|
||||||
const params = this.rootStore?.issueFilter?.appliedFilters;
|
return response;
|
||||||
const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params);
|
|
||||||
|
|
||||||
const issueType = this.getIssueType;
|
|
||||||
if (issueType != null) {
|
|
||||||
const _issues = {
|
|
||||||
...this.issues,
|
|
||||||
[projectId]: {
|
|
||||||
...this.issues[projectId],
|
|
||||||
[issueType]: issueResponse,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
runInAction(() => {
|
|
||||||
this.issues = _issues;
|
|
||||||
this.loader = false;
|
|
||||||
this.error = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return issueResponse;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error: Fetching error in issues", error);
|
console.error("Updating issue error", error);
|
||||||
this.loader = false;
|
// reverting back to original issues in case of error
|
||||||
this.error = error;
|
runInAction(() => {
|
||||||
return error;
|
this.loader = false;
|
||||||
|
this.error = error;
|
||||||
|
this.draftIssues = originalIssues;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
convertDraftIssueToIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
|
await this.updateDraftIssue(workspaceSlug, projectId, { id: issueId, is_draft: false });
|
||||||
|
await this.fetchIssues(workspaceSlug, projectId);
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteDraftIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
|
const originalIssues = { ...this.draftIssues };
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = true;
|
||||||
|
this.error = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// deleting using api
|
||||||
|
await this.draftIssueService.deleteDraftIssue(workspaceSlug, projectId, issueId);
|
||||||
|
await this.fetchIssues(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = null;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Deleting issue error", error);
|
||||||
|
// reverting back to original issues in case of error
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = error;
|
||||||
|
this.draftIssues = originalIssues;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,53 @@
|
|||||||
import { observable, computed, makeObservable } from "mobx";
|
import { observable, computed, makeObservable, runInAction, action } from "mobx";
|
||||||
// helpers
|
// helpers
|
||||||
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
|
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
|
||||||
|
// services
|
||||||
|
import { IssueService } from "services/issue";
|
||||||
|
import { ProjectService } from "services/project";
|
||||||
// types
|
// types
|
||||||
import { RootStore } from "../root";
|
import { RootStore } from "../root";
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types";
|
import type {
|
||||||
|
IIssueDisplayFilterOptions,
|
||||||
|
IIssueDisplayProperties,
|
||||||
|
IIssueFilterOptions,
|
||||||
|
TIssueParams,
|
||||||
|
IProjectViewProps,
|
||||||
|
} from "types";
|
||||||
|
|
||||||
export interface IDraftIssueFilterStore {
|
export interface IDraftIssueFilterStore {
|
||||||
|
loader: boolean;
|
||||||
|
error: any | null;
|
||||||
|
|
||||||
|
// observables
|
||||||
userDisplayProperties: IIssueDisplayProperties;
|
userDisplayProperties: IIssueDisplayProperties;
|
||||||
userDisplayFilters: IIssueDisplayFilterOptions;
|
userDisplayFilters: IIssueDisplayFilterOptions;
|
||||||
userFilters: IIssueFilterOptions;
|
userFilters: IIssueFilterOptions;
|
||||||
|
|
||||||
|
// services
|
||||||
|
projectService: ProjectService;
|
||||||
|
issueService: IssueService;
|
||||||
|
|
||||||
// computed
|
// computed
|
||||||
appliedFilters: TIssueParams[] | null;
|
appliedFilters: TIssueParams[] | null;
|
||||||
|
|
||||||
|
// actions
|
||||||
|
fetchUserProjectFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||||
|
updateUserFilters: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
properties: Partial<IProjectViewProps>
|
||||||
|
) => Promise<void>;
|
||||||
|
updateDisplayProperties: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
properties: Partial<IIssueDisplayProperties>
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DraftIssueFilterStore implements IDraftIssueFilterStore {
|
export class DraftIssueFilterStore implements IDraftIssueFilterStore {
|
||||||
|
loader: boolean = false;
|
||||||
|
error: any | null = null;
|
||||||
|
|
||||||
// observables
|
// observables
|
||||||
userFilters: IIssueFilterOptions = {
|
userFilters: IIssueFilterOptions = {
|
||||||
priority: null,
|
priority: null,
|
||||||
@ -52,8 +85,15 @@ export class DraftIssueFilterStore implements IDraftIssueFilterStore {
|
|||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
|
|
||||||
|
// services
|
||||||
|
projectService: ProjectService;
|
||||||
|
issueService: IssueService;
|
||||||
|
|
||||||
constructor(_rootStore: RootStore) {
|
constructor(_rootStore: RootStore) {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
|
loader: observable,
|
||||||
|
error: observable,
|
||||||
|
|
||||||
// observables
|
// observables
|
||||||
userFilters: observable.ref,
|
userFilters: observable.ref,
|
||||||
userDisplayFilters: observable.ref,
|
userDisplayFilters: observable.ref,
|
||||||
@ -61,9 +101,19 @@ export class DraftIssueFilterStore implements IDraftIssueFilterStore {
|
|||||||
|
|
||||||
// computed
|
// computed
|
||||||
appliedFilters: computed,
|
appliedFilters: computed,
|
||||||
|
|
||||||
|
// actions
|
||||||
|
fetchUserProjectFilters: action,
|
||||||
|
updateUserFilters: action,
|
||||||
|
updateDisplayProperties: action,
|
||||||
|
computedFilter: action,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
|
|
||||||
|
// services
|
||||||
|
this.issueService = new IssueService();
|
||||||
|
this.projectService = new ProjectService();
|
||||||
}
|
}
|
||||||
|
|
||||||
computedFilter = (filters: any, filteredParams: any) => {
|
computedFilter = (filters: any, filteredParams: any) => {
|
||||||
@ -98,12 +148,110 @@ export class DraftIssueFilterStore implements IDraftIssueFilterStore {
|
|||||||
start_target_date: this.userDisplayFilters?.start_target_date || true,
|
start_target_date: this.userDisplayFilters?.start_target_date || true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// this.userDisplayFilters.layout should be list or kanban
|
||||||
const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout, "issues");
|
const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout, "issues");
|
||||||
if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams);
|
if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams);
|
||||||
|
|
||||||
if (this.userDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date";
|
|
||||||
if (this.userDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true;
|
|
||||||
|
|
||||||
return filteredRouteParams;
|
return filteredRouteParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUserFilters = async (workspaceSlug: string, projectId: string, properties: Partial<IProjectViewProps>) => {
|
||||||
|
const newViewProps = {
|
||||||
|
display_filters: {
|
||||||
|
...this.userDisplayFilters,
|
||||||
|
...properties.display_filters,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
...this.userFilters,
|
||||||
|
...properties.filters,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// set sub_group_by to null if group_by is set to null
|
||||||
|
if (newViewProps.display_filters.group_by === null) newViewProps.display_filters.sub_group_by = null;
|
||||||
|
|
||||||
|
// set group_by to state if layout is switched to kanban and group_by is null
|
||||||
|
if (newViewProps.display_filters.layout === "kanban" && newViewProps.display_filters.group_by === null)
|
||||||
|
newViewProps.display_filters.group_by = "state";
|
||||||
|
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.userFilters = newViewProps.filters;
|
||||||
|
this.userDisplayFilters = {
|
||||||
|
...newViewProps.display_filters,
|
||||||
|
// set layout to list if layout is not list or kanban
|
||||||
|
layout:
|
||||||
|
newViewProps.display_filters.layout === "list" || newViewProps.display_filters.layout === "kanban"
|
||||||
|
? newViewProps.display_filters.layout
|
||||||
|
: "list",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
this.projectService.setProjectView(workspaceSlug, projectId, {
|
||||||
|
view_props: newViewProps,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchUserProjectFilters(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Failed to update user filters in issue filter store", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateDisplayProperties = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
properties: Partial<IIssueDisplayProperties>
|
||||||
|
) => {
|
||||||
|
const newProperties: IIssueDisplayProperties = {
|
||||||
|
...this.userDisplayProperties,
|
||||||
|
...properties,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.userDisplayProperties = newProperties;
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.issueService.updateIssueDisplayProperties(workspaceSlug, projectId, newProperties);
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchUserProjectFilters(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Failed to update user display properties in issue filter store", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchUserProjectFilters = async (workspaceSlug: string, projectId: string) => {
|
||||||
|
try {
|
||||||
|
const memberResponse = await this.projectService.projectMemberMe(workspaceSlug, projectId);
|
||||||
|
const issueProperties = await this.issueService.getIssueDisplayProperties(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.userFilters = memberResponse?.view_props?.filters;
|
||||||
|
this.userDisplayFilters = {
|
||||||
|
...(memberResponse?.view_props?.display_filters ?? {}),
|
||||||
|
// set layout to list if layout is not list or kanban
|
||||||
|
layout:
|
||||||
|
memberResponse?.view_props?.display_filters?.layout === "list" ||
|
||||||
|
memberResponse?.view_props?.display_filters?.layout === "kanban"
|
||||||
|
? memberResponse?.view_props?.display_filters?.layout
|
||||||
|
: "list",
|
||||||
|
};
|
||||||
|
this.userDisplayProperties = issueProperties?.properties;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Failed to fetch user filters in issue filter store", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
export * from "./issue_detail.store";
|
export * from "./issue_detail.store";
|
||||||
export * from "./issue_draft.store";
|
|
||||||
export * from "./issue_filters.store";
|
export * from "./issue_filters.store";
|
||||||
export * from "./issue_kanban_view.store";
|
export * from "./issue_kanban_view.store";
|
||||||
export * from "./issue_calendar_view.store";
|
export * from "./issue_calendar_view.store";
|
||||||
|
@ -1,169 +0,0 @@
|
|||||||
// mobx
|
|
||||||
import { action, observable, runInAction, makeAutoObservable } from "mobx";
|
|
||||||
// services
|
|
||||||
import { IssueDraftService } from "services/issue";
|
|
||||||
// types
|
|
||||||
import type { IIssue, IUser } from "types";
|
|
||||||
|
|
||||||
export class DraftIssuesStore {
|
|
||||||
issues: { [key: string]: IIssue } = {};
|
|
||||||
isIssuesLoading: boolean = false;
|
|
||||||
rootStore: any | null = null;
|
|
||||||
issueDraftService;
|
|
||||||
|
|
||||||
constructor(_rootStore: any | null = null) {
|
|
||||||
makeAutoObservable(this, {
|
|
||||||
issues: observable.ref,
|
|
||||||
isIssuesLoading: observable.ref,
|
|
||||||
rootStore: observable.ref,
|
|
||||||
loadDraftIssues: action,
|
|
||||||
getIssueById: action,
|
|
||||||
createDraftIssue: action,
|
|
||||||
updateDraftIssue: action,
|
|
||||||
deleteDraftIssue: action,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
|
||||||
this.issueDraftService = new IssueDraftService();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Fetch all draft issues of a project and hydrate issues field
|
|
||||||
*/
|
|
||||||
|
|
||||||
loadDraftIssues = async (workspaceSlug: string, projectId: string, params?: any) => {
|
|
||||||
this.isIssuesLoading = true;
|
|
||||||
try {
|
|
||||||
const issuesResponse = await this.issueDraftService.getDraftIssues(workspaceSlug, projectId, params);
|
|
||||||
|
|
||||||
const issues = Array.isArray(issuesResponse) ? { allIssues: issuesResponse } : issuesResponse;
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.issues = issues;
|
|
||||||
this.isIssuesLoading = false;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
this.isIssuesLoading = false;
|
|
||||||
console.error("Fetching issues error", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Fetch a single draft issue by id and hydrate issues field
|
|
||||||
* @param workspaceSlug
|
|
||||||
* @param projectId
|
|
||||||
* @param issueId
|
|
||||||
* @returns {IIssue}
|
|
||||||
*/
|
|
||||||
|
|
||||||
getIssueById = async (workspaceSlug: string, projectId: string, issueId: string): Promise<IIssue> => {
|
|
||||||
if (this.issues[issueId]) return this.issues[issueId];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const issueResponse: IIssue = await this.issueDraftService.getDraftIssueById(workspaceSlug, projectId, issueId);
|
|
||||||
|
|
||||||
const issues = {
|
|
||||||
...this.issues,
|
|
||||||
[issueId]: { ...issueResponse },
|
|
||||||
};
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.issues = issues;
|
|
||||||
});
|
|
||||||
|
|
||||||
return issueResponse;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Create a new draft issue and hydrate issues field
|
|
||||||
* @param workspaceSlug
|
|
||||||
* @param projectId
|
|
||||||
* @param issueForm
|
|
||||||
* @param user
|
|
||||||
* @returns {IIssue}
|
|
||||||
*/
|
|
||||||
|
|
||||||
createDraftIssue = async (
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
issueForm: IIssue,
|
|
||||||
user: IUser
|
|
||||||
): Promise<IIssue> => {
|
|
||||||
try {
|
|
||||||
const issueResponse = await this.issueDraftService.createDraftIssue(workspaceSlug, projectId, issueForm);
|
|
||||||
|
|
||||||
const issues = {
|
|
||||||
...this.issues,
|
|
||||||
[issueResponse.id]: { ...issueResponse },
|
|
||||||
};
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.issues = issues;
|
|
||||||
});
|
|
||||||
return issueResponse;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Creating issue error", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateDraftIssue = async (
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
issueId: string,
|
|
||||||
issueForm: Partial<IIssue>,
|
|
||||||
user: IUser
|
|
||||||
) => {
|
|
||||||
// keep a copy of the issue in the store
|
|
||||||
const originalIssue = { ...this.issues[issueId] };
|
|
||||||
|
|
||||||
// immediately update the issue in the store
|
|
||||||
const updatedIssue = { ...this.issues[issueId], ...issueForm };
|
|
||||||
|
|
||||||
try {
|
|
||||||
runInAction(() => {
|
|
||||||
this.issues[issueId] = { ...updatedIssue };
|
|
||||||
});
|
|
||||||
|
|
||||||
// make a patch request to update the issue
|
|
||||||
const issueResponse: IIssue = await this.issueDraftService.updateDraftIssue(
|
|
||||||
workspaceSlug,
|
|
||||||
projectId,
|
|
||||||
issueId,
|
|
||||||
issueForm
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedIssues = { ...this.issues };
|
|
||||||
updatedIssues[issueId] = { ...issueResponse };
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.issues = updatedIssues;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// if there is an error, revert the changes
|
|
||||||
runInAction(() => {
|
|
||||||
this.issues[issueId] = originalIssue;
|
|
||||||
});
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
deleteDraftIssue = async (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => {
|
|
||||||
const issues = { ...this.issues };
|
|
||||||
delete issues[issueId];
|
|
||||||
|
|
||||||
try {
|
|
||||||
runInAction(() => {
|
|
||||||
this.issues = issues;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.issueDraftService.deleteDraftIssue(workspaceSlug, projectId, issueId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Deleting issue error", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
@ -4,7 +4,6 @@ import CommandPaletteStore, { ICommandPaletteStore } from "./command-palette.sto
|
|||||||
import UserStore, { IUserStore } from "store/user.store";
|
import UserStore, { IUserStore } from "store/user.store";
|
||||||
import ThemeStore, { IThemeStore } from "store/theme.store";
|
import ThemeStore, { IThemeStore } from "store/theme.store";
|
||||||
import {
|
import {
|
||||||
DraftIssuesStore,
|
|
||||||
IIssueDetailStore,
|
IIssueDetailStore,
|
||||||
IIssueFilterStore,
|
IIssueFilterStore,
|
||||||
IIssueKanBanViewStore,
|
IIssueKanBanViewStore,
|
||||||
@ -88,7 +87,7 @@ import {
|
|||||||
ArchivedIssueDetailStore,
|
ArchivedIssueDetailStore,
|
||||||
IArchivedIssueDetailStore,
|
IArchivedIssueDetailStore,
|
||||||
} from "store/archived-issues";
|
} from "store/archived-issues";
|
||||||
import { DraftIssueStore, IDraftIssueStore, DraftIssueFilterStore, IDraftIssueFilterStore } from "store/draft-issues";
|
import { DraftIssueFilterStore, IDraftIssueFilterStore, IssueDraftStore, IIssueDraftStore } from "store/draft-issues";
|
||||||
import {
|
import {
|
||||||
IInboxFiltersStore,
|
IInboxFiltersStore,
|
||||||
IInboxIssueDetailsStore,
|
IInboxIssueDetailsStore,
|
||||||
@ -140,7 +139,6 @@ export class RootStore {
|
|||||||
issueDetail: IIssueDetailStore;
|
issueDetail: IIssueDetailStore;
|
||||||
issueKanBanView: IIssueKanBanViewStore;
|
issueKanBanView: IIssueKanBanViewStore;
|
||||||
issueCalendarView: IIssueCalendarViewStore;
|
issueCalendarView: IIssueCalendarViewStore;
|
||||||
draftIssuesStore: DraftIssuesStore;
|
|
||||||
quickAddIssue: IIssueQuickAddStore;
|
quickAddIssue: IIssueQuickAddStore;
|
||||||
|
|
||||||
calendar: ICalendarStore;
|
calendar: ICalendarStore;
|
||||||
@ -156,7 +154,7 @@ export class RootStore {
|
|||||||
archivedIssueDetail: IArchivedIssueDetailStore;
|
archivedIssueDetail: IArchivedIssueDetailStore;
|
||||||
archivedIssueFilters: IArchivedIssueFilterStore;
|
archivedIssueFilters: IArchivedIssueFilterStore;
|
||||||
|
|
||||||
draftIssues: IDraftIssueStore;
|
draftIssues: IIssueDraftStore;
|
||||||
draftIssueFilters: IDraftIssueFilterStore;
|
draftIssueFilters: IDraftIssueFilterStore;
|
||||||
|
|
||||||
inbox: IInboxStore;
|
inbox: IInboxStore;
|
||||||
@ -202,7 +200,6 @@ export class RootStore {
|
|||||||
this.issueDetail = new IssueDetailStore(this);
|
this.issueDetail = new IssueDetailStore(this);
|
||||||
this.issueKanBanView = new IssueKanBanViewStore(this);
|
this.issueKanBanView = new IssueKanBanViewStore(this);
|
||||||
this.issueCalendarView = new IssueCalendarViewStore(this);
|
this.issueCalendarView = new IssueCalendarViewStore(this);
|
||||||
this.draftIssuesStore = new DraftIssuesStore(this);
|
|
||||||
this.quickAddIssue = new IssueQuickAddStore(this);
|
this.quickAddIssue = new IssueQuickAddStore(this);
|
||||||
|
|
||||||
this.calendar = new CalendarStore(this);
|
this.calendar = new CalendarStore(this);
|
||||||
@ -218,7 +215,7 @@ export class RootStore {
|
|||||||
this.archivedIssueDetail = new ArchivedIssueDetailStore(this);
|
this.archivedIssueDetail = new ArchivedIssueDetailStore(this);
|
||||||
this.archivedIssueFilters = new ArchivedIssueFilterStore(this);
|
this.archivedIssueFilters = new ArchivedIssueFilterStore(this);
|
||||||
|
|
||||||
this.draftIssues = new DraftIssueStore(this);
|
this.draftIssues = new IssueDraftStore(this);
|
||||||
this.draftIssueFilters = new DraftIssueFilterStore(this);
|
this.draftIssueFilters = new DraftIssueFilterStore(this);
|
||||||
|
|
||||||
this.inbox = new InboxStore(this);
|
this.inbox = new InboxStore(this);
|
||||||
|
1
web/types/issues.d.ts
vendored
1
web/types/issues.d.ts
vendored
@ -128,6 +128,7 @@ export interface IIssue {
|
|||||||
workspace_detail: IWorkspaceLite;
|
workspace_detail: IWorkspaceLite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ISubIssuesState {
|
export interface ISubIssuesState {
|
||||||
backlog: number;
|
backlog: number;
|
||||||
unstarted: number;
|
unstarted: number;
|
||||||
|
Loading…
Reference in New Issue
Block a user