Compare commits

...

27 Commits

Author SHA1 Message Date
dakshesh14
b8c3abdc80 refactor: moved files to draft-issues 2023-11-07 15:33:12 +05:30
dakshesh14
b0413a50f6 Merge branch 'develop' of https://github.com/makeplane/plane into refactor/draft_issues 2023-11-07 14:41:18 +05:30
dakshesh14
aade1a72b7 fix: draft issue not mutating after confirmation 2023-11-07 14:09:11 +05:30
dakshesh14
3d2d2befaf fix: add filter/display properties 2023-11-07 13:58:00 +05:30
dakshesh14
c57c50aea8 fix: added applied filter in autorun 2023-11-07 13:57:27 +05:30
dakshesh14
50dd42aa8d fix: removed any type 2023-11-07 13:56:26 +05:30
dakshesh14
131f076010 Merge branch 'develop' of https://github.com/makeplane/plane into refactor/draft_issues 2023-11-07 13:38:15 +05:30
dakshesh14
bc492fa94f fix: reverted issue response structure 2023-11-06 21:41:42 +05:30
dakshesh14
2891ed1c3a fix: build errors 2023-11-06 21:34:39 +05:30
dakshesh14
a434b1008d fix: merge conflict 2023-11-06 21:21:17 +05:30
dakshesh14
f489f97aa8 fix: reset data to form logic 2023-11-06 21:20:23 +05:30
dakshesh14
24b82e518c fix: update/delete flow 2023-11-06 16:57:59 +05:30
dakshesh14
f9586ede31 refactor: draft issue filter store 2023-11-06 16:20:45 +05:30
dakshesh14
855c65bc87 fix: focus getting removed from description while typing 2023-11-06 13:31:04 +05:30
dakshesh14
6aaf9642bb fix: merge conflict 2023-11-06 13:10:07 +05:30
dakshesh14
faefb61d2f fix: description is not getting saved 2023-11-03 14:51:52 +05:30
dakshesh14
0c4e197940 fix: not passing grouping in kanban layout 2023-10-30 15:27:21 +05:30
dakshesh14
e3947254c8 Merge branch 'develop' of https://github.com/makeplane/plane into refactor/draft_issues 2023-10-30 15:26:23 +05:30
dakshesh14
b9e7fd93ae fix: reverted to old API response 2023-10-30 15:26:14 +05:30
dakshesh14
308e6781a1 Merge branch 'fix/project-issues-data-structure' of https://github.com/makeplane/plane into refactor/draft_issues 2023-10-30 14:09:13 +05:30
dakshesh14
adda14e8bf refactor: update draft issue grouping 2023-10-30 12:27:31 +05:30
dakshesh14
82e65c44a4 fix: changed store according to new API response 2023-10-30 11:40:38 +05:30
dakshesh14
e9a79b368b fix: merge conflict 2023-10-30 11:10:59 +05:30
dakshesh14
55b9fbffd7 dev: layout setup for draft issues 2023-10-30 11:09:46 +05:30
gurusainath
af9aec6769 chore: handled issues render data structure in project, cycle, module and project-view issues 2023-10-28 18:03:06 +05:30
dakshesh14
bc6a983d1e fix: removed redundant function to get draft issues 2023-10-26 20:42:58 +05:30
dakshesh14
b85c93b0e5 dev: store for draft issue 2023-10-26 20:37:28 +05:30
30 changed files with 1039 additions and 438 deletions

View File

@ -4,8 +4,6 @@ import { observer } from "mobx-react-lite";
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";
// icons
@ -19,14 +17,16 @@ 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, onSubmit } = props;
const { isOpen, handleClose, data, onSuccess } = props;
// store
const { draftIssues: draftIssueStore } = useMobxStore();
// states
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const { user: userStore } = useMobxStore();
@ -50,30 +50,28 @@ export const DeleteDraftIssueModal: React.FC<Props> = observer((props) => {
if (!workspaceSlug || !data || !user) return;
setIsDeleteLoading(true);
await issueDraftService
await draftIssueStore
.deleteDraftIssue(workspaceSlug as string, data.project, data.id)
.then(() => {
setIsDeleteLoading(false);
handleClose();
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 (

View File

@ -22,7 +22,6 @@ import {
import { CreateStateModal } from "components/states";
import { CreateLabelModal } from "components/labels";
// ui
import {} from "components/ui";
import { Button, CustomMenu, Input, ToggleSwitch } from "@plane/ui";
// icons
import { Sparkle, X } from "lucide-react";
@ -62,9 +61,8 @@ interface IssueFormProps {
formData: Partial<IIssue>,
action?: "createDraft" | "createNewIssue" | "updateDraft" | "convertToNewIssue"
) => Promise<void>;
data?: Partial<IIssue> | null;
initialData?: Partial<IIssue> | null;
isOpen: boolean;
prePopulatedData?: Partial<IIssue> | null;
projectId: string;
setActiveProject: React.Dispatch<React.SetStateAction<string | null>>;
createMore: boolean;
@ -92,9 +90,8 @@ interface IssueFormProps {
export const DraftIssueForm: FC<IssueFormProps> = (props) => {
const {
handleFormSubmit,
data,
initialData,
isOpen,
prePopulatedData,
projectId,
setActiveProject,
createMore,
@ -125,39 +122,38 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
const editorSuggestions = useEditorSuggestions();
const {
formState: { errors, isSubmitting },
formState: { errors, isSubmitting, isDirty },
handleSubmit,
reset,
watch,
control,
getValues,
setValue,
setFocus,
} = useForm<IIssue>({
defaultValues: prePopulatedData ?? defaultValues,
defaultValues: initialData ?? defaultValues,
reValidateMode: "onChange",
});
const issueName = watch("name");
const payload: Partial<IIssue> = {
name: watch("name"),
description: watch("description"),
description_html: watch("description_html"),
state: watch("state"),
priority: watch("priority"),
assignees: watch("assignees"),
labels: watch("labels"),
start_date: watch("start_date"),
target_date: watch("target_date"),
project: watch("project"),
parent: watch("parent"),
cycle: watch("cycle"),
module: watch("module"),
name: getValues("name"),
description: getValues("description"),
description_html: getValues("description_html"),
state: getValues("state"),
priority: getValues("priority"),
assignees: getValues("assignees"),
labels: getValues("labels"),
start_date: getValues("start_date"),
target_date: getValues("target_date"),
project: getValues("project"),
parent: getValues("parent"),
cycle: getValues("cycle"),
module: getValues("module"),
};
useEffect(() => {
if (!isOpen || data) return;
if (!isOpen) return;
setLocalStorageValue(
JSON.stringify({
@ -165,26 +161,7 @@ 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();
// };
}, [JSON.stringify(payload), isOpen]);
const handleCreateUpdateIssue = async (
formData: Partial<IIssue>,
@ -192,9 +169,8 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
) => {
await handleFormSubmit(
{
...(data ?? {}),
...(initialData ?? {}),
...formData,
is_draft: action === "createDraft" || action === "updateDraft",
},
action
);
@ -270,14 +246,14 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
};
useEffect(() => {
setFocus("name");
// if form is dirty, don't reset
if (isDirty) return;
reset({
...defaultValues,
...(prePopulatedData ?? {}),
...(data ?? {}),
...(initialData ?? {}),
});
}, [setFocus, prePopulatedData, reset, data]);
}, [reset, initialData, isDirty]);
// update projectId in form when projectId changes
useEffect(() => {
@ -311,7 +287,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
)}
<form
onSubmit={handleSubmit((formData) =>
handleCreateUpdateIssue(formData, data ? "convertToNewIssue" : "createDraft")
handleCreateUpdateIssue(formData, initialData?.id ? "convertToNewIssue" : "createDraft")
)}
>
<div className="space-y-5">
@ -379,6 +355,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
id="name"
name="name"
type="text"
tabIndex={1}
value={value}
onChange={onChange}
ref={ref}
@ -606,7 +583,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
variant="neutral-primary"
loading={isSubmitting}
onClick={handleSubmit((formData) =>
handleCreateUpdateIssue(formData, data?.id ? "updateDraft" : "createDraft")
handleCreateUpdateIssue(formData, initialData?.id ? "updateDraft" : "createDraft")
)}
>
{isSubmitting ? "Saving..." : "Save Draft"}
@ -615,7 +592,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
loading={isSubmitting}
variant="primary"
onClick={handleSubmit((formData) =>
handleCreateUpdateIssue(formData, data ? "convertToNewIssue" : "createNewIssue")
handleCreateUpdateIssue(formData, initialData?.id ? "convertToNewIssue" : "createNewIssue")
)}
>
{isSubmitting ? "Saving..." : "Add Issue"}

View File

@ -6,24 +6,25 @@ 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 useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage";
// components
import { DraftIssueForm } from "components/issues";
import { DraftIssueForm } from "components/draft-issues";
// constants
import { ISSUE_PRIORITIES, ISSUE_STATE_GROUPS, getValueFromObject } from "constants/issue";
// types
import type { IIssue } from "types";
// fetch-keys
import { PROJECT_ISSUES_DETAILS, USER_ISSUE, SUB_ISSUES } from "constants/fetch-keys";
import { SUB_ISSUES } from "constants/fetch-keys";
interface IssuesModalProps {
data?: IIssue | null;
handleClose: () => void;
isOpen: boolean;
isUpdatingSingleIssue?: boolean;
prePopulateData?: Partial<IIssue>;
initialData?: Partial<IIssue>;
fieldsToShow?: (
| "project"
| "name"
@ -43,19 +44,10 @@ interface IssuesModalProps {
// services
const issueService = new IssueService();
const issueDraftService = new IssueDraftService();
const moduleService = new ModuleService();
export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer((props) => {
const {
data,
handleClose,
isOpen,
isUpdatingSingleIssue = false,
prePopulateData: prePopulateDataProps,
fieldsToShow = ["all"],
onSubmit,
} = props;
const { handleClose, isOpen, initialData, fieldsToShow = ["all"], onSubmit } = props;
// states
const [createMore, setCreateMore] = useState(false);
@ -65,7 +57,14 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { project: projectStore, user: userStore } = useMobxStore();
const {
project: projectStore,
draftIssues: draftIssueStore,
draftIssueFilters: draftIssueFilterStore,
issueDetail: issueDetailStore,
issue: issueStore,
user: userStore,
} = useMobxStore();
const user = userStore.currentUser;
const projects = workspaceSlug ? projectStore.projects[workspaceSlug.toString()] : undefined;
@ -85,62 +84,30 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
};
useEffect(() => {
setPreloadedData(prePopulateDataProps ?? {});
setPreloadedData(initialData ?? {});
if (cycleId && !prePopulateDataProps?.cycle) {
if (cycleId && !initialData?.cycle) {
setPreloadedData((prevData) => ({
...(prevData ?? {}),
...prePopulateDataProps,
...initialData,
cycle: cycleId.toString(),
}));
}
if (moduleId && !prePopulateDataProps?.module) {
if (moduleId && !initialData?.module) {
setPreloadedData((prevData) => ({
...(prevData ?? {}),
...prePopulateDataProps,
...initialData,
module: moduleId.toString(),
}));
}
if (
(router.asPath.includes("my-issues") || router.asPath.includes("assigned")) &&
!prePopulateDataProps?.assignees
) {
if ((router.asPath.includes("my-issues") || router.asPath.includes("assigned")) && !initialData?.assignees) {
setPreloadedData((prevData) => ({
...(prevData ?? {}),
...prePopulateDataProps,
assignees: prePopulateDataProps?.assignees ?? [user?.id ?? ""],
...initialData,
assignees: initialData?.assignees ?? [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]);
}, [initialData, cycleId, moduleId, router.asPath, user?.id]);
useEffect(() => {
// if modal is closed, reset active project to null
@ -152,7 +119,7 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
// if data is present, set active project to the project of the
// issue. This has more priority than the project in the url.
if (data && data.project) return setActiveProject(data.project);
if (initialData && initialData.project) return setActiveProject(initialData.project);
if (prePopulateData && prePopulateData.project && !activeProject) return setActiveProject(prePopulateData.project);
@ -162,21 +129,21 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
// in the url. This has the least priority.
if (projects && projects.length > 0 && !activeProject)
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [activeProject, data, projectId, projects, isOpen, prePopulateData]);
}, [activeProject, initialData, projectId, projects, isOpen, prePopulateData]);
const createDraftIssue = async (payload: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject || !user) return;
await issueDraftService
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload)
.then(async () => {
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?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug.toString()));
})
.catch(() => {
setToastAlert({
@ -190,24 +157,34 @@ 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()));
await draftIssueStore
.updateDraftIssue(workspaceSlug.toString(), activeProject, payload as IIssue)
.then((response) => {
if (!createMore) onClose();
const userDisplayFilters = draftIssueFilterStore?.userDisplayFilters;
const groupBy = userDisplayFilters?.group_by || null;
let groupById: null | string = null;
if (groupBy === "priority") {
groupById = getValueFromObject(ISSUE_PRIORITIES, "key") as string;
} else if (groupBy === "labels") {
groupById = getValueFromObject(projectStore?.projectLabels ?? [], "id") as string;
} else if (groupBy === "state_detail.group") {
groupById = getValueFromObject(ISSUE_STATE_GROUPS, "key") as string;
}
draftIssueStore.updateIssueStructure(groupById, null, response);
draftIssueStore.fetchIssues(workspaceSlug.toString(), activeProject);
if (!payload.is_draft) {
if (payload.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!",
@ -254,9 +231,11 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
const createIssue = async (payload: Partial<IIssue>) => {
if (!workspaceSlug || !activeProject || !user) return;
await issueService
.createIssue(workspaceSlug.toString(), activeProject, payload, user)
await issueDetailStore
.createIssue(workspaceSlug.toString(), activeProject, payload)
.then(async (res) => {
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);
@ -266,10 +245,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
message: "Issue created successfully.",
});
if (!createMore) onClose();
if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
})
.catch(() => {
@ -279,6 +254,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 (
@ -294,8 +282,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();
@ -335,15 +324,14 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = observer(
<DraftIssueForm
isOpen={isOpen}
handleFormSubmit={handleFormSubmit}
prePopulatedData={prePopulateData}
data={data}
initialData={initialData}
createMore={createMore}
setCreateMore={setCreateMore}
handleClose={onClose}
handleDiscard={onDiscard}
projectId={activeProject ?? ""}
setActiveProject={setActiveProject}
status={data ? true : false}
status={initialData?.id ? true : false}
user={user ?? undefined}
fieldsToShow={fieldsToShow}
/>

View File

@ -0,0 +1,4 @@
export * from "./draft-issue-form";
export * from "./draft-issue-modal";
export * from "./delete-draft-issue-modal";
export * from "./confirm-issue-discard";

View File

@ -19,3 +19,4 @@ export * from "./project-archived-issue-details";
export * from "./project-archived-issues";
export * from "./project-issue-details";
export * from "./user-profile";
export * from "./project-draft-issues";

View File

@ -3,18 +3,73 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
// ui
import { Breadcrumbs, LayersIcon } from "@plane/ui";
// types
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
// helper
import { renderEmoji } from "helpers/emoji.helper";
export const ProjectDraftIssueHeader: FC = observer(() => {
const router = useRouter();
const { workspaceSlug } = router.query;
const { workspaceSlug, projectId } = router.query;
const { project: projectStore } = useMobxStore();
const { project: projectStore, draftIssueFilters: draftIssueFiltersStore } = useMobxStore();
const { currentProjectDetails } = projectStore;
const activeLayout = draftIssueFiltersStore.userDisplayFilters.layout;
const handleLayoutChange = (layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
layout,
},
});
};
const handleFiltersUpdate = (key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = draftIssueFiltersStore.userFilters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (draftIssueFiltersStore.userFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
[key]: newValues,
},
});
};
const handleDisplayFiltersUpdate = (updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
},
});
};
const handleDisplayPropertiesUpdate = (property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
draftIssueFiltersStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
};
return (
<div className="relative flex w-full flex-shrink-0 flex-row z-10 items-center justify-between gap-x-2 gap-y-4 border-b border-custom-border-200 bg-custom-sidebar-background-100 p-4">
<div className="flex items-center gap-2 flex-grow w-full whitespace-nowrap overflow-ellipsis">
@ -45,6 +100,38 @@ export const ProjectDraftIssueHeader: FC = observer(() => {
</Breadcrumbs>
</div>
</div>
{/* filter/layout/display options */}
<div className="flex items-center gap-2">
<LayoutSelection
layouts={["list", "kanban"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters" placement="bottom-end">
<FilterSelection
filters={draftIssueFiltersStore.userFilters}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.draft_issues[activeLayout] : undefined
}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? undefined}
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined}
/>
</FiltersDropdown>
<FiltersDropdown title="Display" placement="bottom-end">
<DisplayFiltersSelection
displayFilters={draftIssueFiltersStore.userDisplayFilters}
displayProperties={draftIssueFiltersStore.userDisplayProperties}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.draft_issues[activeLayout] : undefined
}
/>
</FiltersDropdown>
</div>
</div>
);
});

View File

@ -135,6 +135,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
const payload: Partial<IIssue> = {
name: getValues("name"),
description: getValues("description"),
description_html: getValues("description_html"),
state: getValues("state"),
priority: getValues("priority"),
assignees: getValues("assignees"),

View File

@ -14,12 +14,6 @@ export * from "./sidebar";
export * from "./label";
export * from "./issue-reaction";
export * from "./peek-overview";
export * from "./confirm-issue-discard";
// draft issue
export * from "./draft-issue-form";
export * from "./draft-issue-modal";
export * from "./delete-draft-issue-modal";
// archived issue
export * from "./delete-archived-issue-modal";

View File

@ -4,3 +4,4 @@ export * from "./module-root";
export * from "./project-view-root";
export * from "./project-root";
export * from "./archived-issue";
export * from "./project-draft-issue";

View File

@ -0,0 +1,81 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { AppliedFiltersList } from "components/issues";
// types
import { IIssueFilterOptions } from "types";
export const ProjectDraftIssueAppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { project: projectStore, draftIssueFilters: draftIssueFiltersStore } = useMobxStore();
const userFilters = draftIssueFiltersStore.userFilters;
// filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {};
Object.entries(userFilters).forEach(([key, value]) => {
if (!value) return;
if (Array.isArray(value) && value.length === 0) return;
appliedFilters[key as keyof IIssueFilterOptions] = value;
});
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!workspaceSlug || !projectId) return;
// remove all values of the key if value is null
if (!value) {
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
[key]: null,
},
});
return;
}
// remove the passed value from the key
let newValues = draftIssueFiltersStore.userFilters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value);
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: {
[key]: newValues,
},
});
};
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null;
});
draftIssueFiltersStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
filters: { ...newFilters },
});
};
// return if no filters are applied
if (Object.keys(appliedFilters).length === 0) return null;
return (
<div className="p-4">
<AppliedFiltersList
appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter}
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
states={projectStore.states?.[projectId?.toString() ?? ""]}
/>
</div>
);
});

View File

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

View File

@ -0,0 +1,152 @@
import { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { DragDropContext } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { KanBanSwimLanes } from "../swimlanes";
import { KanBan } from "../default";
import { DraftIssueQuickActions } from "components/issues";
// types
import { IIssue } from "types";
// constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
export const DraftIssueKanBanLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug } = router.query;
const {
project: projectStore,
draftIssueFilters: draftIssueFiltersStore,
issueKanBanView: issueKanBanViewStore,
draftIssues: draftIssuesStore,
} = useMobxStore();
// derived values
const issues = draftIssuesStore.getDraftIssues;
const displayProperties = draftIssueFiltersStore?.userDisplayProperties;
const userDisplayFilters = draftIssueFiltersStore?.userDisplayFilters;
const group_by: string | null = userDisplayFilters?.group_by || null;
const order_by: string | null = draftIssueFiltersStore?.userDisplayFilters?.order_by || null;
const showEmptyGroup = userDisplayFilters?.show_empty_groups || false;
const sub_group_by: string | null = userDisplayFilters?.sub_group_by || null;
const currentKanBanView = "default";
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
const onDragStart = () => {
setIsDragStarted(true);
};
const onDragEnd = (result: any) => {
setIsDragStarted(false);
if (!result) return;
if (
result.destination &&
result.source &&
result.destination.droppableId === result.source.droppableId &&
result.destination.index === result.source.index
)
return;
currentKanBanView === "default"
? issueKanBanViewStore?.handleDragDrop(result.source, result.destination)
: issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination);
};
const handleIssues = useCallback(
(
sub_group_by: string | null,
group_by: string | null,
issue: IIssue,
action: "update" | "delete" | "convertToIssue"
) => {
if (!workspaceSlug) return;
if (action === "update") {
draftIssuesStore.updateDraftIssue(workspaceSlug.toString(), issue.project, issue);
draftIssuesStore.updateIssueStructure(group_by, sub_group_by, issue);
}
if (action === "delete") draftIssuesStore.deleteDraftIssue(workspaceSlug.toString(), issue.project, issue.id);
if (action === "convertToIssue")
draftIssuesStore.convertDraftIssueToIssue(workspaceSlug.toString(), issue.project, issue.id);
draftIssuesStore.fetchIssues(workspaceSlug.toString(), issue.project);
},
[draftIssuesStore, workspaceSlug]
);
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
issueKanBanViewStore.handleKanBanToggle(toggle, value);
};
const states = projectStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null;
const labels = projectStore?.projectLabels || null;
const members = projectStore?.projectMembers || null;
const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
const orderBy = draftIssueFiltersStore?.userDisplayFilters?.order_by || null;
return (
<div className={`relative min-w-full w-max min-h-full h-max bg-custom-background-90 px-3`}>
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
{currentKanBanView === "default" ? (
<KanBan
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={order_by}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<DraftIssueQuickActions
issue={issue}
handleUpdate={(issue: any, action: any) => handleIssues(sub_group_by, group_by, issue, action)}
/>
)}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={showEmptyGroup}
isDragStarted={isDragStarted}
/>
) : (
<KanBanSwimLanes
issues={issues}
sub_group_by={sub_group_by}
group_by={group_by}
order_by={orderBy}
handleIssues={handleIssues}
quickActions={(sub_group_by, group_by, issue) => (
<DraftIssueQuickActions
issue={issue}
handleUpdate={(issue: any, action: any) => handleIssues(sub_group_by, group_by, issue, action)}
/>
)}
displayProperties={displayProperties}
kanBanToggle={issueKanBanViewStore?.kanBanToggle}
handleKanBanToggle={handleKanBanToggle}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
showEmptyGroup={showEmptyGroup}
isDragStarted={isDragStarted}
/>
)}
</DragDropContext>
</div>
);
});

View File

@ -3,4 +3,5 @@ export * from "./module-root";
export * from "./profile-issues-root";
export * from "./project-root";
export * from "./project-view-root";
export * from "./project-draft-issue-root";
export * from "./archived-issue-root";

View File

@ -0,0 +1,84 @@
import { FC, useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// hooks
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { List } from "../default";
import { DraftIssueQuickActions } from "components/issues";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types
import { IIssue } from "types";
// constants
import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue";
export const DraftIssueListLayout: FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const {
project: projectStore,
draftIssues: draftIssuesStore,
draftIssueFilters: draftIssueFiltersStore,
} = useMobxStore();
const issues = draftIssuesStore.getDraftIssues;
const group_by: string | null = draftIssueFiltersStore?.userDisplayFilters?.group_by || null;
const display_properties = draftIssueFiltersStore?.userDisplayProperties || null;
const handleIssues = useCallback(
(group_by: string | null, issue: IIssue, action: "update" | "delete" | "convertToIssue") => {
if (!workspaceSlug || !projectId) return;
if (action === "update") {
draftIssuesStore.updateDraftIssue(workspaceSlug.toString(), projectId.toString(), issue);
draftIssuesStore.updateIssueStructure(group_by, null, issue);
} else if (action === "delete") {
draftIssuesStore.deleteDraftIssue(workspaceSlug.toString(), projectId.toString(), issue.id);
} else if (action === "convertToIssue") {
draftIssuesStore.convertDraftIssueToIssue(workspaceSlug.toString(), projectId.toString(), issue.id);
}
},
[workspaceSlug, projectId, draftIssuesStore]
);
const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null;
const states = projectStore?.projectStates || null;
const priorities = ISSUE_PRIORITIES || null;
const labels = projectStore?.projectLabels || null;
const members = projectStore?.projectMembers || null;
const stateGroups = ISSUE_STATE_GROUPS || null;
const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null;
const estimates =
projectDetails?.estimate !== null
? projectStore.projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null
: null;
return (
<div className="relative w-full h-full bg-custom-background-90">
<List
issues={issues}
group_by={group_by}
handleIssues={handleIssues}
quickActions={(group_by, issue) => (
<DraftIssueQuickActions
issue={issue}
handleUpdate={(issue: any, action: any) => handleIssues(group_by, issue, action)}
/>
)}
displayProperties={display_properties}
states={states}
stateGroups={stateGroups}
priorities={priorities}
labels={labels}
members={members?.map((m) => m.member) ?? null}
projects={projects}
estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null}
/>
</div>
);
});

View File

@ -0,0 +1,74 @@
import { useState } from "react";
import { CustomMenu } from "@plane/ui";
import { Copy, Pencil, Trash2 } from "lucide-react";
// components
import { CreateUpdateDraftIssueModal, DeleteDraftIssueModal } from "components/draft-issues";
// types
import { IIssue } from "types";
type Props = {
issue: IIssue;
handleUpdate: (data: IIssue, action: any) => Promise<void> | void;
};
export const DraftIssueQuickActions: React.FC<Props> = (props) => {
const { issue, handleUpdate } = props;
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<IIssue | null>(null);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
return (
<>
<DeleteDraftIssueModal data={issue} isOpen={deleteIssueModal} handleClose={() => setDeleteIssueModal(false)} />
<CreateUpdateDraftIssueModal
isOpen={createUpdateIssueModal}
handleClose={() => {
setCreateUpdateIssueModal(false);
setIssueToEdit(null);
}}
initialData={issueToEdit ? { ...issueToEdit } : { ...issue, name: issue?.name, description: issue.description }}
/>
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIssueToEdit(issue);
setCreateUpdateIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Pencil className="h-3 w-3" />
Edit draft issue
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleUpdate(issue, "convertToIssue");
}}
>
<div className="flex items-center gap-2">
<Copy className="h-3 w-3" />
Convert to issue
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setDeleteIssueModal(true);
}}
>
<div className="flex items-center gap-2">
<Trash2 className="h-3 w-3" />
Delete draft issue
</div>
</CustomMenu.MenuItem>
</CustomMenu>
</>
);
};

View File

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

View File

@ -3,4 +3,5 @@ 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";
export * from "./archived-issue-layout-root";

View File

@ -5,17 +5,16 @@ 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";
// components
import { IssueForm, ConfirmIssueDiscard } from "components/issues";
import { IssueForm } from "components/issues";
import { ConfirmIssueDiscard } from "components/draft-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;
@ -41,8 +40,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;
@ -63,6 +60,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
cycleIssue: cycleIssueStore,
moduleIssue: moduleIssueStore,
user: userStore,
draftIssues: draftIssuesStore,
} = useMobxStore();
const user = userStore.currentUser;
@ -217,9 +215,11 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
...formDirtyState,
};
await issueDraftService
.createDraftIssue(workspaceSlug as string, activeProject ?? "", payload)
await draftIssuesStore
.createDraftIssue(workspaceSlug.toString(), activeProject, payload)
.then(() => {
draftIssuesStore.fetchIssues(workspaceSlug.toString(), activeProject);
setToastAlert({
type: "success",
title: "Success!",
@ -230,10 +230,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = observer((prop
setActiveProject(null);
setFormDirtyState(null);
setShowConfirmDiscard(false);
if (payload.assignees?.some((assignee) => assignee === user?.id)) mutate(USER_ISSUE(workspaceSlug as string));
if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent));
})
.catch(() => {
setToastAlert({

View File

@ -5,7 +5,7 @@ import { ChevronDown, PenSquare, Search } from "lucide-react";
// hooks
import useLocalStorage from "hooks/use-local-storage";
// components
import { CreateUpdateDraftIssueModal } from "components/issues";
import { CreateUpdateDraftIssueModal } from "components/draft-issues";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
@ -21,7 +21,7 @@ export const WorkspaceSidebarQuickAction = () => {
<CreateUpdateDraftIssueModal
isOpen={isDraftIssueModalOpen}
handleClose={() => setIsDraftIssueModalOpen(false)}
prePopulateData={storedValue ? JSON.parse(storedValue) : {}}
initialData={storedValue ? JSON.parse(storedValue) : {}}
onSubmit={() => {
localStorage.removeItem("draftedIssue");
clearValue();

View File

@ -283,29 +283,29 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
},
draft_issues: {
list: {
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
filters: ["priority", "state", "assignees", "created_by", "labels", "start_date", "target_date"],
display_properties: true,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels", null],
group_by: ["state", "priority", "labels", "assignees", "created_by", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
values: ["show_empty_groups", "sub_issue"],
},
},
kanban: {
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
filters: ["priority", "state", "assignees", "created_by", "labels", "start_date", "target_date"],
display_properties: true,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels", null],
group_by: ["state", "priority", "labels", "assignees", "created_by"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
values: ["show_empty_groups", "sub_issue"],
},
},
},

View File

@ -4,6 +4,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
@ -29,6 +30,9 @@ const ProjectDraftIssuesPage: NextPageWithLayout = () => {
<X className="h-3 w-3" />
</button>
</div>
<div className="h-full w-full">
<DraftIssueLayoutRoot />
</div>
</div>
);
};

View File

@ -1,13 +1,15 @@
import { APIService } from "services/api.service";
// helpers
import { API_BASE_URL } from "helpers/common.helper";
// types
import { IIssue } from "types";
export class IssueDraftService extends APIService {
constructor() {
super(API_BASE_URL);
}
async getDraftIssues(workspaceSlug: string, projectId: string, params?: any): Promise<any> {
async getDraftIssues(workspaceSlug: string, projectId: string, params?: any): Promise<IIssue> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, {
params,
})

View File

@ -1,10 +1,10 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx";
import { observable, action, computed, makeObservable, runInAction, autorun } from "mobx";
// store
import { RootStore } from "../root";
// types
import { IIssue } from "types";
// services
import { IssueService } from "services/issue";
import { IssueDraftService } from "services/issue";
import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers";
import {
IIssueGroupWithSubGroupsStructure,
@ -13,29 +13,11 @@ import {
IIssueUnGroupedStructure,
} from "store/issue";
export interface IDraftIssueStore {
export interface IIssueDraftStore {
loader: boolean;
error: any | null;
// issues
issues: {
[project_id: string]: {
grouped: IIssueGroupedStructure;
groupWithSubGroups: IIssueGroupWithSubGroupsStructure;
ungrouped: IIssueUnGroupedStructure;
};
};
// computed
getIssueType: IIssueType | null;
getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null;
// action
fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>;
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
}
export class DraftIssueStore implements IDraftIssueStore {
loader: boolean = false;
error: any | null = null;
issues: {
draftIssues: {
[project_id: string]: {
grouped: {
[group_id: string]: IIssue[];
@ -47,9 +29,31 @@ export class DraftIssueStore implements IDraftIssueStore {
};
ungrouped: IIssue[];
};
} = {};
};
rootStore: RootStore;
// computed
getIssueType: IIssueType | null;
getDraftIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null;
// actions
fetchIssues: (workspaceSlug: string, projectId: string) => Promise<any>;
createDraftIssue: (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => Promise<any>;
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void;
deleteDraftIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>;
updateDraftIssue: (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => Promise<any>;
convertDraftIssueToIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<any>;
// service
issueService;
draftIssueService: IssueDraftService;
}
export class IssueDraftStore implements IIssueDraftStore {
loader: boolean = false;
error: any | null = null;
draftIssues: IIssueDraftStore["draftIssues"] = {};
// service
draftIssueService: IssueDraftService;
rootStore;
constructor(_rootStore: RootStore) {
@ -57,24 +61,42 @@ export class DraftIssueStore implements IDraftIssueStore {
// observable
loader: observable.ref,
error: observable.ref,
issues: observable.ref,
draftIssues: observable.ref,
// computed
getIssueType: computed,
getIssues: computed,
getDraftIssues: computed,
// actions
fetchIssues: action,
createDraftIssue: action,
updateIssueStructure: action,
deleteDraftIssue: action,
updateDraftIssue: action,
convertDraftIssueToIssue: action,
});
this.rootStore = _rootStore;
this.issueService = new IssueService();
this.draftIssueService = new IssueDraftService();
autorun(() => {
const workspaceSlug = this.rootStore.workspace.workspaceSlug;
const projectId = this.rootStore.project.projectId;
if (
workspaceSlug &&
projectId &&
this.rootStore.draftIssueFilters.userFilters &&
this.rootStore.draftIssueFilters.userDisplayFilters &&
this.rootStore.draftIssueFilters.appliedFilters
)
this.fetchIssues(workspaceSlug, projectId);
});
}
get getIssueType() {
const groupedLayouts = ["kanban", "list", "calendar"];
const ungroupedLayouts = ["spreadsheet", "gantt_chart"];
const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null;
const issueSubGroup = this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by || null;
const issueLayout = this.rootStore?.draftIssueFilters?.userDisplayFilters?.layout || null;
const issueSubGroup = this.rootStore?.draftIssueFilters?.userDisplayFilters?.sub_group_by || null;
if (!issueLayout) return null;
const _issueState = groupedLayouts.includes(issueLayout)
@ -88,46 +110,135 @@ export class DraftIssueStore implements IDraftIssueStore {
return _issueState || null;
}
get getIssues() {
const projectId: string | null = this.rootStore?.project?.projectId;
get getDraftIssues() {
const issueType = this.getIssueType;
const projectId = this.rootStore?.project?.projectId;
if (!projectId || !issueType) return null;
return this.issues?.[projectId]?.[issueType] || null;
return this.draftIssues?.[projectId]?.[issueType] || null;
}
fetchIssues = async (workspaceSlug: string, projectId: string) => {
try {
this.loader = true;
this.error = null;
this.rootStore.workspace.setWorkspaceSlug(workspaceSlug);
this.rootStore.project.setProjectId(projectId);
const params = this.rootStore?.draftIssueFilters?.appliedFilters;
const issueResponse = await this.draftIssueService.getDraftIssues(workspaceSlug, projectId, params);
const issueType = this.getIssueType;
if (issueType != null) {
const _issues = {
...this.draftIssues,
[projectId]: {
...this.draftIssues[projectId],
[issueType]: issueResponse,
},
};
runInAction(() => {
this.loader = false;
this.error = null;
this.draftIssues = _issues;
});
}
return issueResponse;
} catch (error) {
console.error("Error: Fetching error in issues", error);
this.loader = false;
this.error = error;
return error;
}
};
createDraftIssue = async (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => {
const originalIssues = { ...this.draftIssues };
runInAction(() => {
this.loader = true;
this.error = null;
});
try {
const response = await this.draftIssueService.createDraftIssue(workspaceSlug, projectId, issueForm);
runInAction(() => {
this.loader = false;
this.error = null;
});
return response;
} catch (error) {
console.error("Creating issue error", error);
// reverting back to original issues in case of error
runInAction(() => {
this.loader = false;
this.error = error;
this.draftIssues = originalIssues;
});
}
};
updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
const projectId: string | null = issue?.project;
const issueType = this.getIssueType;
if (!projectId || !issueType) return null;
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =
this.getIssues;
this.getDraftIssues;
if (!issues) return null;
if (issueType === "grouped" && group_id) {
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,
[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;
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)),
[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 || "";
const orderBy = this.rootStore?.draftIssueFilters?.userDisplayFilters?.order_by || "";
if (orderBy === "-created_at") {
issues = sortArrayByDate(issues as any, "created_at");
}
@ -142,43 +253,72 @@ export class DraftIssueStore implements IDraftIssueStore {
}
runInAction(() => {
this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } };
this.draftIssues = { ...this.draftIssues, [projectId]: { ...this.draftIssues[projectId], [issueType]: issues } };
});
};
fetchIssues = async (workspaceSlug: string, projectId: string) => {
try {
updateDraftIssue = async (workspaceSlug: string, projectId: string, issueForm: Partial<IIssue>) => {
const originalIssues = { ...this.draftIssues };
runInAction(() => {
this.loader = true;
this.error = null;
});
this.rootStore.workspace.setWorkspaceSlug(workspaceSlug);
this.rootStore.project.setProjectId(projectId);
const params = this.rootStore?.issueFilter?.appliedFilters;
const issueResponse = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, params);
const issueType = this.getIssueType;
if (issueType != null) {
const _issues = {
...this.issues,
[projectId]: {
...this.issues[projectId],
[issueType]: issueResponse,
},
};
try {
const response = await this.draftIssueService.updateDraftIssue(
workspaceSlug,
projectId,
issueForm?.id!,
issueForm
);
runInAction(() => {
this.issues = _issues;
this.loader = false;
this.error = null;
});
}
return issueResponse;
return response;
} catch (error) {
console.error("Error: Fetching error in issues", error);
console.error("Updating issue error", error);
// reverting back to original issues in case of error
runInAction(() => {
this.loader = false;
this.error = error;
return error;
this.draftIssues = originalIssues;
});
}
};
convertDraftIssueToIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
await this.updateDraftIssue(workspaceSlug, projectId, { id: issueId, is_draft: false });
await this.fetchIssues(workspaceSlug, projectId);
};
deleteDraftIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
const originalIssues = { ...this.draftIssues };
runInAction(() => {
this.loader = true;
this.error = null;
});
try {
// deleting using api
await this.draftIssueService.deleteDraftIssue(workspaceSlug, projectId, issueId);
await this.fetchIssues(workspaceSlug, projectId);
runInAction(() => {
this.loader = false;
this.error = null;
});
} catch (error) {
console.error("Deleting issue error", error);
// reverting back to original issues in case of error
runInAction(() => {
this.loader = false;
this.error = error;
this.draftIssues = originalIssues;
});
}
};
}

View File

@ -1,20 +1,53 @@
import { observable, computed, makeObservable } from "mobx";
import { observable, computed, makeObservable, runInAction, action } from "mobx";
// helpers
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
// services
import { IssueService } from "services/issue";
import { ProjectService } from "services/project";
// types
import { RootStore } from "../root";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types";
import type {
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
IIssueFilterOptions,
TIssueParams,
IProjectViewProps,
} from "types";
export interface IDraftIssueFilterStore {
loader: boolean;
error: any | null;
// observables
userDisplayProperties: IIssueDisplayProperties;
userDisplayFilters: IIssueDisplayFilterOptions;
userFilters: IIssueFilterOptions;
// services
projectService: ProjectService;
issueService: IssueService;
// computed
appliedFilters: TIssueParams[] | null;
// actions
fetchUserProjectFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
updateUserFilters: (
workspaceSlug: string,
projectId: string,
properties: Partial<IProjectViewProps>
) => Promise<void>;
updateDisplayProperties: (
workspaceSlug: string,
projectId: string,
properties: Partial<IIssueDisplayProperties>
) => Promise<void>;
}
export class DraftIssueFilterStore implements IDraftIssueFilterStore {
loader: boolean = false;
error: any | null = null;
// observables
userFilters: IIssueFilterOptions = {
priority: null,
@ -52,8 +85,15 @@ export class DraftIssueFilterStore implements IDraftIssueFilterStore {
// root store
rootStore;
// services
projectService: ProjectService;
issueService: IssueService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
loader: observable,
error: observable,
// observables
userFilters: observable.ref,
userDisplayFilters: observable.ref,
@ -61,9 +101,19 @@ export class DraftIssueFilterStore implements IDraftIssueFilterStore {
// computed
appliedFilters: computed,
// actions
fetchUserProjectFilters: action,
updateUserFilters: action,
updateDisplayProperties: action,
computedFilter: action,
});
this.rootStore = _rootStore;
// services
this.issueService = new IssueService();
this.projectService = new ProjectService();
}
computedFilter = (filters: any, filteredParams: any) => {
@ -98,12 +148,110 @@ export class DraftIssueFilterStore implements IDraftIssueFilterStore {
start_target_date: this.userDisplayFilters?.start_target_date || true,
};
// this.userDisplayFilters.layout should be list or kanban
const filteredParams = handleIssueQueryParamsByLayout(this.userDisplayFilters.layout, "issues");
if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams);
if (this.userDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date";
if (this.userDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true;
return filteredRouteParams;
}
updateUserFilters = async (workspaceSlug: string, projectId: string, properties: Partial<IProjectViewProps>) => {
const newViewProps = {
display_filters: {
...this.userDisplayFilters,
...properties.display_filters,
},
filters: {
...this.userFilters,
...properties.filters,
},
};
// set sub_group_by to null if group_by is set to null
if (newViewProps.display_filters.group_by === null) newViewProps.display_filters.sub_group_by = null;
// set group_by to state if layout is switched to kanban and group_by is null
if (newViewProps.display_filters.layout === "kanban" && newViewProps.display_filters.group_by === null)
newViewProps.display_filters.group_by = "state";
try {
runInAction(() => {
this.userFilters = newViewProps.filters;
this.userDisplayFilters = {
...newViewProps.display_filters,
// set layout to list if layout is not list or kanban
layout:
newViewProps.display_filters.layout === "list" || newViewProps.display_filters.layout === "kanban"
? newViewProps.display_filters.layout
: "list",
};
});
this.projectService.setProjectView(workspaceSlug, projectId, {
view_props: newViewProps,
});
} catch (error) {
this.fetchUserProjectFilters(workspaceSlug, projectId);
runInAction(() => {
this.error = error;
});
console.log("Failed to update user filters in issue filter store", error);
}
};
updateDisplayProperties = async (
workspaceSlug: string,
projectId: string,
properties: Partial<IIssueDisplayProperties>
) => {
const newProperties: IIssueDisplayProperties = {
...this.userDisplayProperties,
...properties,
};
try {
runInAction(() => {
this.userDisplayProperties = newProperties;
});
await this.issueService.updateIssueDisplayProperties(workspaceSlug, projectId, newProperties);
} catch (error) {
this.fetchUserProjectFilters(workspaceSlug, projectId);
runInAction(() => {
this.error = error;
});
console.log("Failed to update user display properties in issue filter store", error);
}
};
fetchUserProjectFilters = async (workspaceSlug: string, projectId: string) => {
try {
const memberResponse = await this.projectService.projectMemberMe(workspaceSlug, projectId);
const issueProperties = await this.issueService.getIssueDisplayProperties(workspaceSlug, projectId);
runInAction(() => {
this.userFilters = memberResponse?.view_props?.filters;
this.userDisplayFilters = {
...(memberResponse?.view_props?.display_filters ?? {}),
// set layout to list if layout is not list or kanban
layout:
memberResponse?.view_props?.display_filters?.layout === "list" ||
memberResponse?.view_props?.display_filters?.layout === "kanban"
? memberResponse?.view_props?.display_filters?.layout
: "list",
};
this.userDisplayProperties = issueProperties?.properties;
});
} catch (error) {
runInAction(() => {
this.error = error;
});
console.log("Failed to fetch user filters in issue filter store", error);
}
};
}

View File

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

View File

@ -1,169 +0,0 @@
// mobx
import { action, observable, runInAction, makeAutoObservable } from "mobx";
// services
import { IssueDraftService } from "services/issue";
// types
import type { IIssue, IUser } from "types";
export class DraftIssuesStore {
issues: { [key: string]: IIssue } = {};
isIssuesLoading: boolean = false;
rootStore: any | null = null;
issueDraftService;
constructor(_rootStore: any | null = null) {
makeAutoObservable(this, {
issues: observable.ref,
isIssuesLoading: observable.ref,
rootStore: observable.ref,
loadDraftIssues: action,
getIssueById: action,
createDraftIssue: action,
updateDraftIssue: action,
deleteDraftIssue: action,
});
this.rootStore = _rootStore;
this.issueDraftService = new IssueDraftService();
}
/**
* @description Fetch all draft issues of a project and hydrate issues field
*/
loadDraftIssues = async (workspaceSlug: string, projectId: string, params?: any) => {
this.isIssuesLoading = true;
try {
const issuesResponse = await this.issueDraftService.getDraftIssues(workspaceSlug, projectId, params);
const issues = Array.isArray(issuesResponse) ? { allIssues: issuesResponse } : issuesResponse;
runInAction(() => {
this.issues = issues;
this.isIssuesLoading = false;
});
} catch (error) {
this.isIssuesLoading = false;
console.error("Fetching issues error", error);
}
};
/**
* @description Fetch a single draft issue by id and hydrate issues field
* @param workspaceSlug
* @param projectId
* @param issueId
* @returns {IIssue}
*/
getIssueById = async (workspaceSlug: string, projectId: string, issueId: string): Promise<IIssue> => {
if (this.issues[issueId]) return this.issues[issueId];
try {
const issueResponse: IIssue = await this.issueDraftService.getDraftIssueById(workspaceSlug, projectId, issueId);
const issues = {
...this.issues,
[issueId]: { ...issueResponse },
};
runInAction(() => {
this.issues = issues;
});
return issueResponse;
} catch (error) {
throw error;
}
};
/**
* @description Create a new draft issue and hydrate issues field
* @param workspaceSlug
* @param projectId
* @param issueForm
* @param user
* @returns {IIssue}
*/
createDraftIssue = async (
workspaceSlug: string,
projectId: string,
issueForm: IIssue,
user: IUser
): Promise<IIssue> => {
try {
const issueResponse = await this.issueDraftService.createDraftIssue(workspaceSlug, projectId, issueForm);
const issues = {
...this.issues,
[issueResponse.id]: { ...issueResponse },
};
runInAction(() => {
this.issues = issues;
});
return issueResponse;
} catch (error) {
console.error("Creating issue error", error);
throw error;
}
};
updateDraftIssue = async (
workspaceSlug: string,
projectId: string,
issueId: string,
issueForm: Partial<IIssue>,
user: IUser
) => {
// keep a copy of the issue in the store
const originalIssue = { ...this.issues[issueId] };
// immediately update the issue in the store
const updatedIssue = { ...this.issues[issueId], ...issueForm };
try {
runInAction(() => {
this.issues[issueId] = { ...updatedIssue };
});
// make a patch request to update the issue
const issueResponse: IIssue = await this.issueDraftService.updateDraftIssue(
workspaceSlug,
projectId,
issueId,
issueForm
);
const updatedIssues = { ...this.issues };
updatedIssues[issueId] = { ...issueResponse };
runInAction(() => {
this.issues = updatedIssues;
});
} catch (error) {
// if there is an error, revert the changes
runInAction(() => {
this.issues[issueId] = originalIssue;
});
return error;
}
};
deleteDraftIssue = async (workspaceSlug: string, projectId: string, issueId: string, user: IUser) => {
const issues = { ...this.issues };
delete issues[issueId];
try {
runInAction(() => {
this.issues = issues;
});
this.issueDraftService.deleteDraftIssue(workspaceSlug, projectId, issueId);
} catch (error) {
console.error("Deleting issue error", error);
}
};
}

View File

@ -4,7 +4,6 @@ import CommandPaletteStore, { ICommandPaletteStore } from "./command-palette.sto
import UserStore, { IUserStore } from "store/user.store";
import ThemeStore, { IThemeStore } from "store/theme.store";
import {
DraftIssuesStore,
IIssueDetailStore,
IIssueFilterStore,
IIssueKanBanViewStore,
@ -88,7 +87,7 @@ import {
ArchivedIssueDetailStore,
IArchivedIssueDetailStore,
} from "store/archived-issues";
import { DraftIssueStore, IDraftIssueStore, DraftIssueFilterStore, IDraftIssueFilterStore } from "store/draft-issues";
import { DraftIssueFilterStore, IDraftIssueFilterStore, IssueDraftStore, IIssueDraftStore } from "store/draft-issues";
import {
IInboxFiltersStore,
IInboxIssueDetailsStore,
@ -140,7 +139,6 @@ export class RootStore {
issueDetail: IIssueDetailStore;
issueKanBanView: IIssueKanBanViewStore;
issueCalendarView: IIssueCalendarViewStore;
draftIssuesStore: DraftIssuesStore;
quickAddIssue: IIssueQuickAddStore;
calendar: ICalendarStore;
@ -156,7 +154,7 @@ export class RootStore {
archivedIssueDetail: IArchivedIssueDetailStore;
archivedIssueFilters: IArchivedIssueFilterStore;
draftIssues: IDraftIssueStore;
draftIssues: IIssueDraftStore;
draftIssueFilters: IDraftIssueFilterStore;
inbox: IInboxStore;
@ -202,7 +200,6 @@ export class RootStore {
this.issueDetail = new IssueDetailStore(this);
this.issueKanBanView = new IssueKanBanViewStore(this);
this.issueCalendarView = new IssueCalendarViewStore(this);
this.draftIssuesStore = new DraftIssuesStore(this);
this.quickAddIssue = new IssueQuickAddStore(this);
this.calendar = new CalendarStore(this);
@ -218,7 +215,7 @@ export class RootStore {
this.archivedIssueDetail = new ArchivedIssueDetailStore(this);
this.archivedIssueFilters = new ArchivedIssueFilterStore(this);
this.draftIssues = new DraftIssueStore(this);
this.draftIssues = new IssueDraftStore(this);
this.draftIssueFilters = new DraftIssueFilterStore(this);
this.inbox = new InboxStore(this);

View File

@ -128,6 +128,7 @@ export interface IIssue {
workspace_detail: IWorkspaceLite;
}
export interface ISubIssuesState {
backlog: number;
unstarted: number;