diff --git a/web/components/core/modals/bulk-delete-issues-modal.tsx b/web/components/core/modals/bulk-delete-issues-modal.tsx index bd4b2d9ba..964b47d48 100644 --- a/web/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/components/core/modals/bulk-delete-issues-modal.tsx @@ -13,7 +13,6 @@ import issuesServices from "services/issue.service"; // hooks import useToast from "hooks/use-toast"; import useIssuesView from "hooks/use-issues-view"; -import useCalendarIssuesView from "hooks/use-calendar-issues-view"; // ui import { DangerButton, SecondaryButton } from "components/ui"; // icons @@ -55,7 +54,6 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user const { setToastAlert } = useToast(); const { displayFilters, params } = useIssuesView(); - const { params: calendarParams } = useCalendarIssuesView(); const { order_by, group_by, ...viewGanttParams } = params; const { @@ -90,14 +88,6 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user if (!Array.isArray(data.delete_issue_ids)) data.delete_issue_ids = [data.delete_issue_ids]; - const calendarFetchKey = cycleId - ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams) - : moduleId - ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams) - : viewId - ? VIEW_ISSUES(viewId.toString(), calendarParams) - : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams); - const ganttFetchKey = cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) : moduleId @@ -122,8 +112,7 @@ export const BulkDeleteIssuesModal: React.FC = ({ isOpen, setIsOpen, user message: "Issues deleted successfully!", }); - if (displayFilters.layout === "calendar") mutate(calendarFetchKey); - else if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey); + if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey); else { if (cycleId) { mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params)); diff --git a/web/components/core/views/calendar-view/calendar.tsx b/web/components/core/views/calendar-view/calendar.tsx index 5e85e1d49..517349378 100644 --- a/web/components/core/views/calendar-view/calendar.tsx +++ b/web/components/core/views/calendar-view/calendar.tsx @@ -8,8 +8,6 @@ import { mutate } from "swr"; import { DragDropContext, DropResult } from "react-beautiful-dnd"; // services import issuesService from "services/issue.service"; -// hooks -import useCalendarIssuesView from "hooks/use-calendar-issues-view"; // components import { SingleCalendarDate, CalendarHeader } from "components/core"; import { IssuePeekOverview } from "components/issues"; diff --git a/web/components/core/views/calendar-view/single-issue.tsx b/web/components/core/views/calendar-view/single-issue.tsx index 161c68a1a..e37ad2c85 100644 --- a/web/components/core/views/calendar-view/single-issue.tsx +++ b/web/components/core/views/calendar-view/single-issue.tsx @@ -10,7 +10,6 @@ import { DraggableProvided, DraggableStateSnapshot } from "react-beautiful-dnd"; import issuesService from "services/issue.service"; import trackEventServices from "services/track_event.service"; // hooks -import useCalendarIssuesView from "hooks/use-calendar-issues-view"; import useIssuesProperties from "hooks/use-issue-properties"; import useToast from "hooks/use-toast"; // components @@ -60,7 +59,7 @@ export const SingleCalendarIssue: React.FC = ({ const { setToastAlert } = useToast(); - const { params } = useCalendarIssuesView(); + const params = {}; const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); diff --git a/web/components/headers/index.ts b/web/components/headers/index.ts index 2cc2e571e..72f1e743e 100644 --- a/web/components/headers/index.ts +++ b/web/components/headers/index.ts @@ -1 +1,2 @@ +export * from "./module-issues"; export * from "./project-issues"; diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx new file mode 100644 index 000000000..35a635f27 --- /dev/null +++ b/web/components/headers/module-issues.tsx @@ -0,0 +1,94 @@ +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, IIssueFilterOptions, TIssueLayouts } from "types"; +// constants +import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; + +export const ModuleIssuesHeader: React.FC = observer(() => { + const router = useRouter(); + const { workspaceSlug, projectId, moduleId } = router.query; + + const { issueFilter: issueFilterStore, moduleFilter: moduleFilterStore } = useMobxStore(); + + const activeLayout = issueFilterStore.userDisplayFilters.layout; + + const handleLayoutChange = useCallback( + (layout: TIssueLayouts) => { + if (!workspaceSlug || !projectId) return; + + issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { + display_filters: { + layout, + }, + }); + }, + [issueFilterStore, projectId, workspaceSlug] + ); + + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + if (!workspaceSlug || !projectId || !moduleId) return; + + const newValues = moduleFilterStore.userModuleFilters?.[key] ?? []; + + if (Array.isArray(value)) { + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + }); + } else { + if (moduleFilterStore.userModuleFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + moduleFilterStore.updateUserModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), { + [key]: newValues, + }); + }, + [moduleId, moduleFilterStore, projectId, workspaceSlug] + ); + + const handleDisplayFiltersUpdate = useCallback( + (updatedDisplayFilter: Partial) => { + if (!workspaceSlug || !projectId) return; + + issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { + display_filters: { + ...updatedDisplayFilter, + }, + }); + }, + [issueFilterStore, projectId, workspaceSlug] + ); + + return ( +
+ handleLayoutChange(layout)} + selectedLayout={activeLayout} + /> + + + + + + +
+ ); +}); diff --git a/web/components/issues/delete-issue-modal.tsx b/web/components/issues/delete-issue-modal.tsx index 0b7219a1d..2f23f3f63 100644 --- a/web/components/issues/delete-issue-modal.tsx +++ b/web/components/issues/delete-issue-modal.tsx @@ -10,7 +10,6 @@ import { Dialog, Transition } from "@headlessui/react"; import issueServices from "services/issue.service"; // hooks import useIssuesView from "hooks/use-issues-view"; -import useCalendarIssuesView from "hooks/use-calendar-issues-view"; import useToast from "hooks/use-toast"; // icons import { ExclamationTriangleIcon } from "@heroicons/react/24/outline"; @@ -52,7 +51,6 @@ export const DeleteIssueModal: React.FC = ({ const isArchivedIssues = router.pathname.includes("archived-issues"); const { displayFilters, params } = useIssuesView(); - const { params: calendarParams } = useCalendarIssuesView(); const { setToastAlert } = useToast(); @@ -73,17 +71,7 @@ export const DeleteIssueModal: React.FC = ({ await issueServices .deleteIssue(workspaceSlug as string, data.project, data.id, user) .then(() => { - if (displayFilters.layout === "calendar") { - const calendarFetchKey = cycleId - ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams) - : moduleId - ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams) - : viewId - ? VIEW_ISSUES(viewId.toString(), calendarParams) - : PROJECT_ISSUES_LIST_WITH_PARAMS(data.project, calendarParams); - - mutate(calendarFetchKey, (prevData) => (prevData ?? []).filter((p) => p.id !== data.id), false); - } else if (displayFilters.layout === "spreadsheet") { + if (displayFilters.layout === "spreadsheet") { if (data.parent) { mutate( SUB_ISSUES(data.parent.toString()), diff --git a/web/components/issues/draft-issue-modal.tsx b/web/components/issues/draft-issue-modal.tsx index 0a6ce7ec7..3cd5b50cf 100644 --- a/web/components/issues/draft-issue-modal.tsx +++ b/web/components/issues/draft-issue-modal.tsx @@ -11,7 +11,6 @@ import issuesService from "services/issue.service"; // hooks import useUser from "hooks/use-user"; import useIssuesView from "hooks/use-issues-view"; -import useCalendarIssuesView from "hooks/use-calendar-issues-view"; import useToast from "hooks/use-toast"; import useLocalStorage from "hooks/use-local-storage"; import useProjects from "hooks/use-projects"; @@ -78,7 +77,6 @@ export const CreateUpdateDraftIssueModal: React.FC = (props) = const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; const { displayFilters, params } = useIssuesView(); - const { params: calendarParams } = useCalendarIssuesView(); const { ...viewGanttParams } = params; const { user } = useUser(); @@ -146,14 +144,6 @@ export const CreateUpdateDraftIssueModal: React.FC = (props) = setActiveProject(projects?.find((p) => p.id === projectId)?.id ?? projects?.[0].id ?? null); }, [activeProject, data, projectId, projects, isOpen, prePopulateData]); - const calendarFetchKey = cycleId - ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams) - : moduleId - ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams) - : viewId - ? VIEW_ISSUES(viewId.toString(), calendarParams) - : PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", calendarParams); - const ganttFetchKey = cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) : moduleId @@ -171,7 +161,6 @@ export const CreateUpdateDraftIssueModal: React.FC = (props) = mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); - if (displayFilters.layout === "calendar") mutate(calendarFetchKey); if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey, { start_target_date: true, @@ -210,7 +199,6 @@ export const CreateUpdateDraftIssueModal: React.FC = (props) = if (isUpdatingSingleIssue) { mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); } else { - if (displayFilters.layout === "calendar") mutate(calendarFetchKey); if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString())); mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); mutate(PROJECT_DRAFT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); @@ -290,7 +278,6 @@ export const CreateUpdateDraftIssueModal: React.FC = (props) = if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module); - if (displayFilters.layout === "calendar") mutate(calendarFetchKey); if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey, { start_target_date: true, diff --git a/web/components/issues/issue-layouts/calendar/index.ts b/web/components/issues/issue-layouts/calendar/index.ts index 0ff8fe1d1..f4ed9faa1 100644 --- a/web/components/issues/issue-layouts/calendar/index.ts +++ b/web/components/issues/issue-layouts/calendar/index.ts @@ -4,6 +4,7 @@ export * from "./types.d"; export * from "./day-tile"; export * from "./header"; export * from "./issue-blocks"; +export * from "./module-root"; export * from "./root"; export * from "./week-days"; export * from "./week-header"; diff --git a/web/components/issues/issue-layouts/calendar/module-root.tsx b/web/components/issues/issue-layouts/calendar/module-root.tsx new file mode 100644 index 000000000..7fc57f867 --- /dev/null +++ b/web/components/issues/issue-layouts/calendar/module-root.tsx @@ -0,0 +1,36 @@ +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 ModuleCalendarLayout: React.FC = observer(() => { + const { module: moduleStore } = 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 = moduleStore.getIssues; + + return ( +
+ + + +
+ ); +}); 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 2a9b63a36..eae9469e7 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/index.ts +++ b/web/components/issues/issue-layouts/filters/applied-filters/index.ts @@ -2,6 +2,7 @@ export * from "./date"; export * from "./filters-list"; export * from "./label"; export * from "./members"; +export * from "./module-root"; export * from "./priority"; export * from "./root"; export * from "./state"; diff --git a/web/components/issues/issue-layouts/filters/applied-filters/module-root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/module-root.tsx new file mode 100644 index 000000000..9816ed871 --- /dev/null +++ b/web/components/issues/issue-layouts/filters/applied-filters/module-root.tsx @@ -0,0 +1,75 @@ +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"; +// types +import { IIssueFilterOptions } from "types"; + +export const ModuleAppliedFiltersRoot: React.FC = observer(() => { + const router = useRouter(); + const { workspaceSlug, projectId, moduleId } = router.query; + + const { project: projectStore, moduleFilter: moduleFilterStore } = useMobxStore(); + + const userFilters = moduleFilterStore.userModuleFilters; + + // filters whose value not null or empty array + const appliedFilters: IIssueFilterOptions = {}; + Object.entries(userFilters).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 (!workspaceSlug || !projectId || !moduleId) return; + + // remove all values of the key if value is null + if (!value) { + moduleFilterStore.updateUserModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), { + [key]: null, + }); + return; + } + + // remove the passed value from the key + let newValues = moduleFilterStore.userModuleFilters?.[key] ?? []; + newValues = newValues.filter((val) => val !== value); + + moduleFilterStore.updateUserModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), { + [key]: newValues, + }); + }; + + const handleClearAllFilters = () => { + if (!workspaceSlug || !projectId || !moduleId) return; + + const newFilters: IIssueFilterOptions = {}; + Object.keys(userFilters).forEach((key) => { + newFilters[key as keyof IIssueFilterOptions] = null; + }); + + moduleFilterStore.updateUserModuleFilters(workspaceSlug.toString(), projectId.toString(), moduleId?.toString(), { + ...newFilters, + }); + }; + + // return if no filters are applied + if (Object.keys(appliedFilters).length === 0) return null; + + return ( + m.member)} + states={projectStore.states?.[projectId?.toString() ?? ""]} + /> + ); +}); diff --git a/web/components/issues/issue-layouts/filters/applied-filters/root.tsx b/web/components/issues/issue-layouts/filters/applied-filters/root.tsx index fa1d9e9b5..c565fcaa1 100644 --- a/web/components/issues/issue-layouts/filters/applied-filters/root.tsx +++ b/web/components/issues/issue-layouts/filters/applied-filters/root.tsx @@ -12,7 +12,7 @@ export const AppliedFiltersRoot: React.FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { issueFilter: issueFilterStore, project: projectStore } = useMobxStore(); + const { issueFilter: issueFilterStore, project: projectStore, moduleFilter: moduleFilterStore } = useMobxStore(); const userFilters = issueFilterStore.userFilters; diff --git a/web/components/issues/issue-layouts/gantt/index.ts b/web/components/issues/issue-layouts/gantt/index.ts index c076f3b65..11a2fea7a 100644 --- a/web/components/issues/issue-layouts/gantt/index.ts +++ b/web/components/issues/issue-layouts/gantt/index.ts @@ -1,2 +1,3 @@ export * from "./blocks"; +export * from "./module-root"; export * from "./root"; diff --git a/web/components/issues/issue-layouts/gantt/module-root.tsx b/web/components/issues/issue-layouts/gantt/module-root.tsx new file mode 100644 index 000000000..f4b05ca31 --- /dev/null +++ b/web/components/issues/issue-layouts/gantt/module-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 ModuleGanttLayout: React.FC = observer(() => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { projectDetails } = useProjectDetails(); + + const { module: moduleStore, issueFilter: issueFilterStore } = useMobxStore(); + + const appliedDisplayFilters = issueFilterStore.userDisplayFilters; + + const issues = moduleStore.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/index.ts b/web/components/issues/issue-layouts/index.ts index 8840e08df..6507b6202 100644 --- a/web/components/issues/issue-layouts/index.ts +++ b/web/components/issues/issue-layouts/index.ts @@ -3,3 +3,4 @@ export * from "./filters"; export * from "./gantt"; export * from "./kanban"; export * from "./spreadsheet"; +export * from "./module-all-layouts"; diff --git a/web/components/issues/issue-layouts/module-all-layouts.tsx b/web/components/issues/issue-layouts/module-all-layouts.tsx new file mode 100644 index 000000000..f7f0b2b43 --- /dev/null +++ b/web/components/issues/issue-layouts/module-all-layouts.tsx @@ -0,0 +1,54 @@ +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 { + KanBanLayout, + ModuleAppliedFiltersRoot, + ModuleCalendarLayout, + ModuleGanttLayout, + ModuleSpreadsheetLayout, +} from "components/issues"; + +export const ModuleAllLayouts: React.FC = observer(() => { + const router = useRouter(); + const { workspaceSlug, projectId, moduleId } = router.query; + + const { module: moduleStore, project: projectStore, issueFilter: issueFilterStore } = useMobxStore(); + + useSWR(workspaceSlug && projectId ? `MODULE_ISSUES` : null, async () => { + if (workspaceSlug && projectId && moduleId) { + await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString()); + + await projectStore.fetchProjectStates(workspaceSlug.toString(), projectId.toString()); + await projectStore.fetchProjectLabels(workspaceSlug.toString(), projectId.toString()); + await projectStore.fetchProjectMembers(workspaceSlug.toString(), projectId.toString()); + + await moduleStore.fetchModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); + await moduleStore.fetchModuleIssues(workspaceSlug.toString(), projectId.toString(), moduleId.toString()); + } + }); + + const activeLayout = issueFilterStore.userDisplayFilters.layout; + + return ( +
+ +
+ {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 1efe34c51..d468cccad 100644 --- a/web/components/issues/issue-layouts/spreadsheet/index.ts +++ b/web/components/issues/issue-layouts/spreadsheet/index.ts @@ -1 +1,2 @@ +export * from "./module-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 new file mode 100644 index 000000000..17abf5e33 --- /dev/null +++ b/web/components/issues/issue-layouts/spreadsheet/module-root.tsx @@ -0,0 +1,160 @@ +import React, { useCallback, useState } from "react"; +import { useRouter } from "next/router"; +import { observer } from "mobx-react-lite"; + +// mobx store +import { useMobxStore } from "lib/mobx/store-provider"; +// hooks +import useUser from "hooks/use-user"; +import useProjectDetails from "hooks/use-project-details"; +// components +import { SpreadsheetColumns, SpreadsheetIssues } from "components/core"; +import { IssuePeekOverview } from "components/issues"; +// ui +import { CustomMenu, 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 ModuleSpreadsheetLayout: React.FC = observer(() => { + const [expandedIssues, setExpandedIssues] = useState([]); + + const router = useRouter(); + const { workspaceSlug, projectId, cycleId, moduleId } = 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 type = cycleId ? "cycle" : moduleId ? "module" : "issue"; + + 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, + }} + /> + ))} +
+ {type === "issue" ? ( + + ) : ( + isAllowed && ( + + + 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/modal.tsx b/web/components/issues/modal.tsx index 28da11878..d483698d6 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -13,7 +13,6 @@ import inboxServices from "services/inbox.service"; // hooks import useUser from "hooks/use-user"; import useIssuesView from "hooks/use-issues-view"; -import useCalendarIssuesView from "hooks/use-calendar-issues-view"; import useToast from "hooks/use-toast"; import useInboxView from "hooks/use-inbox-view"; import useProjects from "hooks/use-projects"; @@ -83,7 +82,6 @@ export const CreateUpdateIssueModal: React.FC = ({ const { workspaceSlug, projectId, cycleId, moduleId, viewId, inboxId } = router.query; const { displayFilters, params } = useIssuesView(); - const { params: calendarParams } = useCalendarIssuesView(); const { ...viewGanttParams } = params; const { params: inboxParams } = useInboxView(); @@ -270,14 +268,6 @@ export const CreateUpdateIssueModal: React.FC = ({ }); }; - const calendarFetchKey = cycleId - ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams) - : moduleId - ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams) - : viewId - ? VIEW_ISSUES(viewId.toString(), calendarParams) - : PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject?.toString() ?? "", calendarParams); - const ganttFetchKey = cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) : moduleId @@ -298,7 +288,6 @@ export const CreateUpdateIssueModal: React.FC = ({ if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module); - if (displayFilters.layout === "calendar") mutate(calendarFetchKey); if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey, { start_target_date: true, @@ -375,7 +364,6 @@ export const CreateUpdateIssueModal: React.FC = ({ if (isUpdatingSingleIssue) { mutate(PROJECT_ISSUES_DETAILS, (prevData) => ({ ...prevData, ...res }), false); } else { - if (displayFilters.layout === "calendar") mutate(calendarFetchKey); if (payload.parent) mutate(SUB_ISSUES(payload.parent.toString())); mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(activeProject ?? "", params)); } diff --git a/web/components/modules/gantt-chart/module-issues-layout.tsx b/web/components/modules/gantt-chart/module-issues-layout.tsx index 1d0a644af..cfac41983 100644 --- a/web/components/modules/gantt-chart/module-issues-layout.tsx +++ b/web/components/modules/gantt-chart/module-issues-layout.tsx @@ -3,7 +3,6 @@ import { useRouter } from "next/router"; // hooks import useIssuesView from "hooks/use-issues-view"; import useUser from "hooks/use-user"; -import useGanttChartModuleIssues from "hooks/gantt-chart/module-issues-view"; import useProjectDetails from "hooks/use-project-details"; // components import { GanttChartRoot, renderIssueBlocksStructure } from "components/gantt-chart"; @@ -22,18 +21,11 @@ export const ModuleIssuesGanttChartView: React.FC = ({ disableUserActions const { user } = useUser(); const { projectDetails } = useProjectDetails(); - const { ganttIssues, mutateGanttIssues } = useGanttChartModuleIssues( - workspaceSlug as string, - projectId as string, - moduleId as string - ); - const isAllowed = projectDetails?.member_role === 20 || projectDetails?.member_role === 15; return ( <> mutateGanttIssues()} projectId={projectId?.toString() ?? ""} workspaceSlug={workspaceSlug?.toString() ?? ""} readOnly={disableUserActions} @@ -43,7 +35,7 @@ export const ModuleIssuesGanttChartView: React.FC = ({ disableUserActions border={false} title="Issues" loaderTitle="Issues" - blocks={ganttIssues ? renderIssueBlocksStructure(ganttIssues as IIssue[]) : null} + blocks={null} blockUpdateHandler={(block, payload) => {}} SidebarBlockRender={IssueGanttSidebarBlock} BlockRender={IssueGanttBlock} diff --git a/web/hooks/gantt-chart/module-issues-view.tsx b/web/hooks/gantt-chart/module-issues-view.tsx deleted file mode 100644 index 5be58702e..000000000 --- a/web/hooks/gantt-chart/module-issues-view.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import useSWR from "swr"; - -// services -import modulesService from "services/modules.service"; -// hooks -import useIssuesView from "hooks/use-issues-view"; -// fetch-keys -import { MODULE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; - -const useGanttChartModuleIssues = ( - workspaceSlug: string | undefined, - projectId: string | undefined, - moduleId: string | undefined -) => { - const { displayFilters, filters } = useIssuesView(); - - const params: any = { - order_by: displayFilters.order_by, - type: displayFilters?.type ? displayFilters?.type : undefined, - sub_issue: displayFilters.sub_issue, - assignees: filters?.assignees ? filters?.assignees.join(",") : undefined, - state: filters?.state ? filters?.state.join(",") : undefined, - priority: filters?.priority ? filters?.priority.join(",") : undefined, - labels: filters?.labels ? filters?.labels.join(",") : undefined, - created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, - start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, - target_date: filters?.target_date ? filters?.target_date.join(",") : undefined, - start_target_date: true, // to fetch only issues with a start and target date - }; - - // all issues under the workspace and project - const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( - workspaceSlug && projectId && moduleId - ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) - : null, - workspaceSlug && projectId && moduleId - ? () => - modulesService.getModuleIssuesWithParams( - workspaceSlug.toString(), - projectId.toString(), - moduleId.toString(), - params - ) - : null - ); - - return { - ganttIssues, - mutateGanttIssues, - }; -}; - -export default useGanttChartModuleIssues; diff --git a/web/hooks/use-calendar-issues-view.tsx b/web/hooks/use-calendar-issues-view.tsx deleted file mode 100644 index b32dcfc3c..000000000 --- a/web/hooks/use-calendar-issues-view.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { useContext } from "react"; - -import { useRouter } from "next/router"; - -import useSWR from "swr"; - -// contexts -import { issueViewContext } from "contexts/issue-view.context"; -// services -import issuesService from "services/issue.service"; -import cyclesService from "services/cycles.service"; -import modulesService from "services/modules.service"; -// types -import { IIssue } from "types"; -// fetch-keys -import { - CYCLE_ISSUES_WITH_PARAMS, - MODULE_ISSUES_WITH_PARAMS, - PROJECT_ISSUES_LIST_WITH_PARAMS, - VIEW_ISSUES, -} from "constants/fetch-keys"; - -const useCalendarIssuesView = () => { - const { - display_filters: displayFilters, - setDisplayFilters, - filters, - setFilters, - resetFilterToDefault, - setNewFilterDefaultView, - } = useContext(issueViewContext); - - const router = useRouter(); - const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query; - - const params: any = { - assignees: filters?.assignees ? filters?.assignees.join(",") : undefined, - state: filters?.state ? filters?.state.join(",") : undefined, - priority: filters?.priority ? filters?.priority.join(",") : undefined, - type: displayFilters?.type ? displayFilters?.type : undefined, - labels: filters?.labels ? filters?.labels.join(",") : undefined, - created_by: filters?.created_by ? filters?.created_by.join(",") : undefined, - start_date: filters?.start_date ? filters?.start_date.join(",") : undefined, - }; - - const { data: projectCalendarIssues, mutate: mutateProjectCalendarIssues } = useSWR( - workspaceSlug && projectId ? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params) : null, - workspaceSlug && projectId - ? () => issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), params) - : null - ); - - const { data: cycleCalendarIssues, mutate: mutateCycleCalendarIssues } = useSWR( - workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), params) : null, - workspaceSlug && projectId && cycleId - ? () => - cyclesService.getCycleIssuesWithParams( - workspaceSlug.toString(), - projectId.toString(), - cycleId.toString(), - params - ) - : null - ); - - const { data: moduleCalendarIssues, mutate: mutateModuleCalendarIssues } = useSWR( - workspaceSlug && projectId && moduleId ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), params) : null, - workspaceSlug && projectId && moduleId - ? () => - modulesService.getModuleIssuesWithParams( - workspaceSlug.toString(), - projectId.toString(), - moduleId.toString(), - params - ) - : null - ); - - const { data: viewCalendarIssues, mutate: mutateViewCalendarIssues } = useSWR( - workspaceSlug && projectId && viewId && params ? VIEW_ISSUES(viewId.toString(), params) : null, - workspaceSlug && projectId && viewId && params - ? () => issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString(), params) - : null - ); - - const calendarIssues = cycleId - ? (cycleCalendarIssues as IIssue[]) - : moduleId - ? (moduleCalendarIssues as IIssue[]) - : viewId - ? (viewCalendarIssues as IIssue[]) - : (projectCalendarIssues as IIssue[]); - - return { - displayFilters, - setDisplayFilters, - calendarIssues: calendarIssues ?? [], - mutateIssues: cycleId - ? mutateCycleCalendarIssues - : moduleId - ? mutateModuleCalendarIssues - : viewId - ? mutateViewCalendarIssues - : mutateProjectCalendarIssues, - filters, - setFilters, - params, - resetFilterToDefault, - setNewFilterDefaultView, - } as const; -}; - -export default useCalendarIssuesView; diff --git a/web/lib/mobx/store-init.tsx b/web/lib/mobx/store-init.tsx index 5a0bcd6ca..04a36fcef 100644 --- a/web/lib/mobx/store-init.tsx +++ b/web/lib/mobx/store-init.tsx @@ -6,11 +6,17 @@ import { useMobxStore } from "lib/mobx/store-provider"; import { useRouter } from "next/router"; const MobxStoreInit = () => { - const { theme: themeStore, user: userStore, workspace: workspaceStore, project: projectStore } = useMobxStore(); + const { + theme: themeStore, + user: userStore, + workspace: workspaceStore, + project: projectStore, + module: moduleStore, + } = useMobxStore(); const { setTheme } = useTheme(); const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId, moduleId } = router.query; useEffect(() => { // sidebar collapsed toggle @@ -47,7 +53,8 @@ const MobxStoreInit = () => { useEffect(() => { if (workspaceSlug) workspaceStore.setWorkspaceSlug(workspaceSlug.toString()); if (projectId) projectStore.setProjectId(projectId.toString()); - }, [workspaceSlug, projectId, workspaceStore, projectStore]); + if (moduleId) moduleStore.setModuleId(moduleId.toString()); + }, [workspaceSlug, projectId, moduleId, workspaceStore, projectStore, moduleStore]); return <>; }; diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx index 29f0d0500..48ec2878a 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/modules/[moduleId].tsx @@ -13,10 +13,8 @@ import useToast from "hooks/use-toast"; import useUserAuth from "hooks/use-user-auth"; // layouts import { ProjectAuthorizationWrapper } from "layouts/auth-layout-legacy"; -// contexts -import { IssueViewContextProvider } from "contexts/issue-view.context"; // components -import { ExistingIssuesListModal, IssuesFilterView, IssuesView } from "components/core"; +import { ExistingIssuesListModal, IssuesFilterView } from "components/core"; import { ModuleDetailsSidebar } from "components/modules"; import { AnalyticsProjectModal } from "components/analytics"; // ui @@ -30,10 +28,12 @@ import { truncateText } from "helpers/string.helper"; import { ISearchIssueResponse } from "types"; // fetch-keys import { MODULE_DETAILS, MODULE_ISSUES, MODULE_LIST } from "constants/fetch-keys"; +import { ModuleAllLayouts } from "components/issues"; +import { ModuleIssuesHeader } from "components/headers"; const SingleModule: React.FC = () => { const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); - const [moduleSidebar, setModuleSidebar] = useState(true); + const [moduleSidebar, setModuleSidebar] = useState(false); const [analyticsModal, setAnalyticsModal] = useState(false); const router = useRouter(); @@ -85,7 +85,7 @@ const SingleModule: React.FC = () => { }; return ( - + <> setModuleIssuesListModal(false)} @@ -124,27 +124,7 @@ const SingleModule: React.FC = () => { ))} } - right={ -
- - setAnalyticsModal(true)} - className="!py-1.5 font-normal rounded-md text-custom-text-200 hover:text-custom-text-100" - outline - > - Analytics - - -
- } + right={} > {error ? ( { analyticsModal ? "mr-[50%]" : "" } duration-300`} > - + { )} -
+ ); }; diff --git a/web/services/modules.service.ts b/web/services/modules.service.ts index 7769f2ef0..4667c5dc7 100644 --- a/web/services/modules.service.ts +++ b/web/services/modules.service.ts @@ -18,12 +18,7 @@ export class ModuleService extends APIService { }); } - async createModule( - workspaceSlug: string, - projectId: string, - data: any, - user: ICurrentUserResponse | undefined - ): Promise { + async createModule(workspaceSlug: string, projectId: string, data: any, user: any): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/`, data) .then((response) => { trackEventServices.trackModuleEvent(response?.data, "MODULE_CREATE", user); @@ -64,7 +59,7 @@ export class ModuleService extends APIService { projectId: string, moduleId: string, data: Partial, - user: ICurrentUserResponse | undefined + user: any ): Promise { return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`, data) .then((response) => { @@ -76,12 +71,7 @@ export class ModuleService extends APIService { }); } - async deleteModule( - workspaceSlug: string, - projectId: string, - moduleId: string, - user: ICurrentUserResponse | undefined - ): Promise { + async deleteModule(workspaceSlug: string, projectId: string, moduleId: string, user: any): Promise { return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/`) .then((response) => { trackEventServices.trackModuleEvent(response?.data, "MODULE_DELETE", user); diff --git a/web/store/issue.ts b/web/store/issue.ts index 13ced147e..7e5786519 100644 --- a/web/store/issue.ts +++ b/web/store/issue.ts @@ -100,7 +100,7 @@ class IssueStore implements IIssueStore { updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { const projectId: string | null = issue?.project; - const issueType: IIssueType | null = this.getIssueType; + const issueType = this.getIssueType; if (!projectId || !issueType) return null; let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = diff --git a/web/store/issue_filters.ts b/web/store/issue_filters.ts index c8af5a51f..910bcd873 100644 --- a/web/store/issue_filters.ts +++ b/web/store/issue_filters.ts @@ -4,7 +4,6 @@ import { ProjectService } from "services/project.service"; import { IssueService } from "services/issue.service"; // helpers import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; -import { renderDateFormat } from "helpers/date-time.helper"; // types import { RootStore } from "./root"; import { @@ -113,13 +112,7 @@ class IssueFilterStore implements IIssueFilterStore { }; get appliedFilters(): TIssueParams[] | null { - if ( - !this.userFilters || - Object.keys(this.userFilters).length === 0 || - !this.userDisplayFilters || - Object.keys(this.userDisplayFilters).length === 0 - ) - return null; + if (!this.userFilters || !this.userDisplayFilters) return null; let filteredRouteParams: any = { priority: this.userFilters?.priority || undefined, diff --git a/web/store/module_filters.ts b/web/store/module_filters.ts new file mode 100644 index 000000000..7166b7eea --- /dev/null +++ b/web/store/module_filters.ts @@ -0,0 +1,148 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// services +import { ProjectService } from "services/project.service"; +import { ModuleService } from "services/modules.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "./root"; +import { IIssueFilterOptions, TIssueParams } from "types"; + +export interface IModuleFilterStore { + loader: boolean; + error: any | null; + userModuleFilters: IIssueFilterOptions; + defaultFilters: IIssueFilterOptions; + + // action + updateUserModuleFilters: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + filterToUpdate: Partial + ) => Promise; + + // computed + appliedFilters: TIssueParams[] | null; +} + +class ModuleFilterStore implements IModuleFilterStore { + loader: boolean = false; + error: any | null = null; + + // observables + userModuleFilters: IIssueFilterOptions = {}; + defaultFilters: IIssueFilterOptions = {}; + + // root store + rootStore; + + // services + projectService; + moduleService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + loader: observable.ref, + error: observable.ref, + + // observables + defaultFilters: observable.ref, + userModuleFilters: observable.ref, + + // actions + updateUserModuleFilters: action, + + // computed + appliedFilters: computed, + }); + + this.rootStore = _rootStore; + + this.projectService = new ProjectService(); + this.moduleService = new ModuleService(); + } + + 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 appliedFilters(): TIssueParams[] | null { + const userDisplayFilters = this.rootStore.issueFilter.userDisplayFilters; + + if (!this.userModuleFilters || !userDisplayFilters) return null; + + let filteredRouteParams: any = { + priority: this.userModuleFilters?.priority || undefined, + state_group: this.userModuleFilters?.state_group || undefined, + state: this.userModuleFilters?.state || undefined, + assignees: this.userModuleFilters?.assignees || undefined, + created_by: this.userModuleFilters?.created_by || undefined, + labels: this.userModuleFilters?.labels || undefined, + start_date: this.userModuleFilters?.start_date || undefined, + target_date: this.userModuleFilters?.target_date || undefined, + group_by: userDisplayFilters?.group_by || "state", + order_by: userDisplayFilters?.order_by || "-created_at", + sub_group_by: userDisplayFilters?.sub_group_by || undefined, + type: userDisplayFilters?.type || undefined, + sub_issue: userDisplayFilters?.sub_issue || true, + show_empty_groups: userDisplayFilters?.show_empty_groups || true, + start_target_date: userDisplayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(userDisplayFilters.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (userDisplayFilters.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (userDisplayFilters.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } + + updateUserModuleFilters = async ( + workspaceSlug: string, + projectId: string, + moduleId: string, + filterToUpdate: Partial + ) => { + const newFilters = { + ...this.userModuleFilters, + ...filterToUpdate, + }; + + try { + runInAction(() => { + this.userModuleFilters = newFilters; + }); + + this.moduleService.patchModule( + workspaceSlug, + projectId, + moduleId, + { + view_props: { + filters: newFilters, + }, + }, + this.rootStore.user.currentUser + ); + } catch (error) { + this.rootStore.module.fetchModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.error = error; + }); + + console.log("Failed to update user filters in issue filter store", error); + } + }; +} + +export default ModuleFilterStore; diff --git a/web/store/modules.ts b/web/store/modules.ts index ab8137bba..feb0d2279 100644 --- a/web/store/modules.ts +++ b/web/store/modules.ts @@ -1,10 +1,11 @@ import { action, computed, observable, makeObservable, runInAction } from "mobx"; -// types -import { RootStore } from "./root"; // services import { ProjectService } from "services/project.service"; import { ModuleService } from "services/modules.service"; -import { IModule } from "@/types"; +// types +import { RootStore } from "./root"; +import { IIssue, IModule } from "types"; +import { IIssueGroupWithSubGroupsStructure, IIssueGroupedStructure, IIssueUnGroupedStructure } from "./issue"; export interface IModuleStore { loader: boolean; @@ -14,13 +15,35 @@ export interface IModuleStore { modules: { [project_id: string]: IModule[]; }; - module_details: { + moduleDetails: { [module_id: string]: IModule; }; + issues: { + [module_id: string]: { + grouped: IIssueGroupedStructure; + groupWithSubGroups: IIssueGroupWithSubGroupsStructure; + ungrouped: IIssueUnGroupedStructure; + }; + }; setModuleId: (moduleSlug: string) => void; - fetchModules: (workspaceSlug: string, projectSlug: string) => void; + fetchModules: (workspaceSlug: string, projectId: string) => void; + fetchModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string) => void; + + // crud operations + createModule: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateModuleDetails: (workspaceSlug: string, projectId: string, moduleId: string, data: Partial) => void; + deleteModule: (workspaceSlug: string, projectId: string, moduleId: string) => void; + addModuleToFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => void; + removeModuleFromFavorites: (workspaceSlug: string, projectId: string, moduleId: string) => void; + + // issue related operations + fetchModuleIssues: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + updateIssueStructure: (group_id: string | null, sub_group_id: string | null, moduleId: string, issue: IIssue) => void; + + // computed + getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; } class ModuleStore implements IModuleStore { @@ -33,10 +56,24 @@ class ModuleStore implements IModuleStore { [project_id: string]: IModule[]; } = {}; - module_details: { + moduleDetails: { [module_id: string]: IModule; } = {}; + issues: { + [module_id: string]: { + grouped: { + [group_id: string]: IIssue[]; + }; + groupWithSubGroups: { + [group_id: string]: { + [sub_group_id: string]: IIssue[]; + }; + }; + ungrouped: IIssue[]; + }; + } = {}; + // root store rootStore; // services @@ -49,11 +86,27 @@ class ModuleStore implements IModuleStore { error: observable.ref, moduleId: observable.ref, + modules: observable.ref, + moduleDetails: observable.ref, + issues: observable.ref, // computed + getIssues: computed, // actions setModuleId: action, + + fetchModules: action, + fetchModuleDetails: action, + + createModule: action, + updateModuleDetails: action, + deleteModule: action, + addModuleToFavorites: action, + removeModuleFromFavorites: action, + + fetchModuleIssues: action, + updateIssueStructure: action, }); this.rootStore = _rootStore; @@ -67,32 +120,302 @@ class ModuleStore implements IModuleStore { return this.modules[this.rootStore.project.projectId] || null; } + get getIssues() { + const moduleId = this.moduleId; + + const issueType = this.rootStore.issue.getIssueType; + + if (!moduleId || !issueType) return null; + + return this.issues?.[moduleId]?.[issueType] || null; + } + // actions setModuleId = (moduleSlug: string) => { this.moduleId = moduleSlug ?? null; }; - fetchModules = async (workspaceSlug: string, projectSlug: string) => { + fetchModules = async (workspaceSlug: string, projectId: string) => { try { - this.loader = true; - this.error = null; + runInAction(() => { + this.loader = true; + this.error = null; + }); - const modulesResponse = await this.moduleService.getModules(workspaceSlug, projectSlug); + const modulesResponse = await this.moduleService.getModules(workspaceSlug, projectId); runInAction(() => { this.modules = { ...this.modules, - [projectSlug]: modulesResponse, + [projectId]: modulesResponse, }; this.loader = false; this.error = null; }); } catch (error) { - console.error("Failed to fetch modules list in project store", error); + console.error("Failed to fetch modules list in module store", error); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + } + }; + + fetchModuleDetails = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + runInAction(() => { + this.loader = true; + this.error = null; + }); + + const response = await this.moduleService.getModuleDetails(workspaceSlug, projectId, moduleId); + + if (!response) return null; + + runInAction(() => { + this.moduleDetails = { + ...this.moduleDetails, + [moduleId]: response, + }; + this.rootStore.moduleFilter.userModuleFilters = response.view_props?.filters ?? {}; + this.loader = false; + this.error = null; + }); + } catch (error) { + console.error("Failed to fetch module details in module store", error); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + } + }; + + createModule = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const response = await this.moduleService.createModule( + workspaceSlug, + projectId, + data, + this.rootStore.user.currentUser + ); + + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: [...this.modules[projectId], response], + }; + this.loader = false; + this.error = null; + }); + + return response; + } catch (error) { + console.error("Failed to create module in module store", error); + + runInAction(() => { + this.loader = false; + this.error = error; + }); + + throw error; + } + }; + + updateModuleDetails = async (workspaceSlug: string, projectId: string, moduleId: string, data: Partial) => { + try { + runInAction(() => { + (this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].map((module) => + module.id === moduleId ? { ...module, ...data } : module + ), + }), + (this.moduleDetails = { + ...this.moduleDetails, + [moduleId]: { + ...this.moduleDetails[moduleId], + ...data, + }, + }); + }); + + await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, data, this.rootStore.user.currentUser); + } catch (error) { + console.error("Failed to update module in module store", error); + + this.fetchModules(workspaceSlug, projectId); + this.fetchModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.error = error; + }); + } + }; + + deleteModule = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].filter((module) => module.id !== moduleId), + }; + }); + + await this.moduleService.deleteModule(workspaceSlug, projectId, moduleId, this.rootStore.user.currentUser); + } catch (error) { + console.error("Failed to delete module in module store", error); + + this.fetchModules(workspaceSlug, projectId); + + runInAction(() => { + this.error = error; + }); + } + }; + + addModuleToFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].map((module) => ({ + ...module, + is_favorite: module.id === moduleId ? true : module.is_favorite, + })), + }; + }); + + await this.moduleService.addModuleToFavorites(workspaceSlug, projectId, { + module: moduleId, + }); + } catch (error) { + console.error("Failed to add module to favorites in module store", error); + + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].map((module) => ({ + ...module, + is_favorite: module.id === moduleId ? false : module.is_favorite, + })), + }; + this.error = error; + }); + } + }; + + removeModuleFromFavorites = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].map((module) => ({ + ...module, + is_favorite: module.id === moduleId ? false : module.is_favorite, + })), + }; + }); + + await this.moduleService.removeModuleFromFavorites(workspaceSlug, projectId, moduleId); + } catch (error) { + console.error("Failed to remove module from favorites in module store", error); + + runInAction(() => { + this.modules = { + ...this.modules, + [projectId]: this.modules[projectId].map((module) => ({ + ...module, + is_favorite: module.id === moduleId ? true : module.is_favorite, + })), + }; + }); + } + }; + + fetchModuleIssues = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + this.loader = true; + this.error = null; + + this.rootStore.workspace.setWorkspaceSlug(workspaceSlug); + this.rootStore.project.setProjectId(projectId); + + const params = this.rootStore?.issueFilter?.appliedFilters; + console.log("params", params); + const issueResponse = await this.moduleService.getModuleIssuesWithParams( + workspaceSlug, + projectId, + moduleId, + params + ); + + const issueType = this.rootStore.issue.getIssueType; + if (issueType != null) { + const _issues = { + ...this.issues, + [moduleId]: { + ...this.issues[moduleId], + [issueType]: issueResponse, + }, + }; + runInAction(() => { + this.issues = _issues; + this.loader = false; + this.error = null; + }); + } + + return issueResponse; + } catch (error) { + console.error("Error: Fetching error module issues in module store", error); this.loader = false; this.error = error; + return error; } }; + + updateIssueStructure = async ( + group_id: string | null, + sub_group_id: string | null, + moduleId: string, + issue: IIssue + ) => { + const issueType = this.rootStore.issue.getIssueType; + + if (!issueType) return null; + + let issues = this.getIssues; + + if (!issues) return null; + + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + issues = { + ...issues, + [group_id]: issues[group_id].map((i: IIssue) => (i?.id === issue?.id ? issue : i)), + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: issues[sub_group_id][group_id].map((i: IIssue) => (i?.id === issue?.id ? issue : i)), + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + issues = issues.map((i: IIssue) => (i?.id === issue?.id ? issue : i)); + } + + runInAction(() => { + this.issues = { ...this.issues, [moduleId]: { ...this.issues[moduleId], [issueType]: issues } }; + }); + }; } export default ModuleStore; diff --git a/web/store/root.ts b/web/store/root.ts index 8d40d55d6..e365b1953 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -15,6 +15,7 @@ import IssueFilterStore, { IIssueFilterStore } from "./issue_filters"; import IssueViewDetailStore from "./issue_detail"; import IssueKanBanViewStore from "./kanban_view"; import CalendarStore, { ICalendarStore } from "./calendar"; +import ModuleFilterStore, { IModuleFilterStore } from "./module_filters"; enableStaticRendering(typeof window === "undefined"); @@ -27,6 +28,7 @@ export class RootStore { project: IProjectStore; issue: IIssueStore; module: IModuleStore; + moduleFilter: IModuleFilterStore; cycle: ICycleStore; view: IViewStore; issueFilter: IIssueFilterStore; @@ -41,6 +43,7 @@ export class RootStore { this.project = new ProjectStore(this); this.projectPublish = new ProjectPublishStore(this); this.module = new ModuleStore(this); + this.moduleFilter = new ModuleFilterStore(this); this.cycle = new CycleStore(this); this.view = new ViewStore(this); this.issue = new IssueStore(this);