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:
Aaryan Khandelwal 2023-10-11 15:59:17 +05:30 committed by GitHub
parent 9f61d8bc06
commit 00b40fbde4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1637 additions and 836 deletions

View File

@ -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}

View File

@ -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 />

View File

@ -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(

View File

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

View 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>
);
});

View 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>
</>
);
};

View File

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

View File

@ -0,0 +1,39 @@
import { observer } from "mobx-react-lite";
import { DragDropContext, DropResult } from "@hello-pangea/dnd";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// components
import { CalendarChart } from "components/issues";
// types
import { IIssueGroupedStructure } from "store/issue";
export const 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>
);
});

View File

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

View File

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

View File

@ -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>
);
});

View File

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

View File

@ -0,0 +1,55 @@
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
// hooks
import useProjectDetails from "hooks/use-project-details";
// components
import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart";
import { IssueGanttBlock, IssueGanttSidebarBlock, IssuePeekOverview } from "components/issues";
// types
import { IIssueUnGroupedStructure } from "store/issue";
export const 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>
</>
);
});

View File

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

View File

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

View File

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

View File

@ -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 />

View File

@ -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>
);
});

View File

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

View File

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

View File

@ -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>
</>
);
});

View File

@ -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>
); );
}; });

View File

@ -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>
); );
}; });

View File

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

View File

@ -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>
); );
}; });

View File

@ -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>
);
};

View 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>
</>
);
});

View 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);
},
}}
/>
)}
</>
);
});

View File

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

View File

@ -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 (
<> <>

View File

@ -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][]) {

View File

@ -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 <></>;
}; };

View File

@ -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>
); );
}; };

View File

@ -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>
); );
}; };

View File

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

View File

@ -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) => {

View File

@ -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(() => {

View 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;

View 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
View 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;

View File

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

View File

@ -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
View File

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