dev: add remaining layouts to cycle (#2413)

This commit is contained in:
Aaryan Khandelwal 2023-10-11 20:08:42 +05:30 committed by GitHub
parent 265e60a536
commit fcfdd74d4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 401 additions and 127 deletions

View File

@ -2,4 +2,3 @@ export * from "./date-filter-modal";
export * from "./date-filter-select";
export * from "./filters-list";
export * from "./workspace-filters-list";
export * from "./issues-view-filter";

View File

@ -0,0 +1,111 @@
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, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types";
// constants
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
export const CycleIssuesHeader: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId } = router.query;
const {
issueFilter: issueFilterStore,
cycleIssueFilter: cycleIssueFilterStore,
project: projectStore,
} = 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 || !cycleId) return;
const newValues = cycleIssueFilterStore.cycleFilters?.[key] ?? [];
if (Array.isArray(value)) {
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
});
} else {
if (cycleIssueFilterStore.cycleFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), {
[key]: newValues,
});
},
[cycleId, cycleIssueFilterStore, projectId, workspaceSlug]
);
const handleDisplayFiltersUpdate = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), {
display_filters: {
...updatedDisplayFilter,
},
});
},
[issueFilterStore, projectId, workspaceSlug]
);
const handleDisplayPropertiesUpdate = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property);
},
[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={cycleIssueFilterStore.cycleFilters}
handleFiltersUpdate={handleFiltersUpdate}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.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="View">
<DisplayFiltersSelection
displayFilters={issueFilterStore.userDisplayFilters}
displayProperties={issueFilterStore.userDisplayProperties}
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
layoutDisplayFiltersOptions={activeLayout ? ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues[activeLayout] : undefined}
/>
</FiltersDropdown>
</div>
);
});

View File

@ -1,3 +1,4 @@
export * from "./cycle-issues";
export * from "./global-issues";
export * from "./module-issues";
export * from "./project-issues";

View File

@ -0,0 +1,39 @@
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 CycleCalendarLayout: React.FC = observer(() => {
const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore } = 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 = cycleIssueStore.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}
layout={issueFilterStore.userDisplayFilters.calendar?.layout}
/>
</DragDropContext>
</div>
);
});

View File

@ -1,5 +1,6 @@
export * from "./dropdowns";
export * from "./calendar";
export * from "./cycle-root";
export * from "./types.d";
export * from "./day-tile";
export * from "./header";

View File

@ -9,7 +9,7 @@ import { CalendarChart } from "components/issues";
import { IIssueGroupedStructure } from "store/issue";
export const ModuleCalendarLayout: React.FC = observer(() => {
const { module: moduleStore, issueFilter: issueFilterStore } = useMobxStore();
const { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
// TODO: add drag and drop functionality
const onDragEnd = (result: DropResult) => {
@ -24,7 +24,7 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
// issueKanBanViewStore?.handleDragDrop(result.source, result.destination);
};
const issues = moduleStore.getIssues;
const issues = moduleIssueStore.getIssues;
return (
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">

View File

@ -6,7 +6,13 @@ import useSWR from "swr";
// mobx react lite
import { observer } from "mobx-react-lite";
// components
import { CycleKanBanLayout, CycleListLayout } from "components/issues";
import {
CycleCalendarLayout,
CycleGanttLayout,
CycleKanBanLayout,
CycleListLayout,
CycleSpreadsheetLayout,
} from "components/issues";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
@ -46,7 +52,17 @@ export const CycleLayoutRoot: React.FC = observer(() => {
return (
<div className="w-full h-full">
{activeLayout === "list" ? <CycleListLayout /> : activeLayout === "kanban" ? <CycleKanBanLayout /> : null}
{activeLayout === "list" ? (
<CycleListLayout />
) : activeLayout === "kanban" ? (
<CycleKanBanLayout />
) : activeLayout === "calendar" ? (
<CycleCalendarLayout />
) : activeLayout === "gantt_chart" ? (
<CycleGanttLayout />
) : activeLayout === "spreadsheet" ? (
<CycleSpreadsheetLayout />
) : null}
</div>
);
});

View File

@ -25,6 +25,17 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => {
} = useMobxStore();
const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined;
const storedFilters = globalViewId ? globalViewFiltersStore.storedFilters[globalViewId.toString()] : undefined;
// filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {};
Object.entries(storedFilters ?? {}).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 (!globalViewId) return;
@ -72,15 +83,16 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => {
});
};
const storedFilters = globalViewId ? globalViewFiltersStore.storedFilters[globalViewId.toString()] : undefined;
// update stored filters when view details are fetched
useEffect(() => {
if (!globalViewId || !viewDetails) return;
if (!globalViewFiltersStore.storedFilters[globalViewId.toString()])
globalViewFiltersStore.updateStoredFilters(globalViewId.toString(), viewDetails?.query_data?.filters ?? {});
}, [globalViewId, globalViewFiltersStore, storedFilters, viewDetails]);
}, [globalViewId, globalViewFiltersStore, viewDetails]);
// return if no filters are applied
if (Object.keys(appliedFilters).length === 0) return null;
return (
<div className="flex items-start justify-between gap-4 p-4">

View File

@ -24,11 +24,11 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
} = useMobxStore();
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] ?? {} : {};
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
// filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {};
Object.entries(storedFilters).forEach(([key, value]) => {
Object.entries(storedFilters ?? {}).forEach(([key, value]) => {
if (!value) return;
if (Array.isArray(value) && value.length === 0) return;
@ -60,7 +60,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
if (!workspaceSlug || !projectId || !viewId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(storedFilters).forEach((key) => {
Object.keys(storedFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = null;
});

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 CycleGanttLayout: React.FC = observer(() => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { projectDetails } = useProjectDetails();
const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
const appliedDisplayFilters = issueFilterStore.userDisplayFilters;
const issues = cycleIssueStore.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

@ -1,4 +1,5 @@
export * from "./blocks";
export * from "./cycle-root";
export * from "./module-root";
export * from "./project-view-root";
export * from "./root";

View File

@ -17,11 +17,11 @@ export const ModuleGanttLayout: React.FC = observer(() => {
const { projectDetails } = useProjectDetails();
const { module: moduleStore, issueFilter: issueFilterStore } = useMobxStore();
const { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
const appliedDisplayFilters = issueFilterStore.userDisplayFilters;
const issues = moduleStore.getIssues;
const issues = moduleIssueStore.getIssues;
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;

View File

@ -0,0 +1,146 @@
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 } from "components/ui";
import { Spinner } from "@plane/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 CycleSpreadsheetLayout: React.FC = observer(() => {
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { user } = useUser();
const { projectDetails } = useProjectDetails();
const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
const issues = cycleIssueStore.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 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 }}
>
{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

@ -1,3 +1,4 @@
export * from "./cycle-root";
export * from "./module-root";
export * from "./project-view-root";
export * from "./root";

View File

@ -30,9 +30,9 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
const { user } = useUser();
const { projectDetails } = useProjectDetails();
const { module: moduleStore, issueFilter: issueFilterStore } = useMobxStore();
const { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
const issues = moduleStore.getIssues;
const issues = moduleIssueStore.getIssues;
const issueDisplayProperties = issueFilterStore.userDisplayProperties;
const handleDisplayFiltersUpdate = useCallback(

View File

@ -11,7 +11,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
// contexts
import { IssueViewContextProvider } from "contexts/issue-view.context";
// components
import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core";
import { ExistingIssuesListModal } from "components/core";
import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles";
import { CycleLayoutRoot } from "components/issues/issue-layouts";
// services
@ -35,6 +35,7 @@ import { getDateRangeStatus } from "helpers/date-time.helper";
import { ISearchIssueResponse } from "types";
// fetch-keys
import { CYCLES_LIST, CYCLE_DETAILS } from "constants/fetch-keys";
import { CycleIssuesHeader } from "components/headers";
const SingleCycle: React.FC = () => {
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
@ -68,10 +69,6 @@ const SingleCycle: React.FC = () => {
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
: "draft";
const openIssuesListModal = () => {
setCycleIssuesListModal(true);
};
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
if (!workspaceSlug || !projectId) return;
@ -132,7 +129,7 @@ const SingleCycle: React.FC = () => {
}
right={
<div className={`flex flex-shrink-0 items-center gap-2 duration-300`}>
<IssuesFilterView />
<CycleIssuesHeader />
<Button variant="neutral-primary" onClick={() => setAnalyticsModal(true)}>
Analytics
</Button>

View File

@ -1,4 +1,4 @@
import { action, computed, observable, makeObservable, runInAction } from "mobx";
import { action, observable, makeObservable, runInAction } from "mobx";
// services
import { ProjectService } from "services/project.service";
import { ModuleService } from "services/modules.service";
@ -37,13 +37,6 @@ export interface IModuleStore {
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 {
@ -90,9 +83,6 @@ class ModuleStore implements IModuleStore {
moduleDetails: observable.ref,
issues: observable.ref,
// computed
getIssues: computed,
// actions
setModuleId: action,
@ -104,9 +94,6 @@ class ModuleStore implements IModuleStore {
deleteModule: action,
addModuleToFavorites: action,
removeModuleFromFavorites: action,
fetchModuleIssues: action,
updateIssueStructure: action,
});
this.rootStore = _rootStore;
@ -120,16 +107,6 @@ class ModuleStore implements IModuleStore {
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
setModuleId = (moduleSlug: string) => {
this.moduleId = moduleSlug ?? null;
@ -332,88 +309,6 @@ class ModuleStore implements IModuleStore {
});
}
};
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;
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;