forked from github/plane
dev: implemented project views using MobX (#2410)
* dev: implemented project views list using mobx * style: views list UI * dev: implemented view issues page using mobx * refactor: project view issues fetching
This commit is contained in:
parent
9f61d8bc06
commit
00b40fbde4
@ -12,7 +12,7 @@ import { CreateUpdateCycleModal } from "components/cycles";
|
|||||||
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import { CreateUpdateModuleModal } from "components/modules";
|
import { CreateUpdateModuleModal } from "components/modules";
|
||||||
import { CreateProjectModal } from "components/project";
|
import { CreateProjectModal } from "components/project";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
import { CreateUpdateProjectViewModal } from "components/views";
|
||||||
import { CreateUpdatePageModal } from "components/pages";
|
import { CreateUpdatePageModal } from "components/pages";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
@ -151,10 +151,9 @@ export const CommandPalette: React.FC = observer(() => {
|
|||||||
setIsOpen={setIsCreateModuleModalOpen}
|
setIsOpen={setIsCreateModuleModalOpen}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
<CreateUpdateViewModal
|
<CreateUpdateProjectViewModal
|
||||||
handleClose={() => setIsCreateViewModalOpen(false)}
|
|
||||||
isOpen={isCreateViewModalOpen}
|
isOpen={isCreateViewModalOpen}
|
||||||
user={user}
|
onClose={() => setIsCreateViewModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
<CreateUpdatePageModal
|
<CreateUpdatePageModal
|
||||||
isOpen={isCreateUpdatePageModalOpen}
|
isOpen={isCreateUpdatePageModalOpen}
|
||||||
|
@ -47,7 +47,9 @@ export const AllViews: React.FC = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full flex flex-col overflow-auto">
|
<div className="relative w-full h-full flex flex-col overflow-auto">
|
||||||
<AppliedFiltersRoot />
|
<div className="p-4">
|
||||||
|
<AppliedFiltersRoot />
|
||||||
|
</div>
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
{activeLayout === "list" ? (
|
{activeLayout === "list" ? (
|
||||||
<ListLayout />
|
<ListLayout />
|
||||||
|
@ -38,29 +38,26 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
|||||||
const { workspaceSlug, globalViewId } = router.query;
|
const { workspaceSlug, globalViewId } = router.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
globalViews: globalViewsStore,
|
|
||||||
globalViewFilters: globalViewFiltersStore,
|
globalViewFilters: globalViewFiltersStore,
|
||||||
workspaceFilter: workspaceFilterStore,
|
workspaceFilter: workspaceFilterStore,
|
||||||
workspace: workspaceStore,
|
workspace: workspaceStore,
|
||||||
project: projectStore,
|
project: projectStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
const queryData = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()]?.query_data : undefined;
|
|
||||||
|
|
||||||
const storedFilters = globalViewId ? globalViewFiltersStore.storedFilters[globalViewId.toString()] : undefined;
|
const storedFilters = globalViewId ? globalViewFiltersStore.storedFilters[globalViewId.toString()] : undefined;
|
||||||
|
|
||||||
const handleFiltersUpdate = useCallback(
|
const handleFiltersUpdate = useCallback(
|
||||||
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
(key: keyof IIssueFilterOptions, value: string | string[]) => {
|
||||||
if (!workspaceSlug || !globalViewId) return;
|
if (!workspaceSlug || !globalViewId) return;
|
||||||
|
|
||||||
const newValues = queryData?.filters?.[key] ?? [];
|
const newValues = storedFilters?.[key] ?? [];
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
value.forEach((val) => {
|
value.forEach((val) => {
|
||||||
if (!newValues.includes(val)) newValues.push(val);
|
if (!newValues.includes(val)) newValues.push(val);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (queryData?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
if (storedFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
else newValues.push(value);
|
else newValues.push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +65,7 @@ export const GlobalIssuesHeader: React.FC<Props> = observer((props) => {
|
|||||||
[key]: newValues,
|
[key]: newValues,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[globalViewId, globalViewFiltersStore, queryData, workspaceSlug]
|
[globalViewId, globalViewFiltersStore, storedFilters, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDisplayFiltersUpdate = useCallback(
|
const handleDisplayFiltersUpdate = useCallback(
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
export * from "./global-issues";
|
export * from "./global-issues";
|
||||||
export * from "./module-issues";
|
export * from "./module-issues";
|
||||||
export * from "./project-issues";
|
export * from "./project-issues";
|
||||||
|
export * from "./project-view-issues";
|
||||||
|
export * from "./project-views";
|
||||||
|
113
web/components/headers/project-view-issues.tsx
Normal file
113
web/components/headers/project-view-issues.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
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 ProjectViewIssuesHeader: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
issueFilter: issueFilterStore,
|
||||||
|
projectViewFilters: projectViewFiltersStore,
|
||||||
|
project: projectStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] : undefined;
|
||||||
|
|
||||||
|
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 || !viewId) return;
|
||||||
|
|
||||||
|
const newValues = storedFilters?.[key] ?? [];
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach((val) => {
|
||||||
|
if (!newValues.includes(val)) newValues.push(val);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (storedFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
|
else newValues.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
projectViewFiltersStore.updateStoredFilters(viewId.toString(), {
|
||||||
|
[key]: newValues,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[projectViewFiltersStore, storedFilters, viewId, 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={storedFilters ?? {}}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
});
|
31
web/components/headers/project-views.tsx
Normal file
31
web/components/headers/project-views.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { CreateUpdateProjectViewModal } from "components/views";
|
||||||
|
// ui
|
||||||
|
import { PrimaryButton } from "components/ui";
|
||||||
|
// icons
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export const ProjectViewsHeader = () => {
|
||||||
|
const [createViewModal, setCreateViewModal] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CreateUpdateProjectViewModal isOpen={createViewModal} onClose={() => setCreateViewModal(false)} />
|
||||||
|
<div>
|
||||||
|
<PrimaryButton
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", { key: "v" });
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon size={14} strokeWidth={2} />
|
||||||
|
Create View
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -5,6 +5,7 @@ export * from "./day-tile";
|
|||||||
export * from "./header";
|
export * from "./header";
|
||||||
export * from "./issue-blocks";
|
export * from "./issue-blocks";
|
||||||
export * from "./module-root";
|
export * from "./module-root";
|
||||||
|
export * from "./project-view-root";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
export * from "./week-days";
|
export * from "./week-days";
|
||||||
export * from "./week-header";
|
export * from "./week-header";
|
||||||
|
@ -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 ProjectViewCalendarLayout: React.FC = observer(() => {
|
||||||
|
const { projectViewIssues: projectViewIssuesStore, 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 = projectViewIssuesStore.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>
|
||||||
|
);
|
||||||
|
});
|
@ -6,8 +6,7 @@ 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 { CycleListLayout } from "./list/cycle-root";
|
import { CycleKanBanLayout, CycleListLayout } from "components/issues";
|
||||||
import { CycleKanBanLayout } from "./kanban/cycle-root";
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ export * from "./label";
|
|||||||
export * from "./members";
|
export * from "./members";
|
||||||
export * from "./module-root";
|
export * from "./module-root";
|
||||||
export * from "./priority";
|
export * from "./priority";
|
||||||
|
export * from "./project-view-root";
|
||||||
export * from "./project";
|
export * from "./project";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
export * from "./state";
|
export * from "./state";
|
||||||
|
@ -0,0 +1,111 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
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";
|
||||||
|
// ui
|
||||||
|
import { PrimaryButton } from "components/ui";
|
||||||
|
// helpers
|
||||||
|
import { areFiltersDifferent } from "helpers/filter.helper";
|
||||||
|
// types
|
||||||
|
import { IIssueFilterOptions } from "types";
|
||||||
|
|
||||||
|
export const ProjectViewAppliedFiltersRoot: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
project: projectStore,
|
||||||
|
projectViews: projectViewsStore,
|
||||||
|
projectViewFilters: projectViewFiltersStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
const viewDetails = viewId ? projectViewsStore.viewDetails[viewId.toString()] : undefined;
|
||||||
|
const storedFilters = viewId ? projectViewFiltersStore.storedFilters[viewId.toString()] ?? {} : {};
|
||||||
|
|
||||||
|
// 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 (!viewId) return;
|
||||||
|
|
||||||
|
// remove all values of the key if value is null
|
||||||
|
if (!value) {
|
||||||
|
projectViewFiltersStore.updateStoredFilters(viewId.toString(), {
|
||||||
|
[key]: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the passed value from the key
|
||||||
|
let newValues = storedFilters?.[key] ?? [];
|
||||||
|
newValues = newValues.filter((val) => val !== value);
|
||||||
|
|
||||||
|
projectViewFiltersStore.updateStoredFilters(viewId.toString(), {
|
||||||
|
[key]: newValues,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAllFilters = () => {
|
||||||
|
if (!workspaceSlug || !projectId || !viewId) return;
|
||||||
|
|
||||||
|
const newFilters: IIssueFilterOptions = {};
|
||||||
|
Object.keys(storedFilters).forEach((key) => {
|
||||||
|
newFilters[key as keyof IIssueFilterOptions] = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
projectViewFiltersStore.updateStoredFilters(viewId.toString(), {
|
||||||
|
...newFilters,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateView = () => {
|
||||||
|
if (!workspaceSlug || !projectId || !viewId || !viewDetails) return;
|
||||||
|
|
||||||
|
projectViewsStore.updateView(workspaceSlug.toString(), projectId.toString(), viewId.toString(), {
|
||||||
|
query_data: {
|
||||||
|
...viewDetails.query_data,
|
||||||
|
...(storedFilters ?? {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// update stored filters when view details are fetched
|
||||||
|
useEffect(() => {
|
||||||
|
if (!viewId || !viewDetails) return;
|
||||||
|
|
||||||
|
if (!projectViewFiltersStore.storedFilters[viewId.toString()])
|
||||||
|
projectViewFiltersStore.updateStoredFilters(viewId.toString(), viewDetails?.query_data ?? {});
|
||||||
|
}, [projectViewFiltersStore, viewDetails, viewId]);
|
||||||
|
|
||||||
|
// return if no filters are applied
|
||||||
|
if (Object.keys(appliedFilters).length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-4 p-4">
|
||||||
|
<AppliedFiltersList
|
||||||
|
appliedFilters={appliedFilters}
|
||||||
|
handleClearAllFilters={handleClearAllFilters}
|
||||||
|
handleRemoveFilter={handleRemoveFilter}
|
||||||
|
labels={projectStore.labels?.[projectId?.toString() ?? ""] ?? []}
|
||||||
|
members={projectStore.members?.[projectId?.toString() ?? ""]?.map((m) => m.member)}
|
||||||
|
states={projectStore.states?.[projectId?.toString() ?? ""]}
|
||||||
|
/>
|
||||||
|
{storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data ?? {}) && (
|
||||||
|
<PrimaryButton className="whitespace-nowrap" onClick={handleUpdateView}>
|
||||||
|
Update view
|
||||||
|
</PrimaryButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./blocks";
|
export * from "./blocks";
|
||||||
export * from "./module-root";
|
export * from "./module-root";
|
||||||
|
export * from "./project-view-root";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
@ -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 ProjectViewGanttLayout: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { projectDetails } = useProjectDetails();
|
||||||
|
|
||||||
|
const { projectViewIssues: projectViewIssuesStore, issueFilter: issueFilterStore } = useMobxStore();
|
||||||
|
|
||||||
|
const appliedDisplayFilters = issueFilterStore.userDisplayFilters;
|
||||||
|
|
||||||
|
const issues = projectViewIssuesStore.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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -7,10 +7,15 @@ export * from "./calendar";
|
|||||||
export * from "./gantt";
|
export * from "./gantt";
|
||||||
export * from "./kanban";
|
export * from "./kanban";
|
||||||
export * from "./spreadsheet";
|
export * from "./spreadsheet";
|
||||||
export * from "./global-views-all-layouts";
|
|
||||||
|
// global view layout
|
||||||
|
export * from "./global-view-all-layouts";
|
||||||
|
|
||||||
// cycle root layout
|
// cycle root layout
|
||||||
export * from "./cycle-layout-root";
|
export * from "./cycle-layout-root";
|
||||||
|
|
||||||
// module root layout
|
// module root layout
|
||||||
export * from "./module-all-layouts";
|
export * from "./module-all-layouts";
|
||||||
|
|
||||||
|
// project view layout
|
||||||
|
export * from "./project-view-all-layouts";
|
||||||
|
@ -1 +1,3 @@
|
|||||||
|
export * from "./cycle-root";
|
||||||
|
export * from "./module-root";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
@ -1 +1,3 @@
|
|||||||
|
export * from "./cycle-root";
|
||||||
|
export * from "./module-root";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
@ -10,10 +10,10 @@ import {
|
|||||||
ModuleAppliedFiltersRoot,
|
ModuleAppliedFiltersRoot,
|
||||||
ModuleCalendarLayout,
|
ModuleCalendarLayout,
|
||||||
ModuleGanttLayout,
|
ModuleGanttLayout,
|
||||||
|
ModuleKanBanLayout,
|
||||||
|
ModuleListLayout,
|
||||||
ModuleSpreadsheetLayout,
|
ModuleSpreadsheetLayout,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
import { ModuleListLayout } from "components/issues/issue-layouts/list/module-root";
|
|
||||||
import { ModuleKanBanLayout } from "components/issues/issue-layouts/kanban/module-root";
|
|
||||||
|
|
||||||
export const ModuleAllLayouts: React.FC = observer(() => {
|
export const ModuleAllLayouts: React.FC = observer(() => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -30,11 +30,11 @@ export const ModuleAllLayouts: React.FC = observer(() => {
|
|||||||
moduleFilter: moduleIssueFilterStore,
|
moduleFilter: moduleIssueFilterStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
|
|
||||||
useSWR(workspaceSlug && projectId && moduleId ? `CYCLE_ISSUES` : null, async () => {
|
useSWR(workspaceSlug && projectId && moduleId ? `MODULE_INFORMATION_${moduleId.toString()}` : null, async () => {
|
||||||
if (workspaceSlug && projectId && moduleId) {
|
if (workspaceSlug && projectId && moduleId) {
|
||||||
// fetching the project display filters and display properties
|
// fetching the project display filters and display properties
|
||||||
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId);
|
await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId);
|
||||||
// fetching the cycle filters
|
// fetching the module filters
|
||||||
await moduleIssueFilterStore.fetchModuleFilters(workspaceSlug, projectId, moduleId);
|
await moduleIssueFilterStore.fetchModuleFilters(workspaceSlug, projectId, moduleId);
|
||||||
|
|
||||||
// fetching the project state, labels and members
|
// fetching the project state, labels and members
|
||||||
@ -42,7 +42,7 @@ export const ModuleAllLayouts: React.FC = observer(() => {
|
|||||||
await projectStore.fetchProjectLabels(workspaceSlug, projectId);
|
await projectStore.fetchProjectLabels(workspaceSlug, projectId);
|
||||||
await projectStore.fetchProjectMembers(workspaceSlug, projectId);
|
await projectStore.fetchProjectMembers(workspaceSlug, projectId);
|
||||||
|
|
||||||
// fetching the cycle issues
|
// fetching the module issues
|
||||||
await moduleIssueStore.fetchIssues(workspaceSlug, projectId, moduleId);
|
await moduleIssueStore.fetchIssues(workspaceSlug, projectId, moduleId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -51,7 +51,9 @@ export const ModuleAllLayouts: React.FC = observer(() => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full h-full flex flex-col overflow-auto">
|
<div className="relative w-full h-full flex flex-col overflow-auto">
|
||||||
<ModuleAppliedFiltersRoot />
|
<div className="p-4">
|
||||||
|
<ModuleAppliedFiltersRoot />
|
||||||
|
</div>
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
{activeLayout === "list" ? (
|
{activeLayout === "list" ? (
|
||||||
<ModuleListLayout />
|
<ModuleListLayout />
|
||||||
|
@ -0,0 +1,72 @@
|
|||||||
|
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 {
|
||||||
|
ModuleKanBanLayout,
|
||||||
|
ModuleListLayout,
|
||||||
|
ProjectViewAppliedFiltersRoot,
|
||||||
|
ProjectViewCalendarLayout,
|
||||||
|
ProjectViewGanttLayout,
|
||||||
|
ProjectViewSpreadsheetLayout,
|
||||||
|
} from "components/issues";
|
||||||
|
|
||||||
|
export const ProjectViewAllLayouts: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
|
|
||||||
|
const {
|
||||||
|
project: projectStore,
|
||||||
|
issueFilter: issueFilterStore,
|
||||||
|
projectViews: projectViewsStore,
|
||||||
|
projectViewIssues: projectViewIssuesStore,
|
||||||
|
projectViewFilters: projectViewFiltersStore,
|
||||||
|
} = useMobxStore();
|
||||||
|
|
||||||
|
useSWR(workspaceSlug && projectId && viewId ? `PROJECT_VIEW_INFORMATION_${viewId.toString()}` : null, async () => {
|
||||||
|
if (workspaceSlug && projectId && viewId) {
|
||||||
|
// fetching the project display filters and display properties
|
||||||
|
await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString());
|
||||||
|
|
||||||
|
// fetching the project state, labels and members
|
||||||
|
await projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString());
|
||||||
|
await projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString());
|
||||||
|
await projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString());
|
||||||
|
|
||||||
|
// fetching the view details
|
||||||
|
await projectViewsStore.fetchViewDetails(workspaceSlug.toString(), projectId.toString(), viewId.toString());
|
||||||
|
// fetching the view issues
|
||||||
|
await projectViewIssuesStore.fetchViewIssues(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
viewId.toString(),
|
||||||
|
projectViewFiltersStore.storedFilters[viewId.toString()] ?? {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeLayout = issueFilterStore.userDisplayFilters.layout;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative h-full w-full flex flex-col overflow-auto">
|
||||||
|
<ProjectViewAppliedFiltersRoot />
|
||||||
|
<div className="h-full w-full">
|
||||||
|
{activeLayout === "list" ? (
|
||||||
|
<ModuleListLayout />
|
||||||
|
) : activeLayout === "kanban" ? (
|
||||||
|
<ModuleKanBanLayout />
|
||||||
|
) : activeLayout === "calendar" ? (
|
||||||
|
<ProjectViewCalendarLayout />
|
||||||
|
) : activeLayout === "gantt_chart" ? (
|
||||||
|
<ProjectViewGanttLayout />
|
||||||
|
) : activeLayout === "spreadsheet" ? (
|
||||||
|
<ProjectViewSpreadsheetLayout />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -1,2 +1,3 @@
|
|||||||
export * from "./module-root";
|
export * from "./module-root";
|
||||||
|
export * from "./project-view-root";
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
@ -24,7 +24,7 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { projectDetails } = useProjectDetails();
|
const { projectDetails } = useProjectDetails();
|
||||||
@ -47,8 +47,6 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
[issueFilterStore, projectId, workspaceSlug]
|
[issueFilterStore, projectId, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
|
||||||
|
|
||||||
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
const columnData = SPREADSHEET_COLUMN.map((column) => ({
|
||||||
...column,
|
...column,
|
||||||
isActive: issueDisplayProperties
|
isActive: issueDisplayProperties
|
||||||
@ -109,45 +107,32 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => {
|
|||||||
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"
|
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 }}
|
style={{ gridTemplateColumns }}
|
||||||
>
|
>
|
||||||
{type === "issue" ? (
|
{isAllowed && (
|
||||||
<button
|
<CustomMenu
|
||||||
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"
|
className="sticky left-0 z-[1]"
|
||||||
onClick={() => {
|
customButton={
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
<button
|
||||||
document.dispatchEvent(e);
|
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>
|
|
||||||
) : (
|
|
||||||
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
|
<PlusIcon className="h-4 w-4" />
|
||||||
</CustomMenu.MenuItem>
|
Add Issue
|
||||||
{true && <CustomMenu.MenuItem onClick={() => {}}>Add an existing issue</CustomMenu.MenuItem>}
|
</button>
|
||||||
</CustomMenu>
|
}
|
||||||
)
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,128 @@
|
|||||||
|
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 { 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 ProjectViewSpreadsheetLayout: React.FC = observer(() => {
|
||||||
|
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = 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 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 }}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Spinner />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -1,13 +1,10 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { mutate } from "swr";
|
|
||||||
|
|
||||||
// headless ui
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
|
||||||
import viewsService from "services/views.service";
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// ui
|
// ui
|
||||||
@ -15,41 +12,39 @@ import { DangerButton, SecondaryButton } from "components/ui";
|
|||||||
// icons
|
// icons
|
||||||
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
import { ExclamationTriangleIcon } from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import type { ICurrentUserResponse, IView } from "types";
|
import { IProjectView } from "types";
|
||||||
// fetch-keys
|
|
||||||
import { VIEWS_LIST } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
data: IProjectView;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
onClose: () => void;
|
||||||
data: IView | null;
|
|
||||||
user: ICurrentUserResponse | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user }) => {
|
export const DeleteProjectViewModal: React.FC<Props> = observer((props) => {
|
||||||
|
const { data, isOpen, onClose } = props;
|
||||||
|
|
||||||
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { projectViews: projectViewsStore } = useMobxStore();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsOpen(false);
|
onClose();
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeletion = async () => {
|
const handleDeleteView = async () => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
setIsDeleteLoading(true);
|
setIsDeleteLoading(true);
|
||||||
if (!workspaceSlug || !data || !projectId) return;
|
|
||||||
|
|
||||||
await viewsService
|
await projectViewsStore
|
||||||
.deleteView(workspaceSlug as string, projectId as string, data.id, user)
|
.deleteView(workspaceSlug.toString(), projectId.toString(), data.id)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mutate<IView[]>(VIEWS_LIST(projectId as string), (views) =>
|
|
||||||
views?.filter((view) => view.id !== data.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
handleClose();
|
handleClose();
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
@ -58,13 +53,13 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user
|
|||||||
message: "View deleted successfully.",
|
message: "View deleted successfully.",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() =>
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: "View could not be deleted. Please try again.",
|
message: "View could not be deleted. Please try again.",
|
||||||
});
|
})
|
||||||
})
|
)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
});
|
});
|
||||||
@ -100,26 +95,17 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user
|
|||||||
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
|
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-500/20 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<ExclamationTriangleIcon
|
<ExclamationTriangleIcon className="h-6 w-6 text-red-600" aria-hidden="true" />
|
||||||
className="h-6 w-6 text-red-600"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<Dialog.Title
|
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||||
as="h3"
|
|
||||||
className="text-lg font-medium leading-6 text-custom-text-100"
|
|
||||||
>
|
|
||||||
Delete View
|
Delete View
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-custom-text-200">
|
<p className="text-sm text-custom-text-200">
|
||||||
Are you sure you want to delete view-{" "}
|
Are you sure you want to delete view-{" "}
|
||||||
<span className="break-words font-medium text-custom-text-100">
|
<span className="break-words font-medium text-custom-text-100">{data?.name}</span>? All of the
|
||||||
{data?.name}
|
data related to the view will be permanently removed. This action cannot be undone.
|
||||||
</span>
|
|
||||||
? All of the data related to the view will be permanently removed. This
|
|
||||||
action cannot be undone.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -127,7 +113,7 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
<div className="flex justify-end gap-2 p-4 sm:px-6">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
<DangerButton onClick={handleDeletion} loading={isDeleteLoading}>
|
<DangerButton onClick={handleDeleteView} loading={isDeleteLoading}>
|
||||||
{isDeleteLoading ? "Deleting..." : "Delete"}
|
{isDeleteLoading ? "Deleting..." : "Delete"}
|
||||||
</DangerButton>
|
</DangerButton>
|
||||||
</div>
|
</div>
|
||||||
@ -138,4 +124,4 @@ export const DeleteViewModal: React.FC<Props> = ({ isOpen, data, setIsOpen, user
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,77 +1,48 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// react-hook-form
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
// services
|
|
||||||
import stateService from "services/project_state.service";
|
|
||||||
// hooks
|
|
||||||
import useProjectMembers from "hooks/use-project-members";
|
|
||||||
// components
|
// components
|
||||||
import { FiltersList } from "components/core";
|
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues";
|
||||||
import { SelectFilters } from "components/views";
|
|
||||||
// ui
|
// ui
|
||||||
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
|
||||||
// helpers
|
|
||||||
import { checkIfArraysHaveSameElements } from "helpers/array.helper";
|
|
||||||
import { getStatesList } from "helpers/state.helper";
|
|
||||||
// types
|
// types
|
||||||
import { IQuery, IView } from "types";
|
import { IProjectView } from "types";
|
||||||
import issuesService from "services/issue.service";
|
// constants
|
||||||
// fetch-keys
|
import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||||
import { PROJECT_ISSUE_LABELS, STATES_LIST } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handleFormSubmit: (values: IView) => Promise<void>;
|
data?: IProjectView | null;
|
||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
status: boolean;
|
handleFormSubmit: (values: IProjectView) => Promise<void>;
|
||||||
data?: IView | null;
|
preLoadedData?: Partial<IProjectView> | null;
|
||||||
preLoadedData?: Partial<IView> | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues: Partial<IView> = {
|
const defaultValues: Partial<IProjectView> = {
|
||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, status, data, preLoadedData }) => {
|
export const ProjectViewForm: React.FC<Props> = observer(({ handleFormSubmit, handleClose, data, preLoadedData }) => {
|
||||||
const router = useRouter();
|
const { project: projectStore } = useMobxStore();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
control,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
register,
|
||||||
reset,
|
reset,
|
||||||
watch,
|
|
||||||
setValue,
|
setValue,
|
||||||
} = useForm<IView>({
|
watch,
|
||||||
|
} = useForm<IProjectView>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
const filters = watch("query");
|
|
||||||
|
|
||||||
const { data: stateGroups } = useSWR(
|
const selectedFilters = watch("query_data");
|
||||||
workspaceSlug && projectId && (filters?.state ?? []).length > 0 ? STATES_LIST(projectId as string) : null,
|
|
||||||
workspaceSlug && (filters?.state ?? []).length > 0
|
|
||||||
? () => stateService.getStates(workspaceSlug as string, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
const states = getStatesList(stateGroups);
|
|
||||||
|
|
||||||
const { data: labels } = useSWR(
|
const handleCreateUpdateView = async (formData: IProjectView) => {
|
||||||
workspaceSlug && projectId && (filters?.labels ?? []).length > 0
|
|
||||||
? PROJECT_ISSUE_LABELS(projectId.toString())
|
|
||||||
: null,
|
|
||||||
workspaceSlug && projectId && (filters?.labels ?? []).length > 0
|
|
||||||
? () => issuesService.getIssueLabels(workspaceSlug.toString(), projectId.toString())
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
const { members } = useProjectMembers(workspaceSlug?.toString(), projectId?.toString());
|
|
||||||
|
|
||||||
const handleCreateUpdateView = async (formData: IView) => {
|
|
||||||
await handleFormSubmit(formData);
|
await handleFormSubmit(formData);
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
@ -80,16 +51,9 @@ export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, statu
|
|||||||
};
|
};
|
||||||
|
|
||||||
const clearAllFilters = () => {
|
const clearAllFilters = () => {
|
||||||
setValue("query", {
|
if (!selectedFilters) return;
|
||||||
assignees: null,
|
|
||||||
created_by: null,
|
setValue("query_data", {});
|
||||||
labels: null,
|
|
||||||
priority: null,
|
|
||||||
state: null,
|
|
||||||
start_date: null,
|
|
||||||
target_date: null,
|
|
||||||
type: null,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -100,16 +64,10 @@ export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, statu
|
|||||||
});
|
});
|
||||||
}, [data, preLoadedData, reset]);
|
}, [data, preLoadedData, reset]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (status && data) {
|
|
||||||
setValue("query", data.query_data);
|
|
||||||
}
|
|
||||||
}, [data, status, setValue]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
|
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{status ? "Update" : "Create"} View</h3>
|
<h3 className="text-lg font-medium leading-6 text-custom-text-100">{data ? "Update" : "Create"} View</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
@ -141,55 +99,57 @@ export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, statu
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<SelectFilters
|
<Controller
|
||||||
filters={filters}
|
control={control}
|
||||||
onSelect={(option) => {
|
name="query_data"
|
||||||
const key = option.key as keyof typeof filters;
|
render={({ field: { onChange, value: filters } }) => (
|
||||||
|
<FiltersDropdown title="Filters">
|
||||||
|
<FilterSelection
|
||||||
|
filters={filters ?? {}}
|
||||||
|
handleFiltersUpdate={(key, value) => {
|
||||||
|
const newValues = filters?.[key] ?? [];
|
||||||
|
|
||||||
if (key === "start_date" || key === "target_date") {
|
if (Array.isArray(value)) {
|
||||||
const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value);
|
value.forEach((val) => {
|
||||||
|
if (!newValues.includes(val)) newValues.push(val);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
|
||||||
|
else newValues.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
setValue("query", {
|
onChange({
|
||||||
...filters,
|
...filters,
|
||||||
[key]: valueExists ? null : option.value,
|
[key]: newValues,
|
||||||
} as IQuery);
|
});
|
||||||
} else {
|
}}
|
||||||
if (!filters?.[key]?.includes(option.value))
|
layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues.list}
|
||||||
setValue("query", {
|
labels={projectStore.projectLabels ?? undefined}
|
||||||
...filters,
|
members={projectStore.projectMembers?.map((m) => m.member) ?? undefined}
|
||||||
[key]: [...((filters?.[key] as any[]) ?? []), option.value],
|
states={projectStore.projectStatesByGroups ?? undefined}
|
||||||
});
|
/>
|
||||||
else {
|
</FiltersDropdown>
|
||||||
setValue("query", {
|
)}
|
||||||
...filters,
|
|
||||||
[key]: (filters?.[key] as any[])?.filter((item) => item !== option.value),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<FiltersList
|
|
||||||
filters={filters}
|
|
||||||
labels={labels}
|
|
||||||
members={members?.map((m) => m.member)}
|
|
||||||
states={states}
|
|
||||||
clearAllFilters={clearAllFilters}
|
|
||||||
setFilters={(query: any) => {
|
|
||||||
setValue("query", {
|
|
||||||
...filters,
|
|
||||||
...query,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{selectedFilters && Object.keys(selectedFilters).length > 0 && (
|
||||||
|
<div>
|
||||||
|
<AppliedFiltersList
|
||||||
|
appliedFilters={selectedFilters}
|
||||||
|
handleClearAllFilters={clearAllFilters}
|
||||||
|
handleRemoveFilter={() => {}}
|
||||||
|
labels={projectStore.projectLabels ?? undefined}
|
||||||
|
members={projectStore.projectMembers?.map((m) => m.member) ?? undefined}
|
||||||
|
states={projectStore.projectStatesByGroups ?? undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end gap-2">
|
<div className="mt-5 flex justify-end gap-2">
|
||||||
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>
|
||||||
<PrimaryButton type="submit" loading={isSubmitting}>
|
<PrimaryButton type="submit" loading={isSubmitting}>
|
||||||
{status
|
{data
|
||||||
? isSubmitting
|
? isSubmitting
|
||||||
? "Updating View..."
|
? "Updating View..."
|
||||||
: "Update View"
|
: "Update View"
|
||||||
@ -200,4 +160,4 @@ export const ViewForm: React.FC<Props> = ({ handleFormSubmit, handleClose, statu
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -3,6 +3,7 @@ export * from "./form";
|
|||||||
export * from "./gantt-chart";
|
export * from "./gantt-chart";
|
||||||
export * from "./modal";
|
export * from "./modal";
|
||||||
export * from "./select-filters";
|
export * from "./select-filters";
|
||||||
export * from "./single-view-item";
|
export * from "./view-list-item";
|
||||||
export * from "./signin";
|
export * from "./signin";
|
||||||
|
export * from "./views-list";
|
||||||
export * from "./workspace-dashboard";
|
export * from "./workspace-dashboard";
|
||||||
|
@ -1,110 +1,77 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import { mutate } from "swr";
|
|
||||||
|
|
||||||
// headless ui
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// services
|
|
||||||
import viewsService from "services/views.service";
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { ViewForm } from "components/views";
|
import { ProjectViewForm } from "components/views";
|
||||||
// types
|
// types
|
||||||
import { ICurrentUserResponse, IView } from "types";
|
import { IProjectView } from "types";
|
||||||
// fetch-keys
|
|
||||||
import { VIEWS_LIST } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
data?: IProjectView | null;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
handleClose: () => void;
|
onClose: () => void;
|
||||||
data?: IView | null;
|
preLoadedData?: Partial<IProjectView> | null;
|
||||||
preLoadedData?: Partial<IView> | null;
|
|
||||||
user: ICurrentUserResponse | undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CreateUpdateViewModal: React.FC<Props> = ({
|
export const CreateUpdateProjectViewModal: React.FC<Props> = observer((props) => {
|
||||||
isOpen,
|
const { data, isOpen, onClose, preLoadedData } = props;
|
||||||
handleClose,
|
|
||||||
data,
|
|
||||||
preLoadedData,
|
|
||||||
user,
|
|
||||||
}) => {
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { projectViews: projectViewsStore } = useMobxStore();
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const onClose = () => {
|
const handleClose = () => {
|
||||||
handleClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const createView = async (payload: IView) => {
|
const createView = async (formData: IProjectView) => {
|
||||||
payload = {
|
|
||||||
...payload,
|
|
||||||
query_data: payload.query,
|
|
||||||
};
|
|
||||||
await viewsService
|
|
||||||
.createView(workspaceSlug as string, projectId as string, payload, user)
|
|
||||||
.then(() => {
|
|
||||||
mutate(VIEWS_LIST(projectId as string));
|
|
||||||
handleClose();
|
|
||||||
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Success!",
|
|
||||||
message: "View created successfully.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "View could not be created. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateView = async (payload: IView) => {
|
|
||||||
const payloadData = {
|
|
||||||
...payload,
|
|
||||||
query_data: payload.query,
|
|
||||||
};
|
|
||||||
await viewsService
|
|
||||||
.updateView(workspaceSlug as string, projectId as string, data?.id ?? "", payloadData, user)
|
|
||||||
.then((res) => {
|
|
||||||
mutate<IView[]>(
|
|
||||||
VIEWS_LIST(projectId as string),
|
|
||||||
(prevData) =>
|
|
||||||
prevData?.map((p) => {
|
|
||||||
if (p.id === res.id) return { ...p, ...payloadData };
|
|
||||||
|
|
||||||
return p;
|
|
||||||
}),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
onClose();
|
|
||||||
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Success!",
|
|
||||||
message: "View updated successfully.",
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "View could not be updated. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: IView) => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await projectViewsStore
|
||||||
|
.createView(workspaceSlug.toString(), projectId.toString(), payload)
|
||||||
|
.then(() => handleClose())
|
||||||
|
.catch(() =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Something went wrong. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateView = async (formData: IProjectView) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
};
|
||||||
|
|
||||||
|
await projectViewsStore
|
||||||
|
.updateView(workspaceSlug.toString(), projectId.toString(), data?.id as string, payload)
|
||||||
|
.then(() => handleClose())
|
||||||
|
.catch(() =>
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "Something went wrong. Please try again.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFormSubmit = async (formData: IProjectView) => {
|
||||||
if (!data) await createView(formData);
|
if (!data) await createView(formData);
|
||||||
else await updateView(formData);
|
else await updateView(formData);
|
||||||
};
|
};
|
||||||
@ -136,11 +103,10 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
|||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
<Dialog.Panel className="relative transform rounded-lg border border-custom-border-200 bg-custom-background-100 px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
|
||||||
<ViewForm
|
<ProjectViewForm
|
||||||
handleFormSubmit={handleFormSubmit}
|
|
||||||
handleClose={handleClose}
|
|
||||||
status={data ? true : false}
|
|
||||||
data={data}
|
data={data}
|
||||||
|
handleClose={handleClose}
|
||||||
|
handleFormSubmit={handleFormSubmit}
|
||||||
preLoadedData={preLoadedData}
|
preLoadedData={preLoadedData}
|
||||||
/>
|
/>
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
@ -150,4 +116,4 @@ export const CreateUpdateViewModal: React.FC<Props> = ({
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</Transition.Root>
|
</Transition.Root>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -1,175 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { mutate } from "swr";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
// icons
|
|
||||||
import { TrashIcon, StarIcon, PencilIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { PhotoFilterOutlined } from "@mui/icons-material";
|
|
||||||
//components
|
|
||||||
import { CustomMenu } from "components/ui";
|
|
||||||
// services
|
|
||||||
import viewsService from "services/views.service";
|
|
||||||
// types
|
|
||||||
import { IView } from "types";
|
|
||||||
// fetch keys
|
|
||||||
import { VIEWS_LIST } from "constants/fetch-keys";
|
|
||||||
// hooks
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// helpers
|
|
||||||
import { truncateText } from "helpers/string.helper";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
view: IView;
|
|
||||||
handleEditView: () => void;
|
|
||||||
handleDeleteView: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SingleViewItem: React.FC<Props> = ({ view, handleEditView, handleDeleteView }) => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
const handleAddToFavorites = () => {
|
|
||||||
if (!workspaceSlug || !projectId || !view) return;
|
|
||||||
|
|
||||||
mutate<IView[]>(
|
|
||||||
VIEWS_LIST(projectId as string),
|
|
||||||
(prevData) =>
|
|
||||||
(prevData ?? []).map((v) => ({
|
|
||||||
...v,
|
|
||||||
is_favorite: v.id === view.id ? true : v.is_favorite,
|
|
||||||
})),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
viewsService
|
|
||||||
.addViewToFavorites(workspaceSlug as string, projectId as string, {
|
|
||||||
view: view.id,
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Couldn't add the view to favorites. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveFromFavorites = () => {
|
|
||||||
if (!workspaceSlug || !view) return;
|
|
||||||
|
|
||||||
mutate<IView[]>(
|
|
||||||
VIEWS_LIST(projectId as string),
|
|
||||||
(prevData) =>
|
|
||||||
(prevData ?? []).map((v) => ({
|
|
||||||
...v,
|
|
||||||
is_favorite: v.id === view.id ? false : v.is_favorite,
|
|
||||||
})),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
viewsService
|
|
||||||
.removeViewFromFavorites(workspaceSlug as string, projectId as string, view.id)
|
|
||||||
.catch(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Couldn't remove the view from favorites. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const viewRedirectionUrl = `/${workspaceSlug}/projects/${projectId}/views/${view.id}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="group hover:bg-custom-background-90 border-b border-custom-border-200">
|
|
||||||
<Link href={viewRedirectionUrl}>
|
|
||||||
<a className="flex items-center justify-between relative rounded px-5 py-4 w-full">
|
|
||||||
<div className="flex items-center justify-between w-full">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div
|
|
||||||
className={`flex items-center justify-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100`}
|
|
||||||
>
|
|
||||||
<PhotoFilterOutlined className="!text-base !leading-6" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<p className="truncate text-sm leading-4 font-medium">
|
|
||||||
{truncateText(view.name, 75)}
|
|
||||||
</p>
|
|
||||||
{view?.description && (
|
|
||||||
<p className="text-xs text-custom-text-200">{view.description}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="ml-2 flex flex-shrink-0">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<p className="rounded bg-custom-background-80 py-1 px-2 text-xs text-custom-text-200 opacity-0 group-hover:opacity-100">
|
|
||||||
{Object.keys(view.query_data)
|
|
||||||
.map((key: string) =>
|
|
||||||
view.query_data[key as keyof typeof view.query_data] !== null
|
|
||||||
? (view.query_data[key as keyof typeof view.query_data] as any).length
|
|
||||||
: 0
|
|
||||||
)
|
|
||||||
.reduce((curr, prev) => curr + prev, 0)}{" "}
|
|
||||||
filters
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{view.is_favorite ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleRemoveFromFavorites();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StarIcon className="h-4 w-4 text-orange-400" fill="#f6ad55" />
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleAddToFavorites();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<StarIcon className="h-4 w-4 " color="rgb(var(--color-text-200))" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<CustomMenu width="auto" ellipsis>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={(e: any) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleEditView();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<PencilIcon className="h-3.5 w-3.5" />
|
|
||||||
<span>Edit View</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={(e: any) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
handleDeleteView();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-start gap-2">
|
|
||||||
<TrashIcon className="h-3.5 w-3.5" />
|
|
||||||
<span>Delete View</span>
|
|
||||||
</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
128
web/components/views/view-list-item.tsx
Normal file
128
web/components/views/view-list-item.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { DeleteProjectViewModal } from "components/views";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu } from "components/ui";
|
||||||
|
// icons
|
||||||
|
import { PencilIcon, Sparkles, StarIcon, TrashIcon } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { IProjectView } from "types";
|
||||||
|
// helpers
|
||||||
|
import { truncateText } from "helpers/string.helper";
|
||||||
|
import { calculateTotalFilters } from "helpers/filter.helper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
view: IProjectView;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectViewListItem: React.FC<Props> = observer((props) => {
|
||||||
|
const { view } = props;
|
||||||
|
|
||||||
|
const [deleteViewModal, setDeleteViewModal] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
|
const { projectViews: projectViewsStore } = useMobxStore();
|
||||||
|
|
||||||
|
const handleAddToFavorites = () => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
projectViewsStore.addViewToFavorites(workspaceSlug.toString(), projectId.toString(), view.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveFromFavorites = () => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
projectViewsStore.removeViewFromFavorites(workspaceSlug.toString(), projectId.toString(), view.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalFilters = calculateTotalFilters(view.query_data ?? {});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteProjectViewModal data={view} isOpen={deleteViewModal} onClose={() => setDeleteViewModal(false)} />
|
||||||
|
<div className="group hover:bg-custom-background-90 border-b border-custom-border-200">
|
||||||
|
<Link href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}>
|
||||||
|
<a className="flex items-center justify-between relative rounded px-5 py-4 w-full">
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="grid place-items-center h-10 w-10 rounded bg-custom-background-90 group-hover:bg-custom-background-100">
|
||||||
|
<Sparkles size={14} strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="truncate text-sm leading-4 font-medium">{truncateText(view.name, 75)}</p>
|
||||||
|
{view?.description && <p className="text-xs text-custom-text-200">{view.description}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-2 flex flex-shrink-0">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<p className="rounded bg-custom-background-80 py-1 px-2 text-xs text-custom-text-200 hidden group-hover:block">
|
||||||
|
{totalFilters} {totalFilters === 1 ? "filter" : "filters"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{view.is_favorite ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleRemoveFromFavorites();
|
||||||
|
}}
|
||||||
|
className="grid place-items-center"
|
||||||
|
>
|
||||||
|
<StarIcon className="text-orange-400" fill="#f6ad55" size={14} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleAddToFavorites();
|
||||||
|
}}
|
||||||
|
className="grid place-items-center"
|
||||||
|
>
|
||||||
|
<StarIcon size={14} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<CustomMenu width="auto" ellipsis>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<PencilIcon size={14} strokeWidth={2} />
|
||||||
|
<span>Edit View</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setDeleteViewModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-start gap-2">
|
||||||
|
<TrashIcon size={14} strokeWidth={2} />
|
||||||
|
<span>Delete View</span>
|
||||||
|
</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
78
web/components/views/views-list.tsx
Normal file
78
web/components/views/views-list.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
|
||||||
|
// mobx store
|
||||||
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
|
// components
|
||||||
|
import { ProjectViewListItem } from "components/views";
|
||||||
|
// ui
|
||||||
|
import { EmptyState, Input, Loader } from "components/ui";
|
||||||
|
// assets
|
||||||
|
import emptyView from "public/empty-state/view.svg";
|
||||||
|
// icons
|
||||||
|
import { Plus, Search } from "lucide-react";
|
||||||
|
|
||||||
|
export const ProjectViewsList = observer(() => {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { projectId } = router.query;
|
||||||
|
|
||||||
|
const { projectViews: projectViewsStore } = useMobxStore();
|
||||||
|
|
||||||
|
const viewsList = projectId ? projectViewsStore.viewsList[projectId.toString()] : undefined;
|
||||||
|
|
||||||
|
if (!viewsList)
|
||||||
|
return (
|
||||||
|
<Loader className="space-y-3 p-8">
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
<Loader.Item height="30px" />
|
||||||
|
</Loader>
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredViewsList = viewsList.filter((v) => v.name.toLowerCase().includes(query.toLowerCase()));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{viewsList.length > 0 ? (
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<div className="w-full flex flex-col overflow-hidden">
|
||||||
|
<div className="flex items-center gap-2.5 w-full px-5 py-3 border-b border-custom-border-200">
|
||||||
|
<Search className="text-custom-text-200" size={14} strokeWidth={2} />
|
||||||
|
<Input
|
||||||
|
className="w-full bg-transparent text-xs leading-5 text-custom-text-200 placeholder:text-custom-text-400 !p-0 focus:outline-none"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Search"
|
||||||
|
mode="trueTransparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{filteredViewsList.length > 0 ? (
|
||||||
|
filteredViewsList.map((view) => <ProjectViewListItem key={view.id} view={view} />)
|
||||||
|
) : (
|
||||||
|
<p className="text-custom-text-300 text-sm text-center mt-10">No results found</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<EmptyState
|
||||||
|
title="Get focused with views"
|
||||||
|
description="Views aid in saving your issues by applying various filters and grouping options."
|
||||||
|
image={emptyView}
|
||||||
|
primaryButton={{
|
||||||
|
icon: <Plus size={14} strokeWidth={2} />,
|
||||||
|
text: "New View",
|
||||||
|
onClick: () => {
|
||||||
|
const e = new KeyboardEvent("keydown", {
|
||||||
|
key: "v",
|
||||||
|
});
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -5,8 +5,6 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useMobxStore } from "lib/mobx/store-provider";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// hooks
|
|
||||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
|
||||||
// components
|
// components
|
||||||
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues";
|
import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
@ -48,10 +46,6 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
|
|
||||||
|
|
||||||
const memberOptions = workspaceMembers?.map((m) => m.member);
|
|
||||||
|
|
||||||
const handleCreateUpdateView = async (formData: Partial<IWorkspaceView>) => {
|
const handleCreateUpdateView = async (formData: Partial<IWorkspaceView>) => {
|
||||||
await handleFormSubmit(formData);
|
await handleFormSubmit(formData);
|
||||||
|
|
||||||
@ -76,12 +70,6 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
|
|||||||
setValue("query_data.filters", {});
|
setValue("query_data.filters", {});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
reset({ ...data });
|
|
||||||
}, [data, reset]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
|
<form onSubmit={handleSubmit(handleCreateUpdateView)}>
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
@ -156,7 +144,7 @@ export const WorkspaceViewForm: React.FC<Props> = observer((props) => {
|
|||||||
handleClearAllFilters={clearAllFilters}
|
handleClearAllFilters={clearAllFilters}
|
||||||
handleRemoveFilter={() => {}}
|
handleRemoveFilter={() => {}}
|
||||||
labels={workspaceStore.workspaceLabels ?? undefined}
|
labels={workspaceStore.workspaceLabels ?? undefined}
|
||||||
members={memberOptions}
|
members={workspaceStore.workspaceMembers?.map((m) => m.member) ?? undefined}
|
||||||
states={undefined}
|
states={undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,6 +11,7 @@ import { CustomMenu } from "components/ui";
|
|||||||
import { PencilIcon, Sparkles, TrashIcon } from "lucide-react";
|
import { PencilIcon, Sparkles, TrashIcon } from "lucide-react";
|
||||||
// helpers
|
// helpers
|
||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
|
import { calculateTotalFilters } from "helpers/filter.helper";
|
||||||
// types
|
// types
|
||||||
import { IWorkspaceView } from "types/workspace-views";
|
import { IWorkspaceView } from "types/workspace-views";
|
||||||
|
|
||||||
@ -25,18 +26,7 @@ export const GlobalViewListItem: React.FC<Props> = observer((props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
|
|
||||||
const totalFilters =
|
const totalFilters = calculateTotalFilters(view.query_data.filters ?? {});
|
||||||
view?.query_data?.filters && Object.keys(view.query_data.filters).length > 0
|
|
||||||
? Object.keys(view.query_data.filters)
|
|
||||||
.map((key) =>
|
|
||||||
view.query_data.filters[key as keyof typeof view.query_data.filters] !== null
|
|
||||||
? isNaN((view.query_data.filters[key as keyof typeof view.query_data.filters] as any).length)
|
|
||||||
? 0
|
|
||||||
: (view.query_data.filters[key as keyof typeof view.query_data.filters] as any).length
|
|
||||||
: 0
|
|
||||||
)
|
|
||||||
.reduce((curr, prev) => curr + prev, 0)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,6 +1,19 @@
|
|||||||
// types
|
// types
|
||||||
import { IIssueFilterOptions } from "types";
|
import { IIssueFilterOptions } from "types";
|
||||||
|
|
||||||
|
export const calculateTotalFilters = (filters: IIssueFilterOptions): number =>
|
||||||
|
filters && Object.keys(filters).length > 0
|
||||||
|
? Object.keys(filters)
|
||||||
|
.map((key) =>
|
||||||
|
filters[key as keyof IIssueFilterOptions] !== null
|
||||||
|
? isNaN((filters[key as keyof IIssueFilterOptions] as string[]).length)
|
||||||
|
? 0
|
||||||
|
: (filters[key as keyof IIssueFilterOptions] as string[]).length
|
||||||
|
: 0
|
||||||
|
)
|
||||||
|
.reduce((curr, prev) => curr + prev, 0)
|
||||||
|
: 0;
|
||||||
|
|
||||||
// check if there is any difference between the saved filters and the current filters
|
// check if there is any difference between the saved filters and the current filters
|
||||||
export const areFiltersDifferent = (filtersSet1: IIssueFilterOptions, filtersSet2: IIssueFilterOptions) => {
|
export const areFiltersDifferent = (filtersSet1: IIssueFilterOptions, filtersSet2: IIssueFilterOptions) => {
|
||||||
for (const [key, value] of Object.entries(filtersSet1) as [keyof IIssueFilterOptions, string[] | null][]) {
|
for (const [key, value] of Object.entries(filtersSet1) as [keyof IIssueFilterOptions, string[] | null][]) {
|
||||||
|
@ -13,12 +13,13 @@ const MobxStoreInit = () => {
|
|||||||
project: projectStore,
|
project: projectStore,
|
||||||
module: moduleStore,
|
module: moduleStore,
|
||||||
globalViews: globalViewsStore,
|
globalViews: globalViewsStore,
|
||||||
|
projectViews: projectViewsStore,
|
||||||
} = useMobxStore();
|
} = useMobxStore();
|
||||||
// theme
|
// theme
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, moduleId, globalViewId } = router.query;
|
const { workspaceSlug, projectId, moduleId, globalViewId, viewId } = router.query;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// sidebar collapsed toggle
|
// sidebar collapsed toggle
|
||||||
@ -49,7 +50,19 @@ const MobxStoreInit = () => {
|
|||||||
if (projectId) projectStore.setProjectId(projectId.toString());
|
if (projectId) projectStore.setProjectId(projectId.toString());
|
||||||
if (moduleId) moduleStore.setModuleId(moduleId.toString());
|
if (moduleId) moduleStore.setModuleId(moduleId.toString());
|
||||||
if (globalViewId) globalViewsStore.setGlobalViewId(globalViewId.toString());
|
if (globalViewId) globalViewsStore.setGlobalViewId(globalViewId.toString());
|
||||||
}, [workspaceSlug, projectId, moduleId, globalViewId, workspaceStore, projectStore, moduleStore, globalViewsStore]);
|
if (viewId) projectViewsStore.setViewId(viewId.toString());
|
||||||
|
}, [
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
moduleId,
|
||||||
|
globalViewId,
|
||||||
|
viewId,
|
||||||
|
workspaceStore,
|
||||||
|
projectStore,
|
||||||
|
moduleStore,
|
||||||
|
globalViewsStore,
|
||||||
|
projectViewsStore,
|
||||||
|
]);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
};
|
};
|
||||||
|
@ -7,15 +7,12 @@ import projectService from "services/project.service";
|
|||||||
import viewsService from "services/views.service";
|
import viewsService from "services/views.service";
|
||||||
// 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 { IssuesFilterView, IssuesView } from "components/core";
|
import { ProjectViewAllLayouts } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu, EmptyState, PrimaryButton } from "components/ui";
|
import { CustomMenu, EmptyState } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { StackedLayersIcon } from "components/icons";
|
import { StackedLayersIcon } from "components/icons";
|
||||||
// images
|
// images
|
||||||
import emptyView from "public/empty-state/view.svg";
|
import emptyView from "public/empty-state/view.svg";
|
||||||
@ -23,6 +20,7 @@ import emptyView from "public/empty-state/view.svg";
|
|||||||
import { truncateText } from "helpers/string.helper";
|
import { truncateText } from "helpers/string.helper";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_DETAILS, VIEWS_LIST, VIEW_DETAILS } from "constants/fetch-keys";
|
import { PROJECT_DETAILS, VIEWS_LIST, VIEW_DETAILS } from "constants/fetch-keys";
|
||||||
|
import { ProjectViewIssuesHeader } from "components/headers";
|
||||||
|
|
||||||
const SingleView: React.FC = () => {
|
const SingleView: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -46,71 +44,53 @@ const SingleView: React.FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IssueViewContextProvider>
|
<ProjectAuthorizationWrapper
|
||||||
<ProjectAuthorizationWrapper
|
breadcrumbs={
|
||||||
breadcrumbs={
|
<Breadcrumbs>
|
||||||
<Breadcrumbs>
|
<BreadcrumbItem
|
||||||
<BreadcrumbItem
|
title={`${activeProject?.name ?? "Project"} Views`}
|
||||||
title={`${activeProject?.name ?? "Project"} Views`}
|
link={`/${workspaceSlug}/projects/${activeProject?.id}/cycles`}
|
||||||
link={`/${workspaceSlug}/projects/${activeProject?.id}/cycles`}
|
|
||||||
/>
|
|
||||||
</Breadcrumbs>
|
|
||||||
}
|
|
||||||
left={
|
|
||||||
<CustomMenu
|
|
||||||
label={
|
|
||||||
<>
|
|
||||||
<StackedLayersIcon height={12} width={12} />
|
|
||||||
{viewDetails?.name && truncateText(viewDetails.name, 40)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
className="ml-1.5"
|
|
||||||
width="auto"
|
|
||||||
>
|
|
||||||
{views?.map((view) => (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={view.id}
|
|
||||||
renderAs="a"
|
|
||||||
href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}
|
|
||||||
>
|
|
||||||
{truncateText(view.name, 40)}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
))}
|
|
||||||
</CustomMenu>
|
|
||||||
}
|
|
||||||
right={
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<IssuesFilterView />
|
|
||||||
<PrimaryButton
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Add Issue
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{error ? (
|
|
||||||
<EmptyState
|
|
||||||
image={emptyView}
|
|
||||||
title="View does not exist"
|
|
||||||
description="The view you are looking for does not exist or has been deleted."
|
|
||||||
primaryButton={{
|
|
||||||
text: "View other views",
|
|
||||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/views`),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
</Breadcrumbs>
|
||||||
<div className="h-full w-full flex flex-col">
|
}
|
||||||
<IssuesView />
|
left={
|
||||||
</div>
|
<CustomMenu
|
||||||
)}
|
label={
|
||||||
</ProjectAuthorizationWrapper>
|
<>
|
||||||
</IssueViewContextProvider>
|
<StackedLayersIcon height={12} width={12} />
|
||||||
|
{viewDetails?.name && truncateText(viewDetails.name, 40)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
className="ml-1.5"
|
||||||
|
width="auto"
|
||||||
|
>
|
||||||
|
{views?.map((view) => (
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
key={view.id}
|
||||||
|
renderAs="a"
|
||||||
|
href={`/${workspaceSlug}/projects/${projectId}/views/${view.id}`}
|
||||||
|
>
|
||||||
|
{truncateText(view.name, 40)}
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
))}
|
||||||
|
</CustomMenu>
|
||||||
|
}
|
||||||
|
right={<ProjectViewIssuesHeader />}
|
||||||
|
>
|
||||||
|
{error ? (
|
||||||
|
<EmptyState
|
||||||
|
image={emptyView}
|
||||||
|
title="View does not exist"
|
||||||
|
description="The view you are looking for does not exist or has been deleted."
|
||||||
|
primaryButton={{
|
||||||
|
text: "View other views",
|
||||||
|
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/views`),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ProjectViewAllLayouts />
|
||||||
|
)}
|
||||||
|
</ProjectAuthorizationWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,138 +1,60 @@
|
|||||||
import React, { useState } from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
// hooks
|
// mobx store
|
||||||
import useUserAuth from "hooks/use-user-auth";
|
import { useMobxStore } from "lib/mobx/store-provider";
|
||||||
// services
|
|
||||||
import viewsService from "services/views.service";
|
|
||||||
import projectService from "services/project.service";
|
|
||||||
// layouts
|
// layouts
|
||||||
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
|
import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy";
|
||||||
|
// components
|
||||||
|
import { ProjectViewsHeader } from "components/headers";
|
||||||
// ui
|
// ui
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
//icons
|
|
||||||
import { PlusIcon } from "components/icons";
|
|
||||||
// images
|
|
||||||
import emptyView from "public/empty-state/view.svg";
|
|
||||||
// fetching keys
|
|
||||||
import { PROJECT_DETAILS, VIEWS_LIST } from "constants/fetch-keys";
|
|
||||||
// components
|
// components
|
||||||
import { PrimaryButton, Loader, EmptyState } from "components/ui";
|
import { ProjectViewsList } from "components/views";
|
||||||
import { DeleteViewModal, CreateUpdateViewModal, SingleViewItem } from "components/views";
|
|
||||||
// types
|
// types
|
||||||
import { IView } from "types";
|
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
import { Input } from "components/ui";
|
||||||
|
|
||||||
const ProjectViews: NextPage = () => {
|
const ProjectViews: NextPage = () => {
|
||||||
const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false);
|
|
||||||
const [selectedViewToUpdate, setSelectedViewToUpdate] = useState<IView | null>(null);
|
|
||||||
|
|
||||||
const [deleteViewModal, setDeleteViewModal] = useState(false);
|
|
||||||
const [selectedViewToDelete, setSelectedViewToDelete] = useState<IView | null>(null);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
const { user } = useUserAuth();
|
const { project: projectStore, projectViews: projectViewsStore } = useMobxStore();
|
||||||
|
|
||||||
const { data: activeProject } = useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null,
|
workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId.toString()}` : null,
|
||||||
workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null
|
workspaceSlug && projectId
|
||||||
|
? () => projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString())
|
||||||
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: views } = useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId ? VIEWS_LIST(projectId as string) : null,
|
workspaceSlug && projectId ? `PROJECT_VIEWS_LIST_${workspaceSlug.toString()}_${projectId.toString()}` : null,
|
||||||
workspaceSlug && projectId ? () => viewsService.getViews(workspaceSlug as string, projectId as string) : null
|
workspaceSlug && projectId
|
||||||
|
? () => projectViewsStore.fetchAllViews(workspaceSlug.toString(), projectId.toString())
|
||||||
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEditView = (view: IView) => {
|
const projectDetails =
|
||||||
setSelectedViewToUpdate(view);
|
workspaceSlug && projectId
|
||||||
setCreateUpdateViewModal(true);
|
? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString())
|
||||||
};
|
: undefined;
|
||||||
|
|
||||||
const handleDeleteView = (view: IView) => {
|
|
||||||
setSelectedViewToDelete(view);
|
|
||||||
setDeleteViewModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectAuthorizationWrapper
|
<ProjectAuthorizationWrapper
|
||||||
breadcrumbs={
|
breadcrumbs={
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
<BreadcrumbItem title="Projects" link={`/${workspaceSlug}/projects`} />
|
||||||
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Views`} />
|
<BreadcrumbItem title={`${projectDetails?.name ?? "Project"} Views`} />
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
}
|
}
|
||||||
right={
|
right={<ProjectViewsHeader />}
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<PrimaryButton
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "v" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<PlusIcon className="h-4 w-4" />
|
|
||||||
Create View
|
|
||||||
</PrimaryButton>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<CreateUpdateViewModal
|
<ProjectViewsList />
|
||||||
isOpen={createUpdateViewModal}
|
|
||||||
handleClose={() => setCreateUpdateViewModal(false)}
|
|
||||||
data={selectedViewToUpdate}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
<DeleteViewModal
|
|
||||||
isOpen={deleteViewModal}
|
|
||||||
data={selectedViewToDelete}
|
|
||||||
setIsOpen={setDeleteViewModal}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
{views ? (
|
|
||||||
views.length > 0 ? (
|
|
||||||
<div className="space-y-5 p-8">
|
|
||||||
<h3 className="text-2xl font-semibold text-custom-text-100">Views</h3>
|
|
||||||
<div className="divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200">
|
|
||||||
{views.map((view) => (
|
|
||||||
<SingleViewItem
|
|
||||||
key={view.id}
|
|
||||||
view={view}
|
|
||||||
handleEditView={() => handleEditView(view)}
|
|
||||||
handleDeleteView={() => handleDeleteView(view)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<EmptyState
|
|
||||||
title="Get focused with views"
|
|
||||||
description="Views aid in saving your issues by applying various filters and grouping options."
|
|
||||||
image={emptyView}
|
|
||||||
primaryButton={{
|
|
||||||
icon: <PlusIcon className="h-4 w-4" />,
|
|
||||||
text: "New View",
|
|
||||||
onClick: () => {
|
|
||||||
const e = new KeyboardEvent("keydown", {
|
|
||||||
key: "v",
|
|
||||||
});
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Loader className="space-y-3 p-8">
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
<Loader.Item height="30px" />
|
|
||||||
</Loader>
|
|
||||||
)}
|
|
||||||
</ProjectAuthorizationWrapper>
|
</ProjectAuthorizationWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -17,8 +17,6 @@ import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
|||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
// constants
|
// constants
|
||||||
import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace";
|
import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace";
|
||||||
// fetch-keys
|
|
||||||
import { GLOBAL_VIEWS_LIST } from "constants/fetch-keys";
|
|
||||||
|
|
||||||
const WorkspaceViews: NextPage = () => {
|
const WorkspaceViews: NextPage = () => {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@ -29,7 +27,7 @@ const WorkspaceViews: NextPage = () => {
|
|||||||
const { globalViews: globalViewsStore } = useMobxStore();
|
const { globalViews: globalViewsStore } = useMobxStore();
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug ? GLOBAL_VIEWS_LIST(workspaceSlug.toString()) : null,
|
workspaceSlug ? `GLOBAL_VIEWS_LIST_${workspaceSlug.toString()}` : null,
|
||||||
workspaceSlug ? () => globalViewsStore.fetchAllGlobalViews(workspaceSlug.toString()) : null
|
workspaceSlug ? () => globalViewsStore.fetchAllGlobalViews(workspaceSlug.toString()) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import trackEventServices from "services/track_event.service";
|
import trackEventServices from "services/track_event.service";
|
||||||
// types
|
// types
|
||||||
import { IView } from "types/views";
|
import { IProjectView } from "types/views";
|
||||||
import { ICurrentUserResponse } from "types";
|
import { ICurrentUserResponse } from "types";
|
||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
@ -11,12 +11,7 @@ export class ViewService extends APIService {
|
|||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createView(
|
async createView(workspaceSlug: string, projectId: string, data: Partial<IProjectView>, user: any): Promise<any> {
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
data: IView,
|
|
||||||
user: ICurrentUserResponse | undefined
|
|
||||||
): Promise<any> {
|
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data)
|
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
trackEventServices.trackViewEvent(response?.data, "VIEW_CREATE", user);
|
trackEventServices.trackViewEvent(response?.data, "VIEW_CREATE", user);
|
||||||
@ -27,29 +22,12 @@ export class ViewService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateView(
|
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
viewId: string,
|
|
||||||
data: IView,
|
|
||||||
user: ICurrentUserResponse | undefined
|
|
||||||
): Promise<any> {
|
|
||||||
return this.put(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
|
|
||||||
.then((response) => {
|
|
||||||
trackEventServices.trackViewEvent(response?.data, "VIEW_UPDATE", user);
|
|
||||||
return response?.data;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
throw error?.response?.data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async patchView(
|
async patchView(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
viewId: string,
|
viewId: string,
|
||||||
data: Partial<IView>,
|
data: Partial<IProjectView>,
|
||||||
user: ICurrentUserResponse | undefined
|
user: any
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
|
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
@ -61,12 +39,7 @@ export class ViewService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteView(
|
async deleteView(workspaceSlug: string, projectId: string, viewId: string, user: any): Promise<any> {
|
||||||
workspaceSlug: string,
|
|
||||||
projectId: string,
|
|
||||||
viewId: string,
|
|
||||||
user: ICurrentUserResponse | undefined
|
|
||||||
): Promise<any> {
|
|
||||||
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
trackEventServices.trackViewEvent(response?.data, "VIEW_DELETE", user);
|
trackEventServices.trackViewEvent(response?.data, "VIEW_DELETE", user);
|
||||||
@ -77,7 +50,7 @@ export class ViewService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getViews(workspaceSlug: string, projectId: string): Promise<IView[]> {
|
async getViews(workspaceSlug: string, projectId: string): Promise<IProjectView[]> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -85,7 +58,7 @@ export class ViewService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getViewDetails(workspaceSlug: string, projectId: string, viewId: string): Promise<IView> {
|
async getViewDetails(workspaceSlug: string, projectId: string, viewId: string): Promise<IProjectView> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -220,8 +220,8 @@ class ProjectStore implements IProjectStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
setProjectId = (projectSlug: string) => {
|
setProjectId = (projectId: string) => {
|
||||||
this.projectId = projectSlug ?? null;
|
this.projectId = projectId ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
setSearchQuery = (query: string) => {
|
setSearchQuery = (query: string) => {
|
||||||
@ -311,15 +311,15 @@ class ProjectStore implements IProjectStore {
|
|||||||
return estimateInfo;
|
return estimateInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchProjectStates = async (workspaceSlug: string, projectSlug: string) => {
|
fetchProjectStates = async (workspaceSlug: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
const stateResponse = await this.stateService.getStates(workspaceSlug, projectSlug);
|
const stateResponse = await this.stateService.getStates(workspaceSlug, projectId);
|
||||||
const _states = {
|
const _states = {
|
||||||
...this.states,
|
...this.states,
|
||||||
[projectSlug]: stateResponse,
|
[projectId]: stateResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
@ -357,15 +357,15 @@ class ProjectStore implements IProjectStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchProjectMembers = async (workspaceSlug: string, projectSlug: string) => {
|
fetchProjectMembers = async (workspaceSlug: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
const membersResponse = await this.projectService.projectMembers(workspaceSlug, projectSlug);
|
const membersResponse = await this.projectService.projectMembers(workspaceSlug, projectId);
|
||||||
const _members = {
|
const _members = {
|
||||||
...this.members,
|
...this.members,
|
||||||
[projectSlug]: membersResponse,
|
[projectId]: membersResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
@ -380,15 +380,15 @@ class ProjectStore implements IProjectStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchProjectEstimates = async (workspaceSlug: string, projectSlug: string) => {
|
fetchProjectEstimates = async (workspaceSlug: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
const estimatesResponse = await this.estimateService.getEstimatesList(workspaceSlug, projectSlug);
|
const estimatesResponse = await this.estimateService.getEstimatesList(workspaceSlug, projectId);
|
||||||
const _estimates = {
|
const _estimates = {
|
||||||
...this.estimates,
|
...this.estimates,
|
||||||
[projectSlug]: estimatesResponse,
|
[projectId]: estimatesResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
@ -497,12 +497,12 @@ class ProjectStore implements IProjectStore {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
leaveProject = async (workspaceSlug: string, projectSlug: string) => {
|
leaveProject = async (workspaceSlug: string, projectId: string) => {
|
||||||
try {
|
try {
|
||||||
this.loader = true;
|
this.loader = true;
|
||||||
this.error = null;
|
this.error = null;
|
||||||
|
|
||||||
const response = await this.projectService.leaveProject(workspaceSlug, projectSlug, this.rootStore.user);
|
const response = await this.projectService.leaveProject(workspaceSlug, projectId, this.rootStore.user);
|
||||||
await this.fetchProjects(workspaceSlug);
|
await this.fetchProjects(workspaceSlug);
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
|
70
web/store/project_view_filters.ts
Normal file
70
web/store/project_view_filters.ts
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { observable, action, makeObservable, runInAction } from "mobx";
|
||||||
|
// types
|
||||||
|
import { RootStore } from "./root";
|
||||||
|
import { IIssueFilterOptions } from "types";
|
||||||
|
|
||||||
|
export interface IProjectViewFiltersStore {
|
||||||
|
// states
|
||||||
|
loader: boolean;
|
||||||
|
error: any | null;
|
||||||
|
|
||||||
|
// observables
|
||||||
|
storedFilters: {
|
||||||
|
[viewId: string]: IIssueFilterOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
// actions
|
||||||
|
updateStoredFilters: (viewId: string, filters: Partial<IIssueFilterOptions>) => void;
|
||||||
|
deleteStoredFilters: (viewId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProjectViewFiltersStore implements IProjectViewFiltersStore {
|
||||||
|
// states
|
||||||
|
loader: boolean = false;
|
||||||
|
error: any | null = null;
|
||||||
|
|
||||||
|
// observables
|
||||||
|
storedFilters: {
|
||||||
|
[viewId: string]: IIssueFilterOptions;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
|
||||||
|
constructor(_rootStore: RootStore) {
|
||||||
|
makeObservable(this, {
|
||||||
|
// states
|
||||||
|
loader: observable.ref,
|
||||||
|
error: observable.ref,
|
||||||
|
|
||||||
|
// observables
|
||||||
|
storedFilters: observable.ref,
|
||||||
|
|
||||||
|
// actions
|
||||||
|
updateStoredFilters: action,
|
||||||
|
deleteStoredFilters: action,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStoredFilters = (viewId: string, filters: Partial<IIssueFilterOptions>) => {
|
||||||
|
runInAction(() => {
|
||||||
|
this.storedFilters = {
|
||||||
|
...this.storedFilters,
|
||||||
|
[viewId]: { ...this.storedFilters[viewId], ...filters },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteStoredFilters = (viewId: string) => {
|
||||||
|
const updatedStoredFilters = { ...this.storedFilters };
|
||||||
|
delete updatedStoredFilters[viewId];
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.storedFilters = updatedStoredFilters;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectViewFiltersStore;
|
159
web/store/project_view_issues.ts
Normal file
159
web/store/project_view_issues.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { observable, action, makeObservable, runInAction, computed } from "mobx";
|
||||||
|
// services
|
||||||
|
import { IssueService } from "services/issue.service";
|
||||||
|
// helpers
|
||||||
|
import { handleIssueQueryParamsByLayout } from "helpers/issue.helper";
|
||||||
|
// types
|
||||||
|
import { RootStore } from "./root";
|
||||||
|
import { IIssueFilterOptions } from "types";
|
||||||
|
import { IIssueGroupWithSubGroupsStructure, IIssueGroupedStructure, IIssueUnGroupedStructure } from "./module_issue";
|
||||||
|
|
||||||
|
export interface IProjectViewIssuesStore {
|
||||||
|
// states
|
||||||
|
loader: boolean;
|
||||||
|
error: any | null;
|
||||||
|
|
||||||
|
// observables
|
||||||
|
viewIssues: {
|
||||||
|
[viewId: string]: {
|
||||||
|
grouped: IIssueGroupedStructure;
|
||||||
|
groupWithSubGroups: IIssueGroupWithSubGroupsStructure;
|
||||||
|
ungrouped: IIssueUnGroupedStructure;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// actions
|
||||||
|
fetchViewIssues: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
viewId: string,
|
||||||
|
filters: IIssueFilterOptions
|
||||||
|
) => Promise<any>;
|
||||||
|
|
||||||
|
// computed
|
||||||
|
getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProjectViewIssuesStore implements IProjectViewIssuesStore {
|
||||||
|
// states
|
||||||
|
loader: boolean = false;
|
||||||
|
error: any | null = null;
|
||||||
|
|
||||||
|
// observables
|
||||||
|
viewIssues: {
|
||||||
|
[viewId: string]: {
|
||||||
|
grouped: IIssueGroupedStructure;
|
||||||
|
groupWithSubGroups: IIssueGroupWithSubGroupsStructure;
|
||||||
|
ungrouped: IIssueUnGroupedStructure;
|
||||||
|
};
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
|
||||||
|
// services
|
||||||
|
issueService;
|
||||||
|
|
||||||
|
constructor(_rootStore: RootStore) {
|
||||||
|
makeObservable(this, {
|
||||||
|
// states
|
||||||
|
loader: observable.ref,
|
||||||
|
error: observable.ref,
|
||||||
|
|
||||||
|
// observables
|
||||||
|
viewIssues: observable.ref,
|
||||||
|
|
||||||
|
// actions
|
||||||
|
fetchViewIssues: action,
|
||||||
|
|
||||||
|
// computed
|
||||||
|
getIssues: computed,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
|
||||||
|
this.issueService = new IssueService();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getIssues() {
|
||||||
|
const viewId: string | null = this.rootStore.projectViews.viewId;
|
||||||
|
const issueType = this.rootStore.issue.getIssueType;
|
||||||
|
|
||||||
|
if (!viewId || !issueType) return null;
|
||||||
|
|
||||||
|
return this.viewIssues?.[viewId]?.[issueType] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchViewIssues = async (workspaceSlug: string, projectId: string, viewId: string, filters: IIssueFilterOptions) => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayFilters = this.rootStore.issueFilter.userDisplayFilters;
|
||||||
|
|
||||||
|
let filteredRouteParams: any = {
|
||||||
|
priority: filters?.priority || undefined,
|
||||||
|
state_group: filters?.state_group || undefined,
|
||||||
|
state: filters?.state || undefined,
|
||||||
|
assignees: filters?.assignees || undefined,
|
||||||
|
created_by: filters?.created_by || undefined,
|
||||||
|
labels: filters?.labels || undefined,
|
||||||
|
start_date: filters?.start_date || undefined,
|
||||||
|
target_date: filters?.target_date || undefined,
|
||||||
|
group_by: displayFilters?.group_by || undefined,
|
||||||
|
order_by: displayFilters?.order_by || "-created_at",
|
||||||
|
type: displayFilters?.type || undefined,
|
||||||
|
sub_issue: displayFilters.sub_issue || undefined,
|
||||||
|
sub_group_by: displayFilters.sub_group_by || undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredParams = handleIssueQueryParamsByLayout(displayFilters.layout ?? "list", "issues");
|
||||||
|
if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams);
|
||||||
|
|
||||||
|
if (displayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date";
|
||||||
|
if (displayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true;
|
||||||
|
|
||||||
|
const response = await this.issueService.getIssuesWithParams(workspaceSlug, projectId, filteredRouteParams);
|
||||||
|
|
||||||
|
const issueType = this.rootStore.issue.getIssueType;
|
||||||
|
|
||||||
|
if (issueType != null) {
|
||||||
|
const newIssues = {
|
||||||
|
...this.viewIssues,
|
||||||
|
[viewId]: {
|
||||||
|
...this.viewIssues[viewId],
|
||||||
|
[issueType]: response,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.viewIssues = newIssues;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectViewIssuesStore;
|
295
web/store/project_views.ts
Normal file
295
web/store/project_views.ts
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
import { observable, action, makeObservable, runInAction } from "mobx";
|
||||||
|
// services
|
||||||
|
import { ViewService } from "services/views.service";
|
||||||
|
// types
|
||||||
|
import { RootStore } from "./root";
|
||||||
|
import { IProjectView } from "types";
|
||||||
|
|
||||||
|
export interface IProjectViewsStore {
|
||||||
|
// states
|
||||||
|
loader: boolean;
|
||||||
|
error: any | null;
|
||||||
|
|
||||||
|
// observables
|
||||||
|
viewId: string | null;
|
||||||
|
viewsList: {
|
||||||
|
[projectId: string]: IProjectView[];
|
||||||
|
};
|
||||||
|
viewDetails: {
|
||||||
|
[viewId: string]: IProjectView;
|
||||||
|
};
|
||||||
|
|
||||||
|
// actions
|
||||||
|
setViewId: (viewId: string) => void;
|
||||||
|
|
||||||
|
fetchAllViews: (workspaceSlug: string, projectId: string) => Promise<IProjectView[]>;
|
||||||
|
fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise<IProjectView>;
|
||||||
|
createView: (workspaceSlug: string, projectId: string, data: Partial<IProjectView>) => Promise<IProjectView>;
|
||||||
|
updateView: (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
viewId: string,
|
||||||
|
data: Partial<IProjectView>
|
||||||
|
) => Promise<IProjectView>;
|
||||||
|
deleteView: (workspaceSlug: string, projectId: string, viewId: string) => Promise<any>;
|
||||||
|
addViewToFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise<any>;
|
||||||
|
removeViewFromFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProjectViewsStore implements IProjectViewsStore {
|
||||||
|
// states
|
||||||
|
loader: boolean = false;
|
||||||
|
error: any | null = null;
|
||||||
|
|
||||||
|
// observables
|
||||||
|
viewId: string | null = null;
|
||||||
|
viewsList: {
|
||||||
|
[projectId: string]: IProjectView[];
|
||||||
|
} = {};
|
||||||
|
viewDetails: { [viewId: string]: IProjectView } = {};
|
||||||
|
|
||||||
|
// root store
|
||||||
|
rootStore;
|
||||||
|
|
||||||
|
// services
|
||||||
|
viewService;
|
||||||
|
|
||||||
|
constructor(_rootStore: RootStore) {
|
||||||
|
makeObservable(this, {
|
||||||
|
// states
|
||||||
|
loader: observable.ref,
|
||||||
|
error: observable.ref,
|
||||||
|
|
||||||
|
// observables
|
||||||
|
viewId: observable.ref,
|
||||||
|
viewsList: observable.ref,
|
||||||
|
viewDetails: observable.ref,
|
||||||
|
|
||||||
|
// actions
|
||||||
|
setViewId: action,
|
||||||
|
|
||||||
|
fetchAllViews: action,
|
||||||
|
fetchViewDetails: action,
|
||||||
|
createView: action,
|
||||||
|
updateView: action,
|
||||||
|
deleteView: action,
|
||||||
|
addViewToFavorites: action,
|
||||||
|
removeViewFromFavorites: action,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rootStore = _rootStore;
|
||||||
|
|
||||||
|
this.viewService = new ViewService();
|
||||||
|
}
|
||||||
|
|
||||||
|
setViewId = (viewId: string) => {
|
||||||
|
this.viewId = viewId;
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAllViews = async (workspaceSlug: string, projectId: string): Promise<IProjectView[]> => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.viewService.getViews(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.viewsList = {
|
||||||
|
...this.viewsList,
|
||||||
|
[projectId]: response,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchViewDetails = async (workspaceSlug: string, projectId: string, viewId: string): Promise<IProjectView> => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.viewService.getViewDetails(workspaceSlug, projectId, viewId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.viewDetails = {
|
||||||
|
...this.viewDetails,
|
||||||
|
[response.id]: response,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.loader = false;
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createView = async (workspaceSlug: string, projectId: string, data: Partial<IProjectView>): Promise<IProjectView> => {
|
||||||
|
try {
|
||||||
|
const response = await this.viewService.createView(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
data,
|
||||||
|
this.rootStore.user.currentUser
|
||||||
|
);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.viewsList = {
|
||||||
|
...this.viewsList,
|
||||||
|
[projectId]: [...(this.viewsList[projectId] ?? []), response],
|
||||||
|
};
|
||||||
|
this.viewDetails = {
|
||||||
|
...this.viewDetails,
|
||||||
|
[response.id]: response,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
runInAction(() => {
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateView = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
viewId: string,
|
||||||
|
data: Partial<IProjectView>
|
||||||
|
): Promise<IProjectView> => {
|
||||||
|
const viewToUpdate = { ...this.viewDetails[viewId], ...data };
|
||||||
|
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.viewsList = {
|
||||||
|
...this.viewsList,
|
||||||
|
[projectId]: this.viewsList[projectId]?.map((view) => (view.id === viewId ? viewToUpdate : view)),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.viewDetails = {
|
||||||
|
...this.viewDetails,
|
||||||
|
[viewId]: viewToUpdate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await this.viewService.patchView(
|
||||||
|
workspaceSlug,
|
||||||
|
projectId,
|
||||||
|
viewId,
|
||||||
|
data,
|
||||||
|
this.rootStore.user.currentUser
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchViewDetails(workspaceSlug, projectId, viewId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteView = async (workspaceSlug: string, projectId: string, viewId: string): Promise<any> => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.viewsList = {
|
||||||
|
...this.viewsList,
|
||||||
|
[projectId]: this.viewsList[projectId]?.filter((view) => view.id !== viewId),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.viewService.deleteView(workspaceSlug, projectId, viewId, this.rootStore.user.currentUser);
|
||||||
|
} catch (error) {
|
||||||
|
this.fetchAllViews(workspaceSlug, projectId);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
addViewToFavorites = async (workspaceSlug: string, projectId: string, viewId: string) => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.viewsList = {
|
||||||
|
...this.viewsList,
|
||||||
|
[projectId]: this.viewsList[projectId].map((view) => ({
|
||||||
|
...view,
|
||||||
|
is_favorite: view.id === viewId ? true : view.is_favorite,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.viewService.addViewToFavorites(workspaceSlug, projectId, {
|
||||||
|
view: viewId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to add view to favorites in view store", error);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.viewsList = {
|
||||||
|
...this.viewsList,
|
||||||
|
[projectId]: this.viewsList[projectId].map((view) => ({
|
||||||
|
...view,
|
||||||
|
is_favorite: view.id === viewId ? false : view.is_favorite,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
this.error = error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
removeViewFromFavorites = async (workspaceSlug: string, projectId: string, viewId: string) => {
|
||||||
|
try {
|
||||||
|
runInAction(() => {
|
||||||
|
this.viewsList = {
|
||||||
|
...this.viewsList,
|
||||||
|
[projectId]: this.viewsList[projectId].map((view) => ({
|
||||||
|
...view,
|
||||||
|
is_favorite: view.id === viewId ? false : view.is_favorite,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.viewService.removeViewFromFavorites(workspaceSlug, projectId, viewId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to remove view from favorites in view store", error);
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
this.viewsList = {
|
||||||
|
...this.viewsList,
|
||||||
|
[projectId]: this.viewsList[projectId].map((view) => ({
|
||||||
|
...view,
|
||||||
|
is_favorite: view.id === viewId ? true : view.is_favorite,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectViewsStore;
|
@ -3,11 +3,12 @@ import { enableStaticRendering } from "mobx-react-lite";
|
|||||||
// store imports
|
// store imports
|
||||||
import UserStore from "./user";
|
import UserStore from "./user";
|
||||||
import ThemeStore from "./theme";
|
import ThemeStore from "./theme";
|
||||||
import ProjectPublishStore, { IProjectPublishStore } from "./project_publish";
|
|
||||||
import IssueStore, { IIssueStore } from "./issue";
|
import IssueStore, { IIssueStore } from "./issue";
|
||||||
import DraftIssuesStore from "./issue_draft";
|
import DraftIssuesStore from "./issue_draft";
|
||||||
import WorkspaceStore, { IWorkspaceStore } from "./workspace";
|
import WorkspaceStore, { IWorkspaceStore } from "./workspace";
|
||||||
|
import WorkspaceFilterStore, { IWorkspaceFilterStore } from "./workspace_filters";
|
||||||
import ProjectStore, { IProjectStore } from "./project";
|
import ProjectStore, { IProjectStore } from "./project";
|
||||||
|
import ProjectPublishStore, { IProjectPublishStore } from "./project_publish";
|
||||||
import ModuleStore, { IModuleStore } from "./modules";
|
import ModuleStore, { IModuleStore } from "./modules";
|
||||||
import ModuleIssueStore, { IModuleIssueStore } from "./module_issue";
|
import ModuleIssueStore, { IModuleIssueStore } from "./module_issue";
|
||||||
import ModuleFilterStore, { IModuleFilterStore } from "./module_filters";
|
import ModuleFilterStore, { IModuleFilterStore } from "./module_filters";
|
||||||
@ -16,14 +17,15 @@ import CycleStore, { ICycleStore } from "./cycles";
|
|||||||
import CycleIssueStore, { ICycleIssueStore } from "./cycle_issue";
|
import CycleIssueStore, { ICycleIssueStore } from "./cycle_issue";
|
||||||
import CycleIssueFilterStore, { ICycleIssueFilterStore } from "./cycle_issue_filters";
|
import CycleIssueFilterStore, { ICycleIssueFilterStore } from "./cycle_issue_filters";
|
||||||
import CycleIssueKanBanViewStore, { ICycleIssueKanBanViewStore } from "./cycle_issue_kanban_view";
|
import CycleIssueKanBanViewStore, { ICycleIssueKanBanViewStore } from "./cycle_issue_kanban_view";
|
||||||
import ViewStore, { IViewStore } from "./views";
|
import ProjectViewsStore, { IProjectViewsStore } from "./project_views";
|
||||||
|
import ProjectViewIssuesStore, { IProjectViewIssuesStore } from "./project_view_issues";
|
||||||
|
import ProjectViewFiltersStore, { IProjectViewFiltersStore } from "./project_view_filters";
|
||||||
import IssueFilterStore, { IIssueFilterStore } from "./issue_filters";
|
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 GlobalViewsStore, { IGlobalViewsStore } from "./global_views";
|
import GlobalViewsStore, { IGlobalViewsStore } from "./global_views";
|
||||||
import GlobalViewIssuesStore, { IGlobalViewIssuesStore } from "./global_view_issues";
|
import GlobalViewIssuesStore, { IGlobalViewIssuesStore } from "./global_view_issues";
|
||||||
import WorkspaceFilterStore, { IWorkspaceFilterStore } from "./workspace_filters";
|
|
||||||
import GlobalViewFiltersStore, { IGlobalViewFiltersStore } from "./global_view_filters";
|
import GlobalViewFiltersStore, { IGlobalViewFiltersStore } from "./global_view_filters";
|
||||||
|
|
||||||
enableStaticRendering(typeof window === "undefined");
|
enableStaticRendering(typeof window === "undefined");
|
||||||
@ -49,7 +51,10 @@ export class RootStore {
|
|||||||
cycleIssueFilter: ICycleIssueFilterStore;
|
cycleIssueFilter: ICycleIssueFilterStore;
|
||||||
cycleIssueKanBanView: ICycleIssueKanBanViewStore;
|
cycleIssueKanBanView: ICycleIssueKanBanViewStore;
|
||||||
|
|
||||||
view: IViewStore;
|
projectViews: IProjectViewsStore;
|
||||||
|
projectViewIssues: IProjectViewIssuesStore;
|
||||||
|
projectViewFilters: IProjectViewFiltersStore;
|
||||||
|
|
||||||
issueFilter: IIssueFilterStore;
|
issueFilter: IIssueFilterStore;
|
||||||
issueDetail: IssueViewDetailStore;
|
issueDetail: IssueViewDetailStore;
|
||||||
issueKanBanView: IssueKanBanViewStore;
|
issueKanBanView: IssueKanBanViewStore;
|
||||||
@ -81,7 +86,10 @@ export class RootStore {
|
|||||||
this.cycleIssueFilter = new CycleIssueFilterStore(this);
|
this.cycleIssueFilter = new CycleIssueFilterStore(this);
|
||||||
this.cycleIssueKanBanView = new CycleIssueKanBanViewStore(this);
|
this.cycleIssueKanBanView = new CycleIssueKanBanViewStore(this);
|
||||||
|
|
||||||
this.view = new ViewStore(this);
|
this.projectViews = new ProjectViewsStore(this);
|
||||||
|
this.projectViewIssues = new ProjectViewIssuesStore(this);
|
||||||
|
this.projectViewFilters = new ProjectViewFiltersStore(this);
|
||||||
|
|
||||||
this.issue = new IssueStore(this);
|
this.issue = new IssueStore(this);
|
||||||
this.issueFilter = new IssueFilterStore(this);
|
this.issueFilter = new IssueFilterStore(this);
|
||||||
this.issueDetail = new IssueViewDetailStore(this);
|
this.issueDetail = new IssueViewDetailStore(this);
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
import { action, computed, observable, makeObservable, runInAction } from "mobx";
|
|
||||||
// types
|
|
||||||
import { RootStore } from "./root";
|
|
||||||
// services
|
|
||||||
import { ProjectService } from "services/project.service";
|
|
||||||
import { IssueService } from "services/issue.service";
|
|
||||||
import { ViewService } from "services/views.service";
|
|
||||||
|
|
||||||
export interface IViewStore {
|
|
||||||
loader: boolean;
|
|
||||||
error: any | null;
|
|
||||||
|
|
||||||
viewId: string | null;
|
|
||||||
views: {
|
|
||||||
[project_id: string]: any[];
|
|
||||||
};
|
|
||||||
|
|
||||||
setViewId: (viewSlug: string) => void;
|
|
||||||
|
|
||||||
fetchViews: (workspaceSlug: string, projectSlug: string) => Promise<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewStore implements IViewStore {
|
|
||||||
loader: boolean = false;
|
|
||||||
error: any | null = null;
|
|
||||||
|
|
||||||
viewId: string | null = null;
|
|
||||||
views: {
|
|
||||||
[project_id: string]: any[];
|
|
||||||
} = {};
|
|
||||||
|
|
||||||
// root store
|
|
||||||
rootStore;
|
|
||||||
// services
|
|
||||||
projectService;
|
|
||||||
viewService;
|
|
||||||
|
|
||||||
constructor(_rootStore: RootStore) {
|
|
||||||
makeObservable(this, {
|
|
||||||
loader: observable,
|
|
||||||
error: observable.ref,
|
|
||||||
|
|
||||||
viewId: observable.ref,
|
|
||||||
views: observable.ref,
|
|
||||||
|
|
||||||
// computed
|
|
||||||
projectViews: computed,
|
|
||||||
// actions
|
|
||||||
setViewId: action,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.rootStore = _rootStore;
|
|
||||||
this.projectService = new ProjectService();
|
|
||||||
this.viewService = new ViewService();
|
|
||||||
}
|
|
||||||
|
|
||||||
// computed
|
|
||||||
get projectViews() {
|
|
||||||
if (!this.rootStore.project.projectId) return null;
|
|
||||||
return this.views[this.rootStore.project.projectId] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// actions
|
|
||||||
setViewId = (viewSlug: string) => {
|
|
||||||
this.viewId = viewSlug ?? null;
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchViews = async (workspaceSlug: string, projectId: string) => {
|
|
||||||
try {
|
|
||||||
this.loader = true;
|
|
||||||
this.error = null;
|
|
||||||
|
|
||||||
const viewsResponse = await this.viewService.getViews(workspaceSlug, projectId);
|
|
||||||
|
|
||||||
runInAction(() => {
|
|
||||||
this.views = {
|
|
||||||
...this.views,
|
|
||||||
[projectId]: viewsResponse,
|
|
||||||
};
|
|
||||||
this.loader = false;
|
|
||||||
this.error = null;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to fetch project views in project store", error);
|
|
||||||
this.loader = false;
|
|
||||||
this.error = error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ViewStore;
|
|
19
web/types/views.d.ts
vendored
19
web/types/views.d.ts
vendored
@ -1,4 +1,6 @@
|
|||||||
export interface IView {
|
import { IIssueFilterOptions } from "./view-props";
|
||||||
|
|
||||||
|
export interface IProjectView {
|
||||||
id: string;
|
id: string;
|
||||||
access: string;
|
access: string;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
@ -8,19 +10,8 @@ export interface IView {
|
|||||||
updated_by: string;
|
updated_by: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
query: IQuery;
|
query: IIssueFilterOptions;
|
||||||
query_data: IQuery;
|
query_data: IIssueFilterOptions;
|
||||||
project: string;
|
project: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IQuery {
|
|
||||||
assignees: string[] | null;
|
|
||||||
created_by: string[] | null;
|
|
||||||
labels: string[] | null;
|
|
||||||
priority: string[] | null;
|
|
||||||
state: string[] | null;
|
|
||||||
start_date: string[] | null;
|
|
||||||
target_date: string[] | null;
|
|
||||||
type: "active" | "backlog" | null;
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user