dev: layout setup for draft issues

This commit is contained in:
dakshesh14 2023-10-30 11:09:46 +05:30
parent bc6a983d1e
commit 55b9fbffd7
18 changed files with 946 additions and 176 deletions

View File

@ -1,17 +1,13 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { Dialog, Transition } from "@headlessui/react";
import { mutate } from "swr"; // store
import { observer } from "mobx-react-lite";
import { useMobxStore } from "lib/mobx/store-provider";
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import { IssueDraftService } from "services/issue";
// hooks // hooks
import useIssuesView from "hooks/use-issues-view";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// icons // icons
import { AlertTriangle } from "lucide-react"; import { AlertTriangle } from "lucide-react";
@ -19,31 +15,29 @@ import { AlertTriangle } from "lucide-react";
import { Button } from "@plane/ui"; import { Button } from "@plane/ui";
// types // types
import type { IIssue } from "types"; import type { IIssue } from "types";
// fetch-keys
import { PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys";
type Props = { 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) => {
const { isOpen, handleClose, data, onSuccess } = props;
export const DeleteDraftIssueModal: React.FC<Props> = (props) => { // router
const { isOpen, handleClose, data, onSubmit } = props; const router = useRouter();
const { workspaceSlug } = router.query;
// store
const { draftIssues: draftIssueStore } = useMobxStore();
// states
const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { params } = useIssuesView();
const { setToastAlert } = useToast();
const { user } = useUser(); const { user } = useUser();
const { setToastAlert } = useToast();
useEffect(() => { useEffect(() => {
setIsDeleteLoading(false); setIsDeleteLoading(false);
@ -58,30 +52,28 @@ export const DeleteDraftIssueModal: React.FC<Props> = (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();
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
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 (
@ -146,4 +138,4 @@ export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
</Dialog> </Dialog>
</Transition.Root> </Transition.Root>
); );
}; });

View File

@ -166,25 +166,6 @@ 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, data]);
// 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>,
action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft" action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft"
@ -193,7 +174,6 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
{ {
...(data ?? {}), ...(data ?? {}),
...formData, ...formData,
is_draft: action === "createDraft" || action === "updateDraft",
}, },
action action
); );

View File

@ -6,28 +6,22 @@ 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 useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import useIssuesView from "hooks/use-issues-view"; import useIssuesView from "hooks/use-issues-view";
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";
import useMyIssues from "hooks/my-issues/use-my-issues";
// components // components
import { DraftIssueForm } from "components/issues"; import { DraftIssueForm } from "components/issues";
// types // types
import type { IIssue } from "types"; import type { IIssue } from "types";
// fetch-keys // fetch-keys
import { import {
PROJECT_ISSUES_DETAILS,
USER_ISSUE,
SUB_ISSUES, SUB_ISSUES,
PROJECT_ISSUES_LIST_WITH_PARAMS,
CYCLE_ISSUES_WITH_PARAMS, CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS, MODULE_ISSUES_WITH_PARAMS,
VIEW_ISSUES,
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
CYCLE_DETAILS, CYCLE_DETAILS,
MODULE_DETAILS, MODULE_DETAILS,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
@ -57,7 +51,6 @@ 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) => {
@ -65,7 +58,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
data, data,
handleClose, handleClose,
isOpen, isOpen,
isUpdatingSingleIssue = false,
prePopulateData: prePopulateDataProps, prePopulateData: prePopulateDataProps,
fieldsToShow = ["all"], fieldsToShow = ["all"],
onSubmit, onSubmit,
@ -77,21 +70,23 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
const [prePopulateData, setPreloadedData] = useState<Partial<IIssue> | undefined>(undefined); const [prePopulateData, setPreloadedData] = useState<Partial<IIssue> | undefined>(undefined);
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { project: projectStore } = useMobxStore(); const {
project: projectStore,
draftIssues: draftIssueStore,
issueDetail: issueDetailStore,
issue: issueStore,
} = useMobxStore();
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined; const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
const { displayFilters, params } = useIssuesView(); const { params } = useIssuesView();
const { ...viewGanttParams } = params;
const { user } = useUser(); const { user } = useUser();
const { clearValue: clearDraftIssueLocalStorage } = useLocalStorage("draftedIssue", {}); const { clearValue: clearDraftIssueLocalStorage } = useLocalStorage("draftedIssue", {});
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const onClose = () => { const onClose = () => {
@ -133,35 +128,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
} }
}, [prePopulateDataProps, cycleId, moduleId, router.asPath, user?.id]); }, [prePopulateDataProps, 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
// and return to avoid activeProject being set to some other project // and return to avoid activeProject being set to some other project
@ -184,32 +150,19 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
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, data, projectId, projects, isOpen, prePopulateData]);
const ganttFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString())
: viewId
? VIEW_ISSUES(viewId.toString(), viewGanttParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "");
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(() => {
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); draftIssueStore.fetchIssues(workspaceSlug.toString(), activeProject);
if (groupedIssues) mutateMyIssues();
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "Issue created successfully.", message: "Issue created successfully.",
}); });
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
mutate(USER_ISSUE(workspaceSlug as string));
}) })
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
@ -223,26 +176,21 @@ 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 { // replace with actual group id and sub group id
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString())); draftIssueStore.updateIssueStructure(null, null, response);
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
}
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!",
@ -261,6 +209,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
const addIssueToCycle = async (issueId: string, cycleId: string) => { const addIssueToCycle = async (issueId: string, cycleId: string) => {
if (!workspaceSlug || !activeProject) return; if (!workspaceSlug || !activeProject) return;
// TODO: switch to store
await issueService await issueService
.addIssueToCycle( .addIssueToCycle(
workspaceSlug as string, workspaceSlug as string,
@ -282,6 +231,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
const addIssueToModule = async (issueId: string, moduleId: string) => { const addIssueToModule = async (issueId: string, moduleId: string) => {
if (!workspaceSlug || !activeProject) return; if (!workspaceSlug || !activeProject) return;
// TODO: switch to store
await moduleService await moduleService
.addIssuesToModule( .addIssuesToModule(
workspaceSlug as string, workspaceSlug as string,
@ -303,31 +253,20 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
const createIssue = async (payload: Partial<IIssue>) => { const createIssue = async (payload: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject) return; if (!workspaceSlug || !activeProject) return;
await issueService await issueDetailStore
.createIssue(workspaceSlug as string, activeProject ?? "", payload, user) .createIssue(workspaceSlug.toString(), activeProject, payload)
.then(async (res) => { .then(async (res) => {
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); 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);
if (displayFilters.layout === "gantt_chart")
mutate(ganttFetchKey, {
start_target_date: true,
order_by: "sort_order",
});
if (groupedIssues) mutateMyIssues();
setToastAlert({ setToastAlert({
type: "success", type: "success",
title: "Success!", title: "Success!",
message: "Issue created successfully.", message: "Issue created successfully.",
}); });
if (!createMore) onClose();
if (payload.assignees_list?.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(() => {
@ -337,6 +276,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 (
@ -354,8 +306,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();

View File

@ -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";

View File

@ -0,0 +1,149 @@
import { useCallback } 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";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// 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, projectId } = router.query;
const {
project: projectStore,
issueFilter: issueFilterStore,
issueKanBanView: issueKanBanViewStore,
draftIssues: draftIssuesStore,
} = useMobxStore();
const issues = draftIssuesStore.getDraftIssues?.data;
const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null;
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
const display_properties = issueFilterStore?.userDisplayProperties || null;
const currentKanBanView = "default";
// const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by
// ? "swimlanes"
// : "default";
const onDragEnd = (result: any) => {
if (!result) return;
if (
result.destination &&
result.source &&
result.destination.droppableId === result.source.droppableId &&
result.destination.index === result.source.index
)
return;
// TODO: use draft issue store instead
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.updateIssueStructure(group_by, sub_group_by, issue);
draftIssuesStore.updateDraftIssue(workspaceSlug.toString(), issue.project, issue);
}
if (action === "delete") draftIssuesStore.deleteDraftIssue(workspaceSlug.toString(), issue.project, issue.id);
if (action === "convertToIssue")
draftIssuesStore.convertDraftIssueToIssue(workspaceSlug.toString(), issue.project, issue.id);
},
[draftIssuesStore, workspaceSlug]
);
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
issueKanBanViewStore.handleKanBanToggle(toggle, value);
};
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 min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_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)}
/>
)}
display_properties={display_properties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
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}
/>
) : (
<KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_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)}
/>
)}
display_properties={display_properties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
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}
/>
)}
</DragDropContext>
</div>
);
});

View File

@ -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";

View File

@ -0,0 +1,80 @@
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, issueFilter: issueFilterStore } = useMobxStore();
const issues = draftIssuesStore.getDraftIssues?.data;
const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null;
const display_properties = issueFilterStore?.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)}
/>
)}
display_properties={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>
);
});

View File

@ -0,0 +1,76 @@
import { useState } from "react";
import { CustomMenu } from "@plane/ui";
import { Copy, Pencil, Trash2 } from "lucide-react";
// components
import { CreateUpdateDraftIssueModal, DeleteDraftIssueModal } from "components/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);
}}
// pre-populate date only if not editing
prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue } : {}}
data={issueToEdit}
/>
<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>
</>
);
};

View File

@ -1,3 +1,4 @@
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";

View File

@ -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, ProjectAppliedFiltersRoot } from "components/issues";
export const DraftIssueLayoutRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { issueFilter: issueFilterStore, draftIssues: draftIssuesStore } = useMobxStore();
useSWR(workspaceSlug && projectId ? `PROJECT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
if (workspaceSlug && projectId) {
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
await draftIssuesStore.fetchIssues(workspaceSlug.toString(), projectId.toString());
}
});
const activeLayout = issueFilterStore.userDisplayFilters.layout;
return (
<div className="relative w-full h-full flex flex-col overflow-hidden">
<ProjectAppliedFiltersRoot />
<div className="w-full h-full overflow-auto">
{activeLayout === "list" ? (
<DraftIssueListLayout />
) : activeLayout === "kanban" ? (
<DraftIssueKanBanLayout />
) : null}
</div>
</div>
);
});

View File

@ -3,3 +3,4 @@ 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";

View File

@ -5,8 +5,6 @@ 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";
@ -15,7 +13,7 @@ import { IssueForm, ConfirmIssueDiscard } from "components/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;
@ -39,8 +37,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;

View File

@ -3,6 +3,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
@ -30,6 +31,10 @@ const ProjectDraftIssues: NextPage = () => {
<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>
</AppLayout> </AppLayout>
</IssueViewContextProvider> </IssueViewContextProvider>

View File

@ -1,2 +1,3 @@
export * from "./issue.store"; export * from "./issue.store";
export * from "./issue_filters.store"; export * from "./issue_filters.store";
export * from "./issue_kanban_view.store";

View File

@ -23,14 +23,23 @@ export interface IIssueDraftStore {
draftIssues: { draftIssues: {
[project_id: string]: { [project_id: string]: {
grouped: { grouped: {
[group_id: string]: IIssue[]; [group_id: string]: {
data: IIssue[];
total_issues: number;
};
}; };
groupWithSubGroups: { groupWithSubGroups: {
[group_id: string]: { [group_id: string]: {
[sub_group_id: string]: IIssue[]; [sub_group_id: string]: {
data: IIssue[];
total_issues: number;
}; };
}; };
ungrouped: IIssue[]; };
ungrouped: {
data: IIssue[];
total_issues: number;
};
}; };
}; };
rootStore: RootStore; rootStore: RootStore;
@ -43,9 +52,9 @@ export interface IIssueDraftStore {
fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>; fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>;
createDraftIssue: (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => 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; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
deleteDraftIssue: (workspaceSlug: string, projectId: string, issueId: string) => void; deleteDraftIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>;
updateDraftIssue: (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => void; updateDraftIssue: (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => Promise<any>;
convertDraftIssueToIssue: (workspaceSlug: string, projectId: string, issueId: string) => void; convertDraftIssueToIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>;
// service // service
draftIssueService: IssueDraftService; draftIssueService: IssueDraftService;
@ -82,7 +91,8 @@ export class IssueDraftStore implements IIssueDraftStore {
get getIssueType() { get getIssueType() {
// FIXME: this is temporary for development // FIXME: this is temporary for development
return "ungrouped"; return "grouped";
// return "ungrouped";
const groupedLayouts = ["kanban", "list", "calendar"]; const groupedLayouts = ["kanban", "list", "calendar"];
const ungroupedLayouts = ["spreadsheet", "gantt_chart"]; const ungroupedLayouts = ["spreadsheet", "gantt_chart"];
@ -120,7 +130,9 @@ export class IssueDraftStore implements IIssueDraftStore {
// const params = this.rootStore?.issueFilter?.appliedFilters; // const params = this.rootStore?.issueFilter?.appliedFilters;
// TODO: use actual params using applied filters // TODO: use actual params using applied filters
const params = {}; const params = {
group_by: "state",
};
const issueResponse = await this.draftIssueService.getDraftIssues(workspaceSlug, projectId, params); const issueResponse = await this.draftIssueService.getDraftIssues(workspaceSlug, projectId, params);
const issueType = this.getIssueType; const issueType = this.getIssueType;
@ -186,24 +198,47 @@ export class IssueDraftStore implements IIssueDraftStore {
if (issueType === "grouped" && group_id) { if (issueType === "grouped" && group_id) {
issues = issues as IIssueGroupedStructure; issues = issues as IIssueGroupedStructure;
const currentIssue = issues?.[group_id]?.find((i: IIssue) => i?.id === issue?.id);
// if issue is already present in the list then update it
if (currentIssue)
issues = { issues = {
...issues, ...issues,
[group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)), [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;
const currentIssue = issues?.[sub_group_id]?.[group_id]?.find((i: IIssue) => i?.id === issue?.id);
// if issue is already present in the list then update it
if (currentIssue)
issues = { issues = {
...issues, ...issues,
[sub_group_id]: { [sub_group_id]: {
...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)), [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?.issueFilter?.userDisplayFilters?.order_by || "";
@ -241,11 +276,18 @@ export class IssueDraftStore implements IIssueDraftStore {
this.updateIssueStructure(group_id, sub_group_id, issueForm as IIssue); this.updateIssueStructure(group_id, sub_group_id, issueForm as IIssue);
try { try {
await this.draftIssueService.updateDraftIssue(workspaceSlug, projectId, issueForm?.id!, issueForm); const response = await this.draftIssueService.updateDraftIssue(
workspaceSlug,
projectId,
issueForm?.id!,
issueForm
);
runInAction(() => { runInAction(() => {
this.loader = false; this.loader = false;
this.error = null; this.error = null;
}); });
return response;
} catch (error) { } catch (error) {
console.error("Updating issue error", error); console.error("Updating issue error", error);
// reverting back to original issues in case of error // reverting back to original issues in case of error
@ -257,10 +299,9 @@ export class IssueDraftStore implements IIssueDraftStore {
} }
}; };
convertDraftIssueToIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { convertDraftIssueToIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
// update draft issue with is_draft being false // TODO: add removing item from draft issue list
this.updateDraftIssue(workspaceSlug, projectId, { id: issueId, is_draft: false }); await this.updateDraftIssue(workspaceSlug, projectId, { id: issueId, is_draft: false });
};
deleteDraftIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { deleteDraftIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
const originalIssues = { ...this.draftIssues }; const originalIssues = { ...this.draftIssues };

View File

@ -0,0 +1,448 @@
import { action, computed, makeObservable, observable, runInAction } from "mobx";
// types
import { RootStore } from "../root";
import { IIssueType } from "./issue.store";
export interface IDraftIssueKanBanViewStore {
kanBanToggle: {
groupByHeaderMinMax: string[];
subgroupByIssuesVisibility: string[];
};
// computed
canUserDragDrop: boolean;
canUserDragDropVertically: boolean;
canUserDragDropHorizontally: boolean;
// actions
handleKanBanToggle: (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => void;
handleSwimlaneDragDrop: (source: any, destination: any) => void;
handleDragDrop: (source: any, destination: any) => void;
}
// TODO: change implementation for draft issues
export class DraftIssueKanBanViewStore implements IDraftIssueKanBanViewStore {
kanBanToggle: {
groupByHeaderMinMax: string[];
subgroupByIssuesVisibility: string[];
} = { groupByHeaderMinMax: [], subgroupByIssuesVisibility: [] };
// root store
rootStore;
constructor(_rootStore: RootStore) {
makeObservable(this, {
kanBanToggle: observable,
// computed
canUserDragDrop: computed,
canUserDragDropVertically: computed,
canUserDragDropHorizontally: computed,
// actions
handleKanBanToggle: action,
handleSwimlaneDragDrop: action,
handleDragDrop: action,
});
this.rootStore = _rootStore;
}
get canUserDragDrop() {
if (
this.rootStore?.issueFilter?.userDisplayFilters?.order_by &&
this.rootStore?.issueFilter?.userDisplayFilters?.order_by === "sort_order" &&
this.rootStore?.issueFilter?.userDisplayFilters?.group_by &&
["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.group_by)
) {
if (!this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by) return true;
if (
this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by &&
["state", "priority"].includes(this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by)
)
return true;
}
return false;
}
get canUserDragDropVertically() {
return false;
}
get canUserDragDropHorizontally() {
return false;
}
handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
this.kanBanToggle = {
...this.kanBanToggle,
[toggle]: this.kanBanToggle[toggle].includes(value)
? this.kanBanToggle[toggle].filter((v) => v !== value)
: [...this.kanBanToggle[toggle], value],
};
};
handleSwimlaneDragDrop = async (source: any, destination: any) => {
const workspaceSlug = this.rootStore?.workspace?.workspaceSlug;
const projectId = this.rootStore?.project?.projectId;
const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType;
const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null;
const currentIssues: any = this.rootStore.issue.getIssues;
const sortOrderDefaultValue = 65535;
if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) {
// update issue payload
let updateIssue: any = {
workspaceSlug: workspaceSlug,
projectId: projectId,
};
// source, destination group and sub group id
let droppableSourceColumnId = source.droppableId;
droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null;
let droppableDestinationColumnId = destination.droppableId;
droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null;
if (!droppableSourceColumnId || !droppableDestinationColumnId) return null;
const source_group_id: string = droppableSourceColumnId[0];
const source_sub_group_id: string = droppableSourceColumnId[1] === "null" ? null : droppableSourceColumnId[1];
const destination_group_id: string = droppableDestinationColumnId[0];
const destination_sub_group_id: string =
droppableDestinationColumnId[1] === "null" ? null : droppableDestinationColumnId[1];
if (source_sub_group_id === destination_sub_group_id) {
if (source_group_id === destination_group_id) {
const _issues = currentIssues[source_sub_group_id][source_group_id];
// update the sort order
if (destination.index === 0) {
updateIssue = {
...updateIssue,
sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue,
};
} else if (destination.index === _issues.length - 1) {
updateIssue = {
...updateIssue,
sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue,
};
} else {
updateIssue = {
...updateIssue,
sort_order: (_issues[destination.index - 1].sort_order + _issues[destination.index].sort_order) / 2,
};
}
const [removed] = _issues.splice(source.index, 1);
_issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order });
updateIssue = { ...updateIssue, issueId: removed?.id };
currentIssues[source_sub_group_id][source_group_id] = _issues;
}
if (source_group_id != destination_group_id) {
const _sourceIssues = currentIssues[source_sub_group_id][source_group_id];
let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || [];
if (_destinationIssues && _destinationIssues.length > 0) {
if (destination.index === 0) {
updateIssue = {
...updateIssue,
sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue,
};
} else if (destination.index === _destinationIssues.length) {
updateIssue = {
...updateIssue,
sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue,
};
} else {
updateIssue = {
...updateIssue,
sort_order:
(_destinationIssues[destination.index - 1].sort_order +
_destinationIssues[destination.index].sort_order) /
2,
};
}
} else {
updateIssue = {
...updateIssue,
sort_order: sortOrderDefaultValue,
};
}
let issueStatePriority = {};
if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state") {
updateIssue = { ...updateIssue, state: destination_group_id };
issueStatePriority = { ...issueStatePriority, state: destination_group_id };
}
if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority") {
updateIssue = { ...updateIssue, priority: destination_group_id };
issueStatePriority = { ...issueStatePriority, priority: destination_group_id };
}
const [removed] = _sourceIssues.splice(source.index, 1);
if (_destinationIssues && _destinationIssues.length > 0)
_destinationIssues.splice(destination.index, 0, {
...removed,
sort_order: updateIssue.sort_order,
...issueStatePriority,
});
else
_destinationIssues = [
..._destinationIssues,
{ ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority },
];
updateIssue = { ...updateIssue, issueId: removed?.id };
currentIssues[source_sub_group_id][source_group_id] = _sourceIssues;
currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues;
}
}
if (source_sub_group_id != destination_sub_group_id) {
const _sourceIssues = currentIssues[source_sub_group_id][source_group_id];
let _destinationIssues = currentIssues[destination_sub_group_id][destination_group_id] || [];
if (_destinationIssues && _destinationIssues.length > 0) {
if (destination.index === 0) {
updateIssue = {
...updateIssue,
sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue,
};
} else if (destination.index === _destinationIssues.length) {
updateIssue = {
...updateIssue,
sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue,
};
} else {
updateIssue = {
...updateIssue,
sort_order:
(_destinationIssues[destination.index - 1].sort_order +
_destinationIssues[destination.index].sort_order) /
2,
};
}
} else {
updateIssue = {
...updateIssue,
sort_order: sortOrderDefaultValue,
};
}
let issueStatePriority = {};
if (source_group_id === destination_group_id) {
if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "state") {
updateIssue = { ...updateIssue, state: destination_sub_group_id };
issueStatePriority = { ...issueStatePriority, state: destination_sub_group_id };
}
if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "priority") {
updateIssue = { ...updateIssue, priority: destination_sub_group_id };
issueStatePriority = { ...issueStatePriority, priority: destination_sub_group_id };
}
} else {
if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "state") {
updateIssue = { ...updateIssue, state: destination_sub_group_id, priority: destination_group_id };
issueStatePriority = {
...issueStatePriority,
state: destination_sub_group_id,
priority: destination_group_id,
};
}
if (this.rootStore.issueFilter?.userDisplayFilters?.sub_group_by === "priority") {
updateIssue = { ...updateIssue, state: destination_group_id, priority: destination_sub_group_id };
issueStatePriority = {
...issueStatePriority,
state: destination_group_id,
priority: destination_sub_group_id,
};
}
}
const [removed] = _sourceIssues.splice(source.index, 1);
if (_destinationIssues && _destinationIssues.length > 0)
_destinationIssues.splice(destination.index, 0, {
...removed,
sort_order: updateIssue.sort_order,
...issueStatePriority,
});
else
_destinationIssues = [
..._destinationIssues,
{ ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority },
];
updateIssue = { ...updateIssue, issueId: removed?.id };
currentIssues[source_sub_group_id][source_group_id] = _sourceIssues;
currentIssues[destination_sub_group_id][destination_group_id] = _destinationIssues;
}
const reorderedIssues = {
...this.rootStore?.issue.issues,
[projectId]: {
...this.rootStore?.issue.issues?.[projectId],
[issueType]: {
...this.rootStore?.issue.issues?.[projectId]?.[issueType],
[issueType]: currentIssues,
},
},
};
runInAction(() => {
this.rootStore.issue.issues = { ...reorderedIssues };
});
this.rootStore.issueDetail?.updateIssue(
updateIssue.workspaceSlug,
updateIssue.projectId,
updateIssue.issueId,
updateIssue
);
}
};
handleDragDrop = async (source: any, destination: any) => {
const workspaceSlug = this.rootStore?.workspace?.workspaceSlug;
const projectId = this.rootStore?.project?.projectId;
const issueType: IIssueType | null = this.rootStore?.issue?.getIssueType;
const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null;
const currentIssues: any = this.rootStore.issue.getIssues;
const sortOrderDefaultValue = 65535;
if (workspaceSlug && projectId && issueType && issueLayout === "kanban" && currentIssues) {
// update issue payload
let updateIssue: any = {
workspaceSlug: workspaceSlug,
projectId: projectId,
};
// source, destination group and sub group id
let droppableSourceColumnId = source.droppableId;
droppableSourceColumnId = droppableSourceColumnId ? droppableSourceColumnId.split("__") : null;
let droppableDestinationColumnId = destination.droppableId;
droppableDestinationColumnId = droppableDestinationColumnId ? droppableDestinationColumnId.split("__") : null;
if (!droppableSourceColumnId || !droppableDestinationColumnId) return null;
const source_group_id: string = droppableSourceColumnId[0];
const destination_group_id: string = droppableDestinationColumnId[0];
if (this.canUserDragDrop) {
// vertical
if (source_group_id === destination_group_id) {
const _issues = currentIssues[source_group_id];
// update the sort order
if (destination.index === 0) {
updateIssue = {
...updateIssue,
sort_order: _issues[destination.index].sort_order - sortOrderDefaultValue,
};
} else if (destination.index === _issues.length - 1) {
updateIssue = {
...updateIssue,
sort_order: _issues[destination.index].sort_order + sortOrderDefaultValue,
};
} else {
updateIssue = {
...updateIssue,
sort_order: (_issues[destination.index - 1].sort_order + _issues[destination.index].sort_order) / 2,
};
}
const [removed] = _issues.splice(source.index, 1);
_issues.splice(destination.index, 0, { ...removed, sort_order: updateIssue.sort_order });
updateIssue = { ...updateIssue, issueId: removed?.id };
currentIssues[source_group_id] = _issues;
}
// horizontal
if (source_group_id != destination_group_id) {
const _sourceIssues = currentIssues[source_group_id];
let _destinationIssues = currentIssues[destination_group_id] || [];
if (_destinationIssues && _destinationIssues.length > 0) {
if (destination.index === 0) {
updateIssue = {
...updateIssue,
sort_order: _destinationIssues[destination.index].sort_order - sortOrderDefaultValue,
};
} else if (destination.index === _destinationIssues.length) {
updateIssue = {
...updateIssue,
sort_order: _destinationIssues[destination.index - 1].sort_order + sortOrderDefaultValue,
};
} else {
updateIssue = {
...updateIssue,
sort_order:
(_destinationIssues[destination.index - 1].sort_order +
_destinationIssues[destination.index].sort_order) /
2,
};
}
} else {
updateIssue = {
...updateIssue,
sort_order: sortOrderDefaultValue,
};
}
let issueStatePriority = {};
if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "state") {
updateIssue = { ...updateIssue, state: destination_group_id };
issueStatePriority = { ...issueStatePriority, state: destination_group_id };
}
if (this.rootStore.issueFilter?.userDisplayFilters?.group_by === "priority") {
updateIssue = { ...updateIssue, priority: destination_group_id };
issueStatePriority = { ...issueStatePriority, priority: destination_group_id };
}
const [removed] = _sourceIssues.splice(source.index, 1);
if (_destinationIssues && _destinationIssues.length > 0)
_destinationIssues.splice(destination.index, 0, {
...removed,
sort_order: updateIssue.sort_order,
...issueStatePriority,
});
else
_destinationIssues = [
..._destinationIssues,
{ ...removed, sort_order: updateIssue.sort_order, ...issueStatePriority },
];
updateIssue = { ...updateIssue, issueId: removed?.id };
currentIssues[source_group_id] = _sourceIssues;
currentIssues[destination_group_id] = _destinationIssues;
}
}
// user can drag the issues only vertically
if (this.canUserDragDropVertically && destination_group_id === destination_group_id) {
}
// user can drag the issues only horizontally
if (this.canUserDragDropHorizontally && destination_group_id != destination_group_id) {
}
const reorderedIssues = {
...this.rootStore?.issue.issues,
[projectId]: {
...this.rootStore?.issue.issues?.[projectId],
[issueType]: {
...this.rootStore?.issue.issues?.[projectId]?.[issueType],
[issueType]: currentIssues,
},
},
};
runInAction(() => {
this.rootStore.issue.issues = { ...reorderedIssues };
});
this.rootStore.issueDetail?.updateIssue(
updateIssue.workspaceSlug,
updateIssue.projectId,
updateIssue.issueId,
updateIssue
);
}
};
}

View File

@ -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";

View File

@ -72,7 +72,14 @@ import {
ArchivedIssueFilterStore, ArchivedIssueFilterStore,
IArchivedIssueFilterStore, IArchivedIssueFilterStore,
} from "store/archived-issues"; } from "store/archived-issues";
import { DraftIssueFilterStore, IDraftIssueFilterStore, IssueDraftStore, IIssueDraftStore } from "store/draft-issues"; import {
DraftIssueFilterStore,
IDraftIssueFilterStore,
IssueDraftStore,
IIssueDraftStore,
DraftIssueKanBanViewStore,
IDraftIssueKanBanViewStore,
} from "store/draft-issues";
import { import {
IInboxFiltersStore, IInboxFiltersStore,
IInboxIssueDetailsStore, IInboxIssueDetailsStore,
@ -134,6 +141,7 @@ export class RootStore {
draftIssues: IIssueDraftStore; draftIssues: IIssueDraftStore;
draftIssueFilters: IDraftIssueFilterStore; draftIssueFilters: IDraftIssueFilterStore;
draftIssueKanBanView: IDraftIssueKanBanViewStore;
inbox: IInboxStore; inbox: IInboxStore;
inboxIssues: IInboxIssuesStore; inboxIssues: IInboxIssuesStore;
@ -188,6 +196,7 @@ export class RootStore {
this.draftIssues = new IssueDraftStore(this); this.draftIssues = new IssueDraftStore(this);
this.draftIssueFilters = new DraftIssueFilterStore(this); this.draftIssueFilters = new DraftIssueFilterStore(this);
this.draftIssueKanBanView = new DraftIssueKanBanViewStore(this);
this.inbox = new InboxStore(this); this.inbox = new InboxStore(this);
this.inboxIssues = new InboxIssuesStore(this); this.inboxIssues = new InboxIssuesStore(this);