forked from github/plane
dev: add remaining layouts to cycle (#2413)
This commit is contained in:
parent
265e60a536
commit
fcfdd74d4f
@ -2,4 +2,3 @@ export * from "./date-filter-modal";
|
|||||||
export * from "./date-filter-select";
|
export * from "./date-filter-select";
|
||||||
export * from "./filters-list";
|
export * from "./filters-list";
|
||||||
export * from "./workspace-filters-list";
|
export * from "./workspace-filters-list";
|
||||||
export * from "./issues-view-filter";
|
|
||||||
|
111
web/components/headers/cycle-issues.tsx
Normal file
111
web/components/headers/cycle-issues.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./cycle-issues";
|
||||||
export * from "./global-issues";
|
export * from "./global-issues";
|
||||||
export * from "./module-issues";
|
export * from "./module-issues";
|
||||||
export * from "./project-issues";
|
export * from "./project-issues";
|
||||||
|
39
web/components/issues/issue-layouts/calendar/cycle-root.tsx
Normal file
39
web/components/issues/issue-layouts/calendar/cycle-root.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
});
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./dropdowns";
|
export * from "./dropdowns";
|
||||||
export * from "./calendar";
|
export * from "./calendar";
|
||||||
|
export * from "./cycle-root";
|
||||||
export * from "./types.d";
|
export * from "./types.d";
|
||||||
export * from "./day-tile";
|
export * from "./day-tile";
|
||||||
export * from "./header";
|
export * from "./header";
|
||||||
|
@ -9,7 +9,7 @@ import { CalendarChart } from "components/issues";
|
|||||||
import { IIssueGroupedStructure } from "store/issue";
|
import { IIssueGroupedStructure } from "store/issue";
|
||||||
|
|
||||||
export const ModuleCalendarLayout: React.FC = observer(() => {
|
export const ModuleCalendarLayout: React.FC = observer(() => {
|
||||||
const { module: moduleStore, issueFilter: issueFilterStore } = useMobxStore();
|
const { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||||
|
|
||||||
// TODO: add drag and drop functionality
|
// TODO: add drag and drop functionality
|
||||||
const onDragEnd = (result: DropResult) => {
|
const onDragEnd = (result: DropResult) => {
|
||||||
@ -24,7 +24,7 @@ export const ModuleCalendarLayout: React.FC = observer(() => {
|
|||||||
// issueKanBanViewStore?.handleDragDrop(result.source, result.destination);
|
// issueKanBanViewStore?.handleDragDrop(result.source, result.destination);
|
||||||
};
|
};
|
||||||
|
|
||||||
const issues = moduleStore.getIssues;
|
const issues = moduleIssueStore.getIssues;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
<div className="h-full w-full pt-4 bg-custom-background-100 overflow-hidden">
|
||||||
|
@ -6,7 +6,13 @@ import useSWR from "swr";
|
|||||||
// mobx react lite
|
// mobx react lite
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { CycleKanBanLayout, CycleListLayout } from "components/issues";
|
import {
|
||||||
|
CycleCalendarLayout,
|
||||||
|
CycleGanttLayout,
|
||||||
|
CycleKanBanLayout,
|
||||||
|
CycleListLayout,
|
||||||
|
CycleSpreadsheetLayout,
|
||||||
|
} from "components/issues";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
@ -46,7 +52,17 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -25,6 +25,17 @@ export const GlobalViewsAppliedFiltersRoot = observer(() => {
|
|||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const viewDetails = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()] : undefined;
|
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) => {
|
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
|
||||||
if (!globalViewId) return;
|
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
|
// update stored filters when view details are fetched
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!globalViewId || !viewDetails) return;
|
if (!globalViewId || !viewDetails) return;
|
||||||
|
|
||||||
if (!globalViewFiltersStore.storedFilters[globalViewId.toString()])
|
if (!globalViewFiltersStore.storedFilters[globalViewId.toString()])
|
||||||
globalViewFiltersStore.updateStoredFilters(globalViewId.toString(), viewDetails?.query_data?.filters ?? {});
|
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 (
|
return (
|
||||||
<div className="flex items-start justify-between gap-4 p-4">
|
<div className="flex items-start justify-between gap-4 p-4">
|
||||||
|
@ -24,11 +24,11 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
|
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
|
// filters whose value not null or empty array
|
||||||
const appliedFilters: IIssueFilterOptions = {};
|
const appliedFilters: IIssueFilterOptions = {};
|
||||||
Object.entries(storedFilters).forEach(([key, value]) => {
|
Object.entries(storedFilters ?? {}).forEach(([key, value]) => {
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
|
|
||||||
if (Array.isArray(value) && value.length === 0) return;
|
if (Array.isArray(value) && value.length === 0) return;
|
||||||
@ -60,7 +60,7 @@ export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
|||||||
if (!workspaceSlug || !projectId || !viewId) return;
|
if (!workspaceSlug || !projectId || !viewId) return;
|
||||||
|
|
||||||
const newFilters: IIssueFilterOptions = {};
|
const newFilters: IIssueFilterOptions = {};
|
||||||
Object.keys(storedFilters).forEach((key) => {
|
Object.keys(storedFilters ?? {}).forEach((key) => {
|
||||||
newFilters[key as keyof IIssueFilterOptions] = null;
|
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
55
web/components/issues/issue-layouts/gantt/cycle-root.tsx
Normal file
55
web/components/issues/issue-layouts/gantt/cycle-root.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -1,4 +1,5 @@
|
|||||||
export * from "./blocks";
|
export * from "./blocks";
|
||||||
|
export * from "./cycle-root";
|
||||||
export * from "./module-root";
|
export * from "./module-root";
|
||||||
export * from "./project-view-root";
|
export * from "./project-view-root";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
@ -17,11 +17,11 @@ export const ModuleGanttLayout: React.FC = observer(() => {
|
|||||||
|
|
||||||
const { projectDetails } = useProjectDetails();
|
const { projectDetails } = useProjectDetails();
|
||||||
|
|
||||||
const { module: moduleStore, issueFilter: issueFilterStore } = useMobxStore();
|
const { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||||
|
|
||||||
const appliedDisplayFilters = issueFilterStore.userDisplayFilters;
|
const appliedDisplayFilters = issueFilterStore.userDisplayFilters;
|
||||||
|
|
||||||
const issues = moduleStore.getIssues;
|
const issues = moduleIssueStore.getIssues;
|
||||||
|
|
||||||
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15;
|
||||||
|
|
||||||
|
146
web/components/issues/issue-layouts/spreadsheet/cycle-root.tsx
Normal file
146
web/components/issues/issue-layouts/spreadsheet/cycle-root.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./cycle-root";
|
||||||
export * from "./module-root";
|
export * from "./module-root";
|
||||||
export * from "./project-view-root";
|
export * from "./project-view-root";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
@ -30,9 +30,9 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { projectDetails } = useProjectDetails();
|
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 issueDisplayProperties = issueFilterStore.userDisplayProperties;
|
||||||
|
|
||||||
const handleDisplayFiltersUpdate = useCallback(
|
const handleDisplayFiltersUpdate = useCallback(
|
||||||
|
@ -11,7 +11,7 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
|
|||||||
// contexts
|
// contexts
|
||||||
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
import { IssueViewContextProvider } from "contexts/issue-view.context";
|
||||||
// components
|
// components
|
||||||
import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles";
|
import { CycleDetailsSidebar, TransferIssues, TransferIssuesModal } from "components/cycles";
|
||||||
import { CycleLayoutRoot } from "components/issues/issue-layouts";
|
import { CycleLayoutRoot } from "components/issues/issue-layouts";
|
||||||
// services
|
// services
|
||||||
@ -35,6 +35,7 @@ import { getDateRangeStatus } from "helpers/date-time.helper";
|
|||||||
import { ISearchIssueResponse } from "types";
|
import { ISearchIssueResponse } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { CYCLES_LIST, CYCLE_DETAILS } from "constants/fetch-keys";
|
import { CYCLES_LIST, CYCLE_DETAILS } from "constants/fetch-keys";
|
||||||
|
import { CycleIssuesHeader } from "components/headers";
|
||||||
|
|
||||||
const SingleCycle: React.FC = () => {
|
const SingleCycle: React.FC = () => {
|
||||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||||
@ -68,10 +69,6 @@ const SingleCycle: React.FC = () => {
|
|||||||
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
||||||
: "draft";
|
: "draft";
|
||||||
|
|
||||||
const openIssuesListModal = () => {
|
|
||||||
setCycleIssuesListModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
|
const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => {
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
@ -132,7 +129,7 @@ const SingleCycle: React.FC = () => {
|
|||||||
}
|
}
|
||||||
right={
|
right={
|
||||||
<div className={`flex flex-shrink-0 items-center gap-2 duration-300`}>
|
<div className={`flex flex-shrink-0 items-center gap-2 duration-300`}>
|
||||||
<IssuesFilterView />
|
<CycleIssuesHeader />
|
||||||
<Button variant="neutral-primary" onClick={() => setAnalyticsModal(true)}>
|
<Button variant="neutral-primary" onClick={() => setAnalyticsModal(true)}>
|
||||||
Analytics
|
Analytics
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
import { action, observable, makeObservable, runInAction } from "mobx";
|
||||||
// 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";
|
||||||
@ -37,13 +37,6 @@ export interface IModuleStore {
|
|||||||
deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => void;
|
deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => void;
|
||||||
addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => void;
|
addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => void;
|
||||||
removeModuleFromFavorites: (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 {
|
||||||
@ -90,9 +83,6 @@ class ModuleStore implements IModuleStore {
|
|||||||
moduleDetails: observable.ref,
|
moduleDetails: observable.ref,
|
||||||
issues: observable.ref,
|
issues: observable.ref,
|
||||||
|
|
||||||
// computed
|
|
||||||
getIssues: computed,
|
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
setModuleId: action,
|
setModuleId: action,
|
||||||
|
|
||||||
@ -104,9 +94,6 @@ class ModuleStore implements IModuleStore {
|
|||||||
deleteModule: action,
|
deleteModule: action,
|
||||||
addModuleToFavorites: action,
|
addModuleToFavorites: action,
|
||||||
removeModuleFromFavorites: action,
|
removeModuleFromFavorites: action,
|
||||||
|
|
||||||
fetchModuleIssues: action,
|
|
||||||
updateIssueStructure: action,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
this.rootStore = _rootStore;
|
||||||
@ -120,16 +107,6 @@ 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;
|
||||||
@ -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;
|
export default ModuleStore;
|
||||||
|
Loading…
Reference in New Issue
Block a user