forked from github/plane
dev: layout setup for draft issues
This commit is contained in:
parent
bc6a983d1e
commit
55b9fbffd7
@ -1,17 +1,13 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
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";
|
||||
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import { IssueDraftService } from "services/issue";
|
||||
// hooks
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import useToast from "hooks/use-toast";
|
||||
// icons
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
@ -19,31 +15,29 @@ import { AlertTriangle } from "lucide-react";
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import type { IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
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) => {
|
||||
const { isOpen, handleClose, data, onSubmit } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
// store
|
||||
const { draftIssues: draftIssueStore } = useMobxStore();
|
||||
|
||||
// states
|
||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId } = router.query;
|
||||
|
||||
const { params } = useIssuesView();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const { user } = useUser();
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
setIsDeleteLoading(false);
|
||||
@ -58,30 +52,28 @@ export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
|
||||
if (!workspaceSlug || !data || !user) return;
|
||||
|
||||
setIsDeleteLoading(true);
|
||||
|
||||
await issueDraftService
|
||||
await draftIssueStore
|
||||
.deleteDraftIssue(workspaceSlug as string, data.project, data.id)
|
||||
.then(() => {
|
||||
setIsDeleteLoading(false);
|
||||
handleClose();
|
||||
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(projectId as string, params));
|
||||
setToastAlert({
|
||||
title: "Success",
|
||||
message: "Draft Issue deleted successfully",
|
||||
type: "success",
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
handleClose();
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
title: "Error",
|
||||
message: "Something went wrong",
|
||||
type: "error",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
handleClose();
|
||||
setIsDeleteLoading(false);
|
||||
});
|
||||
if (onSubmit) await onSubmit();
|
||||
|
||||
if (onSuccess) await onSuccess();
|
||||
};
|
||||
|
||||
return (
|
||||
@ -146,4 +138,4 @@ export const DeleteDraftIssueModal: React.FC<Props> = (props) => {
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -166,25 +166,6 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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 (
|
||||
formData: Partial<IIssue>,
|
||||
action: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue" = "createDraft"
|
||||
@ -193,7 +174,6 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
||||
{
|
||||
...(data ?? {}),
|
||||
...formData,
|
||||
is_draft: action === "createDraft" || action === "updateDraft",
|
||||
},
|
||||
action
|
||||
);
|
||||
|
@ -6,28 +6,22 @@ import { Dialog, Transition } from "@headlessui/react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { IssueService, IssueDraftService } from "services/issue";
|
||||
import { IssueService } from "services/issue";
|
||||
import { ModuleService } from "services/module.service";
|
||||
// hooks
|
||||
import useUser from "hooks/use-user";
|
||||
import useIssuesView from "hooks/use-issues-view";
|
||||
import useToast from "hooks/use-toast";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||
// components
|
||||
import { DraftIssueForm } from "components/issues";
|
||||
// types
|
||||
import type { IIssue } from "types";
|
||||
// fetch-keys
|
||||
import {
|
||||
PROJECT_ISSUES_DETAILS,
|
||||
USER_ISSUE,
|
||||
SUB_ISSUES,
|
||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||
CYCLE_ISSUES_WITH_PARAMS,
|
||||
MODULE_ISSUES_WITH_PARAMS,
|
||||
VIEW_ISSUES,
|
||||
PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS,
|
||||
CYCLE_DETAILS,
|
||||
MODULE_DETAILS,
|
||||
} from "constants/fetch-keys";
|
||||
@ -57,7 +51,6 @@ interface IssuesModalProps {
|
||||
|
||||
// services
|
||||
const issueService = new IssueService();
|
||||
const issueDraftService = new IssueDraftService();
|
||||
const moduleService = new ModuleService();
|
||||
|
||||
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
||||
@ -65,7 +58,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
data,
|
||||
handleClose,
|
||||
isOpen,
|
||||
isUpdatingSingleIssue = false,
|
||||
|
||||
prePopulateData: prePopulateDataProps,
|
||||
fieldsToShow = ["all"],
|
||||
onSubmit,
|
||||
@ -77,21 +70,23 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
const [prePopulateData, setPreloadedData] = useState<Partial<IIssue> | undefined>(undefined);
|
||||
|
||||
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 { displayFilters, params } = useIssuesView();
|
||||
const { ...viewGanttParams } = params;
|
||||
const { params } = useIssuesView();
|
||||
|
||||
const { user } = useUser();
|
||||
|
||||
const { clearValue: clearDraftIssueLocalStorage } = useLocalStorage("draftedIssue", {});
|
||||
|
||||
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
const onClose = () => {
|
||||
@ -133,35 +128,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
}
|
||||
}, [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(() => {
|
||||
// if modal is closed, reset active project to null
|
||||
// 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);
|
||||
}, [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>) => {
|
||||
if (!workspaceSlug || !activeProject || !user) return;
|
||||
|
||||
await issueDraftService
|
||||
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload)
|
||||
.then(async () => {
|
||||
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||
|
||||
if (groupedIssues) mutateMyIssues();
|
||||
await draftIssueStore
|
||||
.createDraftIssue(workspaceSlug.toString(), activeProject, payload)
|
||||
.then(() => {
|
||||
draftIssueStore.fetchIssues(workspaceSlug.toString(), activeProject);
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
message: "Issue created successfully.",
|
||||
});
|
||||
|
||||
if (payload.assignees_list?.some((assignee) => assignee === user?.id))
|
||||
mutate(USER_ISSUE(workspaceSlug as string));
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
@ -223,26 +176,21 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
};
|
||||
|
||||
const updateDraftIssue = async (payload: Partial<IIssue>) => {
|
||||
if (!user) return;
|
||||
if (!user || !workspaceSlug || !activeProject) return;
|
||||
|
||||
await issueDraftService
|
||||
.updateDraftIssue(workspaceSlug as string, activeProject ?? "", data?.id ?? "", payload)
|
||||
.then((res) => {
|
||||
if (isUpdatingSingleIssue) {
|
||||
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
|
||||
} else {
|
||||
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
|
||||
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
|
||||
}
|
||||
await draftIssueStore
|
||||
.updateDraftIssue(workspaceSlug.toString(), activeProject, payload as IIssue)
|
||||
.then((response) => {
|
||||
if (!createMore) onClose();
|
||||
|
||||
// replace with actual group id and sub group id
|
||||
draftIssueStore.updateIssueStructure(null, null, response);
|
||||
|
||||
if (!payload.is_draft) {
|
||||
if (payload.cycle && payload.cycle !== "") addIssueToCycle(res.id, payload.cycle);
|
||||
if (payload.module && payload.module !== "") addIssueToModule(res.id, payload.module);
|
||||
if (payload.cycle && payload.cycle !== "") addIssueToCycle(response.id, payload.cycle);
|
||||
if (payload.module && payload.module !== "") addIssueToModule(response.id, payload.module);
|
||||
}
|
||||
|
||||
if (!createMore) onClose();
|
||||
|
||||
setToastAlert({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
@ -261,6 +209,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
const addIssueToCycle = async (issueId: string, cycleId: string) => {
|
||||
if (!workspaceSlug || !activeProject) return;
|
||||
|
||||
// TODO: switch to store
|
||||
await issueService
|
||||
.addIssueToCycle(
|
||||
workspaceSlug as string,
|
||||
@ -282,6 +231,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
const addIssueToModule = async (issueId: string, moduleId: string) => {
|
||||
if (!workspaceSlug || !activeProject) return;
|
||||
|
||||
// TODO: switch to store
|
||||
await moduleService
|
||||
.addIssuesToModule(
|
||||
workspaceSlug as string,
|
||||
@ -303,31 +253,20 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
const createIssue = async (payload: Partial<IIssue>) => {
|
||||
if (!workspaceSlug || !activeProject) return;
|
||||
|
||||
await issueService
|
||||
.createIssue(workspaceSlug as string, activeProject ?? "", payload, user)
|
||||
await issueDetailStore
|
||||
.createIssue(workspaceSlug.toString(), activeProject, payload)
|
||||
.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.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({
|
||||
type: "success",
|
||||
title: "Success!",
|
||||
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));
|
||||
})
|
||||
.catch(() => {
|
||||
@ -337,6 +276,19 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
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 (
|
||||
@ -354,8 +306,9 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
|
||||
};
|
||||
|
||||
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 === "convertToNewIssue") await convertDraftToIssue(payload);
|
||||
|
||||
clearDraftIssueLocalStorage();
|
||||
|
||||
|
@ -3,3 +3,4 @@ export * from "./module-root";
|
||||
export * from "./profile-issues-root";
|
||||
export * from "./project-root";
|
||||
export * from "./project-view-root";
|
||||
export * from "./project-draft-issue-root";
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
@ -3,3 +3,4 @@ export * from "./module-root";
|
||||
export * from "./profile-issues-root";
|
||||
export * from "./project-root";
|
||||
export * from "./project-view-root";
|
||||
export * from "./project-draft-issue-root";
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
export * from "./cycle-issue";
|
||||
export * from "./module-issue";
|
||||
export * from "./project-issue";
|
||||
export * from "./draft-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, 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>
|
||||
);
|
||||
});
|
@ -3,3 +3,4 @@ export * from "./global-view-layout-root";
|
||||
export * from "./module-layout-root";
|
||||
export * from "./project-layout-root";
|
||||
export * from "./project-view-layout-root";
|
||||
export * from "./draft-issue-layout-root";
|
||||
|
@ -5,8 +5,6 @@ import { mutate } from "swr";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// mobx store
|
||||
import { useMobxStore } from "lib/mobx/store-provider";
|
||||
// services
|
||||
import { IssueDraftService } from "services/issue";
|
||||
// hooks
|
||||
import useToast from "hooks/use-toast";
|
||||
import useLocalStorage from "hooks/use-local-storage";
|
||||
@ -15,7 +13,7 @@ import { IssueForm, ConfirmIssueDiscard } from "components/issues";
|
||||
// types
|
||||
import type { IIssue } from "types";
|
||||
// fetch-keys
|
||||
import { USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys";
|
||||
import { SUB_ISSUES } from "constants/fetch-keys";
|
||||
|
||||
export interface IssuesModalProps {
|
||||
data?: IIssue | null;
|
||||
@ -39,8 +37,6 @@ export interface IssuesModalProps {
|
||||
onSubmit?: (data: Partial<IIssue>) => Promise<void>;
|
||||
}
|
||||
|
||||
const issueDraftService = new IssueDraftService();
|
||||
|
||||
export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((props) => {
|
||||
const { data, handleClose, isOpen, prePopulateData: prePopulateDataProps, fieldsToShow = ["all"], onSubmit } = props;
|
||||
|
||||
|
@ -3,6 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// contexts
|
||||
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||
import { DraftIssueLayoutRoot } from "components/issues";
|
||||
// ui
|
||||
import { ProjectDraftIssueHeader } from "components/headers";
|
||||
// icons
|
||||
@ -30,6 +31,10 @@ const ProjectDraftIssues: NextPage = () => {
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full">
|
||||
<DraftIssueLayoutRoot />
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</IssueViewContextProvider>
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from "./issue.store";
|
||||
export * from "./issue_filters.store";
|
||||
export * from "./issue_kanban_view.store";
|
||||
|
@ -23,14 +23,23 @@ export interface IIssueDraftStore {
|
||||
draftIssues: {
|
||||
[project_id: string]: {
|
||||
grouped: {
|
||||
[group_id: string]: IIssue[];
|
||||
[group_id: string]: {
|
||||
data: IIssue[];
|
||||
total_issues: number;
|
||||
};
|
||||
};
|
||||
groupWithSubGroups: {
|
||||
[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;
|
||||
@ -43,9 +52,9 @@ export interface IIssueDraftStore {
|
||||
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) => void;
|
||||
updateDraftIssue: (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => void;
|
||||
convertDraftIssueToIssue: (workspaceSlug: string, projectId: string, issueId: string) => 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
|
||||
draftIssueService: IssueDraftService;
|
||||
@ -82,7 +91,8 @@ export class IssueDraftStore implements IIssueDraftStore {
|
||||
|
||||
get getIssueType() {
|
||||
// FIXME: this is temporary for development
|
||||
return "ungrouped";
|
||||
return "grouped";
|
||||
// return "ungrouped";
|
||||
|
||||
const groupedLayouts = ["kanban", "list", "calendar"];
|
||||
const ungroupedLayouts = ["spreadsheet", "gantt_chart"];
|
||||
@ -120,7 +130,9 @@ export class IssueDraftStore implements IIssueDraftStore {
|
||||
|
||||
// const params = this.rootStore?.issueFilter?.appliedFilters;
|
||||
// TODO: use actual params using applied filters
|
||||
const params = {};
|
||||
const params = {
|
||||
group_by: "state",
|
||||
};
|
||||
const issueResponse = await this.draftIssueService.getDraftIssues(workspaceSlug, projectId, params);
|
||||
|
||||
const issueType = this.getIssueType;
|
||||
@ -186,24 +198,47 @@ export class IssueDraftStore implements IIssueDraftStore {
|
||||
|
||||
if (issueType === "grouped" && group_id) {
|
||||
issues = issues as IIssueGroupedStructure;
|
||||
issues = {
|
||||
...issues,
|
||||
[group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)),
|
||||
};
|
||||
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,
|
||||
[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) {
|
||||
issues = issues as IIssueGroupWithSubGroupsStructure;
|
||||
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)),
|
||||
},
|
||||
};
|
||||
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,
|
||||
[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") {
|
||||
issues = issues as IIssueUnGroupedStructure;
|
||||
issues = issues.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i));
|
||||
issues = (issues || []) as IIssueUnGroupedStructure;
|
||||
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 || "";
|
||||
@ -241,11 +276,18 @@ export class IssueDraftStore implements IIssueDraftStore {
|
||||
this.updateIssueStructure(group_id, sub_group_id, issueForm as IIssue);
|
||||
|
||||
try {
|
||||
await this.draftIssueService.updateDraftIssue(workspaceSlug, projectId, issueForm?.id!, issueForm);
|
||||
const response = await this.draftIssueService.updateDraftIssue(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueForm?.id!,
|
||||
issueForm
|
||||
);
|
||||
runInAction(() => {
|
||||
this.loader = false;
|
||||
this.error = null;
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Updating issue error", 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) => {
|
||||
// update draft issue with is_draft being false
|
||||
this.updateDraftIssue(workspaceSlug, projectId, { id: issueId, is_draft: false });
|
||||
};
|
||||
convertDraftIssueToIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
|
||||
// TODO: add removing item from draft issue list
|
||||
await this.updateDraftIssue(workspaceSlug, projectId, { id: issueId, is_draft: false });
|
||||
|
||||
deleteDraftIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||
const originalIssues = { ...this.draftIssues };
|
||||
|
448
web/store/draft-issues/issue_kanban_view.store.ts
Normal file
448
web/store/draft-issues/issue_kanban_view.store.ts
Normal 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
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
export * from "./issue_detail.store";
|
||||
export * from "./issue_draft.store";
|
||||
export * from "./issue_filters.store";
|
||||
export * from "./issue_kanban_view.store";
|
||||
export * from "./issue_calendar_view.store";
|
||||
|
@ -72,7 +72,14 @@ import {
|
||||
ArchivedIssueFilterStore,
|
||||
IArchivedIssueFilterStore,
|
||||
} 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 {
|
||||
IInboxFiltersStore,
|
||||
IInboxIssueDetailsStore,
|
||||
@ -134,6 +141,7 @@ export class RootStore {
|
||||
|
||||
draftIssues: IIssueDraftStore;
|
||||
draftIssueFilters: IDraftIssueFilterStore;
|
||||
draftIssueKanBanView: IDraftIssueKanBanViewStore;
|
||||
|
||||
inbox: IInboxStore;
|
||||
inboxIssues: IInboxIssuesStore;
|
||||
@ -188,6 +196,7 @@ export class RootStore {
|
||||
|
||||
this.draftIssues = new IssueDraftStore(this);
|
||||
this.draftIssueFilters = new DraftIssueFilterStore(this);
|
||||
this.draftIssueKanBanView = new DraftIssueKanBanViewStore(this);
|
||||
|
||||
this.inbox = new InboxStore(this);
|
||||
this.inboxIssues = new InboxIssuesStore(this);
|
||||
|
Loading…
Reference in New Issue
Block a user