dev: setup module and module filter store (#2364)

* dev: implement module issues using mobx store

* dev: module filter store setup

* chore: module store crud operations
This commit is contained in:
Aaryan Khandelwal 2023-10-04 15:21:40 +05:30 committed by GitHub
parent 844a3e4b42
commit 0f47762e6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 994 additions and 295 deletions

View File

@ -13,7 +13,6 @@ import issuesServices from "services/issue.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view"; import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
// ui // ui
import { DangerButton, SecondaryButton } from "components/ui"; import { DangerButton, SecondaryButton } from "components/ui";
// icons // icons
@ -55,7 +54,6 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { displayFilters, params } = useIssuesView(); const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { order_by, group_by, ...viewGanttParams } = params; const { order_by, group_by, ...viewGanttParams } = params;
const { const {
@ -90,14 +88,6 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids]; if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids];
const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
: viewId
? VIEW_ISSUES(viewId.toString(), calendarParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams);
const ganttFetchKey = cycleId const ganttFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
: moduleId : moduleId
@ -122,8 +112,7 @@ export const BulkDeleteIssuesModal: React.FC<Props> = ({ isOpen, setIsOpen, user
message: "Issues deleted successfully!", message: "Issues deleted successfully!",
}); });
if (displayFilters.layout === "calendar") mutate(calendarFetchKey); if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
else if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
else { else {
if (cycleId) { if (cycleId) {
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)); mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params));

View File

@ -8,8 +8,6 @@ import { mutate } from "swr";
import { DragDropContext, DropResult } from "react-beautiful-dnd"; import { DragDropContext, DropResult } from "react-beautiful-dnd";
// services // services
import issuesService from "services/issue.service"; import issuesService from "services/issue.service";
// hooks
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
// components // components
import { SingleCalendarDate, CalendarHeader } from "components/core"; import { SingleCalendarDate, CalendarHeader } from "components/core";
import { IssuePeekOverview } from "components/issues"; import { IssuePeekOverview } from "components/issues";

View File

@ -10,7 +10,6 @@ import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd";
import issuesService from "services/issue.service"; import issuesService from "services/issue.service";
import trackEventServices from "services/track_event.service"; import trackEventServices from "services/track_event.service";
// hooks // hooks
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useIssuesProperties from "hooks/use-issue-properties"; import useIssuesProperties from "hooks/use-issue-properties";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// components // components
@ -60,7 +59,7 @@ export const SingleCalendarIssue: React.FC<Props> = ({
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const { params } = useCalendarIssuesView(); const params = {};
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);

View File

@ -1 +1,2 @@
export * from "./module-issues";
export * from "./project-issues"; export * from "./project-issues";

View File

@ -0,0 +1,94 @@
import { useCallback } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelection } from "components/issues";
// types
import { IIssueDisplayFilterOptions, IIssueFilterOptions, TIssueLayouts } from "types";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
export const ModuleIssuesHeader: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;
const { issueFilter: issueFilterStore, moduleFilter: moduleFilterStore } = useMobxStore();
const activeLayout = issueFilterStore.userDisplayFilters.layout;
const handleLayoutChange = useCallback(
(layout: TIssueLayouts) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
layout,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId || !moduleId) return;
const newValues = moduleFilterStore.userModuleFilters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (moduleFilterStore.userModuleFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
moduleFilterStore.updateUserModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), {
[key]: newValues,
});
},
[moduleId, moduleFilterStore, projectId, workspaceSlug]
);
const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
return (
<div className="flex items-center gap-2">
<LayoutSelection
layouts={["list", "kanban", "calendar", "spreadsheet", "gantt_chart"]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown title="Filters">
<FilterSelection
filters={moduleFilterStore.userModuleFilters}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
projectId={projectId?.toString() ?? ""}
/>
</FiltersDropdown>
<FiltersDropdown title="View">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
/>
</FiltersDropdown>
</div>
);
});

View File

@ -10,7 +10,6 @@ import { Dialog, Transition } from "@headlessui/react";
import issueServices from "services/issue.service"; import issueServices from "services/issue.service";
// hooks // hooks
import useIssuesView from "hooks/use-issues-view"; import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// icons // icons
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
@ -52,7 +51,6 @@ export const DeleteIssueModal: React.FC<Props> = ({
const isArchivedIssues = router.pathname.includes("archived-issues"); const isArchivedIssues = router.pathname.includes("archived-issues");
const { displayFilters, params } = useIssuesView(); const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
@ -73,17 +71,7 @@ export const DeleteIssueModal: React.FC<Props> = ({
await issueServices await issueServices
.deleteIssue(workspaceSlug as string, data.project, data.id, user) .deleteIssue(workspaceSlug as string, data.project, data.id, user)
.then(() => { .then(() => {
if (displayFilters.layout === "calendar") { if (displayFilters.layout === "spreadsheet") {
const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
: viewId
? VIEW_ISSUES(viewId.toString(), calendarParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, calendarParams);
mutate<IIssue[]>(calendarFetchKey, (prevData) => (prevData ?? []).filter((p) => p.id !== data.id), false);
} else if (displayFilters.layout === "spreadsheet") {
if (data.parent) { if (data.parent) {
mutate<ISubIssueResponse>( mutate<ISubIssueResponse>(
SUB_ISSUES(data.parent.toString()), SUB_ISSUES(data.parent.toString()),

View File

@ -11,7 +11,6 @@ import issuesService from "services/issue.service";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import useIssuesView from "hooks/use-issues-view"; import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useLocalStorage from "hooks/use-local-storage"; import useLocalStorage from "hooks/use-local-storage";
import useProjects from "hooks/use-projects"; import useProjects from "hooks/use-projects";
@ -78,7 +77,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const { displayFilters, params } = useIssuesView(); const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { ...viewGanttParams } = params; const { ...viewGanttParams } = params;
const { user } = useUser(); const { user } = useUser();
@ -146,14 +144,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null); setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null);
}, [activeProject, data, projectId, projects, isOpen, prePopulateData]); }, [activeProject, data, projectId, projects, isOpen, prePopulateData]);
const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
: viewId
? VIEW_ISSUES(viewId.toString(), calendarParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", calendarParams);
const ganttFetchKey = cycleId const ganttFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
: moduleId : moduleId
@ -171,7 +161,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart") if (displayFilters.layout === "gantt_chart")
mutate(ganttFetchKey, { mutate(ganttFetchKey, {
start_target_date: true, start_target_date: true,
@ -210,7 +199,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
if (isUpdatingSingleIssue) { if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else { } else {
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString())); if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
@ -290,7 +278,6 @@ export const CreateUpdateDraftIssueModal: React.FC<IssuesModalProps> = (props) =
if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module); if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module);
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart") if (displayFilters.layout === "gantt_chart")
mutate(ganttFetchKey, { mutate(ganttFetchKey, {
start_target_date: true, start_target_date: true,

View File

@ -4,6 +4,7 @@ export * from "./types.d";
export * from "./day-tile"; export * from "./day-tile";
export * from "./header"; export * from "./header";
export * from "./issue-blocks"; export * from "./issue-blocks";
export * from "./module-root";
export * from "./root"; export * from "./root";
export * from "./week-days"; export * from "./week-days";
export * from "./week-header"; export * from "./week-header";

View File

@ -0,0 +1,36 @@
import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { CalendarChart } from "components/issues";
// types
import { IIssueGroupedStructure } from "store/issue";
export const ModuleCalendarLayout: React.FC = observer(() => {
const { module: moduleStore } = useMobxStore();
// TODO: add drag and drop functionality
const onDragEnd = (result: DropResult) => {
if (!result) return;
// return if not dropped on the correct place
if (!result.destination) return;
// return if dropped on the same date
if (result.destination.droppableId === result.source.droppableId) return;
// issueKanBanViewStore?.handleDragDrop(result.source, result.destination);
};
const issues = moduleStore.getIssues;
return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
<DragDropContext onDragEnd={onDragEnd}>
<CalendarChart issues={issues as IIssueGroupedStructure | null} />
</DragDropContext>
</div>
);
});

View File

@ -2,6 +2,7 @@ export * from "./date";
export * from "./filters-list"; export * from "./filters-list";
export * from "./label"; export * from "./label";
export * from "./members"; export * from "./members";
export * from "./module-root";
export * from "./priority"; export * from "./priority";
export * from "./root"; export * from "./root";
export * from "./state"; export * from "./state";

View File

@ -0,0 +1,75 @@
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 ModuleAppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;
const { project: projectStore, moduleFilter: moduleFilterStore } = useMobxStore();
const userFilters = moduleFilterStore.userModuleFilters;
// 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 || !moduleId) return;
// remove all values of the key if value is null
if (!value) {
moduleFilterStore.updateUserModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), {
[key]: null,
});
return;
}
// remove the passed value from the key
let newValues = moduleFilterStore.userModuleFilters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value);
moduleFilterStore.updateUserModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), {
[key]: newValues,
});
};
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId || !moduleId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null;
});
moduleFilterStore.updateUserModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId?.toString(), {
...newFilters,
});
};
// return if no filters are applied
if (Object.keys(appliedFilters).length === 0) return null;
return (
<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() ?? ""]}
/>
);
});

View File

@ -12,7 +12,7 @@ export const AppliedFiltersRoot: React.FC = observer(() => {
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId } = router.query;
const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore(); const { issueFilter: issueFilterStore, project: projectStore, moduleFilter: moduleFilterStore } = useMobxStore();
const userFilters = issueFilterStore.userFilters; const userFilters = issueFilterStore.userFilters;

View File

@ -1,2 +1,3 @@
export * from "./blocks"; export * from "./blocks";
export * from "./module-root";
export * from "./root"; export * from "./root";

View File

@ -0,0 +1,55 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
// types
import { IIssueUnGroupedStructure } from "store/issue";
export const ModuleGanttLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { projectDetails } = useProjectDetails();
const { module: moduleStore, issueFilter: issueFilterStore } = useMobxStore();
const appliedDisplayFilters = issueFilterStore.userDisplayFilters;
const issues = moduleStore.getIssues;
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
return (
<>
<IssuePeekOverview
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={!isAllowed}
/>
<div className="w-full h-full">
<GanttChartRoot
border={false}
title="Issues"
loaderTitle="Issues"
blocks={issues ? renderIssueBlocksStructure(issues as IIssueUnGroupedStructure) : null}
blockUpdateHandler={(block, payload) => {
// TODO: update mutation logic
// updateGanttIssue(block, payload, mutateGanttIssues, user, workspaceSlug?.toString())
}}
BlockRender={IssueGanttBlock}
SidebarBlockRender={IssueGanttSidebarBlock}
enableBlockLeftResize={isAllowed}
enableBlockRightResize={isAllowed}
enableBlockMove={isAllowed}
enableReorder={appliedDisplayFilters.order_by === "sort_order" && isAllowed}
/>
</div>
</>
);
});

View File

@ -3,3 +3,4 @@ export * from "./filters";
export * from "./gantt"; export * from "./gantt";
export * from "./kanban"; export * from "./kanban";
export * from "./spreadsheet"; export * from "./spreadsheet";
export * from "./module-all-layouts";

View File

@ -0,0 +1,54 @@
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 {
KanBanLayout,
ModuleAppliedFiltersRoot,
ModuleCalendarLayout,
ModuleGanttLayout,
ModuleSpreadsheetLayout,
} from "components/issues";
export const ModuleAllLayouts: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, moduleId } = router.query;
const { module: moduleStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore();
useSWR(workspaceSlug && projectId ? `MODULE_ISSUES` : null, async () => {
if (workspaceSlug && projectId && moduleId) {
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
await projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString());
await projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString());
await projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString());
await moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
await moduleStore.fetchModuleIssues(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
}
});
const activeLayout = issueFilterStore.userDisplayFilters.layout;
return (
<div className="relative w-full h-full flex flex-col overflow-auto">
<ModuleAppliedFiltersRoot />
<div className="h-full w-full">
{activeLayout === "kanban" ? (
<KanBanLayout />
) : activeLayout === "calendar" ? (
<ModuleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<ModuleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<ModuleSpreadsheetLayout />
) : null}
</div>
</div>
);
});

View File

@ -1 +1,2 @@
export * from "./module-root";
export * from "./root"; export * from "./root";

View File

@ -0,0 +1,160 @@
import React, { useCallback, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useUser from "hooks/use-user";
import useProjectDetails from "hooks/use-project-details";
// components
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
import { IssuePeekOverview } from "components/issues";
// ui
import { CustomMenu, Spinner } from "components/ui";
// icon
import { PlusIcon } from "@heroicons/react/24/outline";
// types
import { IIssue, IIssueDisplayFilterOptions, IIssueDisplayProperties } from "types";
import { IIssueUnGroupedStructure } from "store/issue";
// constants
import { SPREADSHEET_COLUMN } from "constants/spreadsheet";
export const ModuleSpreadsheetLayout: React.FC = observer(() => {
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
const { user } = useUser();
const { projectDetails } = useProjectDetails();
const { module: moduleStore, issueFilter: issueFilterStore } = useMobxStore();
const issues = moduleStore.getIssues;
const issueDisplayProperties = issueFilterStore.userDisplayProperties;
const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
const columnData = SPREADSHEET_COLUMN.map((column) => ({
...column,
isActive: issueDisplayProperties
? column.propertyName === "labels"
? issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
: column.propertyName === "title"
? true
: issueDisplayProperties[column.propertyName as keyof IIssueDisplayProperties]
: false,
}));
const gridTemplateColumns = columnData
.filter((column) => column.isActive)
.map((column) => column.colSize)
.join(" ");
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
return (
<>
<IssuePeekOverview
projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={!isAllowed}
/>
<div className="h-full rounded-lg text-custom-text-200 overflow-x-auto whitespace-nowrap bg-custom-background-100">
<div className="sticky z-[2] top-0 border-b border-custom-border-200 bg-custom-background-90 w-full min-w-max">
<SpreadsheetColumns
columnData={columnData}
displayFilters={issueFilterStore.userDisplayFilters}
gridTemplateColumns={gridTemplateColumns}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
/>
</div>
{issues ? (
<div className="flex flex-col h-full w-full bg-custom-background-100 rounded-sm ">
{(issues as IIssueUnGroupedStructure).map((issue: IIssue, index) => (
<SpreadsheetIssues
key={`${issue.id}_${index}`}
index={index}
issue={issue}
expandedIssues={expandedIssues}
setExpandedIssues={setExpandedIssues}
gridTemplateColumns={gridTemplateColumns}
properties={issueDisplayProperties}
handleIssueAction={() => {}}
disableUserActions={!isAllowed}
user={user}
userAuth={{
isViewer: projectDetails?.member_role === 5,
isGuest: projectDetails?.member_role === 10,
isMember: projectDetails?.member_role === 15,
isOwner: projectDetails?.member_role === 20,
}}
/>
))}
<div
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
style={{ gridTemplateColumns }}
>
{type === "issue" ? (
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
) : (
isAllowed && (
<CustomMenu
className="sticky left-0 z-[1]"
customButton={
<button
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
type="button"
>
<PlusIcon className="h-4 w-4" />
Add Issue
</button>
}
position="left"
optionsClassName="left-5 !w-36"
noBorder
>
<CustomMenu.MenuItem
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "c" });
document.dispatchEvent(e);
}}
>
Create new
</CustomMenu.MenuItem>
{true && <CustomMenu.MenuItem onClick={() => {}}>Add an existing issue</CustomMenu.MenuItem>}
</CustomMenu>
)
)}
</div>
</div>
) : (
<Spinner />
)}
</div>
</>
);
});

View File

@ -13,7 +13,6 @@ import inboxServices from "services/inbox.service";
// hooks // hooks
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import useIssuesView from "hooks/use-issues-view"; import useIssuesView from "hooks/use-issues-view";
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useInboxView from "hooks/use-inbox-view"; import useInboxView from "hooks/use-inbox-view";
import useProjects from "hooks/use-projects"; import useProjects from "hooks/use-projects";
@ -83,7 +82,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
const { workspaceSlug, projectId, cycleId, moduleId, viewId, inboxId } = router.query; const { workspaceSlug, projectId, cycleId, moduleId, viewId, inboxId } = router.query;
const { displayFilters, params } = useIssuesView(); const { displayFilters, params } = useIssuesView();
const { params: calendarParams } = useCalendarIssuesView();
const { ...viewGanttParams } = params; const { ...viewGanttParams } = params;
const { params: inboxParams } = useInboxView(); const { params: inboxParams } = useInboxView();
@ -270,14 +268,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
}); });
}; };
const calendarFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams)
: moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams)
: viewId
? VIEW_ISSUES(viewId.toString(), calendarParams)
: PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", calendarParams);
const ganttFetchKey = cycleId const ganttFetchKey = cycleId
? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString())
: moduleId : moduleId
@ -298,7 +288,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle);
if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module); if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module);
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (displayFilters.layout === "gantt_chart") if (displayFilters.layout === "gantt_chart")
mutate(ganttFetchKey, { mutate(ganttFetchKey, {
start_target_date: true, start_target_date: true,
@ -375,7 +364,6 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
if (isUpdatingSingleIssue) { if (isUpdatingSingleIssue) {
mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); mutate<IIssue>(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false);
} else { } else {
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString())); if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString()));
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params));
} }

View File

@ -3,7 +3,6 @@ import { useRouter } from "next/router";
// hooks // hooks
import useIssuesView from "hooks/use-issues-view"; import useIssuesView from "hooks/use-issues-view";
import useUser from "hooks/use-user"; import useUser from "hooks/use-user";
import useGanttChartModuleIssues from "hooks/gantt-chart/module-issues-view";
import useProjectDetails from "hooks/use-project-details"; import useProjectDetails from "hooks/use-project-details";
// components // components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart"; import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
@ -22,18 +21,11 @@ export const ModuleIssuesGanttChartView: React.FC<Props> = ({ disableUserActions
const { user } = useUser(); const { user } = useUser();
const { projectDetails } = useProjectDetails(); const { projectDetails } = useProjectDetails();
const { ganttIssues, mutateGanttIssues } = useGanttChartModuleIssues(
workspaceSlug as string,
projectId as string,
moduleId as string
);
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15; const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
return ( return (
<> <>
<IssuePeekOverview <IssuePeekOverview
handleMutation={() => mutateGanttIssues()}
projectId={projectId?.toString() ?? ""} projectId={projectId?.toString() ?? ""}
workspaceSlug={workspaceSlug?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""}
readOnly={disableUserActions} readOnly={disableUserActions}
@ -43,7 +35,7 @@ export const ModuleIssuesGanttChartView: React.FC<Props> = ({ disableUserActions
border={false} border={false}
title="Issues" title="Issues"
loaderTitle="Issues" loaderTitle="Issues"
blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null} blocks={null}
blockUpdateHandler={(block, payload) => {}} blockUpdateHandler={(block, payload) => {}}
SidebarBlockRender={IssueGanttSidebarBlock} SidebarBlockRender={IssueGanttSidebarBlock}
BlockRender={IssueGanttBlock} BlockRender={IssueGanttBlock}

View File

@ -1,53 +0,0 @@
import useSWR from "swr";
// services
import modulesService from "services/modules.service";
// hooks
import useIssuesView from "hooks/use-issues-view";
// fetch-keys
import { MODULE_ISSUES_WITH_PARAMS } from "constants/fetch-keys";
const useGanttChartModuleIssues = (
workspaceSlug: string | undefined,
projectId: string | undefined,
moduleId: string | undefined
) => {
const { displayFilters, filters } = useIssuesView();
const params: any = {
order_by: displayFilters.order_by,
type: displayFilters?.type ? displayFilters?.type : undefined,
sub_issue: displayFilters.sub_issue,
assignees: filters?.assignees ? filters?.assignees.join(",") : undefined,
state: filters?.state ? filters?.state.join(",") : undefined,
priority: filters?.priority ? filters?.priority.join(",") : undefined,
labels: filters?.labels ? filters?.labels.join(",") : undefined,
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
target_date: filters?.target_date ? filters?.target_date.join(",") : undefined,
start_target_date: true, // to fetch only issues with a start and target date
};
// all issues under the workspace and project
const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR(
workspaceSlug && projectId && moduleId
? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params)
: null,
workspaceSlug && projectId && moduleId
? () =>
modulesService.getModuleIssuesWithParams(
workspaceSlug.toString(),
projectId.toString(),
moduleId.toString(),
params
)
: null
);
return {
ganttIssues,
mutateGanttIssues,
};
};
export default useGanttChartModuleIssues;

View File

@ -1,113 +0,0 @@
import { useContext } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
// contexts
import { issueViewContext } from "contexts/issue-view.context";
// services
import issuesService from "services/issue.service";
import cyclesService from "services/cycles.service";
import modulesService from "services/modules.service";
// types
import { IIssue } from "types";
// fetch-keys
import {
CYCLE_ISSUES_WITH_PARAMS,
MODULE_ISSUES_WITH_PARAMS,
PROJECT_ISSUES_LIST_WITH_PARAMS,
VIEW_ISSUES,
} from "constants/fetch-keys";
const useCalendarIssuesView = () => {
const {
display_filters: displayFilters,
setDisplayFilters,
filters,
setFilters,
resetFilterToDefault,
setNewFilterDefaultView,
} = useContext(issueViewContext);
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
const params: any = {
assignees: filters?.assignees ? filters?.assignees.join(",") : undefined,
state: filters?.state ? filters?.state.join(",") : undefined,
priority: filters?.priority ? filters?.priority.join(",") : undefined,
type: displayFilters?.type ? displayFilters?.type : undefined,
labels: filters?.labels ? filters?.labels.join(",") : undefined,
created_by: filters?.created_by ? filters?.created_by.join(",") : undefined,
start_date: filters?.start_date ? filters?.start_date.join(",") : undefined,
};
const { data: projectCalendarIssues, mutate: mutateProjectCalendarIssues } = useSWR(
workspaceSlug && projectId ? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params) : null,
workspaceSlug && projectId
? () => issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), params)
: null
);
const { data: cycleCalendarIssues, mutate: mutateCycleCalendarIssues } = useSWR(
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) : null,
workspaceSlug && projectId && cycleId
? () =>
cyclesService.getCycleIssuesWithParams(
workspaceSlug.toString(),
projectId.toString(),
cycleId.toString(),
params
)
: null
);
const { data: moduleCalendarIssues, mutate: mutateModuleCalendarIssues } = useSWR(
workspaceSlug && projectId && moduleId ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) : null,
workspaceSlug && projectId && moduleId
? () =>
modulesService.getModuleIssuesWithParams(
workspaceSlug.toString(),
projectId.toString(),
moduleId.toString(),
params
)
: null
);
const { data: viewCalendarIssues, mutate: mutateViewCalendarIssues } = useSWR(
workspaceSlug && projectId && viewId && params ? VIEW_ISSUES(viewId.toString(), params) : null,
workspaceSlug && projectId && viewId && params
? () => issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), params)
: null
);
const calendarIssues = cycleId
? (cycleCalendarIssues as IIssue[])
: moduleId
? (moduleCalendarIssues as IIssue[])
: viewId
? (viewCalendarIssues as IIssue[])
: (projectCalendarIssues as IIssue[]);
return {
displayFilters,
setDisplayFilters,
calendarIssues: calendarIssues ?? [],
mutateIssues: cycleId
? mutateCycleCalendarIssues
: moduleId
? mutateModuleCalendarIssues
: viewId
? mutateViewCalendarIssues
: mutateProjectCalendarIssues,
filters,
setFilters,
params,
resetFilterToDefault,
setNewFilterDefaultView,
} as const;
};
export default useCalendarIssuesView;

View File

@ -6,11 +6,17 @@ import { useMobxStore } from "lib/mobx/store-provider";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
const MobxStoreInit = () => { const MobxStoreInit = () => {
const { theme: themeStore, user: userStore, workspace: workspaceStore, project: projectStore } = useMobxStore(); const {
theme: themeStore,
user: userStore,
workspace: workspaceStore,
project: projectStore,
module: moduleStore,
} = useMobxStore();
const { setTheme } = useTheme(); const { setTheme } = useTheme();
const router = useRouter(); const router = useRouter();
const { workspaceSlug, projectId } = router.query; const { workspaceSlug, projectId, moduleId } = router.query;
useEffect(() => { useEffect(() => {
// sidebar collapsed toggle // sidebar collapsed toggle
@ -47,7 +53,8 @@ const MobxStoreInit = () => {
useEffect(() => { useEffect(() => {
if (workspaceSlug) workspaceStore.setWorkspaceSlug(workspaceSlug.toString()); if (workspaceSlug) workspaceStore.setWorkspaceSlug(workspaceSlug.toString());
if (projectId) projectStore.setProjectId(projectId.toString()); if (projectId) projectStore.setProjectId(projectId.toString());
}, [workspaceSlug, projectId, workspaceStore, projectStore]); if (moduleId) moduleStore.setModuleId(moduleId.toString());
}, [workspaceSlug, projectId, moduleId, workspaceStore, projectStore, moduleStore]);
return <></>; return <></>;
}; };

View File

@ -13,10 +13,8 @@ import useToast from "hooks/use-toast";
import useUserAuth from "hooks/use-user-auth"; import useUserAuth from "hooks/use-user-auth";
// layouts // layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components // components
import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core"; import { ExistingIssuesListModal, IssuesFilterView } from "components/core";
import { ModuleDetailsSidebar } from "components/modules"; import { ModuleDetailsSidebar } from "components/modules";
import { AnalyticsProjectModal } from "components/analytics"; import { AnalyticsProjectModal } from "components/analytics";
// ui // ui
@ -30,10 +28,12 @@ import { truncateText } from "helpers/string.helper";
import { ISearchIssueResponse } from "types"; import { ISearchIssueResponse } from "types";
// fetch-keys // fetch-keys
import { MODULE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys"; import { MODULE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys";
import { ModuleAllLayouts } from "components/issues";
import { ModuleIssuesHeader } from "components/headers";
const SingleModule: React.FC = () => { const SingleModule: React.FC = () => {
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
const [moduleSidebar, setModuleSidebar] = useState(true); const [moduleSidebar, setModuleSidebar] = useState(false);
const [analyticsModal, setAnalyticsModal] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false);
const router = useRouter(); const router = useRouter();
@ -85,7 +85,7 @@ const SingleModule: React.FC = () => {
}; };
return ( return (
<IssueViewContextProvider> <>
<ExistingIssuesListModal <ExistingIssuesListModal
isOpen={moduleIssuesListModal} isOpen={moduleIssuesListModal}
handleClose={() => setModuleIssuesListModal(false)} handleClose={() => setModuleIssuesListModal(false)}
@ -124,27 +124,7 @@ const SingleModule: React.FC = () => {
))} ))}
</CustomMenu> </CustomMenu>
} }
right={ right={<ModuleIssuesHeader />}
<div className={`flex items-center gap-2 duration-300`}>
<IssuesFilterView />
<SecondaryButton
onClick={() => setAnalyticsModal(true)}
className="!py-1.5 font-normal rounded-md text-custom-text-200 hover:text-custom-text-100"
outline
>
Analytics
</SecondaryButton>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
moduleSidebar ? "rotate-180" : ""
}`}
onClick={() => setModuleSidebar((prevData) => !prevData)}
>
<ArrowLeftIcon className="h-4 w-4" />
</button>
</div>
}
> >
{error ? ( {error ? (
<EmptyState <EmptyState
@ -164,7 +144,7 @@ const SingleModule: React.FC = () => {
analyticsModal ? "mr-[50%]" : "" analyticsModal ? "mr-[50%]" : ""
} duration-300`} } duration-300`}
> >
<IssuesView openIssuesListModal={openIssuesListModal} /> <ModuleAllLayouts />
</div> </div>
<ModuleDetailsSidebar <ModuleDetailsSidebar
module={moduleDetails} module={moduleDetails}
@ -175,7 +155,7 @@ const SingleModule: React.FC = () => {
</> </>
)} )}
</ProjectAuthorizationWrapper> </ProjectAuthorizationWrapper>
</IssueViewContextProvider> </>
); );
}; };

View File

@ -18,12 +18,7 @@ export class ModuleService extends APIService {
}); });
} }
async createModule( async createModule(workspaceSlug: string, projectId: string, data: any, user: any): Promise<IModule> {
workspaceSlug: string,
projectId: string,
data: any,
user: ICurrentUserResponse | undefined
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data)
.then((response) => { .then((response) => {
trackEventServices.trackModuleEvent(response?.data, "MODULE_CREATE", user); trackEventServices.trackModuleEvent(response?.data, "MODULE_CREATE", user);
@ -64,7 +59,7 @@ export class ModuleService extends APIService {
projectId: string, projectId: string,
moduleId: string, moduleId: string,
data: Partial<IModule>, data: Partial<IModule>,
user: ICurrentUserResponse | undefined user: any
): Promise<any> { ): Promise<any> {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, data) return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, data)
.then((response) => { .then((response) => {
@ -76,12 +71,7 @@ export class ModuleService extends APIService {
}); });
} }
async deleteModule( async deleteModule(workspaceSlug: string, projectId: string, moduleId: string, user: any): Promise<any> {
workspaceSlug: string,
projectId: string,
moduleId: string,
user: ICurrentUserResponse | undefined
): Promise<any> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`) return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`)
.then((response) => { .then((response) => {
trackEventServices.trackModuleEvent(response?.data, "MODULE_DELETE", user); trackEventServices.trackModuleEvent(response?.data, "MODULE_DELETE", user);

View File

@ -100,7 +100,7 @@ class IssueStore implements IIssueStore {
updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => {
const projectId: string | null = issue?.project; const projectId: string | null = issue?.project;
const issueType: IIssueType | null = this.getIssueType; const issueType = this.getIssueType;
if (!projectId || !issueType) return null; if (!projectId || !issueType) return null;
let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null =

View File

@ -4,7 +4,6 @@ import { ProjectService } from "services/project.service";
import { IssueService } from "services/issue.service"; import { IssueService } from "services/issue.service";
// helpers // helpers
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
import { renderDateFormat } from "helpers/date-time.helper";
// types // types
import { RootStore } from "./root"; import { RootStore } from "./root";
import { import {
@ -113,13 +112,7 @@ class IssueFilterStore implements IIssueFilterStore {
}; };
get appliedFilters(): TIssueParams[] | null { get appliedFilters(): TIssueParams[] | null {
if ( if (!this.userFilters || !this.userDisplayFilters) return null;
!this.userFilters ||
Object.keys(this.userFilters).length === 0 ||
!this.userDisplayFilters ||
Object.keys(this.userDisplayFilters).length === 0
)
return null;
let filteredRouteParams: any = { let filteredRouteParams: any = {
priority: this.userFilters?.priority || undefined, priority: this.userFilters?.priority || undefined,

148
web/store/module_filters.ts Normal file
View File

@ -0,0 +1,148 @@
import { observable, action, computed, makeObservable, runInAction } from "mobx";
// services
import { ProjectService } from "services/project.service";
import { ModuleService } from "services/modules.service";
// helpers
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
// types
import { RootStore } from "./root";
import { IIssueFilterOptions, TIssueParams } from "types";
export interface IModuleFilterStore {
loader: boolean;
error: any | null;
userModuleFilters: IIssueFilterOptions;
defaultFilters: IIssueFilterOptions;
// action
updateUserModuleFilters: (
workspaceSlug: string,
projectId: string,
moduleId: string,
filterToUpdate: Partial<IIssueFilterOptions>
) => Promise<void>;
// computed
appliedFilters: TIssueParams[] | null;
}
class ModuleFilterStore implements IModuleFilterStore {
loader: boolean = false;
error: any | null = null;
// observables
userModuleFilters: IIssueFilterOptions = {};
defaultFilters: IIssueFilterOptions = {};
// root store
rootStore;
// services
projectService;
moduleService;
constructor(_rootStore: RootStore) {
makeObservable(this, {
loader: observable.ref,
error: observable.ref,
// observables
defaultFilters: observable.ref,
userModuleFilters: observable.ref,
// actions
updateUserModuleFilters: action,
// computed
appliedFilters: computed,
});
this.rootStore = _rootStore;
this.projectService = new ProjectService();
this.moduleService = new ModuleService();
}
computedFilter = (filters: any, filteredParams: any) => {
const computedFilters: any = {};
Object.keys(filters).map((key) => {
if (filters[key] != undefined && filteredParams.includes(key))
computedFilters[key] =
typeof filters[key] === "string" || typeof filters[key] === "boolean" ? filters[key] : filters[key].join(",");
});
return computedFilters;
};
get appliedFilters(): TIssueParams[] | null {
const userDisplayFilters = this.rootStore.issueFilter.userDisplayFilters;
if (!this.userModuleFilters || !userDisplayFilters) return null;
let filteredRouteParams: any = {
priority: this.userModuleFilters?.priority || undefined,
state_group: this.userModuleFilters?.state_group || undefined,
state: this.userModuleFilters?.state || undefined,
assignees: this.userModuleFilters?.assignees || undefined,
created_by: this.userModuleFilters?.created_by || undefined,
labels: this.userModuleFilters?.labels || undefined,
start_date: this.userModuleFilters?.start_date || undefined,
target_date: this.userModuleFilters?.target_date || undefined,
group_by: userDisplayFilters?.group_by || "state",
order_by: userDisplayFilters?.order_by || "-created_at",
sub_group_by: userDisplayFilters?.sub_group_by || undefined,
type: userDisplayFilters?.type || undefined,
sub_issue: userDisplayFilters?.sub_issue || true,
show_empty_groups: userDisplayFilters?.show_empty_groups || true,
start_target_date: userDisplayFilters?.start_target_date || true,
};
const filteredParams = handleIssueQueryParamsByLayout(userDisplayFilters.layout, "issues");
if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams);
if (userDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date";
if (userDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true;
return filteredRouteParams;
}
updateUserModuleFilters = async (
workspaceSlug: string,
projectId: string,
moduleId: string,
filterToUpdate: Partial<IIssueFilterOptions>
) => {
const newFilters = {
...this.userModuleFilters,
...filterToUpdate,
};
try {
runInAction(() => {
this.userModuleFilters = newFilters;
});
this.moduleService.patchModule(
workspaceSlug,
projectId,
moduleId,
{
view_props: {
filters: newFilters,
},
},
this.rootStore.user.currentUser
);
} catch (error) {
this.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId);
runInAction(() => {
this.error = error;
});
console.log("Failed to update user filters in issue filter store", error);
}
};
}
export default ModuleFilterStore;

View File

@ -1,10 +1,11 @@
import { action, computed, observable, makeObservable, runInAction } from "mobx"; import { action, computed, observable, makeObservable, runInAction } from "mobx";
// types
import { RootStore } from "./root";
// services // services
import { ProjectService } from "services/project.service"; import { ProjectService } from "services/project.service";
import { ModuleService } from "services/modules.service"; import { ModuleService } from "services/modules.service";
import { IModule } from "@/types"; // types
import { RootStore } from "./root";
import { IIssue, IModule } from "types";
import { IIssueGroupWithSubGroupsStructure, IIssueGroupedStructure, IIssueUnGroupedStructure } from "./issue";
export interface IModuleStore { export interface IModuleStore {
loader: boolean; loader: boolean;
@ -14,13 +15,35 @@ export interface IModuleStore {
modules: { modules: {
[project_id: string]: IModule[]; [project_id: string]: IModule[];
}; };
module_details: { moduleDetails: {
[module_id: string]: IModule; [module_id: string]: IModule;
}; };
issues: {
[module_id: string]: {
grouped: IIssueGroupedStructure;
groupWithSubGroups: IIssueGroupWithSubGroupsStructure;
ungrouped: IIssueUnGroupedStructure;
};
};
setModuleId: (moduleSlug: string) => void; setModuleId: (moduleSlug: string) => void;
fetchModules: (workspaceSlug: string, projectSlug: string) => void; fetchModules: (workspaceSlug: string, projectId: string) => void;
fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => void;
// crud operations
createModule: (workspaceSlug: string, projectId: string, data: Partial<IModule>) => Promise<IModule>;
updateModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string, data: Partial<IModule>) => void;
deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => void;
addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => void;
removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => void;
// issue related operations
fetchModuleIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise<any>;
updateIssueStructure: (group_id: string | null, sub_group_id: string | null, moduleId: string, issue: IIssue) => void;
// computed
getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null;
} }
class ModuleStore implements IModuleStore { class ModuleStore implements IModuleStore {
@ -33,10 +56,24 @@ class ModuleStore implements IModuleStore {
[project_id: string]: IModule[]; [project_id: string]: IModule[];
} = {}; } = {};
module_details: { moduleDetails: {
[module_id: string]: IModule; [module_id: string]: IModule;
} = {}; } = {};
issues: {
[module_id: string]: {
grouped: {
[group_id: string]: IIssue[];
};
groupWithSubGroups: {
[group_id: string]: {
[sub_group_id: string]: IIssue[];
};
};
ungrouped: IIssue[];
};
} = {};
// root store // root store
rootStore; rootStore;
// services // services
@ -49,11 +86,27 @@ class ModuleStore implements IModuleStore {
error: observable.ref, error: observable.ref,
moduleId: observable.ref, moduleId: observable.ref,
modules: observable.ref,
moduleDetails: observable.ref,
issues: observable.ref,
// computed // computed
getIssues: computed,
// actions // actions
setModuleId: action, setModuleId: action,
fetchModules: action,
fetchModuleDetails: action,
createModule: action,
updateModuleDetails: action,
deleteModule: action,
addModuleToFavorites: action,
removeModuleFromFavorites: action,
fetchModuleIssues: action,
updateIssueStructure: action,
}); });
this.rootStore = _rootStore; this.rootStore = _rootStore;
@ -67,32 +120,302 @@ class ModuleStore implements IModuleStore {
return this.modules[this.rootStore.project.projectId] || null; return this.modules[this.rootStore.project.projectId] || null;
} }
get getIssues() {
const moduleId = this.moduleId;
const issueType = this.rootStore.issue.getIssueType;
if (!moduleId || !issueType) return null;
return this.issues?.[moduleId]?.[issueType] || null;
}
// actions // actions
setModuleId = (moduleSlug: string) => { setModuleId = (moduleSlug: string) => {
this.moduleId = moduleSlug ?? null; this.moduleId = moduleSlug ?? null;
}; };
fetchModules = async (workspaceSlug: string, projectSlug: string) => { fetchModules = async (workspaceSlug: string, projectId: string) => {
try { try {
runInAction(() => {
this.loader = true; this.loader = true;
this.error = null; this.error = null;
});
const modulesResponse = await this.moduleService.getModules(workspaceSlug, projectSlug); const modulesResponse = await this.moduleService.getModules(workspaceSlug, projectId);
runInAction(() => { runInAction(() => {
this.modules = { this.modules = {
...this.modules, ...this.modules,
[projectSlug]: modulesResponse, [projectId]: modulesResponse,
}; };
this.loader = false; this.loader = false;
this.error = null; this.error = null;
}); });
} catch (error) { } catch (error) {
console.error("Failed to fetch modules list in project store", error); console.error("Failed to fetch modules list in module store", error);
runInAction(() => {
this.loader = false; this.loader = false;
this.error = error; this.error = error;
});
} }
}; };
fetchModuleDetails = async (workspaceSlug: string, projectId: string, moduleId: string) => {
try {
runInAction(() => {
this.loader = true;
this.error = null;
});
const response = await this.moduleService.getModuleDetails(workspaceSlug, projectId, moduleId);
if (!response) return null;
runInAction(() => {
this.moduleDetails = {
...this.moduleDetails,
[moduleId]: response,
};
this.rootStore.moduleFilter.userModuleFilters = response.view_props?.filters ?? {};
this.loader = false;
this.error = null;
});
} catch (error) {
console.error("Failed to fetch module details in module store", error);
runInAction(() => {
this.loader = false;
this.error = error;
});
}
};
createModule = async (workspaceSlug: string, projectId: string, data: Partial<IModule>) => {
try {
const response = await this.moduleService.createModule(
workspaceSlug,
projectId,
data,
this.rootStore.user.currentUser
);
runInAction(() => {
this.modules = {
...this.modules,
[projectId]: [...this.modules[projectId], response],
};
this.loader = false;
this.error = null;
});
return response;
} catch (error) {
console.error("Failed to create module in module store", error);
runInAction(() => {
this.loader = false;
this.error = error;
});
throw error;
}
};
updateModuleDetails = async (workspaceSlug: string, projectId: string, moduleId: string, data: Partial<IModule>) => {
try {
runInAction(() => {
(this.modules = {
...this.modules,
[projectId]: this.modules[projectId].map((module) =>
module.id === moduleId ? { ...module, ...data } : module
),
}),
(this.moduleDetails = {
...this.moduleDetails,
[moduleId]: {
...this.moduleDetails[moduleId],
...data,
},
});
});
await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, data, this.rootStore.user.currentUser);
} catch (error) {
console.error("Failed to update module in module store", error);
this.fetchModules(workspaceSlug, projectId);
this.fetchModuleDetails(workspaceSlug, projectId, moduleId);
runInAction(() => {
this.error = error;
});
}
};
deleteModule = async (workspaceSlug: string, projectId: string, moduleId: string) => {
try {
runInAction(() => {
this.modules = {
...this.modules,
[projectId]: this.modules[projectId].filter((module) => module.id !== moduleId),
};
});
await this.moduleService.deleteModule(workspaceSlug, projectId, moduleId, this.rootStore.user.currentUser);
} catch (error) {
console.error("Failed to delete module in module store", error);
this.fetchModules(workspaceSlug, projectId);
runInAction(() => {
this.error = error;
});
}
};
addModuleToFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => {
try {
runInAction(() => {
this.modules = {
...this.modules,
[projectId]: this.modules[projectId].map((module) => ({
...module,
is_favorite: module.id === moduleId ? true : module.is_favorite,
})),
};
});
await this.moduleService.addModuleToFavorites(workspaceSlug, projectId, {
module: moduleId,
});
} catch (error) {
console.error("Failed to add module to favorites in module store", error);
runInAction(() => {
this.modules = {
...this.modules,
[projectId]: this.modules[projectId].map((module) => ({
...module,
is_favorite: module.id === moduleId ? false : module.is_favorite,
})),
};
this.error = error;
});
}
};
removeModuleFromFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => {
try {
runInAction(() => {
this.modules = {
...this.modules,
[projectId]: this.modules[projectId].map((module) => ({
...module,
is_favorite: module.id === moduleId ? false : module.is_favorite,
})),
};
});
await this.moduleService.removeModuleFromFavorites(workspaceSlug, projectId, moduleId);
} catch (error) {
console.error("Failed to remove module from favorites in module store", error);
runInAction(() => {
this.modules = {
...this.modules,
[projectId]: this.modules[projectId].map((module) => ({
...module,
is_favorite: module.id === moduleId ? true : module.is_favorite,
})),
};
});
}
};
fetchModuleIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => {
try {
this.loader = true;
this.error = null;
this.rootStore.workspace.setWorkspaceSlug(workspaceSlug);
this.rootStore.project.setProjectId(projectId);
const params = this.rootStore?.issueFilter?.appliedFilters;
console.log("params", params);
const issueResponse = await this.moduleService.getModuleIssuesWithParams(
workspaceSlug,
projectId,
moduleId,
params
);
const issueType = this.rootStore.issue.getIssueType;
if (issueType != null) {
const _issues = {
...this.issues,
[moduleId]: {
...this.issues[moduleId],
[issueType]: issueResponse,
},
};
runInAction(() => {
this.issues = _issues;
this.loader = false;
this.error = null;
});
}
return issueResponse;
} catch (error) {
console.error("Error: Fetching error module issues in module store", error);
this.loader = false;
this.error = error;
return error;
}
};
updateIssueStructure = async (
group_id: string | null,
sub_group_id: string | null,
moduleId: string,
issue: IIssue
) => {
const issueType = this.rootStore.issue.getIssueType;
if (!issueType) return null;
let issues = this.getIssues;
if (!issues) return null;
if (issueType === "grouped" && group_id) {
issues = issues as IIssueGroupedStructure;
issues = {
...issues,
[group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? issue : i)),
};
}
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 ? issue : i)),
},
};
}
if (issueType === "ungrouped") {
issues = issues as IIssueUnGroupedStructure;
issues = issues.map((i: IIssue) => (i?.id === issue?.id ? issue : i));
}
runInAction(() => {
this.issues = { ...this.issues, [moduleId]: { ...this.issues[moduleId], [issueType]: issues } };
});
};
} }
export default ModuleStore; export default ModuleStore;

View File

@ -15,6 +15,7 @@ import IssueFilterStore, { IIssueFilterStore } from "./issue_filters";
import IssueViewDetailStore from "./issue_detail"; import IssueViewDetailStore from "./issue_detail";
import IssueKanBanViewStore from "./kanban_view"; import IssueKanBanViewStore from "./kanban_view";
import CalendarStore, { ICalendarStore } from "./calendar"; import CalendarStore, { ICalendarStore } from "./calendar";
import ModuleFilterStore, { IModuleFilterStore } from "./module_filters";
enableStaticRendering(typeof window === "undefined"); enableStaticRendering(typeof window === "undefined");
@ -27,6 +28,7 @@ export class RootStore {
project: IProjectStore; project: IProjectStore;
issue: IIssueStore; issue: IIssueStore;
module: IModuleStore; module: IModuleStore;
moduleFilter: IModuleFilterStore;
cycle: ICycleStore; cycle: ICycleStore;
view: IViewStore; view: IViewStore;
issueFilter: IIssueFilterStore; issueFilter: IIssueFilterStore;
@ -41,6 +43,7 @@ export class RootStore {
this.project = new ProjectStore(this); this.project = new ProjectStore(this);
this.projectPublish = new ProjectPublishStore(this); this.projectPublish = new ProjectPublishStore(this);
this.module = new ModuleStore(this); this.module = new ModuleStore(this);
this.moduleFilter = new ModuleFilterStore(this);
this.cycle = new CycleStore(this); this.cycle = new CycleStore(this);
this.view = new ViewStore(this); this.view = new ViewStore(this);
this.issue = new IssueStore(this); this.issue = new IssueStore(this);