diff --git a/web/components/command-palette/command-pallette.tsx b/web/components/command-palette/command-pallette.tsx index 3ec9234dc..f8b09c27d 100644 --- a/web/components/command-palette/command-pallette.tsx +++ b/web/components/command-palette/command-pallette.tsx @@ -12,7 +12,7 @@ import { CreateUpdateCycleModal } from "components/cycles"; import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { CreateUpdateModuleModal } from "components/modules"; import { CreateProjectModal } from "components/project"; -import { CreateUpdateViewModal } from "components/views"; +import { CreateUpdateProjectViewModal } from "components/views"; import { CreateUpdatePageModal } from "components/pages"; // helpers import { copyTextToClipboard } from "helpers/string.helper"; @@ -151,10 +151,9 @@ export const CommandPalette: React.FC = observer(() => { setIsOpen={setIsCreateModuleModalOpen} user={user} /> - setIsCreateViewModalOpen(false)} + setIsCreateViewModalOpen(false)} /> { return (
- +
+ +
{activeLayout === "list" ? ( diff --git a/web/components/headers/global-issues.tsx b/web/components/headers/global-issues.tsx index 962eb85b3..2e4aa588a 100644 --- a/web/components/headers/global-issues.tsx +++ b/web/components/headers/global-issues.tsx @@ -38,29 +38,26 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { const { workspaceSlug, globalViewId } = router.query; const { - globalViews: globalViewsStore, globalViewFilters: globalViewFiltersStore, workspaceFilter: workspaceFilterStore, workspace: workspaceStore, project: projectStore, } = useMobxStore(); - const queryData = globalViewId ? globalViewsStore.globalViewDetails[globalViewId.toString()]?.query_data : undefined; - const storedFilters = globalViewId ? globalViewFiltersStore.storedFilters[globalViewId.toString()] : undefined; const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { if (!workspaceSlug || !globalViewId) return; - const newValues = queryData?.filters?.[key] ?? []; + const newValues = storedFilters?.[key] ?? []; if (Array.isArray(value)) { value.forEach((val) => { if (!newValues.includes(val)) newValues.push(val); }); } 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); } @@ -68,7 +65,7 @@ export const GlobalIssuesHeader: React.FC = observer((props) => { [key]: newValues, }); }, - [globalViewId, globalViewFiltersStore, queryData, workspaceSlug] + [globalViewId, globalViewFiltersStore, storedFilters, workspaceSlug] ); const handleDisplayFiltersUpdate = useCallback( diff --git a/web/components/headers/index.ts b/web/components/headers/index.ts index 874d50677..f658ec366 100644 --- a/web/components/headers/index.ts +++ b/web/components/headers/index.ts @@ -1,3 +1,5 @@ export * from "./global-issues"; export * from "./module-issues"; export * from "./project-issues"; +export * from "./project-view-issues"; +export * from "./project-views"; diff --git a/web/components/headers/project-view-issues.tsx b/web/components/headers/project-view-issues.tsx new file mode 100644 index 000000000..0b7134dfd --- /dev/null +++ b/web/components/headers/project-view-issues.tsx @@ -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) => { + if (!workspaceSlug || !projectId) return; + + issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { + display_filters: { + ...updatedDisplayFilter, + }, + }); + }, + [issueFilterStore, projectId, workspaceSlug] + ); + + const handleDisplayPropertiesUpdate = useCallback( + (property: Partial) => { + if (!workspaceSlug || !projectId) return; + + issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property); + }, + [issueFilterStore, projectId, workspaceSlug] + ); + + return ( +
+ handleLayoutChange(layout)} + selectedLayout={activeLayout} + /> + + m.member)} + states={projectStore.states?.[projectId?.toString() ?? ""] ?? undefined} + /> + + + + +
+ ); +}); diff --git a/web/components/headers/project-views.tsx b/web/components/headers/project-views.tsx new file mode 100644 index 000000000..45f47e551 --- /dev/null +++ b/web/components/headers/project-views.tsx @@ -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 ( + <> + setCreateViewModal(false)} /> +
+ { + const e = new KeyboardEvent("keydown", { key: "v" }); + document.dispatchEvent(e); + }} + > + + Create View + +
+ + ); +}; diff --git a/web/components/issues/issue-layouts/calendar/index.ts b/web/components/issues/issue-layouts/calendar/index.ts index f4ed9faa1..6cb0f51b0 100644 --- a/web/components/issues/issue-layouts/calendar/index.ts +++ b/web/components/issues/issue-layouts/calendar/index.ts @@ -5,6 +5,7 @@ export * from "./day-tile"; export * from "./header"; export * from "./issue-blocks"; export * from "./module-root"; +export * from "./project-view-root"; export * from "./root"; export * from "./week-days"; export * from "./week-header"; diff --git a/web/components/issues/issue-layouts/calendar/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/project-view-root.tsx new file mode 100644 index 000000000..76a0ac525 --- /dev/null +++ b/web/components/issues/issue-layouts/calendar/project-view-root.tsx @@ -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 ( +
+ + + +
+ ); +}); diff --git a/web/components/issues/issue-layouts/cycle-layout-root.tsx b/web/components/issues/issue-layouts/cycle-layout-root.tsx index 17f31aa37..58e0a0fc6 100644 --- a/web/components/issues/issue-layouts/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/cycle-layout-root.tsx @@ -6,8 +6,7 @@ import useSWR from "swr"; // mobx react lite import { observer } from "mobx-react-lite"; // components -import { CycleListLayout } from "./list/cycle-root"; -import { CycleKanBanLayout } from "./kanban/cycle-root"; +import { CycleKanBanLayout, CycleListLayout } from "components/issues"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/index.ts b/web/components/issues/issue-layouts/filters/applied-filters/index.ts index 65f7248f4..4a498fdfd 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/index.ts +++ b/web/components/issues/issue-layouts/filters/applied-filters/index.ts @@ -5,6 +5,7 @@ export * from "./label"; export * from "./members"; export * from "./module-root"; export * from "./priority"; +export * from "./project-view-root"; export * from "./project"; export * from "./root"; export * from "./state"; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/project-view-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/project-view-root.tsx new file mode 100644 index 000000000..8090f934f --- /dev/null +++ b/web/components/issues/issue-layouts/filters/applied-filters/project-view-root.tsx @@ -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 ( +
+ m.member)} + states={projectStore.states?.[projectId?.toString() ?? ""]} + /> + {storedFilters && viewDetails && areFiltersDifferent(storedFilters, viewDetails.query_data ?? {}) && ( + + Update view + + )} +
+ ); +}); diff --git a/web/components/issues/issue-layouts/gantt/index.ts b/web/components/issues/issue-layouts/gantt/index.ts index 11a2fea7a..c1a69905c 100644 --- a/web/components/issues/issue-layouts/gantt/index.ts +++ b/web/components/issues/issue-layouts/gantt/index.ts @@ -1,3 +1,4 @@ export * from "./blocks"; export * from "./module-root"; +export * from "./project-view-root"; export * from "./root"; diff --git a/web/components/issues/issue-layouts/gantt/project-view-root.tsx b/web/components/issues/issue-layouts/gantt/project-view-root.tsx new file mode 100644 index 000000000..0f3642a16 --- /dev/null +++ b/web/components/issues/issue-layouts/gantt/project-view-root.tsx @@ -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 ( + <> + +
+ { + // 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} + /> +
+ + ); +}); diff --git a/web/components/issues/issue-layouts/global-views-all-layouts.tsx b/web/components/issues/issue-layouts/global-view-all-layouts.tsx similarity index 100% rename from web/components/issues/issue-layouts/global-views-all-layouts.tsx rename to web/components/issues/issue-layouts/global-view-all-layouts.tsx diff --git a/web/components/issues/issue-layouts/index.ts b/web/components/issues/issue-layouts/index.ts index 9307a19f7..7dfba5d76 100644 --- a/web/components/issues/issue-layouts/index.ts +++ b/web/components/issues/issue-layouts/index.ts @@ -7,10 +7,15 @@ export * from "./calendar"; export * from "./gantt"; export * from "./kanban"; export * from "./spreadsheet"; -export * from "./global-views-all-layouts"; + +// global view layout +export * from "./global-view-all-layouts"; // cycle root layout export * from "./cycle-layout-root"; // module root layout export * from "./module-all-layouts"; + +// project view layout +export * from "./project-view-all-layouts"; diff --git a/web/components/issues/issue-layouts/kanban/index.ts b/web/components/issues/issue-layouts/kanban/index.ts index 1efe34c51..a46683af6 100644 --- a/web/components/issues/issue-layouts/kanban/index.ts +++ b/web/components/issues/issue-layouts/kanban/index.ts @@ -1 +1,3 @@ +export * from "./cycle-root"; +export * from "./module-root"; export * from "./root"; diff --git a/web/components/issues/issue-layouts/list/index.ts b/web/components/issues/issue-layouts/list/index.ts index 1efe34c51..a46683af6 100644 --- a/web/components/issues/issue-layouts/list/index.ts +++ b/web/components/issues/issue-layouts/list/index.ts @@ -1 +1,3 @@ +export * from "./cycle-root"; +export * from "./module-root"; export * from "./root"; diff --git a/web/components/issues/issue-layouts/module-all-layouts.tsx b/web/components/issues/issue-layouts/module-all-layouts.tsx index 6fb138c06..57768ab27 100644 --- a/web/components/issues/issue-layouts/module-all-layouts.tsx +++ b/web/components/issues/issue-layouts/module-all-layouts.tsx @@ -10,10 +10,10 @@ import { ModuleAppliedFiltersRoot, ModuleCalendarLayout, ModuleGanttLayout, + ModuleKanBanLayout, + ModuleListLayout, ModuleSpreadsheetLayout, } 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(() => { const router = useRouter(); @@ -30,11 +30,11 @@ export const ModuleAllLayouts: React.FC = observer(() => { moduleFilter: moduleIssueFilterStore, } = useMobxStore(); - useSWR(workspaceSlug && projectId && moduleId ? `CYCLE_ISSUES` : null, async () => { + useSWR(workspaceSlug && projectId && moduleId ? `MODULE_INFORMATION_${moduleId.toString()}` : null, async () => { if (workspaceSlug && projectId && moduleId) { // fetching the project display filters and display properties await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId); - // fetching the cycle filters + // fetching the module filters await moduleIssueFilterStore.fetchModuleFilters(workspaceSlug, projectId, moduleId); // fetching the project state, labels and members @@ -42,7 +42,7 @@ export const ModuleAllLayouts: React.FC = observer(() => { await projectStore.fetchProjectLabels(workspaceSlug, projectId); await projectStore.fetchProjectMembers(workspaceSlug, projectId); - // fetching the cycle issues + // fetching the module issues await moduleIssueStore.fetchIssues(workspaceSlug, projectId, moduleId); } }); @@ -51,7 +51,9 @@ export const ModuleAllLayouts: React.FC = observer(() => { return (
- +
+ +
{activeLayout === "list" ? ( diff --git a/web/components/issues/issue-layouts/project-view-all-layouts.tsx b/web/components/issues/issue-layouts/project-view-all-layouts.tsx new file mode 100644 index 000000000..e524081ed --- /dev/null +++ b/web/components/issues/issue-layouts/project-view-all-layouts.tsx @@ -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 ( +
+ +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+
+ ); +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/index.ts b/web/components/issues/issue-layouts/spreadsheet/index.ts index d468cccad..0014c5b8e 100644 --- a/web/components/issues/issue-layouts/spreadsheet/index.ts +++ b/web/components/issues/issue-layouts/spreadsheet/index.ts @@ -1,2 +1,3 @@ export * from "./module-root"; +export * from "./project-view-root"; export * from "./root"; diff --git a/web/components/issues/issue-layouts/spreadsheet/module-root.tsx b/web/components/issues/issue-layouts/spreadsheet/module-root.tsx index 17abf5e33..bf02efafe 100644 --- a/web/components/issues/issue-layouts/spreadsheet/module-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/module-root.tsx @@ -24,7 +24,7 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => { const [expandedIssues, setExpandedIssues] = useState([]); const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId } = router.query; + const { workspaceSlug, projectId } = router.query; const { user } = useUser(); const { projectDetails } = useProjectDetails(); @@ -47,8 +47,6 @@ export const ModuleSpreadsheetLayout: React.FC = observer(() => { [issueFilterStore, projectId, workspaceSlug] ); - const type = cycleId ? "cycle" : moduleId ? "module" : "issue"; - const columnData = SPREADSHEET_COLUMN.map((column) => ({ ...column, 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" style={{ gridTemplateColumns }} > - {type === "issue" ? ( - - ) : ( - isAllowed && ( - - - Add Issue - - } - position="left" - optionsClassName="left-5 !w-36" - noBorder - > - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} + {isAllowed && ( + - Create new - - {true && {}}>Add an existing issue} - - ) + + Add Issue + + } + position="left" + optionsClassName="left-5 !w-36" + noBorder + > + { + const e = new KeyboardEvent("keydown", { key: "c" }); + document.dispatchEvent(e); + }} + > + Create new + + {true && {}}>Add an existing issue} + )}
diff --git a/web/components/issues/issue-layouts/spreadsheet/project-view-root.tsx b/web/components/issues/issue-layouts/spreadsheet/project-view-root.tsx new file mode 100644 index 000000000..8d7670961 --- /dev/null +++ b/web/components/issues/issue-layouts/spreadsheet/project-view-root.tsx @@ -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([]); + + 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) => { + 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 ( + <> + +
+
+ +
+ {issues ? ( +
+ {(issues as IIssueUnGroupedStructure).map((issue: IIssue, index) => ( + {}} + 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, + }} + /> + ))} +
+ +
+
+ ) : ( + + )} +
+ + ); +}); diff --git a/web/components/views/delete-view-modal.tsx b/web/components/views/delete-view-modal.tsx index 61c627430..68b143616 100644 --- a/web/components/views/delete-view-modal.tsx +++ b/web/components/views/delete-view-modal.tsx @@ -1,13 +1,10 @@ import React, { useState } from "react"; - import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// headless ui +import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// services -import viewsService from "services/views.service"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; // ui @@ -15,41 +12,39 @@ import { DangerButton, SecondaryButton } from "components/ui"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; // types -import type { ICurrentUserResponse, IView } from "types"; -// fetch-keys -import { VIEWS_LIST } from "constants/fetch-keys"; +import { IProjectView } from "types"; type Props = { + data: IProjectView; isOpen: boolean; - setIsOpen: React.Dispatch>; - data: IView | null; - user: ICurrentUserResponse | undefined; + onClose: () => void; }; -export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user }) => { +export const DeleteProjectViewModal: React.FC = observer((props) => { + const { data, isOpen, onClose } = props; + const [isDeleteLoading, setIsDeleteLoading] = useState(false); const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { projectViews: projectViewsStore } = useMobxStore(); + const { setToastAlert } = useToast(); const handleClose = () => { - setIsOpen(false); + onClose(); setIsDeleteLoading(false); }; - const handleDeletion = async () => { + const handleDeleteView = async () => { + if (!workspaceSlug || !projectId) return; + setIsDeleteLoading(true); - if (!workspaceSlug || !data || !projectId) return; - await viewsService - .deleteView(workspaceSlug as string, projectId as string, data.id, user) + await projectViewsStore + .deleteView(workspaceSlug.toString(), projectId.toString(), data.id) .then(() => { - mutate(VIEWS_LIST(projectId as string), (views) => - views?.filter((view) => view.id !== data.id) - ); - handleClose(); setToastAlert({ @@ -58,13 +53,13 @@ export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user message: "View deleted successfully.", }); }) - .catch(() => { + .catch(() => setToastAlert({ type: "error", title: "Error!", message: "View could not be deleted. Please try again.", - }); - }) + }) + ) .finally(() => { setIsDeleteLoading(false); }); @@ -100,26 +95,17 @@ export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user
-
- + Delete View

Are you sure you want to delete view-{" "} - - {data?.name} - - ? All of the data related to the view will be permanently removed. This - action cannot be undone. + {data?.name}? All of the + data related to the view will be permanently removed. This action cannot be undone.

@@ -127,7 +113,7 @@ export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user
Cancel - + {isDeleteLoading ? "Deleting..." : "Delete"}
@@ -138,4 +124,4 @@ export const DeleteViewModal: React.FC = ({ isOpen, data, setIsOpen, user ); -}; +}); diff --git a/web/components/views/form.tsx b/web/components/views/form.tsx index 0856bafa4..9ec5b16f7 100644 --- a/web/components/views/form.tsx +++ b/web/components/views/form.tsx @@ -1,77 +1,48 @@ import { useEffect } from "react"; +import { observer } from "mobx-react-lite"; +import { Controller, useForm } from "react-hook-form"; -import { useRouter } from "next/router"; - -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"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // components -import { FiltersList } from "components/core"; -import { SelectFilters } from "components/views"; +import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues"; // ui import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui"; -// helpers -import { checkIfArraysHaveSameElements } from "helpers/array.helper"; -import { getStatesList } from "helpers/state.helper"; // types -import { IQuery, IView } from "types"; -import issuesService from "services/issue.service"; -// fetch-keys -import { PROJECT_ISSUE_LABELS, STATES_LIST } from "constants/fetch-keys"; +import { IProjectView } from "types"; +// constants +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; type Props = { - handleFormSubmit: (values: IView) => Promise; + data?: IProjectView | null; handleClose: () => void; - status: boolean; - data?: IView | null; - preLoadedData?: Partial | null; + handleFormSubmit: (values: IProjectView) => Promise; + preLoadedData?: Partial | null; }; -const defaultValues: Partial = { +const defaultValues: Partial = { name: "", description: "", }; -export const ViewForm: React.FC = ({ handleFormSubmit, handleClose, status, data, preLoadedData }) => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; +export const ProjectViewForm: React.FC = observer(({ handleFormSubmit, handleClose, data, preLoadedData }) => { + const { project: projectStore } = useMobxStore(); const { - register, + control, formState: { errors, isSubmitting }, handleSubmit, + register, reset, - watch, setValue, - } = useForm({ + watch, + } = useForm({ defaultValues, }); - const filters = watch("query"); - const { data: stateGroups } = useSWR( - 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 selectedFilters = watch("query_data"); - const { data: labels } = useSWR( - 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) => { + const handleCreateUpdateView = async (formData: IProjectView) => { await handleFormSubmit(formData); reset({ @@ -80,16 +51,9 @@ export const ViewForm: React.FC = ({ handleFormSubmit, handleClose, statu }; const clearAllFilters = () => { - setValue("query", { - assignees: null, - created_by: null, - labels: null, - priority: null, - state: null, - start_date: null, - target_date: null, - type: null, - }); + if (!selectedFilters) return; + + setValue("query_data", {}); }; useEffect(() => { @@ -100,16 +64,10 @@ export const ViewForm: React.FC = ({ handleFormSubmit, handleClose, statu }); }, [data, preLoadedData, reset]); - useEffect(() => { - if (status && data) { - setValue("query", data.query_data); - } - }, [data, status, setValue]); - return (
-

{status ? "Update" : "Create"} View

+

{data ? "Update" : "Create"} View

= ({ handleFormSubmit, handleClose, statu />
- { - const key = option.key as keyof typeof filters; + ( + + { + const newValues = filters?.[key] ?? []; - if (key === "start_date" || key === "target_date") { - const valueExists = checkIfArraysHaveSameElements(filters?.[key] ?? [], option.value); + if (Array.isArray(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", { - ...filters, - [key]: valueExists ? null : option.value, - } as IQuery); - } else { - if (!filters?.[key]?.includes(option.value)) - setValue("query", { - ...filters, - [key]: [...((filters?.[key] as any[]) ?? []), option.value], - }); - else { - setValue("query", { - ...filters, - [key]: (filters?.[key] as any[])?.filter((item) => item !== option.value), - }); - } - } - }} - /> -
-
- m.member)} - states={states} - clearAllFilters={clearAllFilters} - setFilters={(query: any) => { - setValue("query", { - ...filters, - ...query, - }); - }} + onChange({ + ...filters, + [key]: newValues, + }); + }} + layoutDisplayFiltersOptions={ISSUE_DISPLAY_FILTERS_BY_LAYOUT.issues.list} + labels={projectStore.projectLabels ?? undefined} + members={projectStore.projectMembers?.map((m) => m.member) ?? undefined} + states={projectStore.projectStatesByGroups ?? undefined} + /> + + )} />
+ {selectedFilters && Object.keys(selectedFilters).length > 0 && ( +
+ {}} + labels={projectStore.projectLabels ?? undefined} + members={projectStore.projectMembers?.map((m) => m.member) ?? undefined} + states={projectStore.projectStatesByGroups ?? undefined} + /> +
+ )}
Cancel - {status + {data ? isSubmitting ? "Updating View..." : "Update View" @@ -200,4 +160,4 @@ export const ViewForm: React.FC = ({ handleFormSubmit, handleClose, statu
); -}; +}); diff --git a/web/components/views/index.ts b/web/components/views/index.ts index b5c07cf4a..0b917e3c9 100644 --- a/web/components/views/index.ts +++ b/web/components/views/index.ts @@ -3,6 +3,7 @@ export * from "./form"; export * from "./gantt-chart"; export * from "./modal"; export * from "./select-filters"; -export * from "./single-view-item"; +export * from "./view-list-item"; export * from "./signin"; +export * from "./views-list"; export * from "./workspace-dashboard"; diff --git a/web/components/views/modal.tsx b/web/components/views/modal.tsx index c1ff54231..cdcc37b18 100644 --- a/web/components/views/modal.tsx +++ b/web/components/views/modal.tsx @@ -1,110 +1,77 @@ import React from "react"; - import { useRouter } from "next/router"; - -import { mutate } from "swr"; - -// headless ui +import { observer } from "mobx-react-lite"; import { Dialog, Transition } from "@headlessui/react"; -// services -import viewsService from "services/views.service"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // hooks import useToast from "hooks/use-toast"; // components -import { ViewForm } from "components/views"; +import { ProjectViewForm } from "components/views"; // types -import { ICurrentUserResponse, IView } from "types"; -// fetch-keys -import { VIEWS_LIST } from "constants/fetch-keys"; +import { IProjectView } from "types"; type Props = { + data?: IProjectView | null; isOpen: boolean; - handleClose: () => void; - data?: IView | null; - preLoadedData?: Partial | null; - user: ICurrentUserResponse | undefined; + onClose: () => void; + preLoadedData?: Partial | null; }; -export const CreateUpdateViewModal: React.FC = ({ - isOpen, - handleClose, - data, - preLoadedData, - user, -}) => { +export const CreateUpdateProjectViewModal: React.FC = observer((props) => { + const { data, isOpen, onClose, preLoadedData } = props; + const router = useRouter(); const { workspaceSlug, projectId } = router.query; + const { projectViews: projectViewsStore } = useMobxStore(); + const { setToastAlert } = useToast(); - const onClose = () => { - handleClose(); + const handleClose = () => { + onClose(); }; - const createView = async (payload: IView) => { - 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( - 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) => { + const createView = async (formData: IProjectView) => { 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); else await updateView(formData); }; @@ -136,11 +103,10 @@ export const CreateUpdateViewModal: React.FC = ({ leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - @@ -150,4 +116,4 @@ export const CreateUpdateViewModal: React.FC = ({ ); -}; +}); diff --git a/web/components/views/single-view-item.tsx b/web/components/views/single-view-item.tsx deleted file mode 100644 index 70f07a416..000000000 --- a/web/components/views/single-view-item.tsx +++ /dev/null @@ -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 = ({ view, handleEditView, handleDeleteView }) => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { setToastAlert } = useToast(); - - const handleAddToFavorites = () => { - if (!workspaceSlug || !projectId || !view) return; - - mutate( - 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( - 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 ( -
- - -
-
-
- -
-
-

- {truncateText(view.name, 75)} -

- {view?.description && ( -

{view.description}

- )} -
-
-
-
-

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

- - {view.is_favorite ? ( - - ) : ( - - )} - - { - e.preventDefault(); - e.stopPropagation(); - handleEditView(); - }} - > - - - Edit View - - - { - e.preventDefault(); - e.stopPropagation(); - handleDeleteView(); - }} - > - - - Delete View - - - -
-
-
-
- -
- ); -}; diff --git a/web/components/views/view-list-item.tsx b/web/components/views/view-list-item.tsx new file mode 100644 index 000000000..9417213cb --- /dev/null +++ b/web/components/views/view-list-item.tsx @@ -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 = 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 ( + <> + setDeleteViewModal(false)} /> + + + ); +}); diff --git a/web/components/views/views-list.tsx b/web/components/views/views-list.tsx new file mode 100644 index 000000000..9ba37dc09 --- /dev/null +++ b/web/components/views/views-list.tsx @@ -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 ( + + + + + + ); + + const filteredViewsList = viewsList.filter((v) => v.name.toLowerCase().includes(query.toLowerCase())); + + return ( + <> + {viewsList.length > 0 ? ( +
+
+
+ + setQuery(e.target.value)} + placeholder="Search" + mode="trueTransparent" + /> +
+
+ {filteredViewsList.length > 0 ? ( + filteredViewsList.map((view) => ) + ) : ( +

No results found

+ )} +
+ ) : ( + , + text: "New View", + onClick: () => { + const e = new KeyboardEvent("keydown", { + key: "v", + }); + document.dispatchEvent(e); + }, + }} + /> + )} + + ); +}); diff --git a/web/components/workspace/views/form.tsx b/web/components/workspace/views/form.tsx index ffe5e2b79..8af899e52 100644 --- a/web/components/workspace/views/form.tsx +++ b/web/components/workspace/views/form.tsx @@ -5,8 +5,6 @@ import { Controller, useForm } from "react-hook-form"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -// hooks -import useWorkspaceMembers from "hooks/use-workspace-members"; // components import { AppliedFiltersList, FilterSelection, FiltersDropdown } from "components/issues"; // ui @@ -48,10 +46,6 @@ export const WorkspaceViewForm: React.FC = observer((props) => { defaultValues, }); - const { workspaceMembers } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - - const memberOptions = workspaceMembers?.map((m) => m.member); - const handleCreateUpdateView = async (formData: Partial) => { await handleFormSubmit(formData); @@ -76,12 +70,6 @@ export const WorkspaceViewForm: React.FC = observer((props) => { setValue("query_data.filters", {}); }; - useEffect(() => { - if (!data) return; - - reset({ ...data }); - }, [data, reset]); - return (
@@ -156,7 +144,7 @@ export const WorkspaceViewForm: React.FC = observer((props) => { handleClearAllFilters={clearAllFilters} handleRemoveFilter={() => {}} labels={workspaceStore.workspaceLabels ?? undefined} - members={memberOptions} + members={workspaceStore.workspaceMembers?.map((m) => m.member) ?? undefined} states={undefined} />
diff --git a/web/components/workspace/views/view-list-item.tsx b/web/components/workspace/views/view-list-item.tsx index e3dc1d641..608e432a9 100644 --- a/web/components/workspace/views/view-list-item.tsx +++ b/web/components/workspace/views/view-list-item.tsx @@ -11,6 +11,7 @@ import { CustomMenu } from "components/ui"; import { PencilIcon, Sparkles, TrashIcon } from "lucide-react"; // helpers import { truncateText } from "helpers/string.helper"; +import { calculateTotalFilters } from "helpers/filter.helper"; // types import { IWorkspaceView } from "types/workspace-views"; @@ -25,18 +26,7 @@ export const GlobalViewListItem: React.FC = observer((props) => { const router = useRouter(); const { workspaceSlug } = router.query; - const totalFilters = - 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; + const totalFilters = calculateTotalFilters(view.query_data.filters ?? {}); return ( <> diff --git a/web/helpers/filter.helper.ts b/web/helpers/filter.helper.ts index 8c392b5ae..001c07035 100644 --- a/web/helpers/filter.helper.ts +++ b/web/helpers/filter.helper.ts @@ -1,6 +1,19 @@ // 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 export const areFiltersDifferent = (filtersSet1: IIssueFilterOptions, filtersSet2: IIssueFilterOptions) => { for (const [key, value] of Object.entries(filtersSet1) as [keyof IIssueFilterOptions, string[] | null][]) { diff --git a/web/lib/mobx/store-init.tsx b/web/lib/mobx/store-init.tsx index 9383376bf..bc480f4d2 100644 --- a/web/lib/mobx/store-init.tsx +++ b/web/lib/mobx/store-init.tsx @@ -13,12 +13,13 @@ const MobxStoreInit = () => { project: projectStore, module: moduleStore, globalViews: globalViewsStore, + projectViews: projectViewsStore, } = useMobxStore(); // theme const { setTheme } = useTheme(); // router const router = useRouter(); - const { workspaceSlug, projectId, moduleId, globalViewId } = router.query; + const { workspaceSlug, projectId, moduleId, globalViewId, viewId } = router.query; useEffect(() => { // sidebar collapsed toggle @@ -49,7 +50,19 @@ const MobxStoreInit = () => { if (projectId) projectStore.setProjectId(projectId.toString()); if (moduleId) moduleStore.setModuleId(moduleId.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 <>; }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx index 463a3c7c7..4b3d51f61 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/[viewId].tsx @@ -7,15 +7,12 @@ import projectService from "services/project.service"; import viewsService from "services/views.service"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; -// contexts -import { IssueViewContextProvider } from "contexts/issue-view.context"; // components -import { IssuesFilterView, IssuesView } from "components/core"; +import { ProjectViewAllLayouts } from "components/issues"; // ui -import { CustomMenu, EmptyState, PrimaryButton } from "components/ui"; +import { CustomMenu, EmptyState } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // icons -import { PlusIcon } from "@heroicons/react/24/outline"; import { StackedLayersIcon } from "components/icons"; // images 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"; // fetch-keys import { PROJECT_DETAILS, VIEWS_LIST, VIEW_DETAILS } from "constants/fetch-keys"; +import { ProjectViewIssuesHeader } from "components/headers"; const SingleView: React.FC = () => { const router = useRouter(); @@ -46,71 +44,53 @@ const SingleView: React.FC = () => { ); return ( - - - - - } - left={ - - - {viewDetails?.name && truncateText(viewDetails.name, 40)} - - } - className="ml-1.5" - width="auto" - > - {views?.map((view) => ( - - {truncateText(view.name, 40)} - - ))} - - } - right={ -
- - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} - > - - Add Issue - -
- } - > - {error ? ( - router.push(`/${workspaceSlug}/projects/${projectId}/views`), - }} + + - ) : ( -
- -
- )} -
-
+ + } + left={ + + + {viewDetails?.name && truncateText(viewDetails.name, 40)} + + } + className="ml-1.5" + width="auto" + > + {views?.map((view) => ( + + {truncateText(view.name, 40)} + + ))} + + } + right={} + > + {error ? ( + router.push(`/${workspaceSlug}/projects/${projectId}/views`), + }} + /> + ) : ( + + )} + ); }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx index bbf34dff2..9d1ba1e76 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/views/index.tsx @@ -1,138 +1,60 @@ -import React, { useState } from "react"; +import React from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; -// hooks -import useUserAuth from "hooks/use-user-auth"; -// services -import viewsService from "services/views.service"; -import projectService from "services/project.service"; +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; +// components +import { ProjectViewsHeader } from "components/headers"; // ui 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 -import { PrimaryButton, Loader, EmptyState } from "components/ui"; -import { DeleteViewModal, CreateUpdateViewModal, SingleViewItem } from "components/views"; +import { ProjectViewsList } from "components/views"; // types -import { IView } from "types"; import type { NextPage } from "next"; +import { Search } from "lucide-react"; +import { Input } from "components/ui"; const ProjectViews: NextPage = () => { - const [createUpdateViewModal, setCreateUpdateViewModal] = useState(false); - const [selectedViewToUpdate, setSelectedViewToUpdate] = useState(null); - - const [deleteViewModal, setDeleteViewModal] = useState(false); - const [selectedViewToDelete, setSelectedViewToDelete] = useState(null); - const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { user } = useUserAuth(); + const { project: projectStore, projectViews: projectViewsStore } = useMobxStore(); - const { data: activeProject } = useSWR( - workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, - workspaceSlug && projectId ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null + useSWR( + workspaceSlug && projectId ? `PROJECT_DETAILS_${projectId.toString()}` : null, + workspaceSlug && projectId + ? () => projectStore.fetchProjectDetails(workspaceSlug.toString(), projectId.toString()) + : null ); - const { data: views } = useSWR( - workspaceSlug && projectId ? VIEWS_LIST(projectId as string) : null, - workspaceSlug && projectId ? () => viewsService.getViews(workspaceSlug as string, projectId as string) : null + useSWR( + workspaceSlug && projectId ? `PROJECT_VIEWS_LIST_${workspaceSlug.toString()}_${projectId.toString()}` : null, + workspaceSlug && projectId + ? () => projectViewsStore.fetchAllViews(workspaceSlug.toString(), projectId.toString()) + : null ); - const handleEditView = (view: IView) => { - setSelectedViewToUpdate(view); - setCreateUpdateViewModal(true); - }; - - const handleDeleteView = (view: IView) => { - setSelectedViewToDelete(view); - setDeleteViewModal(true); - }; + const projectDetails = + workspaceSlug && projectId + ? projectStore.getProjectById(workspaceSlug.toString(), projectId.toString()) + : undefined; return ( - + } - right={ -
- { - const e = new KeyboardEvent("keydown", { key: "v" }); - document.dispatchEvent(e); - }} - > - - Create View - -
- } + right={} > - setCreateUpdateViewModal(false)} - data={selectedViewToUpdate} - user={user} - /> - - {views ? ( - views.length > 0 ? ( -
-

Views

-
- {views.map((view) => ( - handleEditView(view)} - handleDeleteView={() => handleDeleteView(view)} - /> - ))} -
-
- ) : ( - , - text: "New View", - onClick: () => { - const e = new KeyboardEvent("keydown", { - key: "v", - }); - document.dispatchEvent(e); - }, - }} - /> - ) - ) : ( - - - - - - )} +
); }; diff --git a/web/pages/[workspaceSlug]/workspace-views/index.tsx b/web/pages/[workspaceSlug]/workspace-views/index.tsx index 3d9285b76..9b46cdf9a 100644 --- a/web/pages/[workspaceSlug]/workspace-views/index.tsx +++ b/web/pages/[workspaceSlug]/workspace-views/index.tsx @@ -17,8 +17,6 @@ import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import type { NextPage } from "next"; // constants import { DEFAULT_GLOBAL_VIEWS_LIST } from "constants/workspace"; -// fetch-keys -import { GLOBAL_VIEWS_LIST } from "constants/fetch-keys"; const WorkspaceViews: NextPage = () => { const [query, setQuery] = useState(""); @@ -29,7 +27,7 @@ const WorkspaceViews: NextPage = () => { const { globalViews: globalViewsStore } = useMobxStore(); useSWR( - workspaceSlug ? GLOBAL_VIEWS_LIST(workspaceSlug.toString()) : null, + workspaceSlug ? `GLOBAL_VIEWS_LIST_${workspaceSlug.toString()}` : null, workspaceSlug ? () => globalViewsStore.fetchAllGlobalViews(workspaceSlug.toString()) : null ); diff --git a/web/services/views.service.ts b/web/services/views.service.ts index 89e93480a..e17c8c70d 100644 --- a/web/services/views.service.ts +++ b/web/services/views.service.ts @@ -1,7 +1,7 @@ import APIService from "services/api.service"; import trackEventServices from "services/track_event.service"; // types -import { IView } from "types/views"; +import { IProjectView } from "types/views"; import { ICurrentUserResponse } from "types"; // helpers import { API_BASE_URL } from "helpers/common.helper"; @@ -11,12 +11,7 @@ export class ViewService extends APIService { super(API_BASE_URL); } - async createView( - workspaceSlug: string, - projectId: string, - data: IView, - user: ICurrentUserResponse | undefined - ): Promise { + async createView(workspaceSlug: string, projectId: string, data: Partial, user: any): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`, data) .then((response) => { 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 { - 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( workspaceSlug: string, projectId: string, viewId: string, - data: Partial, - user: ICurrentUserResponse | undefined + data: Partial, + user: any ): Promise { return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`, data) .then((response) => { @@ -61,12 +39,7 @@ export class ViewService extends APIService { }); } - async deleteView( - workspaceSlug: string, - projectId: string, - viewId: string, - user: ICurrentUserResponse | undefined - ): Promise { + async deleteView(workspaceSlug: string, projectId: string, viewId: string, user: any): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`) .then((response) => { trackEventServices.trackViewEvent(response?.data, "VIEW_DELETE", user); @@ -77,7 +50,7 @@ export class ViewService extends APIService { }); } - async getViews(workspaceSlug: string, projectId: string): Promise { + async getViews(workspaceSlug: string, projectId: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/`) .then((response) => response?.data) .catch((error) => { @@ -85,7 +58,7 @@ export class ViewService extends APIService { }); } - async getViewDetails(workspaceSlug: string, projectId: string, viewId: string): Promise { + async getViewDetails(workspaceSlug: string, projectId: string, viewId: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/views/${viewId}/`) .then((response) => response?.data) .catch((error) => { diff --git a/web/store/project.ts b/web/store/project.ts index 558498b77..45fb40bcc 100644 --- a/web/store/project.ts +++ b/web/store/project.ts @@ -220,8 +220,8 @@ class ProjectStore implements IProjectStore { } // actions - setProjectId = (projectSlug: string) => { - this.projectId = projectSlug ?? null; + setProjectId = (projectId: string) => { + this.projectId = projectId ?? null; }; setSearchQuery = (query: string) => { @@ -311,15 +311,15 @@ class ProjectStore implements IProjectStore { return estimateInfo; }; - fetchProjectStates = async (workspaceSlug: string, projectSlug: string) => { + fetchProjectStates = async (workspaceSlug: string, projectId: string) => { try { this.loader = true; this.error = null; - const stateResponse = await this.stateService.getStates(workspaceSlug, projectSlug); + const stateResponse = await this.stateService.getStates(workspaceSlug, projectId); const _states = { ...this.states, - [projectSlug]: stateResponse, + [projectId]: stateResponse, }; runInAction(() => { @@ -357,15 +357,15 @@ class ProjectStore implements IProjectStore { } }; - fetchProjectMembers = async (workspaceSlug: string, projectSlug: string) => { + fetchProjectMembers = async (workspaceSlug: string, projectId: string) => { try { this.loader = true; this.error = null; - const membersResponse = await this.projectService.projectMembers(workspaceSlug, projectSlug); + const membersResponse = await this.projectService.projectMembers(workspaceSlug, projectId); const _members = { ...this.members, - [projectSlug]: membersResponse, + [projectId]: membersResponse, }; runInAction(() => { @@ -380,15 +380,15 @@ class ProjectStore implements IProjectStore { } }; - fetchProjectEstimates = async (workspaceSlug: string, projectSlug: string) => { + fetchProjectEstimates = async (workspaceSlug: string, projectId: string) => { try { this.loader = true; this.error = null; - const estimatesResponse = await this.estimateService.getEstimatesList(workspaceSlug, projectSlug); + const estimatesResponse = await this.estimateService.getEstimatesList(workspaceSlug, projectId); const _estimates = { ...this.estimates, - [projectSlug]: estimatesResponse, + [projectId]: estimatesResponse, }; runInAction(() => { @@ -497,12 +497,12 @@ class ProjectStore implements IProjectStore { } }; - leaveProject = async (workspaceSlug: string, projectSlug: string) => { + leaveProject = async (workspaceSlug: string, projectId: string) => { try { this.loader = true; 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); runInAction(() => { diff --git a/web/store/project_view_filters.ts b/web/store/project_view_filters.ts new file mode 100644 index 000000000..d7b39e9bf --- /dev/null +++ b/web/store/project_view_filters.ts @@ -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) => 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) => { + 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; diff --git a/web/store/project_view_issues.ts b/web/store/project_view_issues.ts new file mode 100644 index 000000000..5929ab3f9 --- /dev/null +++ b/web/store/project_view_issues.ts @@ -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; + + // 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; diff --git a/web/store/project_views.ts b/web/store/project_views.ts new file mode 100644 index 000000000..5863790ad --- /dev/null +++ b/web/store/project_views.ts @@ -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; + fetchViewDetails: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + createView: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateView: ( + workspaceSlug: string, + projectId: string, + viewId: string, + data: Partial + ) => Promise; + deleteView: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + addViewToFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + removeViewFromFavorites: (workspaceSlug: string, projectId: string, viewId: string) => Promise; +} + +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 => { + 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 => { + 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): Promise => { + 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 + ): Promise => { + 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 => { + 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; diff --git a/web/store/root.ts b/web/store/root.ts index 60cdefd5a..5820c5092 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -3,11 +3,12 @@ import { enableStaticRendering } from "mobx-react-lite"; // store imports import UserStore from "./user"; import ThemeStore from "./theme"; -import ProjectPublishStore, { IProjectPublishStore } from "./project_publish"; import IssueStore, { IIssueStore } from "./issue"; import DraftIssuesStore from "./issue_draft"; import WorkspaceStore, { IWorkspaceStore } from "./workspace"; +import WorkspaceFilterStore, { IWorkspaceFilterStore } from "./workspace_filters"; import ProjectStore, { IProjectStore } from "./project"; +import ProjectPublishStore, { IProjectPublishStore } from "./project_publish"; import ModuleStore, { IModuleStore } from "./modules"; import ModuleIssueStore, { IModuleIssueStore } from "./module_issue"; import ModuleFilterStore, { IModuleFilterStore } from "./module_filters"; @@ -16,14 +17,15 @@ import CycleStore, { ICycleStore } from "./cycles"; import CycleIssueStore, { ICycleIssueStore } from "./cycle_issue"; import CycleIssueFilterStore, { ICycleIssueFilterStore } from "./cycle_issue_filters"; 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 IssueViewDetailStore from "./issue_detail"; import IssueKanBanViewStore from "./kanban_view"; import CalendarStore, { ICalendarStore } from "./calendar"; import GlobalViewsStore, { IGlobalViewsStore } from "./global_views"; import GlobalViewIssuesStore, { IGlobalViewIssuesStore } from "./global_view_issues"; -import WorkspaceFilterStore, { IWorkspaceFilterStore } from "./workspace_filters"; import GlobalViewFiltersStore, { IGlobalViewFiltersStore } from "./global_view_filters"; enableStaticRendering(typeof window === "undefined"); @@ -49,7 +51,10 @@ export class RootStore { cycleIssueFilter: ICycleIssueFilterStore; cycleIssueKanBanView: ICycleIssueKanBanViewStore; - view: IViewStore; + projectViews: IProjectViewsStore; + projectViewIssues: IProjectViewIssuesStore; + projectViewFilters: IProjectViewFiltersStore; + issueFilter: IIssueFilterStore; issueDetail: IssueViewDetailStore; issueKanBanView: IssueKanBanViewStore; @@ -81,7 +86,10 @@ export class RootStore { this.cycleIssueFilter = new CycleIssueFilterStore(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.issueFilter = new IssueFilterStore(this); this.issueDetail = new IssueViewDetailStore(this); diff --git a/web/store/views.ts b/web/store/views.ts deleted file mode 100644 index 019992cbc..000000000 --- a/web/store/views.ts +++ /dev/null @@ -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; -} - -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; diff --git a/web/types/views.d.ts b/web/types/views.d.ts index e1246af5a..4f55e8c74 100644 --- a/web/types/views.d.ts +++ b/web/types/views.d.ts @@ -1,4 +1,6 @@ -export interface IView { +import { IIssueFilterOptions } from "./view-props"; + +export interface IProjectView { id: string; access: string; created_at: Date; @@ -8,19 +10,8 @@ export interface IView { updated_by: string; name: string; description: string; - query: IQuery; - query_data: IQuery; + query: IIssueFilterOptions; + query_data: IIssueFilterOptions; project: 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; -}