From d6abb87a3a16c25704190138e0acb4ba0560597a Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 23 Nov 2023 14:47:04 +0530 Subject: [PATCH] chore: implemented new store and issue layouts for issues and updated new data structure for issues (#2843) * fix: Implemented new workflow in the issue store and updated the quick add workflow in list layout * fix: initial load and mutaion of issues in list layout * dev: implemented the new project issues store with grouped, subGrouped and unGrouped issue computed functions * dev: default display properties data made as a function * conflict: merge conflict resolved * dev: implemented quick add logic in kanban * chore: implemented quick add logic in calendar and spreadsheet layout * fix: spreadsheet layout quick add fix * dev: optimised the issues workflow and handled the issues order_by filter * dev: project issue CRUD operations in new issue store architecture * dev: issues filtering in calendar layout * fix: build error * dev/issue_filters_store * chore: updated filters computed structure * conflict: merge conflicts resolved in project issues * dev: implemented gantt chart for project issues using the new mobx store * dev: initialized cycle and module issue filters store * dev: issue store and list layout store updates * dev: quick add and update, delete issue in the list * refactor list root changes * dev: store new structure * refactor spreadsheet and gnatt project roots * fix errors for base gantt and spreadsheet roots * connect Calendar project view * minor house keeping * connect Kanban View to th enew store * generalise base calendar issue actions * dev: store project issues and issue filters * dev: store project issues and filters * dev: updated undefined with displayFilters in project issue store * Add Quick add to all the layouts * connect module views to store * dev: Rendering list issues in project issues * dev: removed console log * dev: module filters store * fix errors and connect modules list and quick add for list * dev: module issue store * dev: modle filter store issue fixed and updates cycle issue filters * minor house keeping changes * dev: cycle issues and cycle filters * connecty cycles to teh store * dev: project view issues and issue filtrs * connect project views * dev: updated applied filters in layouts * dev: replaced project id with view id in project views * dev: in cycle and module store made cycledId and moduleId as optional * fix minor issues and build errots * dev: project draft and archived issues store and filters --------- Co-authored-by: Anmol Singh Bhatia Co-authored-by: Aaryan Khandelwal Co-authored-by: rahulramesha --- packages/ui/src/spinners/circular-spinner.tsx | 2 +- .../gantt-chart/sidebar/sidebar.tsx | 14 +- web/components/headers/cycle-issues.tsx | 69 ++-- web/components/headers/module-issues.tsx | 72 ++-- web/components/headers/project-issues.tsx | 77 ++--- .../headers/project-view-issues.tsx | 62 ++-- web/components/issues/delete-issue-modal.tsx | 18 +- .../calendar/base-calendar-root.tsx | 89 +++++ .../issue-layouts/calendar/calendar.tsx | 23 +- .../issue-layouts/calendar/day-tile.tsx | 43 ++- .../issues/issue-layouts/calendar/index.ts | 2 +- .../issue-layouts/calendar/issue-blocks.tsx | 100 +++--- ...ssue-form.tsx => quick-add-issue-form.tsx} | 72 ++-- .../calendar/roots/cycle-root.tsx | 85 ++--- .../calendar/roots/module-root.tsx | 90 ++--- .../calendar/roots/project-root.tsx | 78 ++--- .../calendar/roots/project-view-root.tsx | 65 ++-- .../issue-layouts/calendar/week-days.tsx | 29 +- .../applied-filters/roots/cycle-root.tsx | 60 ++-- .../applied-filters/roots/module-root.tsx | 58 ++-- .../applied-filters/roots/project-root.tsx | 34 +- .../roots/project-view-root.tsx | 70 ++-- .../issue-layouts/gantt/base-gantt-root.tsx | 94 +++++ .../issues/issue-layouts/gantt/cycle-root.tsx | 52 +-- .../issues/issue-layouts/gantt/index.ts | 4 +- .../issue-layouts/gantt/module-root.tsx | 52 +-- .../issue-layouts/gantt/project-root.tsx | 12 + .../issue-layouts/gantt/project-view-root.tsx | 54 +-- ...ssue-form.tsx => quick-add-issue-form.tsx} | 23 +- .../issues/issue-layouts/gantt/root.tsx | 58 ---- .../issue-layouts/kanban/base-kanban-root.tsx | 194 +++++++++++ .../issues/issue-layouts/kanban/block.tsx | 14 +- .../issue-layouts/kanban/blocks-list.tsx | 51 +-- .../issues/issue-layouts/kanban/default.tsx | 89 +++-- .../issues/issue-layouts/kanban/index.ts | 2 +- .../issue-layouts/kanban/properties.tsx | 34 +- ...ssue-form.tsx => quick-add-issue-form.tsx} | 143 ++++---- .../issue-layouts/kanban/roots/cycle-root.tsx | 185 ++-------- .../kanban/roots/module-root.tsx | 192 +++-------- .../kanban/roots/profile-issues-root.tsx | 21 +- .../kanban/roots/project-root.tsx | 156 +-------- .../kanban/roots/project-view-root.tsx | 124 ++----- .../issues/issue-layouts/kanban/swimlanes.tsx | 62 ++-- .../issue-layouts/list/base-list-root.tsx | 120 +++++++ .../issues/issue-layouts/list/block.tsx | 44 ++- .../issues/issue-layouts/list/blocks-list.tsx | 21 +- .../issues/issue-layouts/list/default.tsx | 238 ++++++++----- .../list/headers/group-by-card.tsx | 94 +++-- .../issues/issue-layouts/list/index.ts | 2 +- .../list/inline-create-issue-form.tsx | 178 ---------- .../issue-layouts/list/list-view-types.d.ts | 6 + .../issues/issue-layouts/list/properties.tsx | 19 +- .../list/quick-add-issue-form.tsx | 148 ++++++++ .../list/roots/archived-issue-root.tsx | 77 ++--- .../issue-layouts/list/roots/cycle-root.tsx | 104 ++---- .../issue-layouts/list/roots/module-root.tsx | 103 ++---- .../list/roots/profile-issues-root.tsx | 79 ++--- .../issue-layouts/list/roots/project-root.tsx | 101 ++---- .../list/roots/project-view-root.tsx | 72 ++-- .../issues/issue-layouts/properties/state.tsx | 15 +- .../quick-action-dropdowns/archived-issue.tsx | 9 +- .../quick-action-dropdowns/cycle-issue.tsx | 16 +- .../quick-action-dropdowns/module-issue.tsx | 16 +- .../quick-action-dropdowns/project-issue.tsx | 11 +- .../issue-layouts/roots/cycle-layout-root.tsx | 81 +++-- .../roots/module-layout-root.tsx | 67 ++-- .../roots/project-layout-root.tsx | 61 ++-- .../roots/project-view-layout-root.tsx | 82 ++--- .../spreadsheet/base-spreadsheet-root.tsx | 113 ++++++ .../spreadsheet/columns/created-on-column.tsx | 2 +- .../issues/issue-layouts/spreadsheet/index.ts | 2 +- ...ssue-form.tsx => quick-add-issue-form.tsx} | 102 ++++-- .../spreadsheet/roots/cycle-root.tsx | 64 +--- .../spreadsheet/roots/module-root.tsx | 65 +--- .../spreadsheet/roots/project-root.tsx | 83 +---- .../spreadsheet/roots/project-view-root.tsx | 67 +--- .../spreadsheet/spreadsheet-view.tsx | 16 +- web/components/issues/issue-layouts/types.ts | 5 + .../issues/issue-peek-overview/root.tsx | 2 +- web/components/issues/modal.tsx | 33 +- .../page-views/workspace-dashboard.tsx | 1 + web/lib/wrappers/posthog-wrapper.tsx | 2 +- web/package.json | 1 + web/services/cycle.service.ts | 16 + web/services/issue/issue.service.ts | 13 +- web/services/issue/issue_archive.service.ts | 10 + web/services/issue/issue_draft.service.tsx | 10 + web/services/module.service.ts | 25 +- web/store/cycle-issues/index.ts | 1 + web/store/cycle-issues/issue_filters.store.ts | 201 +++++++++++ web/store/issue/issue.store.ts | 87 ++--- web/store/issue/issue_quick_add.store.ts | 265 +++++--------- web/store/issues/index.ts | 40 +++ .../project-issues/archived/filter.store.ts | 140 ++++++++ .../project-issues/archived/issue.store.ts | 127 +++++++ .../project-issues/base-issue-filter.store.ts | 29 ++ .../issues/project-issues/base-issue.store.ts | 152 +++++++++ .../project-issues/cycle/filter.store.ts | 253 ++++++++++++++ .../project-issues/cycle/issue.store.ts | 315 +++++++++++++++++ .../project-issues/draft/filter.store.ts | 137 ++++++++ .../project-issues/draft/issue.store.ts | 176 ++++++++++ .../project-issues/issue-filters.store.ts | 252 ++++++++++++++ .../project-issues/module/filter.store.ts | 261 ++++++++++++++ .../project-issues/module/issue.store.ts | 322 ++++++++++++++++++ .../project-view/filter.store.ts | 255 ++++++++++++++ .../project-view/issue.store.ts | 215 ++++++++++++ .../project-issues/project/filter.store.ts | 140 ++++++++ .../project-issues/project/issue.store.ts | 215 ++++++++++++ web/store/issues/types.ts | 27 ++ web/store/module-issues/index.ts | 1 + .../module-issues/issue_filters.store.ts | 201 +++++++++++ web/store/project/project-label.store.ts | 8 + web/store/project/project-members.store.ts | 8 + web/store/project/project-state.store.ts | 7 + web/store/project/project.store.ts | 7 + web/store/root.ts | 97 ++++++ 116 files changed, 6137 insertions(+), 3026 deletions(-) create mode 100644 web/components/issues/issue-layouts/calendar/base-calendar-root.tsx rename web/components/issues/issue-layouts/calendar/{inline-create-issue-form.tsx => quick-add-issue-form.tsx} (70%) create mode 100644 web/components/issues/issue-layouts/gantt/base-gantt-root.tsx create mode 100644 web/components/issues/issue-layouts/gantt/project-root.tsx rename web/components/issues/issue-layouts/gantt/{inline-create-issue-form.tsx => quick-add-issue-form.tsx} (90%) delete mode 100644 web/components/issues/issue-layouts/gantt/root.tsx create mode 100644 web/components/issues/issue-layouts/kanban/base-kanban-root.tsx rename web/components/issues/issue-layouts/kanban/{inline-create-issue-form.tsx => quick-add-issue-form.tsx} (57%) create mode 100644 web/components/issues/issue-layouts/list/base-list-root.tsx delete mode 100644 web/components/issues/issue-layouts/list/inline-create-issue-form.tsx create mode 100644 web/components/issues/issue-layouts/list/list-view-types.d.ts create mode 100644 web/components/issues/issue-layouts/list/quick-add-issue-form.tsx create mode 100644 web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx rename web/components/issues/issue-layouts/spreadsheet/{inline-create-issue-form.tsx => quick-add-issue-form.tsx} (58%) create mode 100644 web/components/issues/issue-layouts/types.ts create mode 100644 web/store/cycle-issues/index.ts create mode 100644 web/store/cycle-issues/issue_filters.store.ts create mode 100644 web/store/issues/index.ts create mode 100644 web/store/issues/project-issues/archived/filter.store.ts create mode 100644 web/store/issues/project-issues/archived/issue.store.ts create mode 100644 web/store/issues/project-issues/base-issue-filter.store.ts create mode 100644 web/store/issues/project-issues/base-issue.store.ts create mode 100644 web/store/issues/project-issues/cycle/filter.store.ts create mode 100644 web/store/issues/project-issues/cycle/issue.store.ts create mode 100644 web/store/issues/project-issues/draft/filter.store.ts create mode 100644 web/store/issues/project-issues/draft/issue.store.ts create mode 100644 web/store/issues/project-issues/issue-filters.store.ts create mode 100644 web/store/issues/project-issues/module/filter.store.ts create mode 100644 web/store/issues/project-issues/module/issue.store.ts create mode 100644 web/store/issues/project-issues/project-view/filter.store.ts create mode 100644 web/store/issues/project-issues/project-view/issue.store.ts create mode 100644 web/store/issues/project-issues/project/filter.store.ts create mode 100644 web/store/issues/project-issues/project/issue.store.ts create mode 100644 web/store/issues/types.ts create mode 100644 web/store/module-issues/index.ts create mode 100644 web/store/module-issues/issue_filters.store.ts diff --git a/packages/ui/src/spinners/circular-spinner.tsx b/packages/ui/src/spinners/circular-spinner.tsx index e7e952295..9ac8286f2 100644 --- a/packages/ui/src/spinners/circular-spinner.tsx +++ b/packages/ui/src/spinners/circular-spinner.tsx @@ -17,7 +17,7 @@ export const Spinner: React.FC = ({ aria-hidden="true" height={height} width={width} - className={`mr-2 animate-spin fill-blue-600 text-custom-text-200 ${className}`} + className={`animate-spin fill-blue-600 text-custom-text-200 ${className}`} viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg" diff --git a/web/components/gantt-chart/sidebar/sidebar.tsx b/web/components/gantt-chart/sidebar/sidebar.tsx index 72fbe1267..b3179f124 100644 --- a/web/components/gantt-chart/sidebar/sidebar.tsx +++ b/web/components/gantt-chart/sidebar/sidebar.tsx @@ -12,6 +12,7 @@ import { GanttInlineCreateIssueForm, IssueGanttSidebarBlock } from "components/i import { findTotalDaysInRange } from "helpers/date-time.helper"; // types import { IGanttBlock, IBlockUpdateData } from "components/gantt-chart/types"; +import { IIssue } from "types"; type Props = { title: string; @@ -19,11 +20,18 @@ type Props = { blocks: IGanttBlock[] | null; enableReorder: boolean; enableQuickIssueCreate?: boolean; + quickAddCallback?: ( + workspaceSlug: string, + projectId: string, + data: IIssue, + viewId?: string + ) => Promise; + viewId?: string; }; export const IssueGanttSidebar: React.FC = (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, blockUpdateHandler, blocks, enableReorder, enableQuickIssueCreate } = props; + const { title, blockUpdateHandler, blocks, enableReorder, enableQuickIssueCreate, quickAddCallback, viewId } = props; const router = useRouter(); const { cycleId } = router.query; @@ -152,7 +160,9 @@ export const IssueGanttSidebar: React.FC = (props) => { )} {droppableProvided.placeholder} - {enableQuickIssueCreate && } + {enableQuickIssueCreate && ( + + )} )} diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index fea9e7019..8bff1c999 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -19,25 +19,32 @@ import { renderEmoji } from "helpers/emoji.helper"; import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueLayouts } from "types"; // constants import { ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue"; +import { EFilterType } from "store/issues/types"; export const CycleIssuesHeader: React.FC = observer(() => { const [analyticsModal, setAnalyticsModal] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId, cycleId } = router.query; + const { workspaceSlug, projectId, cycleId } = router.query as { + workspaceSlug: string; + projectId: string; + cycleId: string; + }; const { - issueFilter: issueFilterStore, cycle: cycleStore, - cycleIssueFilter: cycleIssueFilterStore, + cycleIssueFilters: cycleIssueFiltersStore, + projectIssuesFilter: projectIssueFiltersStore, project: { currentProjectDetails }, - projectLabel: { projectLabels }, projectMember: { projectMembers }, + projectLabel: { projectLabels }, projectState: projectStateStore, commandPalette: commandPaletteStore, + + cycleIssuesFilter: { issueFilters, updateFilters }, } = useMobxStore(); - const activeLayout = issueFilterStore.userDisplayFilters.layout; + const activeLayout = projectIssueFiltersStore.issueFilters?.displayFilters?.layout; const { setValue, storedValue } = useLocalStorage("cycle_sidebar_collapsed", "false"); @@ -49,58 +56,44 @@ export const CycleIssuesHeader: React.FC = observer(() => { const handleLayoutChange = useCallback( (layout: TIssueLayouts) => { if (!workspaceSlug || !projectId) return; - - issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { - display_filters: { - layout, - }, - }); + updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, { layout: layout }, cycleId); }, - [issueFilterStore, projectId, workspaceSlug] + [workspaceSlug, projectId, cycleId, updateFilters] ); const handleFiltersUpdate = useCallback( (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId || !cycleId) return; - - const newValues = cycleIssueFilterStore.cycleFilters?.[key] ?? []; + if (!workspaceSlug || !projectId) return; + const newValues = issueFilters?.filters?.[key] ?? []; if (Array.isArray(value)) { value.forEach((val) => { if (!newValues.includes(val)) newValues.push(val); }); } else { - if (cycleIssueFilterStore.cycleFilters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); else newValues.push(value); } - cycleIssueFilterStore.updateCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString(), { - [key]: newValues, - }); + updateFilters(workspaceSlug, projectId, EFilterType.FILTERS, { [key]: newValues }, cycleId); }, - [cycleId, cycleIssueFilterStore, projectId, workspaceSlug] + [workspaceSlug, projectId, cycleId, issueFilters, updateFilters] ); - const handleDisplayFiltersUpdate = useCallback( + const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !projectId) return; - - issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { - display_filters: { - ...updatedDisplayFilter, - }, - }); + updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_FILTERS, updatedDisplayFilter, cycleId); }, - [issueFilterStore, projectId, workspaceSlug] + [workspaceSlug, projectId, cycleId, updateFilters] ); - const handleDisplayPropertiesUpdate = useCallback( + const handleDisplayProperties = useCallback( (property: Partial) => { if (!workspaceSlug || !projectId) return; - - issueFilterStore.updateDisplayProperties(workspaceSlug.toString(), projectId.toString(), property); + updateFilters(workspaceSlug, projectId, EFilterType.DISPLAY_PROPERTIES, property, cycleId); }, - [issueFilterStore, projectId, workspaceSlug] + [workspaceSlug, projectId, cycleId, updateFilters] ); const cyclesList = cycleStore.projectCycles; @@ -173,25 +166,25 @@ export const CycleIssuesHeader: React.FC = observer(() => { /> m.member)} - states={projectStateStore.states?.[projectId?.toString() ?? ""] ?? undefined} + states={projectStateStore.states?.[projectId ?? ""] ?? undefined} /> - )} + )} */} ); }); diff --git a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index fad4d814e..1300ea17f 100644 --- a/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -1,179 +1,48 @@ -import React, { useCallback, useState } from "react"; +import React from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { DragDropContext } from "@hello-pangea/dnd"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -// components -import { KanBanSwimLanes } from "../swimlanes"; -import { KanBan } from "../default"; +// ui import { CycleIssueQuickActions } from "components/issues"; -import { Spinner } from "@plane/ui"; // types import { IIssue } from "types"; -// constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +import { EIssueActions } from "../../types"; +// components +import { BaseKanBanRoot } from "../base-kanban-root"; export interface ICycleKanBanLayout {} export const CycleKanBanLayout: React.FC = observer(() => { - // router const router = useRouter(); - const { workspaceSlug, cycleId } = router.query; + const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string }; + // store - const { - project: projectStore, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - cycleIssue: cycleIssueStore, - issueFilter: issueFilterStore, - cycleIssueKanBanView: cycleIssueKanBanViewStore, - issueDetail: issueDetailStore, - } = useMobxStore(); + const { cycleIssues: cycleIssueStore, cycleIssueKanBanView: cycleIssueKanBanViewStore } = useMobxStore(); - const issues = cycleIssueStore?.getIssues; - - const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null; - - const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - - const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null; - - const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; - - const displayProperties = issueFilterStore?.userDisplayProperties || null; - - const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by - ? "swimlanes" - : "default"; - - const [isDragStarted, setIsDragStarted] = useState(false); - - // const onDragStart = () => { - // setIsDragStarted(true); - // }; - - const onDragEnd = (result: any) => { - setIsDragStarted(false); - - if (!result) return; - - if ( - result.destination && - result.source && - result.destination.droppableId === result.source.droppableId && - result.destination.index === result.source.index - ) - return; - - currentKanBanView === "default" - ? cycleIssueKanBanViewStore?.handleDragDrop(result.source, result.destination) - : cycleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); - }; - - const handleIssues = useCallback( - (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { + const issueActions = { + [EIssueActions.UPDATE]: async (issue: IIssue) => { if (!workspaceSlug || !cycleId) return; - if (action === "update") { - cycleIssueStore.updateIssueStructure(group_by, sub_group_by, issue); - issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - } - if (action === "delete") cycleIssueStore.deleteIssue(group_by, sub_group_by, issue); - if (action === "remove" && issue.bridge_id) { - cycleIssueStore.deleteIssue(group_by, sub_group_by, issue); - cycleIssueStore.removeIssueFromCycle( - workspaceSlug.toString(), - issue.project, - cycleId.toString(), - issue.bridge_id - ); - } + cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId); + }, + [EIssueActions.DELETE]: async (issue: IIssue) => { + if (!workspaceSlug || !cycleId) return; + cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId); + }, + [EIssueActions.REMOVE]: async (issue: IIssue) => { + if (!workspaceSlug || !cycleId || !issue.bridge_id) return; + cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id); }, - [cycleIssueStore, issueDetailStore, cycleId, workspaceSlug] - ); - - const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { - cycleIssueKanBanViewStore.handleKanBanToggle(toggle, value); }; - - const states = projectStateStore?.projectStates || null; - const priorities = ISSUE_PRIORITIES || null; - const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; - // const estimates = - // currentProjectDetails?.estimate !== null - // ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null - // : null; - return ( - <> - {cycleIssueStore.loader ? ( -
- -
- ) : ( -
- - {currentKanBanView === "default" ? ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} - /> - )} - displayProperties={displayProperties} - kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={projectLabels} - members={projectMembers?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - isDragStarted={isDragStarted} - /> - ) : ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromCycle={async () => handleIssues(sub_group_by, group_by, issue, "remove")} - /> - )} - displayProperties={displayProperties} - kanBanToggle={cycleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={projectLabels} - members={projectMembers?.map((m) => m.member) ?? null} - projects={projects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - isDragStarted={isDragStarted} - /> - )} - -
- )} - + ); }); diff --git a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx index a3429ef64..59bfa7e36 100644 --- a/web/components/issues/issue-layouts/kanban/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -1,176 +1,74 @@ -import React, { useCallback, useState } from "react"; +import React from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { DragDropContext } from "@hello-pangea/dnd"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { KanBanSwimLanes } from "../swimlanes"; -import { KanBan } from "../default"; import { ModuleIssueQuickActions } from "components/issues"; -import { Spinner } from "@plane/ui"; // types import { IIssue } from "types"; // constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +import { EIssueActions } from "../../types"; +import { BaseKanBanRoot } from "../base-kanban-root"; export interface IModuleKanBanLayout {} export const ModuleKanBanLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, moduleId } = router.query; + const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string }; + // store const { - project: { workspaceProjects }, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - moduleIssue: moduleIssueStore, - issueFilter: issueFilterStore, + moduleIssues: moduleIssueStore, moduleIssueKanBanView: moduleIssueKanBanViewStore, issueDetail: issueDetailStore, } = useMobxStore(); - const issues = moduleIssueStore?.getIssues; + // const handleIssues = useCallback( + // (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => { + // if (!workspaceSlug || !moduleId) return; - const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null; + // if (action === "update") { + // moduleIssueStore.updateIssueStructure(group_by, sub_group_by, issue); + // issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); + // } + // if (action === "delete") moduleIssueStore.deleteIssue(group_by, sub_group_by, issue); + // if (action === "remove" && issue.bridge_id) { + // moduleIssueStore.deleteIssue(group_by, null, issue); + // moduleIssueStore.removeIssueFromModule( + // workspaceSlug.toString(), + // issue.project, + // moduleId.toString(), + // issue.bridge_id + // ); + // } + // }, + // [moduleIssueStore, issueDetailStore, moduleId, workspaceSlug] + // ); - const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - - const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null; - - const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; - - const displayProperties = issueFilterStore?.userDisplayProperties || null; - - const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by - ? "swimlanes" - : "default"; - - const [isDragStarted, setIsDragStarted] = useState(false); - - // const onDragStart = () => { - // setIsDragStarted(true); - // }; - - const onDragEnd = (result: any) => { - setIsDragStarted(false); - if (!result) return; - - if ( - result.destination && - result.source && - result.destination.droppableId === result.source.droppableId && - result.destination.index === result.source.index - ) - return; - - currentKanBanView === "default" - ? moduleIssueKanBanViewStore?.handleDragDrop(result.source, result.destination) - : moduleIssueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); - }; - - const handleIssues = useCallback( - (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { + const issueActions = { + [EIssueActions.UPDATE]: async (issue: IIssue) => { if (!workspaceSlug || !moduleId) return; - if (action === "update") { - moduleIssueStore.updateIssueStructure(group_by, sub_group_by, issue); - issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - } - if (action === "delete") moduleIssueStore.deleteIssue(group_by, sub_group_by, issue); - if (action === "remove" && issue.bridge_id) { - moduleIssueStore.deleteIssue(group_by, null, issue); - moduleIssueStore.removeIssueFromModule( - workspaceSlug.toString(), - issue.project, - moduleId.toString(), - issue.bridge_id - ); - } + moduleIssueStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue, moduleId); + }, + [EIssueActions.DELETE]: async (issue: IIssue) => { + if (!workspaceSlug || !moduleId) return; + moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId); + }, + [EIssueActions.REMOVE]: async (issue: IIssue) => { + if (!workspaceSlug || !moduleId || !issue.bridge_id) return; + moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, issue.id, moduleId, issue.bridge_id); }, - [moduleIssueStore, issueDetailStore, moduleId, workspaceSlug] - ); - - const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { - moduleIssueKanBanViewStore.handleKanBanToggle(toggle, value); }; - - const states = projectStateStore?.projectStates || null; - const priorities = ISSUE_PRIORITIES || null; - const stateGroups = ISSUE_STATE_GROUPS || null; - // const estimates = - // currentProjectDetails?.estimate !== null - // ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null - // : null; - return ( - <> - {moduleIssueStore.loader ? ( -
- -
- ) : ( -
- - {currentKanBanView === "default" ? ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} - /> - )} - displayProperties={displayProperties} - kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={projectLabels} - members={projectMembers?.map((m) => m.member) ?? null} - projects={workspaceProjects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - isDragStarted={isDragStarted} - /> - ) : ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - handleRemoveFromModule={async () => handleIssues(sub_group_by, group_by, issue, "remove")} - /> - )} - displayProperties={displayProperties} - kanBanToggle={moduleIssueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={projectLabels} - members={projectMembers?.map((m) => m.member) ?? null} - projects={workspaceProjects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - isDragStarted={isDragStarted} - /> - )} - -
- )} - + ); }); diff --git a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index 4682f8e26..5c5388afb 100644 --- a/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -13,6 +13,7 @@ import { Spinner } from "@plane/ui"; import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; // types import { IIssue } from "types"; +import { EIssueActions } from "../../types"; export interface IProfileIssuesKanBanLayout {} @@ -71,14 +72,14 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { }; const handleIssues = useCallback( - (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete") => { + (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => { if (!workspaceSlug) return; - if (action === "update") { + if (action === EIssueActions.UPDATE) { profileIssuesStore.updateIssueStructure(group_by, sub_group_by, issue); issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); } - if (action === "delete") profileIssuesStore.deleteIssue(group_by, sub_group_by, issue); + if (action === EIssueActions.DELETE) profileIssuesStore.deleteIssue(group_by, sub_group_by, issue); }, [profileIssuesStore, issueDetailStore, workspaceSlug] ); @@ -104,7 +105,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { {currentKanBanView === "default" ? ( { quickActions={(sub_group_by, group_by, issue) => ( handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)} /> )} displayProperties={displayProperties} @@ -130,7 +132,8 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { /> ) : ( { quickActions={(sub_group_by, group_by, issue) => ( handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} + handleDelete={async () => handleIssues(sub_group_by, group_by, issue, EIssueActions.DELETE)} + handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, EIssueActions.UPDATE)} /> )} displayProperties={displayProperties} diff --git a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx index 070d13a89..e38c0a9ea 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx @@ -1,18 +1,14 @@ -import { useCallback, useState } from "react"; import { useRouter } from "next/router"; -import { DragDropContext } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { KanBanSwimLanes } from "../swimlanes"; -import { KanBan } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; -import { Spinner } from "@plane/ui"; // types import { IIssue } from "types"; // constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +import { EIssueActions } from "../../types"; +import { BaseKanBanRoot } from "../base-kanban-root"; export interface IKanBanLayout {} @@ -21,149 +17,31 @@ export const KanBanLayout: React.FC = observer(() => { const { workspaceSlug } = router.query as { workspaceSlug: string }; const { - project: { workspaceProjects }, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - issue: issueStore, - issueFilter: issueFilterStore, + projectIssues: issueStore, issueKanBanView: issueKanBanViewStore, issueDetail: issueDetailStore, } = useMobxStore(); - const issues = issueStore?.getIssues; - - const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null; - - const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - - const order_by: string | null = issueFilterStore?.userDisplayFilters?.order_by || null; - - const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; - - const displayProperties = issueFilterStore?.userDisplayProperties || null; - - const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by - ? "swimlanes" - : "default"; - - const [isDragStarted, setIsDragStarted] = useState(false); - - const onDragStart = () => { - setIsDragStarted(true); - }; - - const onDragEnd = (result: any) => { - setIsDragStarted(false); - - if (!result) return; - - if ( - result.destination && - result.source && - result.source.droppableId && - result.destination.droppableId && - result.destination.droppableId === result.source.droppableId && - result.destination.index === result.source.index - ) - return; - - currentKanBanView === "default" - ? issueKanBanViewStore?.handleDragDrop(result.source, result.destination) - : issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); - }; - - const handleIssues = useCallback( - (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: "update" | "delete") => { + const issueActions = { + [EIssueActions.UPDATE]: async (issue: IIssue) => { if (!workspaceSlug) return; - if (action === "update") { - issueStore.updateIssueStructure(group_by, sub_group_by, issue); - issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - } - if (action === "delete") issueStore.deleteIssue(group_by, sub_group_by, issue); + await issueDetailStore.updateIssue(workspaceSlug, issue.project, issue.id, issue); }, - [issueStore, issueDetailStore, workspaceSlug] - ); + [EIssueActions.DELETE]: async (issue: IIssue) => { + if (!workspaceSlug) return; - const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => { - issueKanBanViewStore.handleKanBanToggle(toggle, value); + await issueDetailStore.deleteIssue(workspaceSlug, issue.project, issue.id); + }, }; - const states = projectStateStore?.projectStates || null; - const priorities = ISSUE_PRIORITIES || null; - const stateGroups = ISSUE_STATE_GROUPS || null; - // const estimates = - // currentProjectDetails?.estimate !== null - // ? projectStore.projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null - // : null; - return ( - <> - {issueStore.loader ? ( -
- -
- ) : ( -
- - {currentKanBanView === "default" ? ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - /> - )} - displayProperties={displayProperties} - kanBanToggle={issueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={projectLabels} - members={projectMembers?.map((m) => m.member) ?? null} - projects={workspaceProjects} - enableQuickIssueCreate - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - isDragStarted={isDragStarted} - /> - ) : ( - ( - handleIssues(sub_group_by, group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(sub_group_by, group_by, data, "update")} - /> - )} - displayProperties={displayProperties} - kanBanToggle={issueKanBanViewStore?.kanBanToggle} - handleKanBanToggle={handleKanBanToggle} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={projectLabels} - members={projectMembers?.map((m) => m.member) ?? null} - projects={workspaceProjects} - showEmptyGroup={userDisplayFilters?.show_empty_groups || true} - isDragStarted={isDragStarted} - /> - )} - -
- )} - + ); }); diff --git a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx index c03b50934..c5cb3a03a 100644 --- a/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx @@ -1,107 +1,47 @@ import React from "react"; -import { DragDropContext } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; -// components -import { KanBanSwimLanes } from "../swimlanes"; -import { KanBan } from "../default"; +import { useRouter } from "next/router"; // store import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; -// constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +// constant +import { IIssue } from "types"; +import { EIssueActions } from "../../types"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; +// components +import { BaseKanBanRoot } from "../base-kanban-root"; export interface IViewKanBanLayout {} export const ProjectViewKanBanLayout: React.FC = observer(() => { + const router = useRouter(); + const { workspaceSlug } = router.query as { workspaceSlug: string }; + const { - project: projectStore, - projectMember: { projectMembers }, - projectState: projectStateStore, - issue: issueStore, - issueFilter: issueFilterStore, - issueKanBanView: issueKanBanViewStore, - }: RootStore = useMobxStore(); + viewIssues: projectViewIssuesStore, + issueKanBanView: projectViewIssueKanBanViewStore, + issueDetail: issueDetailStore, + } = useMobxStore(); - const issues = issueStore?.getIssues; + const issueActions = { + [EIssueActions.UPDATE]: async (issue: IIssue) => { + if (!workspaceSlug) return; - const sub_group_by: string | null = issueFilterStore?.userDisplayFilters?.sub_group_by || null; + await issueDetailStore.updateIssue(workspaceSlug, issue.project, issue.id, issue); + }, + [EIssueActions.DELETE]: async (issue: IIssue) => { + if (!workspaceSlug) return; - const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - - const display_properties = issueFilterStore?.userDisplayProperties || null; - - const currentKanBanView: "swimlanes" | "default" = issueFilterStore?.userDisplayFilters?.sub_group_by - ? "swimlanes" - : "default"; - - const onDragEnd = (result: any) => { - if (!result) return; - - if ( - result.destination && - result.source && - result.destination.droppableId === result.source.droppableId && - result.destination.index === result.source.index - ) - return; - - currentKanBanView === "default" - ? issueKanBanViewStore?.handleDragDrop(result.source, result.destination) - : issueKanBanViewStore?.handleSwimlaneDragDrop(result.source, result.destination); + await issueDetailStore.deleteIssue(workspaceSlug, issue.project, issue.id); + }, }; - const updateIssue = (sub_group_by: string | null, group_by: string | null, issue: any) => { - issueStore.updateIssueStructure(group_by, sub_group_by, issue); - }; - - const states = projectStateStore?.projectStates || null; - const priorities = ISSUE_PRIORITIES || null; - // const labels = projectStore?.projectLabels || null; - const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = projectStateStore?.projectStates || null; - const estimates = null; - - return null; - - // return ( - //
- // - // {currentKanBanView === "default" ? ( - // {}} - // handleKanBanToggle={() => {}} - // states={states} - // stateGroups={stateGroups} - // priorities={priorities} - // labels={labels} - // members={members} - // projects={projects} - // estimates={estimates} - // /> - // ) : ( - // {}} - // handleKanBanToggle={() => {}} - // states={states} - // stateGroups={stateGroups} - // priorities={priorities} - // labels={labels} - // members={members} - // projects={projects} - // estimates={estimates} - // /> - // )} - // - //
- // ); + return ( + + ); }); diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 7162025e0..efd8e1759 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -6,11 +6,14 @@ import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root"; import { KanBan } from "./default"; // types import { IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types"; +import { IIssueResponse, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "store/issues/types"; // constants import { getValueFromObject } from "constants/issue"; +import { EIssueActions } from "../types"; interface ISubGroupSwimlaneHeader { - issues: any; + issues: IIssueResponse; + issueIds: any; sub_group_by: string | null; group_by: string | null; list: any; @@ -20,6 +23,7 @@ interface ISubGroupSwimlaneHeader { } const SubGroupSwimlaneHeader: React.FC = ({ issues, + issueIds, sub_group_by, group_by, list, @@ -29,9 +33,9 @@ const SubGroupSwimlaneHeader: React.FC = ({ }) => { const calculateIssueCount = (column_id: string) => { let issueCount = 0; - issues && - Object.keys(issues)?.forEach((_issueKey: any) => { - issueCount += issues?.[_issueKey]?.[column_id]?.length || 0; + issueIds && + Object.keys(issueIds)?.forEach((_issueKey: any) => { + issueCount += issueIds?.[_issueKey]?.[column_id]?.length || 0; }); return issueCount; }; @@ -58,6 +62,8 @@ const SubGroupSwimlaneHeader: React.FC = ({ }; interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { + issues: IIssueResponse; + issueIds: any; order_by: string | null; showEmptyGroup: boolean; states: IState[] | null; @@ -66,15 +72,9 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { labels: IIssueLabel[] | null; members: IUserLite[] | null; projects: IProject[] | null; - issues: any; - handleIssues: ( - sub_group_by: string | null, - group_by: string | null, - issue: IIssue, - action: "update" | "delete" - ) => void; + handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; - displayProperties: IIssueDisplayProperties; + displayProperties: IIssueDisplayProperties | null; kanBanToggle: any; handleKanBanToggle: any; isDragStarted?: boolean; @@ -82,6 +82,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { const SubGroupSwimlane: React.FC = observer((props) => { const { issues, + issueIds, sub_group_by, group_by, order_by, @@ -104,9 +105,9 @@ const SubGroupSwimlane: React.FC = observer((props) => { const calculateIssueCount = (column_id: string) => { let issueCount = 0; - issues?.[column_id] && - Object.keys(issues?.[column_id])?.forEach((_list: any) => { - issueCount += issues?.[column_id]?.[_list]?.length || 0; + issueIds?.[column_id] && + Object.keys(issueIds?.[column_id])?.forEach((_list: any) => { + issueCount += issueIds?.[column_id]?.[_list]?.length || 0; }); return issueCount; }; @@ -134,7 +135,8 @@ const SubGroupSwimlane: React.FC = observer((props) => { {!kanBanToggle?.subgroupByIssuesVisibility.includes(getValueFromObject(_list, listKey) as string) && (
= observer((props) => { }); export interface IKanBanSwimLanes { - issues: any; + issues: IIssueResponse; + issueIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues; sub_group_by: string | null; group_by: string | null; order_by: string | null; - handleIssues: ( - sub_group_by: string | null, - group_by: string | null, - issue: IIssue, - action: "update" | "delete" - ) => void; + handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue, action: EIssueActions) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; - displayProperties: IIssueDisplayProperties; + displayProperties: IIssueDisplayProperties | null; kanBanToggle: any; handleKanBanToggle: any; showEmptyGroup: boolean; @@ -190,6 +188,7 @@ export interface IKanBanSwimLanes { export const KanBanSwimLanes: React.FC = observer((props) => { const { issues, + issueIds, sub_group_by, group_by, order_by, @@ -214,6 +213,7 @@ export const KanBanSwimLanes: React.FC = observer((props) => { {group_by && group_by === "project" && ( = observer((props) => { {group_by && group_by === "state" && ( = observer((props) => { {group_by && group_by === "state_detail.group" && ( = observer((props) => { {group_by && group_by === "priority" && ( = observer((props) => { {group_by && group_by === "labels" && ( = observer((props) => { {group_by && group_by === "assignees" && ( = observer((props) => { {group_by && group_by === "created_by" && ( = observer((props) => { {sub_group_by && sub_group_by === "project" && ( = observer((props) => { {sub_group_by && sub_group_by === "state" && ( = observer((props) => { {sub_group_by && sub_group_by === "state" && ( = observer((props) => { {sub_group_by && sub_group_by === "state_detail.group" && ( = observer((props) => { {sub_group_by && sub_group_by === "priority" && ( = observer((props) => { {sub_group_by && sub_group_by === "labels" && ( = observer((props) => { {sub_group_by && sub_group_by === "assignees" && ( = observer((props) => { {sub_group_by && sub_group_by === "created_by" && ( ; + issueActions: { + [EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => void; + [EIssueActions.UPDATE]?: (group_by: string | null, issue: IIssue) => void; + [EIssueActions.REMOVE]?: (group_by: string | null, issue: IIssue) => void; + }; + getProjects: (projectStore: IProjectStore) => IProject[] | null; + viewId?: string; +} + +export const BaseListRoot = observer((props: IBaseListRoot) => { + const { issueFilterStore, issueStore, QuickActions, issueActions, getProjects, viewId } = props; + + const { + project: projectStore, + projectMember: { projectMembers }, + projectState: projectStateStore, + projectLabel: { projectLabels }, + } = useMobxStore(); + + const issueIds = issueStore.getIssuesIds || []; + const issues = issueStore.getIssues; + + const displayFilters = issueFilterStore?.issueFilters?.displayFilters; + const group_by = displayFilters?.group_by || null; + const showEmptyGroup = displayFilters?.show_empty_groups ?? false; + + const displayProperties = issueFilterStore?.issueFilters?.displayProperties; + + const states = projectStateStore?.projectStates; + const priorities = ISSUE_PRIORITIES; + const labels = projectLabels; + const stateGroups = ISSUE_STATE_GROUPS; + const projects = getProjects(projectStore); + const members = projectMembers?.map((m) => m.member) ?? null; + const handleIssues = async (issue: IIssue, action: EIssueActions) => { + if (issueActions[action]) { + issueActions[action]!(group_by, issue); + } + }; + + return ( + <> + {issueStore.loader === "mutation" ? ( +
+ +
+ ) : ( +
+ ( + handleIssues(issue, EIssueActions.DELETE)} + handleUpdate={ + issueActions[EIssueActions.UPDATE] + ? async (data) => handleIssues(data, EIssueActions.UPDATE) + : undefined + } + handleRemoveFromView={ + issueActions[EIssueActions.REMOVE] ? async () => handleIssues(issue, EIssueActions.REMOVE) : undefined + } + /> + )} + displayProperties={displayProperties} + states={states} + stateGroups={stateGroups} + priorities={priorities} + labels={labels} + members={members} + projects={projects} + issueIds={issueIds} + showEmptyGroup={showEmptyGroup} + enableIssueQuickAdd={true} + isReadonly={false} + quickAddCallback={issueStore.quickAddIssue} + viewId={viewId} + /> +
+ )} + + ); +}); diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 28f5f765a..ea275db85 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -1,26 +1,27 @@ // components -import { KanBanProperties } from "./properties"; +import { ListProperties } from "./properties"; import { IssuePeekOverview } from "components/issues/issue-peek-overview"; // ui -import { Tooltip } from "@plane/ui"; +import { Spinner, Tooltip } from "@plane/ui"; // types import { IIssue, IIssueDisplayProperties } from "types"; +import { EIssueActions } from "../types"; interface IssueBlockProps { columnId: string; + issue: IIssue; - handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; + handleIssues: (issue: IIssue, action: EIssueActions) => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - displayProperties: IIssueDisplayProperties; + displayProperties: IIssueDisplayProperties | undefined; isReadonly?: boolean; - showEmptyGroup?: boolean; } export const IssueBlock: React.FC = (props) => { - const { columnId, issue, handleIssues, quickActions, displayProperties, showEmptyGroup, isReadonly } = props; + const { columnId, issue, handleIssues, quickActions, displayProperties, isReadonly } = props; const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => { - handleIssues(group_by, issueToUpdate, "update"); + handleIssues(issueToUpdate, EIssueActions.UPDATE); }; return ( @@ -31,16 +32,18 @@ export const IssueBlock: React.FC = (props) => { {issue?.project_detail?.identifier}-{issue.sequence_id}
)} + {issue?.tempId !== undefined && (
)} + { - handleIssues(!columnId && columnId === "null" ? null : columnId, issueToUpdate as IIssue, "update"); + handleIssues(issueToUpdate as IIssue, EIssueActions.UPDATE); }} > @@ -49,15 +52,22 @@ export const IssueBlock: React.FC = (props) => {
- - {quickActions(!columnId && columnId === "null" ? null : columnId, issue)} + {!issue?.tempId ? ( + <> + + {quickActions(!columnId && columnId === "null" ? null : columnId, issue)} + + ) : ( +
+ +
+ )}
diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 22a92a159..d313328fd 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -3,33 +3,34 @@ import { FC } from "react"; import { IssueBlock } from "components/issues"; // types import { IIssue, IIssueDisplayProperties } from "types"; +import { IIssueResponse, IGroupedIssues, TUnGroupedIssues } from "store/issues/types"; +import { EIssueActions } from "../types"; interface Props { columnId: string; - issues: IIssue[]; + issueIds: IGroupedIssues | TUnGroupedIssues | any; + issues: IIssueResponse; isReadonly?: boolean; - handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; + handleIssues: (issue: IIssue, action: EIssueActions) => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - displayProperties: IIssueDisplayProperties; - showEmptyGroup?: boolean; + displayProperties: IIssueDisplayProperties | undefined; } export const IssueBlocksList: FC = (props) => { - const { columnId, issues, handleIssues, quickActions, displayProperties, showEmptyGroup, isReadonly } = props; + const { columnId, issueIds, issues, handleIssues, quickActions, displayProperties, isReadonly } = props; return (
- {issues && issues.length > 0 ? ( - issues.map((issue) => ( + {issueIds && issueIds.length > 0 ? ( + issueIds.map((issueId: string) => ( )) ) : ( diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 4f2d215db..6727c387a 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -1,243 +1,321 @@ import React from "react"; -import { observer } from "mobx-react-lite"; // components import { ListGroupByHeaderRoot } from "./headers/group-by-root"; -import { IssueBlocksList, ListInlineCreateIssueForm } from "components/issues"; +import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues"; // types -import { IEstimatePoint, IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types"; +import { IIssue, IIssueDisplayProperties, IIssueLabel, IProject, IState, IUserLite } from "types"; +import { IIssueResponse, IGroupedIssues, TUnGroupedIssues } from "store/issues/types"; +import { EIssueActions } from "../types"; // constants import { getValueFromObject } from "constants/issue"; export interface IGroupByList { + issueIds: IGroupedIssues | TUnGroupedIssues | any; issues: any; group_by: string | null; list: any; - isReadonly?: boolean; listKey: string; - handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; - quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - displayProperties: IIssueDisplayProperties; + states: IState[] | null; is_list?: boolean; - enableQuickIssueCreate?: boolean; + handleIssues: (issue: IIssue, action: EIssueActions) => Promise; + quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; + displayProperties: IIssueDisplayProperties | undefined; + enableIssueQuickAdd: boolean; showEmptyGroup?: boolean; + isReadonly: boolean; + quickAddCallback?: ( + workspaceSlug: string, + projectId: string, + data: IIssue, + viewId?: string + ) => Promise; + viewId?: string; } -const GroupByList: React.FC = observer((props) => { +const GroupByList: React.FC = (props) => { const { + issueIds, issues, group_by, list, - isReadonly, listKey, + is_list = false, + states, handleIssues, quickActions, displayProperties, - is_list = false, - enableQuickIssueCreate, + enableIssueQuickAdd, showEmptyGroup, + isReadonly, + quickAddCallback, + viewId, } = props; + const prePopulateQuickAddData = (groupByKey: string | null, value: any) => { + const defaultState = states?.find((state) => state.default); + if (groupByKey === null) return { state: defaultState?.id }; + else { + if (groupByKey === "state") return { state: groupByKey === "state" ? value : defaultState?.id }; + else return { state: defaultState?.id, [groupByKey]: value }; + } + }; + + const validateEmptyIssueGroups = (issues: IIssue[]) => { + const issuesCount = issues?.length || 0; + if (!showEmptyGroup && issuesCount <= 0) return false; + return true; + }; + return (
{list && list.length > 0 && - list.map((_list: any) => ( -
-
- -
- {issues && ( - - )} - {enableQuickIssueCreate && ( - - )} -
- ))} + list.map( + (_list: any) => + validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[getValueFromObject(_list, listKey) as string]) && ( +
+
+ +
+ + {issues && ( + + )} + + {enableIssueQuickAdd && ( +
+ +
+ )} +
+ ) + )}
); -}); +}; -// TODO: update all the types export interface IList { - issues: any; + issueIds: IGroupedIssues | TUnGroupedIssues | any; + issues: IIssueResponse | undefined; group_by: string | null; - isReadonly?: boolean; - handleDragDrop?: (result: any) => void | undefined; - handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; + handleIssues: (issue: IIssue, action: EIssueActions) => Promise; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; - displayProperties: IIssueDisplayProperties; + displayProperties: IIssueDisplayProperties | undefined; + showEmptyGroup: boolean; + enableIssueQuickAdd: boolean; + isReadonly: boolean; states: IState[] | null; labels: IIssueLabel[] | null; members: IUserLite[] | null; projects: IProject[] | null; stateGroups: any; priorities: any; - enableQuickIssueCreate?: boolean; - estimates: IEstimatePoint[] | null; - showEmptyGroup?: boolean; + quickAddCallback?: ( + workspaceSlug: string, + projectId: string, + data: IIssue, + viewId?: string + ) => Promise; + viewId?: string; } -export const List: React.FC = observer((props) => { +export const List: React.FC = (props) => { const { + issueIds, issues, group_by, - isReadonly, handleIssues, quickActions, + quickAddCallback, + viewId, displayProperties, + showEmptyGroup, + enableIssueQuickAdd, + isReadonly, + states, + stateGroups, + priorities, labels, members, projects, - stateGroups, - priorities, - showEmptyGroup, - enableQuickIssueCreate, } = props; return (
{group_by === null && ( )} {group_by && group_by === "project" && projects && ( )} {group_by && group_by === "state" && states && ( )} {group_by && group_by === "state_detail.group" && stateGroups && ( )} {group_by && group_by === "priority" && priorities && ( )} {group_by && group_by === "labels" && labels && ( )} {group_by && group_by === "assignees" && members && ( )} {group_by && group_by === "created_by" && members && ( )}
); -}); +}; diff --git a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx index 764b7bcf7..0833fffdc 100644 --- a/web/components/issues/issue-layouts/list/headers/group-by-card.tsx +++ b/web/components/issues/issue-layouts/list/headers/group-by-card.tsx @@ -1,8 +1,5 @@ import React from "react"; import { useRouter } from "next/router"; -// services -import { ModuleService } from "services/module.service"; -import { IssueService } from "services/issue"; // lucide icons import { CircleDashed, Plus } from "lucide-react"; // components @@ -23,18 +20,15 @@ interface IHeaderGroupByCard { issuePayload: Partial; } -const moduleService = new ModuleService(); -const issueService = new IssueService(); - export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }: IHeaderGroupByCard) => { const router = useRouter(); const { workspaceSlug, projectId, moduleId, cycleId } = router.query; - const [isOpen, setIsOpen] = React.useState(false); - const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false); const { setToastAlert } = useToast(); - const verticalAlignPosition = false; + const [isOpen, setIsOpen] = React.useState(false); + + const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false); const renderExistingIssueModal = moduleId || cycleId; const ExistingIssuesListModalPayload = moduleId ? { module: true } : { cycle: true }; @@ -46,15 +40,15 @@ export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }: issues: data.map((i) => i.id), }; - await moduleService - .addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload) - .catch(() => - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the module. Please try again.", - }) - ); + // await moduleService + // .addIssuesToModule(workspaceSlug as string, projectId as string, moduleId as string, payload, user) + // .catch(() => + // setToastAlert({ + // type: "error", + // title: "Error!", + // message: "Selected issues could not be added to the module. Please try again.", + // }) + // ); }; const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { @@ -64,46 +58,27 @@ export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }: issues: data.map((i) => i.id), }; - await issueService - .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload) - .catch(() => { - setToastAlert({ - type: "error", - title: "Error!", - message: "Selected issues could not be added to the cycle. Please try again.", - }); - }); + // await issueService + // .addIssueToCycle(workspaceSlug as string, projectId as string, cycleId as string, payload, user) + // .catch(() => { + // setToastAlert({ + // type: "error", + // title: "Error!", + // message: "Selected issues could not be added to the cycle. Please try again.", + // }); + // }); }; return ( <> - setIsOpen(false)} prePopulateData={issuePayload} /> - {renderExistingIssueModal && ( - setOpenExistingIssueListModal(false)} - searchParams={ExistingIssuesListModalPayload} - handleOnSubmit={moduleId ? handleAddIssuesToModule : handleAddIssuesToCycle} - /> - )} -
+
{icon ? icon : }
-
-
- {title} -
-
- {count || 0} -
+
+
{title}
+
{count || 0}
{renderExistingIssueModal ? ( @@ -130,6 +105,25 @@ export const HeaderGroupByCard = observer(({ icon, title, count, issuePayload }:
)} + + setIsOpen(false)} + handleSubmit={(data: Partial) => { + console.log(data); + return Promise.resolve(); + }} + prePopulateData={issuePayload} + /> + + {renderExistingIssueModal && ( + setOpenExistingIssueListModal(false)} + searchParams={ExistingIssuesListModalPayload} + handleOnSubmit={moduleId ? handleAddIssuesToModule : handleAddIssuesToCycle} + /> + )}
); diff --git a/web/components/issues/issue-layouts/list/index.ts b/web/components/issues/issue-layouts/list/index.ts index be3968fdd..c245a2cca 100644 --- a/web/components/issues/issue-layouts/list/index.ts +++ b/web/components/issues/issue-layouts/list/index.ts @@ -2,4 +2,4 @@ export * from "./roots"; export * from "./block"; export * from "./roots"; export * from "./blocks-list"; -export * from "./inline-create-issue-form"; +export * from "./quick-add-issue-form"; diff --git a/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx deleted file mode 100644 index 813e25d0a..000000000 --- a/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { useEffect, useState, useRef } from "react"; -import { useRouter } from "next/router"; -import { useForm } from "react-hook-form"; -import { observer } from "mobx-react-lite"; -import { PlusIcon } from "lucide-react"; -// hooks -import useToast from "hooks/use-toast"; -import useKeypress from "hooks/use-keypress"; -import useProjectDetails from "hooks/use-project-details"; -import useOutsideClickDetector from "hooks/use-outside-click-detector"; -// store -import { useMobxStore } from "lib/mobx/store-provider"; -// helpers -import { createIssuePayload } from "helpers/issue.helper"; -// types -import { IIssue } from "types"; - -type Props = { - groupId?: string; - prePopulatedData?: Partial; - onSuccess?: (data: IIssue) => Promise | void; -}; - -const defaultValues: Partial = { - name: "", -}; - -const Inputs = (props: any) => { - const { register, setFocus, projectDetails } = props; - - useEffect(() => { - setFocus("name"); - }, [setFocus]); - - return ( - <> -

{projectDetails?.identifier ?? "..."}

- - - ); -}; - -export const ListInlineCreateIssueForm: React.FC = observer((props) => { - const { prePopulatedData, groupId } = props; - - // router - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - // store - const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore(); - - const { projectDetails } = useProjectDetails(); - - const { - reset, - handleSubmit, - setFocus, - register, - formState: { errors, isSubmitting }, - } = useForm({ defaultValues }); - - // ref - const ref = useRef(null); - - // states - const [isOpen, setIsOpen] = useState(false); - - const handleClose = () => setIsOpen(false); - - // hooks - useKeypress("Escape", handleClose); - useOutsideClickDetector(ref, handleClose); - const { setToastAlert } = useToast(); - - // derived values - const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!); - - useEffect(() => { - if (!isOpen) reset({ ...defaultValues }); - }, [isOpen, reset]); - - useEffect(() => { - if (!errors) return; - - Object.keys(errors).forEach((key) => { - const error = errors[key as keyof IIssue]; - - setToastAlert({ - type: "error", - title: "Error!", - message: error?.message?.toString() || "Some error occurred. Please try again.", - }); - }); - }, [errors, setToastAlert]); - - const onSubmitHandler = async (formData: IIssue) => { - if (isSubmitting || !workspaceSlug || !projectId) return; - - // resetting the form so that user can add another issue quickly - reset({ ...defaultValues }); - - const payload = createIssuePayload(workspaceDetail!, projectDetails!, { - ...(prePopulatedData ?? {}), - ...formData, - }); - - try { - quickAddStore.createIssue( - workspaceSlug.toString(), - projectId.toString(), - { - group_id: groupId ?? null, - sub_group_id: null, - }, - payload - ); - - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); - } catch (err: any) { - Object.keys(err || {}).forEach((key) => { - const error = err?.[key]; - const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null; - - setToastAlert({ - type: "error", - title: "Error!", - message: errorTitle || "Some error occurred. Please try again.", - }); - }); - } - }; - - return ( -
- {isOpen && ( - - - - )} - - {isOpen && ( -

- Press {"'"}Enter{"'"} to add another issue -

- )} - - {!isOpen && ( -
- -
- )} -
- ); -}); diff --git a/web/components/issues/issue-layouts/list/list-view-types.d.ts b/web/components/issues/issue-layouts/list/list-view-types.d.ts new file mode 100644 index 000000000..8f3dd8cb0 --- /dev/null +++ b/web/components/issues/issue-layouts/list/list-view-types.d.ts @@ -0,0 +1,6 @@ +export interface IQuickActionProps { + issue: IIssue; + handleDelete: () => Promise; + handleUpdate?: (data: IIssue) => Promise; + handleRemoveFromView?: () => Promise; +} diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx index 58944c76c..ef6e6981b 100644 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ b/web/components/issues/issue-layouts/list/properties.tsx @@ -13,17 +13,16 @@ import { Tooltip } from "@plane/ui"; // types import { IIssue, IIssueDisplayProperties, IState, TIssuePriorities } from "types"; -export interface IKanBanProperties { +export interface IListProperties { columnId: string; issue: IIssue; handleIssues: (group_by: string | null, issue: IIssue) => void; - displayProperties: IIssueDisplayProperties; + displayProperties: IIssueDisplayProperties | undefined; isReadonly?: boolean; - showEmptyGroup?: boolean; } -export const KanBanProperties: FC = observer((props) => { - const { columnId: group_id, issue, handleIssues, displayProperties, isReadonly, showEmptyGroup } = props; +export const ListProperties: FC = observer((props) => { + const { columnId: group_id, issue, handleIssues, displayProperties, isReadonly } = props; const handleState = (state: IState) => { handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id }); @@ -60,7 +59,7 @@ export const KanBanProperties: FC = observer((props) => { {displayProperties && displayProperties?.state && ( = observer((props) => { )} {/* label */} - {displayProperties && displayProperties?.labels && (showEmptyGroup || issue?.labels.length > 0) && ( + {displayProperties && displayProperties?.labels && ( = observer((props) => { )} {/* assignee */} - {displayProperties && displayProperties?.assignee && (showEmptyGroup || issue?.assignees?.length > 0) && ( + {displayProperties && displayProperties?.assignee && ( = observer((props) => { )} {/* start date */} - {displayProperties && displayProperties?.start_date && (showEmptyGroup || issue?.start_date) && ( + {displayProperties && displayProperties?.start_date && ( handleStartDate(date)} @@ -111,7 +110,7 @@ export const KanBanProperties: FC = observer((props) => { )} {/* target/due date */} - {displayProperties && displayProperties?.due_date && (showEmptyGroup || issue?.target_date) && ( + {displayProperties && displayProperties?.due_date && ( handleTargetDate(date)} diff --git a/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx new file mode 100644 index 000000000..014b9f544 --- /dev/null +++ b/web/components/issues/issue-layouts/list/quick-add-issue-form.tsx @@ -0,0 +1,148 @@ +import { FC, useEffect, useState, useRef } from "react"; +import { useRouter } from "next/router"; +import { useForm } from "react-hook-form"; +import { PlusIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +// hooks +import useToast from "hooks/use-toast"; +import useKeypress from "hooks/use-keypress"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; +// store +import { useMobxStore } from "lib/mobx/store-provider"; +// constants +import { IIssue, IProject } from "types"; +// types +import { createIssuePayload } from "helpers/issue.helper"; + +interface IInputProps { + formKey: string; + register: any; + setFocus: any; + projectDetail: IProject | null; +} +const Inputs: FC = (props) => { + const { formKey, register, setFocus, projectDetail } = props; + + useEffect(() => { + setFocus(formKey); + }, [formKey, setFocus]); + + return ( +
+
{projectDetail?.identifier ?? "..."}
+ +
+ ); +}; + +interface IListQuickAddIssueForm { + prePopulatedData?: Partial; + quickAddCallback?: ( + workspaceSlug: string, + projectId: string, + data: IIssue, + viewId?: string + ) => Promise; + viewId?: string; +} + +const defaultValues: Partial = { + name: "", +}; + +export const ListQuickAddIssueForm: FC = observer((props) => { + const { prePopulatedData, quickAddCallback, viewId } = props; + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + + const { workspace: workspaceStore, project: projectStore } = useMobxStore(); + + const workspaceDetail = (workspaceSlug && workspaceStore.getWorkspaceBySlug(workspaceSlug)) || null; + const projectDetail: IProject | null = + (workspaceSlug && projectId && projectStore.getProjectById(workspaceSlug, projectId)) || null; + + const ref = useRef(null); + + const [isOpen, setIsOpen] = useState(false); + const handleClose = () => setIsOpen(false); + + useKeypress("Escape", handleClose); + useOutsideClickDetector(ref, handleClose); + const { setToastAlert } = useToast(); + + const { + reset, + handleSubmit, + setFocus, + register, + formState: { errors, isSubmitting }, + } = useForm({ defaultValues }); + + useEffect(() => { + if (!isOpen) reset({ ...defaultValues }); + }, [isOpen, reset]); + + const onSubmitHandler = async (formData: IIssue) => { + if (isSubmitting || !workspaceDetail || !projectDetail) return; + + reset({ ...defaultValues }); + + const payload = createIssuePayload(workspaceDetail, projectDetail, { + ...(prePopulatedData ?? {}), + ...formData, + }); + + try { + quickAddCallback && (await quickAddCallback(workspaceSlug, projectId, { ...payload }, viewId)); + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); + } catch (err: any) { + setToastAlert({ + type: "error", + title: "Error!", + message: err?.message || "Some error occurred. Please try again.", + }); + } + }; + + return ( +
+ {isOpen ? ( +
+
+ + +
{`Press 'Enter' to add another issue`}
+
+ ) : ( +
setIsOpen(true)} + > + + New Issue +
+ )} +
+ ); +}); diff --git a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx index d991049ac..9aab9e871 100644 --- a/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/archived-issue-root.tsx @@ -4,73 +4,42 @@ import { observer } from "mobx-react-lite"; // hooks import { useMobxStore } from "lib/mobx/store-provider"; // components -import { List } from "../default"; import { ArchivedIssueQuickActions } from "components/issues"; -// helpers -import { orderArrayBy } from "helpers/array.helper"; // types import { IIssue } from "types"; // constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +import { BaseListRoot } from "../base-list-root"; +import { IProjectStore } from "store/project"; +import { EIssueActions } from "../../types"; export const ArchivedIssueListLayout: FC = observer(() => { const router = useRouter(); const { workspaceSlug, projectId } = router.query; - const { - project: projectStore, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - projectEstimates: { projectEstimates }, - archivedIssues: archivedIssueStore, - archivedIssueFilters: archivedIssueFiltersStore, - } = useMobxStore(); + const { archivedIssues: archivedIssueStore, archivedIssueFilters: archivedIssueFiltersStore } = useMobxStore(); - // derived values - const issues = archivedIssueStore.getIssues; - const displayProperties = archivedIssueFiltersStore?.userDisplayProperties || null; - const group_by: string | null = archivedIssueFiltersStore?.userDisplayFilters?.group_by || null; - const showEmptyGroup = archivedIssueFiltersStore?.userDisplayFilters?.show_empty_groups || false; + const issueActions = { + [EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => { + if (!workspaceSlug || !projectId) return; - const handleIssues = (group_by: string | null, issue: IIssue, action: "delete" | "update") => { - if (!workspaceSlug || !projectId) return; - - if (action === "delete") { - archivedIssueStore.deleteArchivedIssue(group_by === "null" ? null : group_by, null, issue); - archivedIssueStore.fetchIssues(workspaceSlug.toString(), projectId.toString()); - } + archivedIssueStore.deleteArchivedIssue(group_by, null, issue); + }, }; - const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null; + const getProjects = (projectStore: IProjectStore) => { + if (!workspaceSlug) return null; + return projectStore?.projects[workspaceSlug.toString()] || null; + }; - const states = projectStateStore?.projectStates || null; - const priorities = ISSUE_PRIORITIES || null; - const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; - const estimates = - projectDetails?.estimate !== null ? projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null : null; + return null; - return ( -
- ( - handleIssues(group_by, issue, "delete")} /> - )} - displayProperties={displayProperties} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={projectLabels} - members={projectMembers?.map((m) => m.member) ?? null} - projects={projects} - estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} - showEmptyGroup={showEmptyGroup} - /> -
- ); + // return ( + // + // ); }); diff --git a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx index c47b3ceb8..6ba49827d 100644 --- a/web/components/issues/issue-layouts/list/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -1,96 +1,52 @@ -import React, { useCallback } from "react"; +import React from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { List } from "../default"; import { CycleIssueQuickActions } from "components/issues"; -// helpers -import { orderArrayBy } from "helpers/array.helper"; // types import { IIssue } from "types"; // constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +import { BaseListRoot } from "../base-list-root"; +import { IProjectStore } from "store/project"; +import { EIssueActions } from "../../types"; export interface ICycleListLayout {} export const CycleListLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, cycleId } = router.query; + const { workspaceSlug, cycleId } = router.query as { workspaceSlug: string; cycleId: string }; // store - const { - project: projectStore, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - projectEstimates: { projectEstimates }, - issueFilter: issueFilterStore, - cycleIssue: cycleIssueStore, - issueDetail: issueDetailStore, - } = useMobxStore(); - const { currentProjectDetails } = projectStore; + const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore(); - const issues = cycleIssueStore?.getIssues; - - const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - - const displayProperties = issueFilterStore?.userDisplayProperties || null; - - const handleIssues = useCallback( - (group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { + const issueActions = { + [EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => { if (!workspaceSlug || !cycleId) return; - - if (action === "update") { - cycleIssueStore.updateIssueStructure(group_by, null, issue); - issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - } - if (action === "delete") cycleIssueStore.deleteIssue(group_by, null, issue); - if (action === "remove" && issue.bridge_id) { - cycleIssueStore.deleteIssue(group_by, null, issue); - cycleIssueStore.removeIssueFromCycle( - workspaceSlug.toString(), - issue.project, - cycleId.toString(), - issue.bridge_id - ); - } + cycleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, cycleId); }, - [cycleIssueStore, issueDetailStore, cycleId, workspaceSlug] - ); - - const states = projectStateStore?.projectStates || null; - const priorities = ISSUE_PRIORITIES || null; - const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; - const estimates = - currentProjectDetails?.estimate !== null - ? projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null - : null; + [EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => { + if (!workspaceSlug || !cycleId) return; + cycleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, cycleId); + }, + [EIssueActions.REMOVE]: (group_by: string | null, issue: IIssue) => { + if (!workspaceSlug || !cycleId || !issue.bridge_id) return; + cycleIssueStore.removeIssueFromCycle(workspaceSlug, issue.project, cycleId, issue.id, issue.bridge_id); + }, + }; + const getProjects = (projectStore: IProjectStore) => { + if (!workspaceSlug) return null; + return projectStore?.projects[workspaceSlug] || null; + }; return ( -
- ( - handleIssues(group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(group_by, data, "update")} - handleRemoveFromCycle={async () => handleIssues(group_by, issue, "remove")} - /> - )} - displayProperties={displayProperties} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={projectLabels} - members={projectMembers?.map((m) => m.member) ?? null} - projects={projects} - estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} - /> -
+ ); }); diff --git a/web/components/issues/issue-layouts/list/roots/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx index e27379df2..fd08e246e 100644 --- a/web/components/issues/issue-layouts/list/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -1,96 +1,53 @@ -import React, { useCallback } from "react"; +import React from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { List } from "../default"; import { ModuleIssueQuickActions } from "components/issues"; -// helpers -import { orderArrayBy } from "helpers/array.helper"; // types import { IIssue } from "types"; +import { EIssueActions } from "../../types"; // constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +import { BaseListRoot } from "../base-list-root"; +import { IProjectStore } from "store/project"; export interface IModuleListLayout {} export const ModuleListLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, moduleId } = router.query; + const { workspaceSlug, moduleId } = router.query as { workspaceSlug: string; moduleId: string }; - const { - project: projectStore, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - projectEstimates: { projectEstimates }, - issueFilter: issueFilterStore, - moduleIssue: moduleIssueStore, - issueDetail: issueDetailStore, - } = useMobxStore(); - const { currentProjectDetails } = projectStore; + const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore(); - const issues = moduleIssueStore?.getIssues; - - const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; - - const displayProperties = issueFilterStore?.userDisplayProperties || null; - - const handleIssues = useCallback( - (group_by: string | null, issue: IIssue, action: "update" | "delete" | "remove") => { + const issueActions = { + [EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => { if (!workspaceSlug || !moduleId) return; - - if (action === "update") { - moduleIssueStore.updateIssueStructure(group_by, null, issue); - issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - } - if (action === "delete") moduleIssueStore.deleteIssue(group_by, null, issue); - if (action === "remove" && issue.bridge_id) { - moduleIssueStore.deleteIssue(group_by, null, issue); - moduleIssueStore.removeIssueFromModule( - workspaceSlug.toString(), - issue.project, - moduleId.toString(), - issue.bridge_id - ); - } + moduleIssueStore.updateIssue(workspaceSlug, issue.project, issue.id, issue, moduleId); }, - [moduleIssueStore, issueDetailStore, moduleId, workspaceSlug] - ); + [EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => { + if (!workspaceSlug || !moduleId) return; + moduleIssueStore.removeIssue(workspaceSlug, issue.project, issue.id, moduleId); + }, + [EIssueActions.REMOVE]: (group_by: string | null, issue: IIssue) => { + if (!workspaceSlug || !moduleId || !issue.bridge_id) return; + moduleIssueStore.removeIssueFromModule(workspaceSlug, issue.project, moduleId, issue.id, issue.bridge_id); + }, + }; - const states = projectStateStore?.projectStates || null; - const priorities = ISSUE_PRIORITIES || null; - const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; - const estimates = - currentProjectDetails?.estimate !== null - ? projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null - : null; + const getProjects = (projectStore: IProjectStore) => { + if (!workspaceSlug) return null; + return projectStore?.projects[workspaceSlug] || null; + }; return ( -
- ( - handleIssues(group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(group_by, data, "update")} - handleRemoveFromModule={async () => handleIssues(group_by, issue, "remove")} - /> - )} - displayProperties={displayProperties} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={projectLabels} - members={projectMembers?.map((m) => m.member) ?? null} - projects={projects} - estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} - /> -
+ ); }); diff --git a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index bfb727843..1d37ff007 100644 --- a/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -1,24 +1,19 @@ -import { FC, useCallback } from "react"; +import { FC } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { List } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; // types import { IIssue } from "types"; -// constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; - -export interface IProfileIssuesListLayout {} +import { EIssueActions } from "../../types"; +import { IProjectStore } from "store/project"; +//components +import { BaseListRoot } from "../base-list-root"; export const ProfileIssuesListLayout: FC = observer(() => { const { - workspace: workspaceStore, - projectState: projectStateStore, - project: projectStore, - projectMember: { projectMembers }, profileIssueFilters: profileIssueFiltersStore, profileIssues: profileIssuesStore, issueDetail: issueDetailStore, @@ -27,53 +22,29 @@ export const ProfileIssuesListLayout: FC = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; - const issues = profileIssuesStore?.getIssues; - - const group_by: string | null = profileIssueFiltersStore?.userDisplayFilters?.group_by || null; - - const displayProperties = profileIssueFiltersStore?.userDisplayProperties || null; - - const handleIssues = useCallback( - (group_by: string | null, issue: IIssue, action: "update" | "delete") => { + const issueActions = { + [EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => { if (!workspaceSlug) return; - if (action === "update") { - profileIssuesStore.updateIssueStructure(group_by, null, issue); - issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - } - if (action === "delete") profileIssuesStore.deleteIssue(group_by, null, issue); + profileIssuesStore.updateIssueStructure(group_by, null, issue); + issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); }, - [profileIssuesStore, issueDetailStore, workspaceSlug] - ); + [EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => { + profileIssuesStore.deleteIssue(group_by, null, issue); + }, + }; - const states = projectStateStore?.projectStates || null; - const priorities = ISSUE_PRIORITIES || null; - const labels = workspaceStore.workspaceLabels || null; - const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = projectStore?.workspaceProjects || null; + const getProjects = (projectStore: IProjectStore) => projectStore?.workspaceProjects || null; - return ( -
- ( - handleIssues(group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(group_by, data, "update")} - /> - )} - displayProperties={displayProperties} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={labels} - members={projectMembers?.map((m) => m.member) ?? null} - projects={projects} - estimates={null} - /> -
- ); + return null; + + // return ( + // + // ); }); diff --git a/web/components/issues/issue-layouts/list/roots/project-root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx index 91a04f57d..f22a71546 100644 --- a/web/components/issues/issue-layouts/list/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -1,95 +1,46 @@ -import { FC, useCallback } from "react"; +import { FC } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // hooks import { useMobxStore } from "lib/mobx/store-provider"; // components -import { List } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; -import { Spinner } from "@plane/ui"; -// helpers -import { orderArrayBy } from "helpers/array.helper"; // types import { IIssue } from "types"; +import { EIssueActions } from "../../types"; // constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +import { BaseListRoot } from "../base-list-root"; +import { IProjectStore } from "store/project"; export const ListLayout: FC = observer(() => { const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + + if (!workspaceSlug || !projectId) return null; + // store - const { - project: projectStore, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - projectEstimates: { projectEstimates }, - issue: issueStore, - issueDetail: issueDetailStore, - issueFilter: issueFilterStore, - } = useMobxStore(); - const { currentProjectDetails } = projectStore; + const { projectIssuesFilter: projectIssuesFilterStore, projectIssues: projectIssuesStore } = useMobxStore(); - const issues = issueStore?.getIssues; - - const userDisplayFilters = issueFilterStore?.userDisplayFilters || null; - const group_by: string | null = userDisplayFilters?.group_by || null; - const displayProperties = issueFilterStore?.userDisplayProperties || null; - - const handleIssues = useCallback( - (group_by: string | null, issue: IIssue, action: "update" | "delete") => { - if (!workspaceSlug) return; - - if (action === "update") { - issueStore.updateIssueStructure(group_by, null, issue); - issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); - } - if (action === "delete") issueStore.deleteIssue(group_by, null, issue); + const issueActions = { + [EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => { + if (!workspaceSlug || !projectId) return; + projectIssuesStore.updateIssue(workspaceSlug, projectId, issue.id, issue); }, - [issueStore, issueDetailStore, workspaceSlug] - ); + [EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => { + if (!workspaceSlug || !projectId) return; + projectIssuesStore.removeIssue(workspaceSlug, projectId, issue.id); + }, + }; - const states = projectStateStore?.projectStates || null; - const priorities = ISSUE_PRIORITIES || null; - const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; - const estimates = - currentProjectDetails?.estimate !== null - ? projectEstimates?.find((e) => e.id === currentProjectDetails?.estimate) || null - : null; + const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; return ( - <> - {issueStore.loader ? ( -
- -
- ) : ( -
- ( - handleIssues(group_by, issue, "delete")} - handleUpdate={async (data) => handleIssues(group_by, data, "update")} - /> - )} - displayProperties={displayProperties} - states={states} - stateGroups={stateGroups} - priorities={priorities} - labels={projectLabels} - members={projectMembers?.map((m) => m.member) ?? null} - projects={projects} - enableQuickIssueCreate - estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} - showEmptyGroup={userDisplayFilters.show_empty_groups} - /> -
- )} - + ); }); diff --git a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx index 66c2828a8..ad53f32b1 100644 --- a/web/components/issues/issue-layouts/list/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx @@ -1,57 +1,49 @@ import React from "react"; import { observer } from "mobx-react-lite"; -// components -import { List } from "../default"; + // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; // constants -import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; +import { useRouter } from "next/router"; +import { EIssueActions } from "../../types"; +import { IProjectStore } from "store/project"; +import { IIssue } from "types"; +// components +import { BaseListRoot } from "../base-list-root"; +import { ProjectIssueQuickActions } from "../../quick-action-dropdowns"; export interface IViewListLayout {} export const ProjectViewListLayout: React.FC = observer(() => { - const { - project: projectStore, - issue: issueStore, - issueFilter: issueFilterStore, - projectState: projectStateStore, - }: RootStore = useMobxStore(); + const { viewIssues: projectViewIssueStore, viewIssuesFilter: projectViewIssueFilterStore }: RootStore = + useMobxStore(); - const issues = issueStore?.getIssues; + const router = useRouter(); + const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - const group_by: string | null = issueFilterStore?.userDisplayFilters?.group_by || null; + if (!workspaceSlug || !projectId) return null; - const display_properties = issueFilterStore?.userDisplayProperties || null; - - const updateIssue = (group_by: string | null, issue: any) => { - issueStore.updateIssueStructure(group_by, null, issue); + const issueActions = { + [EIssueActions.UPDATE]: (group_by: string | null, issue: IIssue) => { + if (!workspaceSlug || !projectId) return; + projectViewIssueStore.updateIssue(workspaceSlug, projectId, issue.id, issue); + }, + [EIssueActions.DELETE]: (group_by: string | null, issue: IIssue) => { + if (!workspaceSlug || !projectId) return; + projectViewIssueStore.removeIssue(workspaceSlug, projectId, issue.id); + }, }; - const states = projectStateStore?.projectStates || null; - const priorities = ISSUE_PRIORITIES || null; - // const labels = projectStore?.projectLabels || null; - const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = projectStateStore?.projectStates || null; - const estimates = null; + const getProjects = (projectStore: IProjectStore) => projectStore.workspaceProjects; - return null; - - // return ( - //
- // - //
- // ); + return ( + + ); }); diff --git a/web/components/issues/issue-layouts/properties/state.tsx b/web/components/issues/issue-layouts/properties/state.tsx index 2af8881f9..708475db4 100644 --- a/web/components/issues/issue-layouts/properties/state.tsx +++ b/web/components/issues/issue-layouts/properties/state.tsx @@ -17,7 +17,7 @@ import { RootStore } from "store/root"; export interface IIssuePropertyState { view?: "profile" | "workspace" | "project"; projectId: string | null; - value: IState; + value: any | string | null; onChange: (state: IState) => void; disabled?: boolean; hideDropdownArrow?: boolean; @@ -62,6 +62,9 @@ export const IssuePropertyState: React.FC = observer((props projectStateStore.fetchProjectStates(workspaceSlug, projectId).then(() => setIsLoading(false)); }; + const selectedOption: IState | undefined = + (projectStates && value && projectStates?.find((state) => state.id === value)) || undefined; + const dropdownOptions = projectStates?.map((state) => ({ value: state.id, query: state.name, @@ -91,10 +94,10 @@ export const IssuePropertyState: React.FC = observer((props : dropdownOptions?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); const label = ( - +
- {value && } - {value?.name ?? "State"} + {selectedOption && } + {selectedOption?.name ?? "State"}
); @@ -104,8 +107,8 @@ export const IssuePropertyState: React.FC = observer((props {workspaceSlug && projectId && ( { const selectedState = projectStates?.find((state) => state.id === data); if (selectedState) onChange(selectedState); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx index 428fca5d4..fd9235f3f 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/archived-issue.tsx @@ -9,14 +9,9 @@ import { DeleteArchivedIssueModal } from "components/issues"; // helpers import { copyUrlToClipboard } from "helpers/string.helper"; // types -import { IIssue } from "types"; +import { IQuickActionProps } from "../list/list-view-types"; -type Props = { - issue: IIssue; - handleDelete: () => Promise; -}; - -export const ArchivedIssueQuickActions: React.FC = (props) => { +export const ArchivedIssueQuickActions: React.FC = (props) => { const { issue, handleDelete } = props; const router = useRouter(); diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx index 9736c575e..907ca3608 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/cycle-issue.tsx @@ -10,16 +10,10 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { copyUrlToClipboard } from "helpers/string.helper"; // types import { IIssue } from "types"; +import { IQuickActionProps } from "../list/list-view-types"; -type Props = { - issue: IIssue; - handleDelete: () => Promise; - handleUpdate: (data: IIssue) => Promise; - handleRemoveFromCycle: () => Promise; -}; - -export const CycleIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, handleRemoveFromCycle } = props; +export const CycleIssueQuickActions: React.FC = (props) => { + const { issue, handleDelete, handleUpdate, handleRemoveFromView } = props; const router = useRouter(); const { workspaceSlug } = router.query; @@ -59,7 +53,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} data={issueToEdit} onSubmit={async (data) => { - if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data }); }} /> @@ -92,7 +86,7 @@ export const CycleIssueQuickActions: React.FC = (props) => { onClick={(e) => { e.preventDefault(); e.stopPropagation(); - handleRemoveFromCycle(); + handleRemoveFromView && handleRemoveFromView(); }} >
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx index 93594de9e..01ddfac3b 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/module-issue.tsx @@ -10,16 +10,10 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { copyUrlToClipboard } from "helpers/string.helper"; // types import { IIssue } from "types"; +import { IQuickActionProps } from "../list/list-view-types"; -type Props = { - issue: IIssue; - handleDelete: () => Promise; - handleUpdate: (data: IIssue) => Promise; - handleRemoveFromModule: () => Promise; -}; - -export const ModuleIssueQuickActions: React.FC = (props) => { - const { issue, handleDelete, handleUpdate, handleRemoveFromModule } = props; +export const ModuleIssueQuickActions: React.FC = (props) => { + const { issue, handleDelete, handleUpdate, handleRemoveFromView } = props; const router = useRouter(); const { workspaceSlug } = router.query; @@ -59,7 +53,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} data={issueToEdit} onSubmit={async (data) => { - if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data }); }} /> @@ -92,7 +86,7 @@ export const ModuleIssueQuickActions: React.FC = (props) => { onClick={(e) => { e.preventDefault(); e.stopPropagation(); - handleRemoveFromModule(); + handleRemoveFromView && handleRemoveFromView(); }} >
diff --git a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx index 7999be31d..057dbf3cd 100644 --- a/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx +++ b/web/components/issues/issue-layouts/quick-action-dropdowns/project-issue.tsx @@ -10,14 +10,9 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues"; import { copyUrlToClipboard } from "helpers/string.helper"; // types import { IIssue } from "types"; +import { IQuickActionProps } from "../list/list-view-types"; -type Props = { - issue: IIssue; - handleDelete: () => Promise; - handleUpdate: (data: IIssue) => Promise; -}; - -export const ProjectIssueQuickActions: React.FC = (props) => { +export const ProjectIssueQuickActions: React.FC = (props) => { const { issue, handleDelete, handleUpdate } = props; const router = useRouter(); @@ -58,7 +53,7 @@ export const ProjectIssueQuickActions: React.FC = (props) => { prePopulateData={!issueToEdit && createUpdateIssueModal ? { ...issue, name: `${issue.name} (copy)` } : {}} data={issueToEdit} onSubmit={async (data) => { - if (issueToEdit) handleUpdate({ ...issueToEdit, ...data }); + if (issueToEdit && handleUpdate) handleUpdate({ ...issueToEdit, ...data }); }} /> diff --git a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx index 4d9fa7f08..956af7abf 100644 --- a/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/cycle-layout-root.tsx @@ -24,28 +24,29 @@ export const CycleLayoutRoot: React.FC = observer(() => { const [transferIssuesModal, setTransferIssuesModal] = useState(false); const router = useRouter(); - const { workspaceSlug, projectId, cycleId } = router.query; + const { workspaceSlug, projectId, cycleId } = router.query as { + workspaceSlug: string; + projectId: string; + cycleId: string; + }; const { - issueFilter: issueFilterStore, cycle: cycleStore, - cycleIssue: cycleIssueStore, - cycleIssueFilter: cycleIssueFilterStore, + cycleIssues: { loader, getIssues, fetchIssues }, + cycleIssuesFilter: { issueFilters, fetchFilters }, } = useMobxStore(); - useSWR(workspaceSlug && projectId && cycleId ? `CYCLE_FILTERS_AND_ISSUES_${cycleId.toString()}` : null, async () => { - if (workspaceSlug && projectId && cycleId) { - // fetching the project display filters and display properties - await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString()); - // fetching the cycle filters - await cycleIssueFilterStore.fetchCycleFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); - - // fetching the cycle issues - await cycleIssueStore.fetchIssues(workspaceSlug.toString(), projectId.toString(), cycleId.toString()); + useSWR( + workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null, + async () => { + if (workspaceSlug && projectId && cycleId) { + await fetchFilters(workspaceSlug, projectId, cycleId); + await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader", cycleId); + } } - }); + ); - const activeLayout = issueFilterStore.userDisplayFilters.layout; + const activeLayout = issueFilters?.displayFilters?.layout; const cycleDetails = cycleId ? cycleStore.cycle_details[cycleId.toString()] : undefined; const cycleStatus = @@ -53,41 +54,35 @@ export const CycleLayoutRoot: React.FC = observer(() => { ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) : "draft"; - const issueCount = cycleIssueStore.getIssuesCount; - - if (!cycleIssueStore.getIssues) - return ( -
- -
- ); - return ( <> setTransferIssuesModal(false)} isOpen={transferIssuesModal} /> +
{cycleStatus === "completed" && setTransferIssuesModal(true)} />} - {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? ( - - ) : ( -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} + + {loader === "init-loader" ? ( +
+
+ ) : ( + <> + {/* */} +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ )}
diff --git a/web/components/issues/issue-layouts/roots/module-layout-root.tsx b/web/components/issues/issue-layouts/roots/module-layout-root.tsx index 3386725bc..df314ab3c 100644 --- a/web/components/issues/issue-layouts/roots/module-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/module-layout-root.tsx @@ -27,60 +27,47 @@ export const ModuleLayoutRoot: React.FC = observer(() => { }; const { - issueFilter: issueFilterStore, - moduleIssue: moduleIssueStore, - moduleFilter: moduleIssueFilterStore, + moduleIssues: { loader, getIssues, fetchIssues }, + moduleIssuesFilter: { issueFilters, fetchFilters }, } = useMobxStore(); useSWR( - workspaceSlug && projectId && moduleId ? `MODULE_FILTERS_AND_ISSUES_${moduleId.toString()}` : null, + workspaceSlug && projectId && moduleId ? `MODULE_ISSUES_V3_${workspaceSlug}_${projectId}_${moduleId}` : null, async () => { if (workspaceSlug && projectId && moduleId) { - // fetching the project display filters and display properties - await issueFilterStore.fetchUserProjectFilters(workspaceSlug, projectId); - // fetching the module filters - await moduleIssueFilterStore.fetchModuleFilters(workspaceSlug, projectId, moduleId); - - // fetching the module issues - await moduleIssueStore.fetchIssues(workspaceSlug, projectId, moduleId); + await fetchFilters(workspaceSlug, projectId, moduleId); + await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader", moduleId); } } ); - const activeLayout = issueFilterStore.userDisplayFilters.layout; - - const issueCount = moduleIssueStore.getIssuesCount; - - if (!moduleIssueStore.getIssues) - return ( -
- -
- ); + const activeLayout = issueFilters?.displayFilters?.layout || undefined; return (
- {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? ( - - ) : ( -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} + + {loader === "init-loader" ? ( +
+
+ ) : ( + <> + {/* */} +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ )}
); diff --git a/web/components/issues/issue-layouts/roots/project-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-layout-root.tsx index 0539542da..ab7556c70 100644 --- a/web/components/issues/issue-layouts/roots/project-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-layout-root.tsx @@ -17,48 +17,49 @@ import { import { Spinner } from "@plane/ui"; export const ProjectLayoutRoot: React.FC = observer(() => { + // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; - const { issue: issueStore, issueFilter: issueFilterStore } = useMobxStore(); + const { + projectIssues: { loader, getIssues, fetchIssues }, + projectIssuesFilter: { issueFilters, fetchFilters }, + } = useMobxStore(); - useSWR(workspaceSlug && projectId ? `PROJECT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => { + useSWR(workspaceSlug && projectId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => { if (workspaceSlug && projectId) { - await issueFilterStore.fetchUserProjectFilters(workspaceSlug.toString(), projectId.toString()); - await issueStore.fetchIssues(workspaceSlug.toString(), projectId.toString()); + await fetchFilters(workspaceSlug, projectId); + await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader"); } }); - const activeLayout = issueFilterStore.userDisplayFilters.layout; - - const issueCount = issueStore.getIssuesCount; - - if (!issueStore.getIssues) - return ( -
- -
- ); + const activeLayout = issueFilters?.displayFilters?.layout; return (
- {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? ( - - ) : ( -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} + + {loader === "init-loader" ? ( +
+
+ ) : ( + <> + {/* {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 && } */} +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ )}
); diff --git a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx index ce8051dea..47b0700a5 100644 --- a/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx +++ b/web/components/issues/issue-layouts/roots/project-view-layout-root.tsx @@ -19,65 +19,51 @@ import { Spinner } from "@plane/ui"; export const ProjectViewLayoutRoot: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId, viewId } = router.query; + const { workspaceSlug, projectId, viewId } = router.query as { + workspaceSlug: string; + projectId: string; + viewId?: string; + }; const { - issueFilter: issueFilterStore, - projectViews: projectViewsStore, - projectViewIssues: projectViewIssuesStore, - projectViewFilters: projectViewFiltersStore, + viewIssues: { loader, getIssues, fetchIssues }, + viewIssuesFilter: { issueFilters, fetchFilters }, } = useMobxStore(); - useSWR( - workspaceSlug && projectId && viewId ? `PROJECT_VIEW_FILTERS_AND_ISSUES_${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 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()] ?? {} - ); - } + useSWR(workspaceSlug && projectId && viewId ? `PROJECT_ISSUES_V3_${workspaceSlug}_${projectId}` : null, async () => { + if (workspaceSlug && projectId && viewId) { + await fetchFilters(workspaceSlug, projectId, viewId); + // await fetchIssues(workspaceSlug, projectId, getIssues ? "mutation" : "init-loader"); } - ); + }); - const activeLayout = issueFilterStore.userDisplayFilters.layout; - - const issueCount = projectViewIssuesStore.getIssuesCount; - - if (!projectViewIssuesStore.getIssues) - return ( -
- -
- ); + const activeLayout = issueFilters?.displayFilters?.layout; return (
- {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 ? ( - - ) : ( -
- {activeLayout === "list" ? ( - - ) : activeLayout === "kanban" ? ( - - ) : activeLayout === "calendar" ? ( - - ) : activeLayout === "gantt_chart" ? ( - - ) : activeLayout === "spreadsheet" ? ( - - ) : null} + + {loader === "init-loader" ? ( +
+
+ ) : ( + <> + {/* {(activeLayout === "list" || activeLayout === "spreadsheet") && issueCount === 0 && } */} +
+ {activeLayout === "list" ? ( + + ) : activeLayout === "kanban" ? ( + + ) : activeLayout === "calendar" ? ( + + ) : activeLayout === "gantt_chart" ? ( + + ) : activeLayout === "spreadsheet" ? ( + + ) : null} +
+ )}
); diff --git a/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx new file mode 100644 index 000000000..cc7ba1602 --- /dev/null +++ b/web/components/issues/issue-layouts/spreadsheet/base-spreadsheet-root.tsx @@ -0,0 +1,113 @@ +import { IIssueUnGroupedStructure } from "store/issue"; +import { SpreadsheetView } from "./spreadsheet-view"; +import { useCallback } from "react"; +import { IIssue, IIssueDisplayFilterOptions } from "types"; +import { useRouter } from "next/router"; +import { useMobxStore } from "lib/mobx/store-provider"; +import { + ICycleIssuesFilterStore, + ICycleIssuesStore, + IModuleIssuesFilterStore, + IModuleIssuesStore, + IProjectIssuesFilterStore, + IProjectIssuesStore, + IViewIssuesFilterStore, + IViewIssuesStore, +} from "store/issues"; +import { observer } from "mobx-react-lite"; +import { EFilterType, TUnGroupedIssues } from "store/issues/types"; + +interface IBaseSpreadsheetRoot { + issueFiltersStore: + | IViewIssuesFilterStore + | ICycleIssuesFilterStore + | IModuleIssuesFilterStore + | IProjectIssuesFilterStore; + issueStore: IProjectIssuesStore | IModuleIssuesStore | ICycleIssuesStore | IViewIssuesStore; + viewId?: string; +} + +export const BaseSpreadsheetRoot = observer((props: IBaseSpreadsheetRoot) => { + const { issueFiltersStore, issueStore, viewId } = props; + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; + + const { + issueDetail: issueDetailStore, + projectMember: { projectMembers }, + projectState: projectStateStore, + projectLabel: { projectLabels }, + user: userStore, + } = useMobxStore(); + + const user = userStore.currentUser; + + const issuesResponse = issueStore.getIssues; + const issueIds = (issueStore.getIssuesIds ?? []) as TUnGroupedIssues; + + const issues = issueIds?.map((id) => issuesResponse?.[id]); + + const handleIssueAction = async (issue: IIssue, action: "copy" | "delete" | "edit") => { + if (!workspaceSlug || !projectId || !user) return; + + if (action === "delete") { + issueDetailStore.deleteIssue(workspaceSlug.toString(), projectId.toString(), issue.id); + // issueStore.removeIssueFromStructure(null, null, issue); + } else if (action === "edit") { + issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue); + // issueStore.updateIssueStructure(null, null, issue); + } + }; + + const handleDisplayFiltersUpdate = useCallback( + (updatedDisplayFilter: Partial) => { + if (!workspaceSlug || !projectId) return; + + issueFiltersStore.updateFilters( + workspaceSlug, + projectId, + EFilterType.DISPLAY_FILTERS, + { + ...updatedDisplayFilter, + }, + viewId + ); + }, + [issueFiltersStore, projectId, workspaceSlug] + ); + + const handleUpdateIssue = useCallback( + (issue: IIssue, data: Partial) => { + if (!workspaceSlug || !projectId || !user) return; + + const payload = { + ...issue, + ...data, + }; + + // TODO: add update logic from the new store + // issueStore.updateIssueStructure(null, null, payload); + issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data); + }, + [issueDetailStore, projectId, user, workspaceSlug] + ); + + return ( + m.member)} + labels={projectLabels || undefined} + states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined} + handleIssueAction={handleIssueAction} + handleUpdateIssue={handleUpdateIssue} + disableUserActions={false} + quickAddCallback={issueStore.quickAddIssue} + viewId={viewId} + enableQuickCreateIssue + /> + ); +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx index 8c901a9d8..644af7bc1 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/created-on-column.tsx @@ -15,7 +15,7 @@ type Props = { export const SpreadsheetCreatedOnColumn: React.FC = ({ issue, expandedIssues }) => { const isExpanded = expandedIssues.indexOf(issue.id) > -1; - const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); + const { subIssues, isLoading } = useSubIssue(issue.project, issue.id, isExpanded); return ( <> diff --git a/web/components/issues/issue-layouts/spreadsheet/index.ts b/web/components/issues/issue-layouts/spreadsheet/index.ts index 5b14a2dab..10fc26219 100644 --- a/web/components/issues/issue-layouts/spreadsheet/index.ts +++ b/web/components/issues/issue-layouts/spreadsheet/index.ts @@ -2,4 +2,4 @@ export * from "./columns"; export * from "./roots"; export * from "./spreadsheet-column"; export * from "./spreadsheet-view"; -export * from "./inline-create-issue-form"; +export * from "./quick-add-issue-form"; diff --git a/web/components/issues/issue-layouts/spreadsheet/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx similarity index 58% rename from web/components/issues/issue-layouts/spreadsheet/inline-create-issue-form.tsx rename to web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx index 126d44498..b663ed523 100644 --- a/web/components/issues/issue-layouts/spreadsheet/inline-create-issue-form.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/quick-add-issue-form.tsx @@ -6,19 +6,26 @@ import { PlusIcon } from "lucide-react"; // hooks import useToast from "hooks/use-toast"; import useKeypress from "hooks/use-keypress"; -import useProjectDetails from "hooks/use-project-details"; import useOutsideClickDetector from "hooks/use-outside-click-detector"; // store import { useMobxStore } from "lib/mobx/store-provider"; // helpers import { createIssuePayload } from "helpers/issue.helper"; // types -import { IIssue } from "types"; +import { IIssue, IProject } from "types"; type Props = { + formKey: keyof IIssue; groupId?: string; + subGroupId?: string | null; prePopulatedData?: Partial; - onSuccess?: (data: IIssue) => Promise | void; + quickAddCallback?: ( + workspaceSlug: string, + projectId: string, + data: IIssue, + viewId?: string + ) => Promise; + viewId?: string; }; const defaultValues: Partial = { @@ -48,17 +55,15 @@ const Inputs = (props: any) => { ); }; -export const SpreadsheetInlineCreateIssueForm: React.FC = observer((props) => { - const { prePopulatedData, groupId } = props; +export const SpreadsheetQuickAddIssueForm: React.FC = observer((props) => { + const { formKey, groupId, subGroupId = null, prePopulatedData, quickAddCallback, viewId } = props; // router const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string }; // store - const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore(); - - const { projectDetails } = useProjectDetails(); + const { workspace: workspaceStore, project: projectStore } = useMobxStore(); const { reset, @@ -82,7 +87,9 @@ export const SpreadsheetInlineCreateIssueForm: React.FC = observer((props const { setToastAlert } = useToast(); // derived values - const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!); + const workspaceDetail = (workspaceSlug && workspaceStore.getWorkspaceBySlug(workspaceSlug)) || null; + const projectDetail: IProject | null = + (workspaceSlug && projectId && projectStore.getProjectById(workspaceSlug, projectId)) || null; useEffect(() => { setFocus("name"); @@ -106,43 +113,70 @@ export const SpreadsheetInlineCreateIssueForm: React.FC = observer((props }); }, [errors, setToastAlert]); - const onSubmitHandler = async (formData: IIssue) => { - if (isSubmitting || !workspaceSlug || !projectId) return; + // const onSubmitHandler = async (formData: IIssue) => { + // if (isSubmitting || !workspaceSlug || !projectId) return; + + // // resetting the form so that user can add another issue quickly + // reset({ ...defaultValues }); + + // const payload = createIssuePayload(workspaceDetail!, projectDetails!, { + // ...(prePopulatedData ?? {}), + // ...formData, + // }); + + // try { + // quickAddStore.createIssue( + // workspaceSlug.toString(), + // projectId.toString(), + // { + // group_id: groupId ?? null, + // sub_group_id: null, + // }, + // payload + // ); + + // setToastAlert({ + // type: "success", + // title: "Success!", + // message: "Issue created successfully.", + // }); + // } catch (err: any) { + // Object.keys(err || {}).forEach((key) => { + // const error = err?.[key]; + // const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null; + + // setToastAlert({ + // type: "error", + // title: "Error!", + // message: errorTitle || "Some error occurred. Please try again.", + // }); + // }); + // } + // }; + + const onSubmitHandler = async (formData: IIssue) => { + if (isSubmitting || !workspaceDetail || !projectDetail) return; - // resetting the form so that user can add another issue quickly reset({ ...defaultValues }); - const payload = createIssuePayload(workspaceDetail!, projectDetails!, { + const payload = createIssuePayload(workspaceDetail, projectDetail, { ...(prePopulatedData ?? {}), ...formData, }); try { - quickAddStore.createIssue( - workspaceSlug.toString(), - projectId.toString(), - { - group_id: groupId ?? null, - sub_group_id: null, - }, - payload - ); - + quickAddCallback && (await quickAddCallback(workspaceSlug, projectId, { ...payload } as IIssue, viewId)); setToastAlert({ type: "success", title: "Success!", message: "Issue created successfully.", }); } catch (err: any) { - Object.keys(err || {}).forEach((key) => { - const error = err?.[key]; - const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null; - - setToastAlert({ - type: "error", - title: "Error!", - message: errorTitle || "Some error occurred. Please try again.", - }); + console.error(err); + setToastAlert({ + type: "error", + title: "Error!", + message: err?.message || "Some error occurred. Please try again.", }); } }; @@ -156,7 +190,7 @@ export const SpreadsheetInlineCreateIssueForm: React.FC = observer((props onSubmit={handleSubmit(onSubmitHandler)} className="flex border-[0.5px] border-t-0 border-custom-border-100 px-4 items-center gap-x-5 bg-custom-background-100 shadow-custom-shadow-sm z-10" > - +
)} diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx index 2d362cb44..fffc89552 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/cycle-root.tsx @@ -1,70 +1,18 @@ -import React, { useCallback } from "react"; -import { useRouter } from "next/router"; +import React from "react"; import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { SpreadsheetView } from "components/issues"; -// types -import { IIssue, IIssueDisplayFilterOptions } from "types"; -// constants -import { IIssueUnGroupedStructure } from "store/issue"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; +import { useRouter } from "next/router"; export const CycleSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query; + const { cycleId } = router.query as { cycleId: string }; - const { - issueFilter: issueFilterStore, - cycleIssue: cycleIssueStore, - issueDetail: issueDetailStore, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - } = useMobxStore(); - - const issues = cycleIssueStore.getIssues; - - const handleDisplayFiltersUpdate = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - - issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { - display_filters: { - ...updatedDisplayFilter, - }, - }); - }, - [issueFilterStore, projectId, workspaceSlug] - ); - - const handleUpdateIssue = useCallback( - (issue: IIssue, data: Partial) => { - if (!workspaceSlug || !projectId) return; - - const payload = { - ...issue, - ...data, - }; - - cycleIssueStore.updateIssueStructure(null, null, payload); - issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data); - }, - [issueDetailStore, cycleIssueStore, projectId, workspaceSlug] - ); + const { cycleIssues: cycleIssueStore, cycleIssuesFilter: cycleIssueFilterStore } = useMobxStore(); return ( - m.member)} - labels={projectLabels || undefined} - states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined} - handleIssueAction={() => {}} - handleUpdateIssue={handleUpdateIssue} - disableUserActions={false} - /> + ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx index 60fda0282..4135e3112 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/module-root.tsx @@ -1,71 +1,18 @@ -import React, { useCallback } from "react"; -import { useRouter } from "next/router"; +import React from "react"; import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { SpreadsheetView } from "components/issues"; -// types -import { IIssue, IIssueDisplayFilterOptions } from "types"; -// constants -import { IIssueUnGroupedStructure } from "store/issue"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; +import { useRouter } from "next/router"; export const ModuleSpreadsheetLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { - issueFilter: issueFilterStore, - moduleIssue: moduleIssueStore, - issueDetail: issueDetailStore, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - } = useMobxStore(); - - const issues = moduleIssueStore.getIssues; - - const handleDisplayFiltersUpdate = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - - issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { - display_filters: { - ...updatedDisplayFilter, - }, - }); - }, - [issueFilterStore, projectId, workspaceSlug] - ); - - const handleUpdateIssue = useCallback( - (issue: IIssue, data: Partial) => { - if (!workspaceSlug || !projectId) return; - - const payload = { - ...issue, - ...data, - }; - - moduleIssueStore.updateIssueStructure(null, null, payload); - issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data); - }, - [issueDetailStore, moduleIssueStore, projectId, workspaceSlug] - ); + const { moduleId } = router.query as { moduleId: string }; + const { moduleIssues: moduleIssueStore, moduleIssuesFilter: moduleIssueFilterStore } = useMobxStore(); return ( - m.member)} - labels={projectLabels ?? undefined} - states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined} - handleIssueAction={() => {}} - handleUpdateIssue={handleUpdateIssue} - disableUserActions={false} - /> + ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx index a14a8d803..a3a67f5f9 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx @@ -1,86 +1,11 @@ -import React, { useCallback } from "react"; -import { useRouter } from "next/router"; +import React from "react"; import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; -// components -import { SpreadsheetView } from "components/issues"; -// types -import { IIssue, IIssueDisplayFilterOptions } from "types"; -// constants -import { IIssueUnGroupedStructure } from "store/issue"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const ProjectSpreadsheetLayout: React.FC = observer(() => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { - issue: issueStore, - issueFilter: issueFilterStore, - issueDetail: issueDetailStore, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - user: userStore, - } = useMobxStore(); - - const user = userStore.currentUser; - const issues = issueStore.getIssues; - - const handleDisplayFiltersUpdate = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - - issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { - display_filters: { - ...updatedDisplayFilter, - }, - }); - }, - [issueFilterStore, projectId, workspaceSlug] - ); - - const handleIssueAction = async (issue: IIssue, action: "copy" | "delete" | "edit") => { - if (!workspaceSlug || !projectId || !user) return; - - if (action === "delete") { - issueDetailStore.deleteIssue(workspaceSlug.toString(), projectId.toString(), issue.id); - issueStore.removeIssueFromStructure(null, null, issue); - } else if (action === "edit") { - issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, issue); - issueStore.updateIssueStructure(null, null, issue); - } - }; - - const handleUpdateIssue = useCallback( - (issue: IIssue, data: Partial) => { - if (!workspaceSlug || !projectId || !user) return; - - const payload = { - ...issue, - ...data, - }; - - issueStore.updateIssueStructure(null, null, payload); - issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data); - }, - [issueStore, issueDetailStore, projectId, user, workspaceSlug] - ); - - return ( - m.member)} - labels={projectLabels || undefined} - states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined} - handleIssueAction={handleIssueAction} - handleUpdateIssue={handleUpdateIssue} - disableUserActions={false} - enableQuickCreateIssue - /> - ); + const { projectIssues: projectIssuesStore, projectIssuesFilter: projectIssueFiltersStore } = useMobxStore(); + return ; }); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx index 2f35f2499..92975f7ff 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-view-root.tsx @@ -1,70 +1,11 @@ -import React, { useCallback } from "react"; -import { useRouter } from "next/router"; +import React from "react"; import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { SpreadsheetView } from "components/issues"; -// types -import { IIssue, IIssueDisplayFilterOptions } from "types"; -// constants -import { IIssueUnGroupedStructure } from "store/issue"; +import { BaseSpreadsheetRoot } from "../base-spreadsheet-root"; export const ProjectViewSpreadsheetLayout: React.FC = observer(() => { - const router = useRouter(); - const { workspaceSlug, projectId } = router.query; - - const { - issueFilter: issueFilterStore, - projectViewIssues: projectViewIssueStore, - issueDetail: issueDetailStore, - projectLabel: { projectLabels }, - projectMember: { projectMembers }, - projectState: projectStateStore, - } = useMobxStore(); - - const issues = projectViewIssueStore.getIssues; - - const handleDisplayFiltersUpdate = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - - issueFilterStore.updateUserFilters(workspaceSlug.toString(), projectId.toString(), { - display_filters: { - ...updatedDisplayFilter, - }, - }); - }, - [issueFilterStore, projectId, workspaceSlug] - ); - - const handleUpdateIssue = useCallback( - (issue: IIssue, data: Partial) => { - if (!workspaceSlug || !projectId) return; - - const payload = { - ...issue, - ...data, - }; - - projectViewIssueStore.updateIssueStructure(null, null, payload); - issueDetailStore.updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data); - }, - [issueDetailStore, projectViewIssueStore, projectId, workspaceSlug] - ); - - return ( - m.member)} - labels={projectLabels || undefined} - states={projectId ? projectStateStore.states?.[projectId.toString()] : undefined} - handleIssueAction={() => {}} - handleUpdateIssue={handleUpdateIssue} - disableUserActions={false} - /> - ); + const { viewIssues: projectViewIssuesStore, viewIssuesFilter: projectViewIssueFiltersStore } = useMobxStore(); + return ; }); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index c258d5437..6f603967a 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; // components -import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetInlineCreateIssueForm } from "components/issues"; +import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetQuickAddIssueForm } from "components/issues"; import { IssuePeekOverview } from "components/issues/issue-peek-overview"; import { Spinner } from "@plane/ui"; // types @@ -19,6 +19,13 @@ type Props = { handleIssueAction: (issue: IIssue, action: "copy" | "delete" | "edit") => void; handleUpdateIssue: (issue: IIssue, data: Partial) => void; openIssuesListModal?: (() => void) | null; + quickAddCallback?: ( + workspaceSlug: string, + projectId: string, + data: IIssue, + viewId?: string + ) => Promise; + viewId?: string; disableUserActions: boolean; enableQuickCreateIssue?: boolean; }; @@ -34,7 +41,8 @@ export const SpreadsheetView: React.FC = observer((props) => { states, handleIssueAction, handleUpdateIssue, - openIssuesListModal, + quickAddCallback, + viewId, disableUserActions, enableQuickCreateIssue, } = props; @@ -132,7 +140,9 @@ export const SpreadsheetView: React.FC = observer((props) => {
- {enableQuickCreateIssue && } + {enableQuickCreateIssue && ( + + )}
{/* {!disableUserActions && diff --git a/web/components/issues/issue-layouts/types.ts b/web/components/issues/issue-layouts/types.ts new file mode 100644 index 000000000..f4c2d8100 --- /dev/null +++ b/web/components/issues/issue-layouts/types.ts @@ -0,0 +1,5 @@ +export enum EIssueActions { + UPDATE = "update", + DELETE = "delete", + REMOVE = "remove", +} diff --git a/web/components/issues/issue-peek-overview/root.tsx b/web/components/issues/issue-peek-overview/root.tsx index 6c8631b5b..0bbc1c591 100644 --- a/web/components/issues/issue-peek-overview/root.tsx +++ b/web/components/issues/issue-peek-overview/root.tsx @@ -109,7 +109,7 @@ export const IssuePeekOverview: FC = observer((props) => { const handleDeleteIssue = async () => { if (isArchived) await archivedIssuesStore.deleteArchivedIssue(workspaceSlug, projectId, issue!); - else await issueStore.deleteIssue(workspaceSlug, projectId, issue!); + else await issueStore.removeIssueFromStructure(workspaceSlug, projectId, issue!); const { query } = router; if (query.peekIssueId) { issueDetailStore.setPeekId(null); diff --git a/web/components/issues/modal.tsx b/web/components/issues/modal.tsx index ab6a09e9b..e4a5bbc29 100644 --- a/web/components/issues/modal.tsx +++ b/web/components/issues/modal.tsx @@ -39,12 +39,21 @@ export interface IssuesModalProps { | "cycle" )[]; onSubmit?: (data: Partial) => Promise; + handleSubmit?: (data: Partial) => Promise; } const issueDraftService = new IssueDraftService(); export const CreateUpdateIssueModal: React.FC = observer((props) => { - const { data, handleClose, isOpen, prePopulateData: prePopulateDataProps, fieldsToShow = ["all"], onSubmit } = props; + const { + data, + handleClose, + isOpen, + prePopulateData: prePopulateDataProps, + fieldsToShow = ["all"], + onSubmit, + handleSubmit, + } = props; // states const [createMore, setCreateMore] = useState(false); @@ -186,18 +195,22 @@ export const CreateUpdateIssueModal: React.FC = observer((prop await issueDetailStore .createIssue(workspaceSlug.toString(), activeProject, payload) .then(async (res) => { - issueStore.fetchIssues(workspaceSlug.toString(), activeProject); + if (handleSubmit) { + await handleSubmit(res); + } else { + issueStore.fetchIssues(workspaceSlug.toString(), activeProject); - if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); - if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module); + if (payload.cycle && payload.cycle !== "") await addIssueToCycle(res.id, payload.cycle); + if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module); - setToastAlert({ - type: "success", - title: "Success!", - message: "Issue created successfully.", - }); + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); - if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); + if (payload.parent && payload.parent !== "") mutate(SUB_ISSUES(payload.parent)); + } }) .catch(() => { setToastAlert({ diff --git a/web/components/page-views/workspace-dashboard.tsx b/web/components/page-views/workspace-dashboard.tsx index b7fcb7cb6..358e54ec9 100644 --- a/web/components/page-views/workspace-dashboard.tsx +++ b/web/components/page-views/workspace-dashboard.tsx @@ -18,6 +18,7 @@ export const WorkspaceDashboardView = observer(() => { const router = useRouter(); const { workspaceSlug } = router.query; // store + const { user: userStore, project: projectStore, commandPalette: commandPaletteStore } = useMobxStore(); const user = userStore.currentUser; diff --git a/web/lib/wrappers/posthog-wrapper.tsx b/web/lib/wrappers/posthog-wrapper.tsx index 1e7c35c0d..783207924 100644 --- a/web/lib/wrappers/posthog-wrapper.tsx +++ b/web/lib/wrappers/posthog-wrapper.tsx @@ -41,7 +41,7 @@ const PosthogWrapper: FC = (props) => { api_host: posthogHost || "https://app.posthog.com", // Enable debug mode in development loaded: (posthog) => { - if (process.env.NODE_ENV === "development") posthog.debug(); + // if (process.env.NODE_ENV === "development") posthog.debug(); }, autocapture: false, capture_pageview: false, // Disable automatic pageview capture, as we capture manually diff --git a/web/package.json b/web/package.json index 6ff5b781a..beca40f32 100644 --- a/web/package.json +++ b/web/package.json @@ -34,6 +34,7 @@ "date-fns": "^2.30.0", "dotenv": "^16.0.3", "js-cookie": "^3.0.1", + "lodash": "^4.17.21", "lodash.debounce": "^4.0.8", "lucide-react": "^0.274.0", "mobx": "^6.10.0", diff --git a/web/services/cycle.service.ts b/web/services/cycle.service.ts index 892d8fb1b..93dbd8635 100644 --- a/web/services/cycle.service.ts +++ b/web/services/cycle.service.ts @@ -4,6 +4,7 @@ import { APIService } from "services/api.service"; import type { CycleDateCheckData, ICycle, IIssue } from "types"; // helpers import { API_BASE_URL } from "helpers/common.helper"; +import { IIssueResponse } from "store/issues/types"; export class CycleService extends APIService { constructor() { @@ -50,6 +51,21 @@ export class CycleService extends APIService { }); } + async getV3CycleIssues( + workspaceSlug: string, + projectId: string, + cycleId: string, + queries?: any + ): Promise { + return this.get(`/api/v3/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/cycle-issues/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async getCycleIssuesWithParams( workspaceSlug: string, projectId: string, diff --git a/web/services/issue/issue.service.ts b/web/services/issue/issue.service.ts index d1dba8ea3..7b3ef0624 100644 --- a/web/services/issue/issue.service.ts +++ b/web/services/issue/issue.service.ts @@ -1,7 +1,8 @@ // services import { APIService } from "services/api.service"; // type -import type { IIssue, IIssueActivity, ISubIssueResponse, IIssueDisplayProperties } from "types"; +import type { IUser, IIssue, IIssueActivity, ISubIssueResponse, IIssueDisplayProperties } from "types"; +import { IIssueResponse } from "store/issues/types"; // helper import { API_BASE_URL } from "helpers/common.helper"; @@ -26,6 +27,16 @@ export class IssueService extends APIService { }); } + async getV3Issues(workspaceSlug: string, projectId: string, queries?: any): Promise { + return this.get(`/api/v3/workspaces/${workspaceSlug}/projects/${projectId}/issues/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async getIssuesWithParams( workspaceSlug: string, projectId: string, diff --git a/web/services/issue/issue_archive.service.ts b/web/services/issue/issue_archive.service.ts index 02cb6357e..7adc045ec 100644 --- a/web/services/issue/issue_archive.service.ts +++ b/web/services/issue/issue_archive.service.ts @@ -17,6 +17,16 @@ export class IssueArchiveService extends APIService { }); } + async getV3ArchivedIssues(workspaceSlug: string, projectId: string, queries?: any): Promise { + return this.get(`/api/v3/workspaces/${workspaceSlug}/projects/${projectId}/archived-issues/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async unarchiveIssue(workspaceSlug: string, projectId: string, issueId: string): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/unarchive/${issueId}/`) .then((response) => response?.data) diff --git a/web/services/issue/issue_draft.service.tsx b/web/services/issue/issue_draft.service.tsx index b329febfb..8d8ddecb3 100644 --- a/web/services/issue/issue_draft.service.tsx +++ b/web/services/issue/issue_draft.service.tsx @@ -17,6 +17,16 @@ export class IssueDraftService extends APIService { }); } + async getV3DraftIssues(workspaceSlug: string, projectId: string, params?: any): Promise { + return this.get(`/api/v3/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, { + params, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + async createDraftIssue(workspaceSlug: string, projectId: string, data: any): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, data) .then((response) => response?.data) diff --git a/web/services/module.service.ts b/web/services/module.service.ts index 9a4e6049b..f122b09c4 100644 --- a/web/services/module.service.ts +++ b/web/services/module.service.ts @@ -1,7 +1,8 @@ // services import { APIService } from "services/api.service"; // types -import type { IModule, IIssue } from "types"; +import type { IModule, IIssue, IUser } from "types"; +import { IIssueResponse } from "store/issues/types"; import { API_BASE_URL } from "helpers/common.helper"; export class ModuleService extends APIService { @@ -70,17 +71,27 @@ export class ModuleService extends APIService { }); } + async getV3ModuleIssues( + workspaceSlug: string, + projectId: string, + moduleId: string, + queries?: any + ): Promise { + return this.get(`/api/v3/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + async getModuleIssuesWithParams( workspaceSlug: string, projectId: string, moduleId: string, queries?: any - ): Promise< - | IIssue[] - | { - [key: string]: IIssue[]; - } - > { + ): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/module-issues/`, { params: queries, }) diff --git a/web/store/cycle-issues/index.ts b/web/store/cycle-issues/index.ts new file mode 100644 index 000000000..2b168d86c --- /dev/null +++ b/web/store/cycle-issues/index.ts @@ -0,0 +1 @@ +export * from "./issue_filters.store"; diff --git a/web/store/cycle-issues/issue_filters.store.ts b/web/store/cycle-issues/issue_filters.store.ts new file mode 100644 index 000000000..a10a0a2e4 --- /dev/null +++ b/web/store/cycle-issues/issue_filters.store.ts @@ -0,0 +1,201 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// services +import { CycleService } from "services/cycle.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { IIssueFilterOptions, TIssueParams } from "types"; + +export interface ICycleIssueFiltersStore { + loader: boolean; + error: any | null; + + // observables + userCycleFilters: { + [cycleId: string]: { + filters?: IIssueFilterOptions; + }; + }; + + // action + fetchCycleFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + updateCycleFilters: ( + workspaceSlug: string, + projectId: string, + cycleId: string, + filterToUpdate: Partial + ) => Promise; + + // computed + appliedFilters: TIssueParams[] | undefined; + cycleFilters: + | { + filters: IIssueFilterOptions; + } + | undefined; +} + +export class CycleIssueFiltersStore implements ICycleIssueFiltersStore { + // observables + loader: boolean = false; + error: any | null = null; + userCycleFilters: { + [cycleId: string]: { + filters?: IIssueFilterOptions; + }; + } = {}; + // root store + rootStore; + // services + cycleService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + // observables + userCycleFilters: observable.ref, + // actions + fetchCycleFilters: action, + updateCycleFilters: action, + // computed + appliedFilters: computed, + cycleFilters: computed, + }); + + this.rootStore = _rootStore; + this.cycleService = new CycleService(); + } + + 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[] | undefined { + const userDisplayFilters = this.rootStore?.projectIssuesFilter.issueFilters?.displayFilters; + + const cycleId = this.rootStore.cycle.cycleId; + + if (!cycleId) return undefined; + + const cycleFilters = this.userCycleFilters[cycleId]?.filters; + + if (!cycleFilters || !userDisplayFilters) return undefined; + + let filteredRouteParams: any = { + priority: cycleFilters?.priority || undefined, + state_group: cycleFilters?.state_group || undefined, + state: cycleFilters?.state || undefined, + assignees: cycleFilters?.assignees || undefined, + created_by: cycleFilters?.created_by || undefined, + labels: cycleFilters?.labels || undefined, + start_date: cycleFilters?.start_date || undefined, + target_date: cycleFilters?.target_date || 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; + } + + get cycleFilters(): + | { + filters: IIssueFilterOptions; + } + | undefined { + const cycleId = this.rootStore.cycle.cycleId; + + if (!cycleId) return undefined; + + const activeCycleFilters = this.userCycleFilters[cycleId]; + + if (!activeCycleFilters) return undefined; + + return { + filters: activeCycleFilters?.filters ?? {}, + }; + } + + fetchCycleFilters = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const cycleResponse = await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId); + + runInAction(() => { + this.userCycleFilters = { + ...this.userCycleFilters, + [cycleId]: { + filters: cycleResponse?.view_props?.filters ?? {}, + }, + }; + }); + } catch (error) { + runInAction(() => { + this.error = error; + }); + + console.log("Failed to fetch user filters in issue filter store", error); + } + }; + + updateCycleFilters = async ( + workspaceSlug: string, + projectId: string, + cycleId: string, + properties: Partial + ) => { + const newViewProps = { + filters: { + ...this.userCycleFilters[cycleId]?.filters, + ...properties, + }, + }; + + let updatedCycleFilters = this.userCycleFilters; + if (!updatedCycleFilters) updatedCycleFilters = {}; + if (!updatedCycleFilters[cycleId]) updatedCycleFilters[cycleId] = {}; + + updatedCycleFilters[cycleId] = newViewProps; + + try { + runInAction(() => { + this.userCycleFilters = { ...updatedCycleFilters }; + }); + + const payload = { + view_props: { + filters: newViewProps.filters, + }, + }; + + const user = this.rootStore.user.currentUser ?? undefined; + + await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, payload); + } catch (error) { + this.fetchCycleFilters(workspaceSlug, projectId, cycleId); + + runInAction(() => { + this.error = error; + }); + + console.log("Failed to update user filters in issue filter store", error); + } + }; +} diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index b2db11fb9..db9f7ceb6 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -5,7 +5,6 @@ import { RootStore } from "../root"; import { IIssue } from "types"; // services import { IssueService } from "services/issue"; -import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; import { IBlockUpdateData } from "components/gantt-chart"; export type IIssueType = "grouped" | "groupWithSubGroups" | "ungrouped"; @@ -18,8 +17,9 @@ export type IIssueGroupWithSubGroupsStructure = { export type IIssueUnGroupedStructure = IIssue[]; export interface IIssueStore { - loader: boolean; + loader: "initial-load" | "mutation" | null; error: any | null; + // issues issues: { [project_id: string]: { @@ -33,15 +33,14 @@ export interface IIssueStore { getIssues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null; getIssuesCount: number; // action - fetchIssues: (workspaceSlug: string, projectId: string) => Promise; + fetchIssues: (workspaceSlug: string, projectId: string, loadType?: "initial-load" | "mutation") => Promise; updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; removeIssueFromStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; - deleteIssue: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; updateGanttIssueStructure: (workspaceSlug: string, issue: IIssue, payload: IBlockUpdateData) => void; } export class IssueStore implements IIssueStore { - loader: boolean = false; + loader: "initial-load" | "mutation" | null = null; error: any | null = null; issues: { [project_id: string]: { @@ -74,7 +73,6 @@ export class IssueStore implements IIssueStore { fetchIssues: action, updateIssueStructure: action, removeIssueFromStructure: action, - deleteIssue: action, updateGanttIssueStructure: action, }); @@ -84,14 +82,13 @@ export class IssueStore implements IIssueStore { autorun(() => { const workspaceSlug = this.rootStore.workspace.workspaceSlug; const projectId = this.rootStore.project.projectId; - if ( workspaceSlug && projectId && this.rootStore.issueFilter.userFilters && this.rootStore.issueFilter.userDisplayFilters ) - this.fetchIssues(workspaceSlug, projectId); + this.fetchIssues(workspaceSlug, projectId, "mutation"); }); } @@ -100,13 +97,16 @@ export class IssueStore implements IIssueStore { const ungroupedLayouts = ["spreadsheet", "gantt_chart"]; const issueLayout = this.rootStore?.issueFilter?.userDisplayFilters?.layout || null; + const issueGroup = this.rootStore?.issueFilter?.userDisplayFilters?.group_by || null; const issueSubGroup = this.rootStore?.issueFilter?.userDisplayFilters?.sub_group_by || null; if (!issueLayout) return null; const _issueState = groupedLayouts.includes(issueLayout) - ? issueSubGroup - ? "groupWithSubGroups" - : "grouped" + ? issueGroup + ? issueSubGroup + ? "groupWithSubGroups" + : "grouped" + : "ungrouped" : ungroupedLayouts.includes(issueLayout) ? "ungrouped" : null; @@ -200,20 +200,6 @@ export class IssueStore implements IIssueStore { : [...(issues ?? []), issue]; } - const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; - if (orderBy === "-created_at") { - issues = sortArrayByDate(issues as any, "created_at"); - } - if (orderBy === "-updated_at") { - issues = sortArrayByDate(issues as any, "updated_at"); - } - if (orderBy === "start_date") { - issues = sortArrayByDate(issues as any, "updated_at"); - } - if (orderBy === "priority") { - issues = sortArrayByPriority(issues as any, "priority"); - } - runInAction(() => { this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; }); @@ -222,7 +208,6 @@ export class IssueStore implements IIssueStore { removeIssueFromStructure = (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { const projectId: string | null = issue?.project; const issueType = this.getIssueType; - if (!projectId || !issueType) return null; let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = @@ -257,7 +242,7 @@ export class IssueStore implements IIssueStore { }; updateGanttIssueStructure = async (workspaceSlug: string, issue: IIssue, payload: IBlockUpdateData) => { - if (!issue || !workspaceSlug) return; + if (!issue || !workspaceSlug || !this.getIssues) return; const issues = this.getIssues as IIssueUnGroupedStructure; @@ -296,45 +281,13 @@ export class IssueStore implements IIssueStore { this.rootStore.issueDetail.updateIssue(workspaceSlug, issue.project, issue.id, newPayload); }; - deleteIssue = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { - const projectId: string | null = issue?.project; - const issueType = this.getIssueType; - if (!projectId || !issueType) return null; - - let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = - this.getIssues; - if (!issues) return null; - - if (issueType === "grouped" && group_id) { - issues = issues as IIssueGroupedStructure; - issues = { - ...issues, - [group_id]: issues[group_id].filter((i) => i?.id !== issue?.id), - }; - } - 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].filter((i) => i?.id !== issue?.id), - }, - }; - } - if (issueType === "ungrouped") { - issues = issues as IIssueUnGroupedStructure; - issues = issues.filter((i) => i?.id !== issue?.id); - } - - runInAction(() => { - this.issues = { ...this.issues, [projectId]: { ...this.issues[projectId], [issueType]: issues } }; - }); - }; - - fetchIssues = async (workspaceSlug: string, projectId: string) => { + fetchIssues = async ( + workspaceSlug: string, + projectId: string, + loadType: "initial-load" | "mutation" = "initial-load" + ) => { try { - this.loader = true; + this.loader = loadType; this.error = null; this.rootStore.workspace.setWorkspaceSlug(workspaceSlug); @@ -354,7 +307,7 @@ export class IssueStore implements IIssueStore { }; runInAction(() => { this.issues = _issues; - this.loader = false; + this.loader = null; this.error = null; }); } @@ -362,7 +315,7 @@ export class IssueStore implements IIssueStore { return issueResponse; } catch (error) { console.error("Error: Fetching error in issues", error); - this.loader = false; + this.loader = null; this.error = error; return error; } diff --git a/web/store/issue/issue_quick_add.store.ts b/web/store/issue/issue_quick_add.store.ts index da71ebb57..f94194902 100644 --- a/web/store/issue/issue_quick_add.store.ts +++ b/web/store/issue/issue_quick_add.store.ts @@ -1,222 +1,123 @@ -import { observable, action, makeObservable, runInAction } from "mobx"; -// services -import { IssueService } from "services/issue"; +import { action, makeObservable, runInAction } from "mobx"; // types import { RootStore } from "../root"; import { IIssue } from "types"; // uuid import { sortArrayByDate, sortArrayByPriority } from "constants/kanban-helpers"; import { IIssueGroupWithSubGroupsStructure, IIssueGroupedStructure, IIssueUnGroupedStructure } from "./issue.store"; +// services +import { IssueService } from "services/issue"; export interface IIssueQuickAddStore { - loader: boolean; - error: any | null; - - createIssue: ( + updateQuickAddIssueStructure: ( workspaceSlug: string, - projectId: string, - grouping: { - group_id: string | null; - sub_group_id: string | null; - }, - data: Partial - ) => Promise; - updateIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; - updateQuickAddIssueStructure: (group_id: string | null, sub_group_id: string | null, issue: IIssue) => void; + group_id: string | null, + sub_group_id: string | null, + issue: IIssue + ) => void; } export class IssueQuickAddStore implements IIssueQuickAddStore { - loader: boolean = false; - error: any | null = null; - - // root store rootStore; - // service issueService; constructor(_rootStore: RootStore) { makeObservable(this, { - // observable - loader: observable.ref, - error: observable.ref, - - createIssue: action, - updateIssueStructure: action, + updateQuickAddIssueStructure: action, }); this.rootStore = _rootStore; this.issueService = new IssueService(); } - - createIssue = async ( - workspaceSlug: string, - projectId: string, - grouping: { - group_id: string | null; - sub_group_id: string | null; - }, - data: Partial - ) => { - runInAction(() => { - this.loader = true; - this.error = null; - }); - - const { group_id, sub_group_id } = grouping; - + createIssue = async (workspaceSlug: string, projectId: string, data: Partial) => { try { - this.updateIssueStructure(group_id, sub_group_id, data as IIssue); - + const user = this.rootStore.user.currentUser ?? undefined; const response = await this.issueService.createIssue(workspaceSlug, projectId, data); - - this.updateQuickAddIssueStructure(group_id, sub_group_id, { - ...data, - ...response, - }); - - runInAction(() => { - this.loader = false; - this.error = null; - }); - return response; } catch (error) { - this.loader = false; - this.error = error; - throw error; } }; - updateIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { - const projectId: string | null = issue?.project; - const issueType = this.rootStore.issue.getIssueType; - if (!projectId || !issueType) return null; + // same as above function but will use temp id instead of real id + updateQuickAddIssueStructure = async ( + workspaceSlug: string, + group_id: string | null, + sub_group_id: string | null, + issue: IIssue + ) => { + try { + const response: any = await this.createIssue(workspaceSlug, issue?.project, issue); + issue = { ...response, tempId: issue?.tempId }; - let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = - this.rootStore.issue.getIssues; - if (!issues) return null; + const projectId: string | null = issue?.project; + const issueType = this.rootStore.issue.getIssueType; + if (!projectId || !issueType) return null; - if (group_id === "null") group_id = null; - if (sub_group_id === "null") sub_group_id = null; + let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = + this.rootStore.issue.getIssues; + if (!issues) return null; - if (issueType === "grouped" && group_id) { - issues = issues as IIssueGroupedStructure; - const _currentIssueId = issues?.[group_id]?.find((_i) => _i?.id === issue.id); - issues = { - ...issues, - [group_id]: _currentIssueId - ? issues[group_id]?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)) - : [...(issues?.[group_id] ?? []), issue], - }; - } - if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { - issues = issues as IIssueGroupWithSubGroupsStructure; - const _currentIssueId = issues?.[sub_group_id]?.[group_id]?.find((_i) => _i?.id === issue.id); - issues = { - ...issues, - [sub_group_id]: { - ...issues[sub_group_id], + if (issueType === "grouped" && group_id) { + issues = issues as IIssueGroupedStructure; + const _currentIssueId = issues?.[group_id]?.find((_i) => _i?.tempId === issue.tempId); + issues = { + ...issues, [group_id]: _currentIssueId - ? issues?.[sub_group_id]?.[group_id]?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)) - : [...(issues?.[sub_group_id]?.[group_id] ?? []), issue], - }, - }; - } - if (issueType === "ungrouped") { - issues = issues as IIssueUnGroupedStructure; - const _currentIssueId = issues?.find((_i) => _i?.id === issue.id); - issues = _currentIssueId - ? issues?.map((i: IIssue) => (i?.id === issue?.id ? { ...i, ...issue } : i)) - : [...(issues ?? []), issue]; - } - - const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; - if (orderBy === "-created_at") { - issues = sortArrayByDate(issues as any, "created_at"); - } - if (orderBy === "-updated_at") { - issues = sortArrayByDate(issues as any, "updated_at"); - } - if (orderBy === "start_date") { - issues = sortArrayByDate(issues as any, "updated_at"); - } - if (orderBy === "priority") { - issues = sortArrayByPriority(issues as any, "priority"); - } - - runInAction(() => { - this.rootStore.issue.issues = { - ...this.rootStore.issue.issues, - [projectId]: { ...this.rootStore.issue.issues[projectId], [issueType]: issues }, - }; - }); - }; - - // same as above function but will use temp id instead of real id - updateQuickAddIssueStructure = async (group_id: string | null, sub_group_id: string | null, issue: IIssue) => { - const projectId: string | null = issue?.project; - const issueType = this.rootStore.issue.getIssueType; - if (!projectId || !issueType) return null; - - let issues: IIssueGroupedStructure | IIssueGroupWithSubGroupsStructure | IIssueUnGroupedStructure | null = - this.rootStore.issue.getIssues; - if (!issues) return null; - - if (issueType === "grouped" && group_id) { - issues = issues as IIssueGroupedStructure; - const _currentIssueId = issues?.[group_id]?.find((_i) => _i?.tempId === issue.tempId); - issues = { - ...issues, - [group_id]: _currentIssueId - ? issues[group_id]?.map((i: IIssue) => - i?.tempId === issue?.tempId ? { ...i, ...issue, tempId: undefined } : i - ) - : [...(issues?.[group_id] ?? []), issue], - }; - } - if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { - issues = issues as IIssueGroupWithSubGroupsStructure; - const _currentIssueId = issues?.[sub_group_id]?.[group_id]?.find((_i) => _i?.tempId === issue.tempId); - issues = { - ...issues, - [sub_group_id]: { - ...issues[sub_group_id], - [group_id]: _currentIssueId - ? issues?.[sub_group_id]?.[group_id]?.map((i: IIssue) => + ? issues[group_id]?.map((i: IIssue) => i?.tempId === issue?.tempId ? { ...i, ...issue, tempId: undefined } : i ) - : [...(issues?.[sub_group_id]?.[group_id] ?? []), issue], - }, - }; - } - if (issueType === "ungrouped") { - issues = issues as IIssueUnGroupedStructure; - const _currentIssueId = issues?.find((_i) => _i?.tempId === issue.tempId); - issues = _currentIssueId - ? issues?.map((i: IIssue) => (i?.tempId === issue?.tempId ? { ...i, ...issue, tempId: undefined } : i)) - : [...(issues ?? []), issue]; - } + : [...(issues?.[group_id] ?? []), issue], + }; + } + if (issueType === "groupWithSubGroups" && group_id && sub_group_id) { + issues = issues as IIssueGroupWithSubGroupsStructure; + const _currentIssueId = issues?.[sub_group_id]?.[group_id]?.find((_i) => _i?.tempId === issue.tempId); + issues = { + ...issues, + [sub_group_id]: { + ...issues[sub_group_id], + [group_id]: _currentIssueId + ? issues?.[sub_group_id]?.[group_id]?.map((i: IIssue) => + i?.tempId === issue?.tempId ? { ...i, ...issue, tempId: undefined } : i + ) + : [...(issues?.[sub_group_id]?.[group_id] ?? []), issue], + }, + }; + } + if (issueType === "ungrouped") { + issues = issues as IIssueUnGroupedStructure; + const _currentIssueId = issues?.find((_i) => _i?.tempId === issue.tempId); + issues = _currentIssueId + ? issues?.map((i: IIssue) => (i?.tempId === issue?.tempId ? { ...i, ...issue, tempId: undefined } : i)) + : [...(issues ?? []), issue]; + } - const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; - if (orderBy === "-created_at") { - issues = sortArrayByDate(issues as any, "created_at"); - } - if (orderBy === "-updated_at") { - issues = sortArrayByDate(issues as any, "updated_at"); - } - if (orderBy === "start_date") { - issues = sortArrayByDate(issues as any, "updated_at"); - } - if (orderBy === "priority") { - issues = sortArrayByPriority(issues as any, "priority"); - } + const orderBy = this.rootStore?.issueFilter?.userDisplayFilters?.order_by || ""; + if (orderBy === "-created_at") { + issues = sortArrayByDate(issues as any, "created_at"); + } + if (orderBy === "-updated_at") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "start_date") { + issues = sortArrayByDate(issues as any, "updated_at"); + } + if (orderBy === "priority") { + issues = sortArrayByPriority(issues as any, "priority"); + } - runInAction(() => { - this.rootStore.issue.issues = { - ...this.rootStore.issue.issues, - [projectId]: { ...this.rootStore.issue.issues[projectId], [issueType]: issues }, - }; - }); + runInAction(() => { + this.rootStore.issue.issues = { + ...this.rootStore.issue.issues, + [projectId]: { ...this.rootStore.issue.issues[projectId], [issueType]: issues }, + }; + }); + + return response; + } catch (error) { + console.log("error", error); + throw error; + } }; } diff --git a/web/store/issues/index.ts b/web/store/issues/index.ts new file mode 100644 index 000000000..d1d5ff81e --- /dev/null +++ b/web/store/issues/index.ts @@ -0,0 +1,40 @@ +/** project issues and issue-filters starts */ + +// issue and filter helpers +export * from "./project-issues/base-issue.store"; +export * from "./project-issues/base-issue-filter.store"; + +// project display filters and display properties +export * from "./project-issues/issue-filters.store"; + +// project issues and filters +export * from "./project-issues/project/issue.store"; +export * from "./project-issues/project/filter.store"; + +// module issues and filters +export * from "./project-issues/module/issue.store"; +export * from "./project-issues/module/filter.store"; + +// cycle +export * from "./project-issues/cycle/issue.store"; +export * from "./project-issues/cycle/filter.store"; + +// project views +export * from "./project-issues/project-view/issue.store"; +export * from "./project-issues/project-view/filter.store"; + +// archived +export * from "./project-issues/archived/issue.store"; +export * from "./project-issues/archived/filter.store"; + +// draft +export * from "./project-issues/draft/issue.store"; +export * from "./project-issues/draft/filter.store"; + +/** project issues and issue-filters ends */ + +/** profile issues and issue-filters starts */ +/** profile issues and issue-filters ends */ + +/** global issues and issue-filters starts */ +/** global issues and issue-filters ends */ diff --git a/web/store/issues/project-issues/archived/filter.store.ts b/web/store/issues/project-issues/archived/filter.store.ts new file mode 100644 index 000000000..77e30c4fc --- /dev/null +++ b/web/store/issues/project-issues/archived/filter.store.ts @@ -0,0 +1,140 @@ +import { computed, makeObservable } from "mobx"; +// base class +import { IssueFilterBaseStore } from "store/issues"; + +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "store/root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; +import { EFilterType } from "store/issues/types"; + +interface IProjectIssuesFilters { + filters: IIssueFilterOptions | undefined; + displayFilters: IIssueDisplayFilterOptions | undefined; + displayProperties: IIssueDisplayProperties | undefined; +} + +export interface IProjectArchivedIssuesFilterStore { + // computed + issueFilters: IProjectIssuesFilters | undefined; + appliedFilters: TIssueParams[] | undefined; + // action + fetchFilters: (workspaceSlug: string, projectId: string) => Promise; + updateFilters: ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties + ) => Promise; +} + +export class ProjectArchivedIssuesFilterStore + extends IssueFilterBaseStore + implements IProjectArchivedIssuesFilterStore +{ + // root store + rootStore; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // computed + issueFilters: computed, + appliedFilters: computed, + }); + + // root store + this.rootStore = _rootStore; + } + + get issueFilters() { + const projectId = this.rootStore.project.projectId; + if (!projectId) return undefined; + const displayFilters = this.rootStore.issuesFilter.issueDisplayFilters(projectId); + + const _filters: IProjectIssuesFilters = { + filters: displayFilters?.filters, + displayFilters: displayFilters?.displayFilters, + displayProperties: displayFilters?.displayProperties, + }; + + return _filters; + } + + get appliedFilters() { + const userFilters = this.issueFilters; + if (!userFilters) return undefined; + + let filteredRouteParams: any = { + priority: userFilters?.filters?.priority || undefined, + state_group: userFilters?.filters?.state_group || undefined, + state: userFilters?.filters?.state || undefined, + assignees: userFilters?.filters?.assignees || undefined, + mentions: userFilters?.filters?.mentions || undefined, + created_by: userFilters?.filters?.created_by || undefined, + labels: userFilters?.filters?.labels || undefined, + start_date: userFilters?.filters?.start_date || undefined, + target_date: userFilters?.filters?.target_date || undefined, + type: userFilters?.displayFilters?.type || undefined, + sub_issue: userFilters?.displayFilters?.sub_issue || true, + show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, + start_target_date: userFilters?.displayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + return filteredRouteParams; + } + + fetchFilters = async (workspaceSlug: string, projectId: string) => { + try { + await this.rootStore.issuesFilter.fetchDisplayFilters(workspaceSlug, projectId); + await this.rootStore.issuesFilter.fetchDisplayProperties(workspaceSlug, projectId); + return; + } catch (error) { + throw Error; + } + }; + + updateFilters = async ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties + ) => { + try { + switch (filterType) { + case EFilterType.FILTERS: + await this.rootStore.issuesFilter.updateDisplayFilters( + workspaceSlug, + projectId, + filterType, + filters as IIssueFilterOptions + ); + break; + case EFilterType.DISPLAY_FILTERS: + await this.rootStore.issuesFilter.updateDisplayFilters( + workspaceSlug, + projectId, + filterType, + filters as IIssueDisplayFilterOptions + ); + break; + case EFilterType.DISPLAY_PROPERTIES: + await this.rootStore.issuesFilter.updateDisplayProperties( + workspaceSlug, + projectId, + filters as IIssueDisplayProperties + ); + break; + } + + return; + } catch (error) { + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/archived/issue.store.ts b/web/store/issues/project-issues/archived/issue.store.ts new file mode 100644 index 000000000..06bd601b1 --- /dev/null +++ b/web/store/issues/project-issues/archived/issue.store.ts @@ -0,0 +1,127 @@ +import { action, observable, makeObservable, computed, runInAction, autorun } from "mobx"; +// base class +import { IssueBaseStore } from "store/issues"; +// services +import { IssueArchiveService } from "services/issue"; +// types +import { IIssueResponse, TLoader, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "../../types"; +import { RootStore } from "store/root"; + +export interface IProjectArchivedIssuesStore { + // observable + loader: TLoader; + issues: { [project_id: string]: IIssueResponse } | undefined; + // computed + getIssues: IIssueResponse | undefined; + getIssuesIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined; + // actions + fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + removeIssueFromArchived: (workspaceSlug: string, projectId: string, issueId: string) => Promise; +} + +export class ProjectArchivedIssuesStore extends IssueBaseStore implements IProjectArchivedIssuesStore { + loader: TLoader = "init-loader"; + issues: { [project_id: string]: IIssueResponse } | undefined = undefined; + // root store + rootStore; + // service + archivedIssueService; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // observable + loader: observable.ref, + issues: observable.ref, + // computed + getIssues: computed, + getIssuesIds: computed, + // action + fetchIssues: action, + removeIssue: action, + removeIssueFromArchived: action, + }); + + this.rootStore = _rootStore; + this.archivedIssueService = new IssueArchiveService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + if (!workspaceSlug || !projectId) return; + + const userFilters = this.rootStore?.projectArchivedIssuesFilter?.issueFilters?.filters; + if (userFilters) this.fetchIssues(workspaceSlug, projectId, "mutation"); + }); + } + + get getIssues() { + const projectId = this.rootStore?.project.projectId; + if (!projectId || !this.issues || !this.issues[projectId]) return undefined; + + return this.issues[projectId]; + } + + get getIssuesIds() { + const projectId = this.rootStore?.project.projectId; + const displayFilters = this.rootStore?.projectArchivedIssuesFilter?.issueFilters?.displayFilters; + if (!displayFilters) return undefined; + + const groupBy = displayFilters?.group_by; + const orderBy = displayFilters?.order_by; + const layout = displayFilters?.layout; + + if (!projectId || !this.issues || !this.issues[projectId]) return undefined; + + let issues: IIssueResponse | IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined = undefined; + + if (layout === "list" && orderBy) { + if (groupBy) issues = this.groupedIssues(groupBy, orderBy, this.issues[projectId]); + else issues = this.unGroupedIssues(orderBy, this.issues[projectId]); + } + + return issues; + } + + fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader") => { + try { + this.loader = loadType; + + const params = this.rootStore?.projectArchivedIssuesFilter?.appliedFilters; + const response = await this.archivedIssueService.getV3ArchivedIssues(workspaceSlug, projectId, params); + + const _issues = { ...this.issues, [projectId]: { ...response } }; + + runInAction(() => { + this.issues = _issues; + this.loader = undefined; + }); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId); + this.loader = undefined; + throw error; + } + }; + + removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + await this.archivedIssueService.unarchiveIssue(workspaceSlug, projectId, issueId); + return; + } catch (error) { + throw error; + } + }; + + removeIssueFromArchived = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + await this.archivedIssueService.deleteArchivedIssue(workspaceSlug, projectId, issueId); + return; + } catch (error) { + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/base-issue-filter.store.ts b/web/store/issues/project-issues/base-issue-filter.store.ts new file mode 100644 index 000000000..2cd2e3bc9 --- /dev/null +++ b/web/store/issues/project-issues/base-issue-filter.store.ts @@ -0,0 +1,29 @@ +// types +import { RootStore } from "store/root"; + +export interface IIssueFilterBaseStore { + // helper methods + computedFilter(filters: any, filteredParams: any): any; +} + +export class IssueFilterBaseStore implements IIssueFilterBaseStore { + // root store + rootStore; + + constructor(_rootStore: RootStore) { + // root store + this.rootStore = _rootStore; + } + + // helper methods + 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; + }; +} diff --git a/web/store/issues/project-issues/base-issue.store.ts b/web/store/issues/project-issues/base-issue.store.ts new file mode 100644 index 000000000..4f5901a91 --- /dev/null +++ b/web/store/issues/project-issues/base-issue.store.ts @@ -0,0 +1,152 @@ +import _ from "lodash"; +// types +import { IIssue, TIssueGroupByOptions, TIssueOrderByOptions } from "types"; +import { RootStore } from "store/root"; +import { IIssueResponse } from "../types"; +// constants +import { ISSUE_PRIORITIES, ISSUE_STATE_GROUPS } from "constants/issue"; +// helpers +import { renderDateFormat } from "helpers/date-time.helper"; + +export interface IIssueBaseStore { + groupedIssues( + groupBy: TIssueGroupByOptions, + orderBy: TIssueOrderByOptions, + issues: IIssueResponse, + isCalendarIssues?: boolean + ): { [group_id: string]: string[] }; + subGroupedIssues( + subGroupBy: TIssueGroupByOptions, + groupBy: TIssueGroupByOptions, + orderBy: TIssueOrderByOptions, + issues: IIssueResponse + ): { [sub_group_id: string]: { [group_id: string]: string[] } }; + unGroupedIssues(orderBy: TIssueOrderByOptions, issues: IIssueResponse): string[]; + issueDisplayFiltersDefaultData(groupBy: string | null): string[]; + issuesSortWithOrderBy(issueObject: IIssueResponse, key: Partial): IIssue[]; + getGroupArray(value: string[] | string | null, isDate?: boolean): string[]; +} + +export class IssueBaseStore implements IIssueBaseStore { + // root store + rootStore; + + constructor(_rootStore: RootStore) { + this.rootStore = _rootStore; + } + + groupedIssues = ( + groupBy: TIssueGroupByOptions, + orderBy: TIssueOrderByOptions, + issues: IIssueResponse, + isCalendarIssues: boolean = false + ) => { + const _issues: { [group_id: string]: string[] } = {}; + + this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => { + _issues[group] = []; + }); + + const projectIssues = this.issuesSortWithOrderBy(issues, orderBy); + + for (const issue in projectIssues) { + const _issue = projectIssues[issue]; + const groupArray = this.getGroupArray(_.get(_issue, groupBy as keyof IIssue), isCalendarIssues); + + for (const group of groupArray) { + if (group && _issues[group]) _issues[group].push(_issue.id); + else if (group) _issues[group] = [_issue.id]; + } + } + + return _issues; + }; + + subGroupedIssues = ( + subGroupBy: TIssueGroupByOptions, + groupBy: TIssueGroupByOptions, + orderBy: TIssueOrderByOptions, + issues: IIssueResponse + ) => { + const _issues: { [sub_group_id: string]: { [group_id: string]: string[] } } = {}; + + this.issueDisplayFiltersDefaultData(subGroupBy).forEach((sub_group: any) => { + const groupByIssues: { [group_id: string]: string[] } = {}; + this.issueDisplayFiltersDefaultData(groupBy).forEach((group) => { + groupByIssues[group] = []; + }); + _issues[sub_group] = groupByIssues; + }); + + const projectIssues = this.issuesSortWithOrderBy(issues, orderBy); + + for (const issue in projectIssues) { + const _issue = projectIssues[issue]; + const subGroupArray = this.getGroupArray(_.get(_issue, subGroupBy as keyof IIssue)); + const groupArray = this.getGroupArray(_.get(_issue, groupBy as keyof IIssue)); + + for (const subGroup of subGroupArray) { + for (const group of groupArray) { + if (subGroup && group && issues[subGroup]) { + _issues[subGroup][group].push(_issue.id); + } + } + } + } + + return _issues; + }; + + unGroupedIssues = (orderBy: TIssueOrderByOptions, issues: IIssueResponse) => + this.issuesSortWithOrderBy(issues, orderBy).map((issue) => issue.id); + + issueDisplayFiltersDefaultData = (groupBy: string | null): string[] => { + switch (groupBy) { + case "state": + return this.rootStore?.projectState.projectStateIds(); + case "state_detail.group": + return ISSUE_STATE_GROUPS.map((i) => i.key); + case "priority": + return ISSUE_PRIORITIES.map((i) => i.key); + case "labels": + return this.rootStore?.projectLabel?.projectLabelIds(true); + case "created_by": + return this.rootStore?.projectMember?.projectMemberIds(true); + case "assignees": + return this.rootStore?.projectMember?.projectMemberIds(true); + case "project": + return this.rootStore?.project?.workspaceProjectIds(); + default: + return []; + } + }; + + issuesSortWithOrderBy = (issueObject: IIssueResponse, key: Partial): IIssue[] => { + let array = _.values(issueObject); + array = _.sortBy(array, "created_at"); + switch (key) { + case "sort_order": + return _.sortBy(array, "sort_order"); + case "-created_at": + return _.reverse(_.sortBy(array, "created_at")); + case "-updated_at": + return _.reverse(_.sortBy(array, "updated_at")); + case "start_date": + return _.sortBy(array, "start_date"); + case "target_date": + return _.sortBy(array, "target_date"); + case "priority": { + const sortArray = ISSUE_PRIORITIES.map((i) => i.key); + return _.sortBy(array, (_issue: IIssue) => _.indexOf(sortArray, _issue.priority)); + } + default: + return array; + } + }; + + getGroupArray(value: string[] | string | null, isDate: boolean = false) { + if (Array.isArray(value)) return value; + else if (isDate) return [renderDateFormat(value) || "None"]; + else return [value || "None"]; + } +} diff --git a/web/store/issues/project-issues/cycle/filter.store.ts b/web/store/issues/project-issues/cycle/filter.store.ts new file mode 100644 index 000000000..6e73d7613 --- /dev/null +++ b/web/store/issues/project-issues/cycle/filter.store.ts @@ -0,0 +1,253 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// base class +import { IssueFilterBaseStore } from "store/issues"; +// services +import { ProjectService, ProjectMemberService } from "services/project"; +import { IssueService } from "services/issue"; +import { CycleService } from "services/cycle.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "store/root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; +import { EFilterType } from "store/issues/types"; + +interface ICycleIssuesFilterOptions { + filters: IIssueFilterOptions; +} + +interface IProjectIssuesFilters { + filters: IIssueFilterOptions | undefined; + displayFilters: IIssueDisplayFilterOptions | undefined; + displayProperties: IIssueDisplayProperties | undefined; +} + +export interface ICycleIssuesFilterStore { + // observable + loader: boolean; + filters: { [cycleId: string]: ICycleIssuesFilterOptions } | undefined; + // computed + issueFilters: IProjectIssuesFilters | undefined; + appliedFilters: TIssueParams[] | undefined; + // actions + fetchCycleFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + updateCycleFilters: ( + workspaceSlug: string, + projectId: string, + cycleId: string, + type: EFilterType, + filters: IIssueFilterOptions + ) => Promise; + + fetchFilters: (workspaceSlug: string, projectId: string, cycleId: string) => Promise; + updateFilters: ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties, + cycleId?: string | undefined + ) => Promise; +} + +export class CycleIssuesFilterStore extends IssueFilterBaseStore implements ICycleIssuesFilterStore { + // observables + loader: boolean = false; + filters: { [projectId: string]: ICycleIssuesFilterOptions } | undefined = undefined; + // root store + rootStore; + // services + projectService; + projectMemberService; + issueService; + cycleService; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // observables + loader: observable.ref, + filters: observable.ref, + // computed + issueFilters: computed, + appliedFilters: computed, + // actions + fetchCycleFilters: action, + updateCycleFilters: action, + fetchFilters: action, + updateFilters: action, + }); + + this.rootStore = _rootStore; + + this.projectService = new ProjectService(); + this.projectMemberService = new ProjectMemberService(); + this.issueService = new IssueService(); + this.cycleService = new CycleService(); + } + + get issueFilters() { + const projectId = this.rootStore.project.projectId; + const cycleId = this.rootStore.cycle.cycleId; + if (!projectId || !cycleId) return undefined; + + const displayFilters = this.rootStore.issuesFilter.issueDisplayFilters(projectId); + const cycleFilters = this.filters?.[cycleId]; + + const _filters: IProjectIssuesFilters = { + filters: cycleFilters?.filters, + displayFilters: displayFilters?.displayFilters, + displayProperties: displayFilters?.displayProperties, + }; + + return _filters; + } + + get appliedFilters() { + const userFilters = this.issueFilters; + if (!userFilters) return undefined; + + let filteredRouteParams: any = { + priority: userFilters?.filters?.priority || undefined, + state_group: userFilters?.filters?.state_group || undefined, + state: userFilters?.filters?.state || undefined, + assignees: userFilters?.filters?.assignees || undefined, + mentions: userFilters?.filters?.mentions || undefined, + created_by: userFilters?.filters?.created_by || undefined, + labels: userFilters?.filters?.labels || undefined, + start_date: userFilters?.filters?.start_date || undefined, + target_date: userFilters?.filters?.target_date || undefined, + type: userFilters?.displayFilters?.type || undefined, + sub_issue: userFilters?.displayFilters?.sub_issue || true, + show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, + start_target_date: userFilters?.displayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (userFilters?.displayFilters?.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } + + fetchCycleFilters = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + const cycleFilters = await this.cycleService.getCycleDetails(workspaceSlug, projectId, cycleId); + + const filters: IIssueFilterOptions = { + assignees: cycleFilters?.view_props?.filters?.assignees || null, + mentions: cycleFilters?.view_props?.filters?.mentions || null, + created_by: cycleFilters?.view_props?.filters?.created_by || null, + labels: cycleFilters?.view_props?.filters?.labels || null, + priority: cycleFilters?.view_props?.filters?.priority || null, + project: cycleFilters?.view_props?.filters?.project || null, + start_date: cycleFilters?.view_props?.filters?.start_date || null, + state: cycleFilters?.view_props?.filters?.state || null, + state_group: cycleFilters?.view_props?.filters?.state_group || null, + subscriber: cycleFilters?.view_props?.filters?.subscriber || null, + target_date: cycleFilters?.view_props?.filters?.target_date || null, + }; + + const issueFilters: ICycleIssuesFilterOptions = { + filters: filters, + }; + + let _filters = { ...this.filters }; + if (!_filters) _filters = {}; + if (!_filters[cycleId]) _filters[cycleId] = { filters: {} }; + _filters[cycleId] = issueFilters; + + runInAction(() => { + this.filters = _filters; + }); + + return filters; + } catch (error) { + this.fetchFilters(workspaceSlug, projectId, cycleId); + throw error; + } + }; + + updateCycleFilters = async ( + workspaceSlug: string, + projectId: string, + cycleId: string, + type: EFilterType, + filters: IIssueFilterOptions + ) => { + try { + let _cycleIssueFilters = { ...this.filters }; + if (!_cycleIssueFilters) _cycleIssueFilters = {}; + if (!_cycleIssueFilters[cycleId]) _cycleIssueFilters[cycleId] = { filters: {} }; + + const _filters = { filters: { ..._cycleIssueFilters[cycleId].filters } }; + + if (type === EFilterType.FILTERS) _filters.filters = { ..._filters.filters, ...filters }; + + _cycleIssueFilters[cycleId] = { filters: _filters.filters }; + + runInAction(() => { + this.filters = _cycleIssueFilters; + }); + + await this.cycleService.patchCycle(workspaceSlug, projectId, cycleId, { + view_props: { filters: _filters.filters }, + }); + + return _filters; + } catch (error) { + this.fetchFilters(workspaceSlug, projectId, cycleId); + throw error; + } + }; + + fetchFilters = async (workspaceSlug: string, projectId: string, cycleId: string) => { + try { + await this.rootStore.issuesFilter.fetchDisplayFilters(workspaceSlug, projectId); + await this.rootStore.issuesFilter.fetchDisplayProperties(workspaceSlug, projectId); + await this.fetchCycleFilters(workspaceSlug, projectId, cycleId); + return; + } catch (error) { + this.fetchFilters(workspaceSlug, projectId, cycleId); + throw error; + } + }; + + updateFilters = async ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties, + cycleId?: string | undefined + ) => { + try { + if (!cycleId) throw new Error(); + switch (filterType) { + case EFilterType.FILTERS: + await this.updateCycleFilters(workspaceSlug, projectId, cycleId, filterType, filters as IIssueFilterOptions); + break; + case EFilterType.DISPLAY_FILTERS: + await this.rootStore.issuesFilter.updateDisplayFilters( + workspaceSlug, + projectId, + filterType, + filters as IIssueDisplayFilterOptions + ); + break; + case EFilterType.DISPLAY_PROPERTIES: + await this.rootStore.issuesFilter.updateDisplayProperties( + workspaceSlug, + projectId, + filters as IIssueDisplayProperties + ); + break; + } + + return; + } catch (error) { + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/cycle/issue.store.ts b/web/store/issues/project-issues/cycle/issue.store.ts new file mode 100644 index 000000000..1c36a1e9b --- /dev/null +++ b/web/store/issues/project-issues/cycle/issue.store.ts @@ -0,0 +1,315 @@ +import { action, observable, makeObservable, computed, runInAction, autorun } from "mobx"; +// base class +import { IssueBaseStore } from "store/issues"; +// services +import { IssueService } from "services/issue"; +import { CycleService } from "services/cycle.service"; +// types +import { TIssueGroupByOptions } from "types"; +import { IIssue } from "types/issues"; +import { IIssueResponse, TLoader, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "../../types"; +import { RootStore } from "store/root"; + +export interface ICycleIssuesStore { + // observable + loader: TLoader; + issues: { [cycle_id: string]: IIssueResponse } | undefined; + // computed + getIssues: IIssueResponse | undefined; + getIssuesIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined; + // actions + fetchIssues: ( + workspaceSlug: string, + projectId: string, + loadType: TLoader, + cycleId?: string | undefined + ) => Promise; + createIssue: ( + workspaceSlug: string, + projectId: string, + data: Partial, + cycleId?: string | undefined + ) => Promise; + updateIssue: ( + workspaceSlug: string, + projectId: string, + issueId: string, + data: Partial, + cycleId?: string | undefined + ) => Promise; + removeIssue: ( + workspaceSlug: string, + projectId: string, + issueId: string, + cycleId?: string | undefined + ) => Promise; + quickAddIssue: ( + workspaceSlug: string, + projectId: string, + data: IIssue, + cycleId?: string | undefined + ) => Promise; + removeIssueFromCycle: ( + workspaceSlug: string, + projectId: string, + cycleId: string, + issueId: string, + issueBridgeId: string + ) => Promise; +} + +export class CycleIssuesStore extends IssueBaseStore implements ICycleIssuesStore { + loader: TLoader = "init-loader"; + issues: { [cycle_id: string]: IIssueResponse } | undefined = undefined; + // root store + rootStore; + // service + cycleService; + issueService; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // observable + loader: observable.ref, + issues: observable.ref, + // computed + getIssues: computed, + getIssuesIds: computed, + // action + fetchIssues: action, + createIssue: action, + updateIssue: action, + removeIssue: action, + quickAddIssue: action, + removeIssueFromCycle: action, + }); + + this.rootStore = _rootStore; + this.issueService = new IssueService(); + this.cycleService = new CycleService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + const cycleId = this.rootStore.cycle.cycleId; + if (!workspaceSlug || !projectId || !cycleId) return; + + const userFilters = this.rootStore?.cycleIssuesFilter?.issueFilters?.filters; + if (userFilters) this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); + }); + } + + get getIssues() { + const cycleId = this.rootStore?.cycle?.cycleId; + if (!cycleId || !this.issues || !this.issues[cycleId]) return undefined; + + return this.issues[cycleId]; + } + + get getIssuesIds() { + const cycleId = this.rootStore?.cycle?.cycleId; + const displayFilters = this.rootStore?.cycleIssuesFilter?.issueFilters?.displayFilters; + + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; + const orderBy = displayFilters?.order_by; + const layout = displayFilters?.layout; + + if (!cycleId || !this.issues || !this.issues[cycleId]) return undefined; + + let issues: IIssueResponse | IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined = undefined; + + if (layout === "list" && orderBy) { + if (groupBy) issues = this.groupedIssues(groupBy, orderBy, this.issues[cycleId]); + else issues = this.unGroupedIssues(orderBy, this.issues[cycleId]); + } else if (layout === "kanban" && groupBy && orderBy) { + if (subGroupBy) issues = this.subGroupedIssues(subGroupBy, groupBy, orderBy, this.issues[cycleId]); + else issues = this.groupedIssues(groupBy, orderBy, this.issues[cycleId]); + } else if (layout === "calendar") + issues = this.groupedIssues("target_date" as TIssueGroupByOptions, "target_date", this.issues[cycleId], true); + else if (layout === "spreadsheet") issues = this.unGroupedIssues(orderBy ?? "-created_at", this.issues[cycleId]); + else if (layout === "gantt_chart") issues = this.unGroupedIssues(orderBy ?? "sort_order", this.issues[cycleId]); + + return issues; + } + + fetchIssues = async ( + workspaceSlug: string, + projectId: string, + loadType: TLoader = "init-loader", + cycleId: string | undefined = undefined + ) => { + if (!cycleId) return undefined; + + try { + this.loader = loadType; + + const params = this.rootStore?.cycleIssuesFilter?.appliedFilters; + const response = await this.cycleService.getV3CycleIssues(workspaceSlug, projectId, cycleId, params); + + const _issues = { ...this.issues, [cycleId]: { ...response } }; + + runInAction(() => { + this.issues = _issues; + this.loader = undefined; + }); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); + this.loader = undefined; + throw error; + } + }; + + createIssue = async ( + workspaceSlug: string, + projectId: string, + data: Partial, + cycleId: string | undefined = undefined + ) => { + if (!cycleId) return undefined; + + try { + const response = await this.rootStore.projectIssues.createIssue(workspaceSlug, projectId, data); + const issueToCycle = await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, { + issues: [response.id], + }); + + let _issues = this.issues; + if (!_issues) _issues = {}; + if (!_issues[cycleId]) _issues[cycleId] = {}; + _issues[cycleId] = { ..._issues[cycleId], ...{ [response.id]: response } }; + + runInAction(() => { + this.issues = _issues; + }); + + return issueToCycle; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); + throw error; + } + }; + + updateIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + data: Partial, + cycleId: string | undefined = undefined + ) => { + if (!cycleId) return undefined; + + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[cycleId]) _issues[cycleId] = {}; + _issues[cycleId][issueId] = { ..._issues[cycleId][issueId], ...data }; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.rootStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); + throw error; + } + }; + + removeIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + cycleId: string | undefined = undefined + ) => { + if (!cycleId) return undefined; + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[cycleId]) _issues[cycleId] = {}; + delete _issues?.[cycleId]?.[issueId]; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.rootStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); + throw error; + } + }; + + quickAddIssue = async ( + workspaceSlug: string, + projectId: string, + data: IIssue, + cycleId: string | undefined = undefined + ) => { + if (!cycleId) return; + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[cycleId]) _issues[cycleId] = {}; + _issues[cycleId] = { ..._issues[cycleId], ...{ [data.id as keyof IIssue]: data } }; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.createIssue(workspaceSlug, projectId, data, cycleId); + + if (this.issues) { + delete this.issues[cycleId][data.id as keyof IIssue]; + + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[cycleId]) _issues[cycleId] = {}; + _issues[cycleId] = { ..._issues[cycleId], ...{ [response.id as keyof IIssue]: response } }; + + runInAction(() => { + this.issues = _issues; + }); + } + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); + throw error; + } + }; + + removeIssueFromCycle = async ( + workspaceSlug: string, + projectId: string, + cycleId: string, + issueId: string, + issueBridgeId: string + ) => { + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[cycleId]) _issues[cycleId] = {}; + delete _issues?.[cycleId]?.[issueId]; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueBridgeId); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", cycleId); + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/draft/filter.store.ts b/web/store/issues/project-issues/draft/filter.store.ts new file mode 100644 index 000000000..7cfca229b --- /dev/null +++ b/web/store/issues/project-issues/draft/filter.store.ts @@ -0,0 +1,137 @@ +import { computed, makeObservable } from "mobx"; +// base class +import { IssueFilterBaseStore } from "store/issues"; + +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "store/root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; +import { EFilterType } from "store/issues/types"; + +interface IProjectIssuesFilters { + filters: IIssueFilterOptions | undefined; + displayFilters: IIssueDisplayFilterOptions | undefined; + displayProperties: IIssueDisplayProperties | undefined; +} + +export interface IProjectDraftIssuesFilterStore { + // computed + issueFilters: IProjectIssuesFilters | undefined; + appliedFilters: TIssueParams[] | undefined; + // action + fetchFilters: (workspaceSlug: string, projectId: string) => Promise; + updateFilters: ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties + ) => Promise; +} + +export class ProjectDraftIssuesFilterStore extends IssueFilterBaseStore implements IProjectDraftIssuesFilterStore { + // root store + rootStore; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // computed + issueFilters: computed, + appliedFilters: computed, + }); + + // root store + this.rootStore = _rootStore; + } + + get issueFilters() { + const projectId = this.rootStore.project.projectId; + if (!projectId) return undefined; + const displayFilters = this.rootStore.issuesFilter.issueDisplayFilters(projectId); + + const _filters: IProjectIssuesFilters = { + filters: displayFilters?.filters, + displayFilters: displayFilters?.displayFilters, + displayProperties: displayFilters?.displayProperties, + }; + + return _filters; + } + + get appliedFilters() { + const userFilters = this.issueFilters; + if (!userFilters) return undefined; + + let filteredRouteParams: any = { + priority: userFilters?.filters?.priority || undefined, + state_group: userFilters?.filters?.state_group || undefined, + state: userFilters?.filters?.state || undefined, + assignees: userFilters?.filters?.assignees || undefined, + mentions: userFilters?.filters?.mentions || undefined, + created_by: userFilters?.filters?.created_by || undefined, + labels: userFilters?.filters?.labels || undefined, + start_date: userFilters?.filters?.start_date || undefined, + target_date: userFilters?.filters?.target_date || undefined, + type: userFilters?.displayFilters?.type || undefined, + sub_issue: userFilters?.displayFilters?.sub_issue || true, + show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, + start_target_date: userFilters?.displayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + return filteredRouteParams; + } + + fetchFilters = async (workspaceSlug: string, projectId: string) => { + try { + await this.rootStore.issuesFilter.fetchDisplayFilters(workspaceSlug, projectId); + await this.rootStore.issuesFilter.fetchDisplayProperties(workspaceSlug, projectId); + return; + } catch (error) { + throw Error; + } + }; + + updateFilters = async ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties + ) => { + try { + switch (filterType) { + case EFilterType.FILTERS: + await this.rootStore.issuesFilter.updateDisplayFilters( + workspaceSlug, + projectId, + filterType, + filters as IIssueFilterOptions + ); + break; + case EFilterType.DISPLAY_FILTERS: + await this.rootStore.issuesFilter.updateDisplayFilters( + workspaceSlug, + projectId, + filterType, + filters as IIssueDisplayFilterOptions + ); + break; + case EFilterType.DISPLAY_PROPERTIES: + await this.rootStore.issuesFilter.updateDisplayProperties( + workspaceSlug, + projectId, + filters as IIssueDisplayProperties + ); + break; + } + + return; + } catch (error) { + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/draft/issue.store.ts b/web/store/issues/project-issues/draft/issue.store.ts new file mode 100644 index 000000000..1687c8d4e --- /dev/null +++ b/web/store/issues/project-issues/draft/issue.store.ts @@ -0,0 +1,176 @@ +import { action, observable, makeObservable, computed, runInAction, autorun } from "mobx"; +// base class +import { IssueBaseStore } from "store/issues"; +// services +import { IssueService } from "services/issue/issue.service"; +// types +import { IIssue } from "types/issues"; +import { IIssueResponse, TLoader, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "../../types"; +import { RootStore } from "store/root"; + +export interface IProjectDraftIssuesStore { + // observable + loader: TLoader; + issues: { [project_id: string]: IIssueResponse } | undefined; + // computed + getIssues: IIssueResponse | undefined; + getIssuesIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined; + // actions + fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; + createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; +} + +export class ProjectDraftIssuesStore extends IssueBaseStore implements IProjectDraftIssuesStore { + loader: TLoader = "init-loader"; + issues: { [project_id: string]: IIssueResponse } | undefined = undefined; + // root store + rootStore; + // service + issueService; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // observable + loader: observable.ref, + issues: observable.ref, + // computed + getIssues: computed, + getIssuesIds: computed, + // action + fetchIssues: action, + createIssue: action, + updateIssue: action, + removeIssue: action, + }); + + this.rootStore = _rootStore; + this.issueService = new IssueService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + if (!workspaceSlug || !projectId) return; + + const userFilters = this.rootStore?.projectDraftIssuesFilter?.issueFilters?.filters; + if (userFilters) this.fetchIssues(workspaceSlug, projectId, "mutation"); + }); + } + + get getIssues() { + const projectId = this.rootStore?.project.projectId; + if (!projectId || !this.issues || !this.issues[projectId]) return undefined; + + return this.issues[projectId]; + } + + get getIssuesIds() { + const projectId = this.rootStore?.project.projectId; + const displayFilters = this.rootStore?.projectDraftIssuesFilter?.issueFilters?.displayFilters; + if (!displayFilters) return undefined; + + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; + const orderBy = displayFilters?.order_by; + const layout = displayFilters?.layout; + + if (!projectId || !this.issues || !this.issues[projectId]) return undefined; + + let issues: IIssueResponse | IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined = undefined; + + if (layout === "list" && orderBy) { + if (groupBy) issues = this.groupedIssues(groupBy, orderBy, this.issues[projectId]); + else issues = this.unGroupedIssues(orderBy, this.issues[projectId]); + } else if (layout === "kanban" && groupBy && orderBy) { + if (subGroupBy) issues = this.subGroupedIssues(subGroupBy, groupBy, orderBy, this.issues[projectId]); + else issues = this.groupedIssues(groupBy, orderBy, this.issues[projectId]); + } + + return issues; + } + + fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader") => { + try { + this.loader = loadType; + + const params = this.rootStore?.projectDraftIssuesFilter?.appliedFilters; + const response = await this.issueService.getV3Issues(workspaceSlug, projectId, params); + + const _issues = { ...this.issues, [projectId]: { ...response } }; + + runInAction(() => { + this.issues = _issues; + this.loader = undefined; + }); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId); + this.loader = undefined; + throw error; + } + }; + + createIssue = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const response = await this.issueService.createIssue(workspaceSlug, projectId, data); + + let _issues = this.issues; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + _issues[projectId] = { ..._issues[projectId], ...{ [response.id]: response } }; + + runInAction(() => { + this.issues = _issues; + }); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; + + updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + _issues[projectId][issueId] = { ..._issues[projectId][issueId], ...data }; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; + + removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + delete _issues?.[projectId]?.[issueId]; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/issue-filters.store.ts b/web/store/issues/project-issues/issue-filters.store.ts new file mode 100644 index 000000000..b45c74f20 --- /dev/null +++ b/web/store/issues/project-issues/issue-filters.store.ts @@ -0,0 +1,252 @@ +import { action, makeObservable, observable, runInAction } from "mobx"; +// services +import { IssueService } from "services/issue"; +import { ProjectMemberService, ProjectService } from "services/project"; +// types +import { RootStore } from "store/root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "types"; +import { EFilterType } from "store/issues/types"; + +interface IProjectIssuesFiltersOptions { + filters: IIssueFilterOptions; + displayFilters: IIssueDisplayFilterOptions; +} + +interface IProjectIssuesDisplayOptions { + filters: IIssueFilterOptions; + displayFilters: IIssueDisplayFilterOptions; + displayProperties: IIssueDisplayProperties; +} + +export interface IIssuesFilterStore { + // observables + projectIssueFilters: { [projectId: string]: IProjectIssuesDisplayOptions } | undefined; + // computed + // helpers + issueDisplayFilters: (projectId: string) => IProjectIssuesDisplayOptions | undefined; + // actions + fetchDisplayFilters: (workspaceSlug: string, projectId: string) => Promise; + updateDisplayFilters: ( + workspaceSlug: string, + projectId: string, + type: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions + ) => Promise; + fetchDisplayProperties: (workspaceSlug: string, projectId: string) => Promise; + updateDisplayProperties: ( + workspaceSlug: string, + projectId: string, + properties: IIssueDisplayProperties + ) => Promise; +} + +export class IssuesFilterStore implements IIssuesFilterStore { + // observables + projectIssueFilters: { [projectId: string]: IProjectIssuesDisplayOptions } | undefined = undefined; + // root store + rootStore; + // services + projectMemberService; + projectService; + issueService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // observables + projectIssueFilters: observable.ref, + // computed + // actions + fetchDisplayFilters: action, + updateDisplayFilters: action, + fetchDisplayProperties: action, + updateDisplayProperties: action, + }); + // root store + this.rootStore = _rootStore; + // services + this.projectMemberService = new ProjectMemberService(); + this.projectService = new ProjectService(); + this.issueService = new IssueService(); + } + + // computed + + // helpers + issueDisplayFilters = (projectId: string) => { + if (!projectId) return undefined; + return this.projectIssueFilters?.[projectId] || undefined; + }; + + // actions + fetchDisplayFilters = async (workspaceSlug: string, projectId: string) => { + try { + const _filters = await this.projectMemberService.projectMemberMe(workspaceSlug, projectId); + + const filters: IIssueFilterOptions = { + assignees: _filters?.view_props?.filters?.assignees || null, + mentions: _filters?.view_props?.filters?.mentions || null, + created_by: _filters?.view_props?.filters?.created_by || null, + labels: _filters?.view_props?.filters?.labels || null, + priority: _filters?.view_props?.filters?.priority || null, + project: _filters?.view_props?.filters?.project || null, + start_date: _filters?.view_props?.filters?.start_date || null, + state: _filters?.view_props?.filters?.state || null, + state_group: _filters?.view_props?.filters?.state_group || null, + subscriber: _filters?.view_props?.filters?.subscriber || null, + target_date: _filters?.view_props?.filters?.target_date || null, + }; + + const displayFilters: IIssueDisplayFilterOptions = { + calendar: { + show_weekends: _filters?.view_props?.display_filters?.calendar?.show_weekends || false, + layout: _filters?.view_props?.display_filters?.calendar?.layout || "month", + }, + group_by: _filters?.view_props?.display_filters?.group_by || null, + sub_group_by: _filters?.view_props?.display_filters?.sub_group_by || null, + layout: _filters?.view_props?.display_filters?.layout || "list", + order_by: _filters?.view_props?.display_filters?.order_by || "-created_at", + show_empty_groups: _filters?.view_props?.display_filters?.show_empty_groups || false, + start_target_date: _filters?.view_props?.display_filters?.start_target_date || false, + sub_issue: _filters?.view_props?.display_filters?.sub_issue || false, + type: _filters?.view_props?.display_filters?.type || null, + }; + + const issueFilters: IProjectIssuesFiltersOptions = { + filters: filters, + displayFilters: displayFilters, + }; + + let _projectIssueFilters = this.projectIssueFilters; + if (!_projectIssueFilters) _projectIssueFilters = {}; + if (!_projectIssueFilters[projectId]) + _projectIssueFilters[projectId] = { filters: {}, displayFilters: {}, displayProperties: {} }; + _projectIssueFilters[projectId] = { + ..._projectIssueFilters[projectId], + ...issueFilters, + }; + + runInAction(() => { + this.projectIssueFilters = _projectIssueFilters; + }); + + return issueFilters; + } catch (error) { + throw error; + } + }; + + updateDisplayFilters = async ( + workspaceSlug: string, + projectId: string, + type: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions + ) => { + try { + let _projectIssueFilters = { ...this.projectIssueFilters }; + if (!_projectIssueFilters) _projectIssueFilters = {}; + if (!_projectIssueFilters[projectId]) + _projectIssueFilters[projectId] = { filters: {}, displayFilters: {}, displayProperties: {} }; + + const _filters = { + filters: { ..._projectIssueFilters[projectId].filters }, + displayFilters: { ..._projectIssueFilters[projectId].displayFilters }, + }; + + if (type === EFilterType.FILTERS) _filters.filters = { ..._filters.filters, ...filters }; + else if (type === EFilterType.DISPLAY_FILTERS) + _filters.displayFilters = { ..._filters.displayFilters, ...filters }; + + // set sub_group_by to null if group_by is set to null + if (_filters.displayFilters.group_by === null) _filters.displayFilters.sub_group_by = null; + + // set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same + if ( + _filters.displayFilters.layout === "kanban" && + _filters.displayFilters.group_by === _filters.displayFilters.sub_group_by + ) + _filters.displayFilters.sub_group_by = null; + + // set group_by to state if layout is switched to kanban and group_by is null + if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null) + _filters.displayFilters.group_by = "state"; + + _projectIssueFilters[projectId] = { + filters: _filters.filters, + displayFilters: _filters.displayFilters, + displayProperties: _projectIssueFilters[projectId].displayProperties, + }; + + runInAction(() => { + this.projectIssueFilters = _projectIssueFilters; + }); + + await this.projectService.setProjectView(workspaceSlug, projectId, { + view_props: { filters: _filters.filters, display_filters: _filters.displayFilters }, + }); + + return _filters; + } catch (error) { + this.fetchDisplayFilters(workspaceSlug, projectId); + throw error; + } + }; + + fetchDisplayProperties = async (workspaceSlug: string, projectId: string) => { + try { + const _issueDisplayProperties = await this.issueService.getIssueDisplayProperties(workspaceSlug, projectId); + + const displayProperties: IIssueDisplayProperties = { + assignee: _issueDisplayProperties?.properties?.assignee || false, + start_date: _issueDisplayProperties?.properties?.start_date || false, + due_date: _issueDisplayProperties?.properties?.due_date || false, + labels: _issueDisplayProperties?.properties?.labels || false, + key: _issueDisplayProperties?.properties?.key || false, + priority: _issueDisplayProperties?.properties?.priority || false, + state: _issueDisplayProperties?.properties?.state || false, + sub_issue_count: _issueDisplayProperties?.properties?.sub_issue_count || false, + link: _issueDisplayProperties?.properties?.link || false, + attachment_count: _issueDisplayProperties?.properties?.attachment_count || false, + estimate: _issueDisplayProperties?.properties?.estimate || false, + created_on: _issueDisplayProperties?.properties?.created_on || false, + updated_on: _issueDisplayProperties?.properties?.updated_on || false, + }; + + let _projectIssueFilters = { ...this.projectIssueFilters }; + if (!_projectIssueFilters) _projectIssueFilters = {}; + if (!_projectIssueFilters[projectId]) + _projectIssueFilters[projectId] = { filters: {}, displayFilters: {}, displayProperties: {} }; + _projectIssueFilters[projectId] = { ..._projectIssueFilters[projectId], displayProperties: displayProperties }; + + runInAction(() => { + this.projectIssueFilters = _projectIssueFilters; + }); + + return displayProperties; + } catch (error) { + throw error; + } + }; + + updateDisplayProperties = async (workspaceSlug: string, projectId: string, properties: IIssueDisplayProperties) => { + try { + let _issueFilters = { ...this.projectIssueFilters }; + if (!_issueFilters) _issueFilters = {}; + if (!_issueFilters[projectId]) + _issueFilters[projectId] = { filters: {}, displayFilters: {}, displayProperties: {} }; + + const updatedDisplayProperties = { ..._issueFilters[projectId].displayProperties, ...properties }; + _issueFilters[projectId] = { ..._issueFilters[projectId], displayProperties: updatedDisplayProperties }; + + runInAction(() => { + this.projectIssueFilters = _issueFilters; + }); + + await this.issueService.updateIssueDisplayProperties(workspaceSlug, projectId, updatedDisplayProperties); + + return properties; + } catch (error) { + this.fetchDisplayProperties(workspaceSlug, projectId); + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/module/filter.store.ts b/web/store/issues/project-issues/module/filter.store.ts new file mode 100644 index 000000000..a30103314 --- /dev/null +++ b/web/store/issues/project-issues/module/filter.store.ts @@ -0,0 +1,261 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// base class +import { IssueFilterBaseStore } from "store/issues"; +// services +import { ProjectService, ProjectMemberService } from "services/project"; +import { IssueService } from "services/issue"; +import { ModuleService } from "services/module.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "store/root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; +import { EFilterType } from "store/issues/types"; + +interface IModuleIssuesFilterOptions { + filters: IIssueFilterOptions; +} + +interface IProjectIssuesFilters { + filters: IIssueFilterOptions | undefined; + displayFilters: IIssueDisplayFilterOptions | undefined; + displayProperties: IIssueDisplayProperties | undefined; +} + +export interface IModuleIssuesFilterStore { + // observable + loader: boolean; + filters: { [moduleId: string]: IModuleIssuesFilterOptions } | undefined; + // computed + issueFilters: IProjectIssuesFilters | undefined; + appliedFilters: TIssueParams[] | undefined; + // actions + fetchModuleFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + updateModuleFilters: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + type: EFilterType, + filters: IIssueFilterOptions + ) => Promise; + + fetchFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + updateFilters: ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties, + moduleId?: string | undefined + ) => Promise; +} + +export class ModuleIssuesFilterStore extends IssueFilterBaseStore implements IModuleIssuesFilterStore { + // observables + loader: boolean = false; + filters: { [projectId: string]: IModuleIssuesFilterOptions } | undefined = undefined; + // root store + rootStore; + // services + projectService; + projectMemberService; + issueService; + moduleService; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // observables + loader: observable.ref, + filters: observable.ref, + // computed + issueFilters: computed, + appliedFilters: computed, + // actions + fetchModuleFilters: action, + updateModuleFilters: action, + fetchFilters: action, + updateFilters: action, + }); + + this.rootStore = _rootStore; + + this.projectService = new ProjectService(); + this.projectMemberService = new ProjectMemberService(); + this.issueService = new IssueService(); + this.moduleService = new ModuleService(); + } + + get issueFilters() { + const projectId = this.rootStore.project.projectId; + const moduleId = this.rootStore.module.moduleId; + if (!projectId || !moduleId) return undefined; + + const displayFilters = this.rootStore.issuesFilter.issueDisplayFilters(projectId); + const moduleFilters = this.filters?.[moduleId]; + + const _filters: IProjectIssuesFilters = { + filters: moduleFilters?.filters, + displayFilters: displayFilters?.displayFilters, + displayProperties: displayFilters?.displayProperties, + }; + + return _filters; + } + + get appliedFilters() { + const userFilters = this.issueFilters; + if (!userFilters) return undefined; + + let filteredRouteParams: any = { + priority: userFilters?.filters?.priority || undefined, + state_group: userFilters?.filters?.state_group || undefined, + state: userFilters?.filters?.state || undefined, + assignees: userFilters?.filters?.assignees || undefined, + mentions: userFilters?.filters?.mentions || undefined, + created_by: userFilters?.filters?.created_by || undefined, + labels: userFilters?.filters?.labels || undefined, + start_date: userFilters?.filters?.start_date || undefined, + target_date: userFilters?.filters?.target_date || undefined, + type: userFilters?.displayFilters?.type || undefined, + sub_issue: userFilters?.displayFilters?.sub_issue || true, + show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, + start_target_date: userFilters?.displayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (userFilters?.displayFilters?.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } + + fetchModuleFilters = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + const moduleFilters = await this.moduleService.getModuleDetails(workspaceSlug, projectId, moduleId); + + const filters: IIssueFilterOptions = { + assignees: moduleFilters?.view_props?.filters?.assignees || null, + mentions: moduleFilters?.view_props?.filters?.mentions || null, + created_by: moduleFilters?.view_props?.filters?.created_by || null, + labels: moduleFilters?.view_props?.filters?.labels || null, + priority: moduleFilters?.view_props?.filters?.priority || null, + project: moduleFilters?.view_props?.filters?.project || null, + start_date: moduleFilters?.view_props?.filters?.start_date || null, + state: moduleFilters?.view_props?.filters?.state || null, + state_group: moduleFilters?.view_props?.filters?.state_group || null, + subscriber: moduleFilters?.view_props?.filters?.subscriber || null, + target_date: moduleFilters?.view_props?.filters?.target_date || null, + }; + + const issueFilters: IModuleIssuesFilterOptions = { + filters: filters, + }; + + let _filters = { ...this.filters }; + if (!_filters) _filters = {}; + if (!_filters[moduleId]) _filters[moduleId] = { filters: {} }; + _filters[moduleId] = issueFilters; + + runInAction(() => { + this.filters = _filters; + }); + + return filters; + } catch (error) { + this.fetchFilters(workspaceSlug, projectId, moduleId); + throw error; + } + }; + + updateModuleFilters = async ( + workspaceSlug: string, + projectId: string, + moduleId: string, + type: EFilterType, + filters: IIssueFilterOptions + ) => { + if (!moduleId) return; + try { + let _moduleIssueFilters = { ...this.filters }; + if (!_moduleIssueFilters) _moduleIssueFilters = {}; + if (!_moduleIssueFilters[moduleId]) _moduleIssueFilters[moduleId] = { filters: {} }; + + const _filters = { filters: { ..._moduleIssueFilters[moduleId].filters } }; + + if (type === EFilterType.FILTERS) _filters.filters = { ..._filters.filters, ...filters }; + + _moduleIssueFilters[moduleId] = { filters: _filters.filters }; + + runInAction(() => { + this.filters = _moduleIssueFilters; + }); + + await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, { + view_props: { filters: _filters.filters }, + }); + + return _filters; + } catch (error) { + this.fetchFilters(workspaceSlug, projectId, moduleId); + throw error; + } + }; + + fetchFilters = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + await this.rootStore.issuesFilter.fetchDisplayFilters(workspaceSlug, projectId); + await this.rootStore.issuesFilter.fetchDisplayProperties(workspaceSlug, projectId); + await this.fetchModuleFilters(workspaceSlug, projectId, moduleId); + return; + } catch (error) { + this.fetchFilters(workspaceSlug, projectId, moduleId); + throw error; + } + }; + + updateFilters = async ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties, + moduleId?: string | undefined + ) => { + try { + if (!moduleId) throw new Error(); + + switch (filterType) { + case EFilterType.FILTERS: + await this.updateModuleFilters( + workspaceSlug, + projectId, + moduleId, + filterType, + filters as IIssueFilterOptions + ); + break; + case EFilterType.DISPLAY_FILTERS: + await this.rootStore.issuesFilter.updateDisplayFilters( + workspaceSlug, + projectId, + filterType, + filters as IIssueDisplayFilterOptions + ); + break; + case EFilterType.DISPLAY_PROPERTIES: + await this.rootStore.issuesFilter.updateDisplayProperties( + workspaceSlug, + projectId, + filters as IIssueDisplayProperties + ); + break; + } + + return; + } catch (error) { + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/module/issue.store.ts b/web/store/issues/project-issues/module/issue.store.ts new file mode 100644 index 000000000..966a41443 --- /dev/null +++ b/web/store/issues/project-issues/module/issue.store.ts @@ -0,0 +1,322 @@ +import { action, observable, makeObservable, computed, runInAction, autorun } from "mobx"; +// base class +import { IssueBaseStore } from "store/issues"; +// services +import { IssueService } from "services/issue"; +import { ModuleService } from "services/module.service"; +// types +import { TIssueGroupByOptions } from "types"; +import { IIssue } from "types/issues"; +import { IIssueResponse, TLoader, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "../../types"; +import { RootStore } from "store/root"; + +export interface IModuleIssuesStore { + // observable + loader: TLoader; + issues: { [module_id: string]: IIssueResponse } | undefined; + // computed + getIssues: IIssueResponse | undefined; + getIssuesIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined; + // actions + fetchIssues: ( + workspaceSlug: string, + projectId: string, + loadType: TLoader, + moduleId?: string | undefined + ) => Promise; + createIssue: ( + workspaceSlug: string, + projectId: string, + data: Partial, + moduleId?: string | undefined + ) => Promise; + updateIssue: ( + workspaceSlug: string, + projectId: string, + issueId: string, + data: Partial, + moduleId?: string | undefined + ) => Promise; + removeIssue: ( + workspaceSlug: string, + projectId: string, + issueId: string, + moduleId?: string | undefined + ) => Promise; + quickAddIssue: ( + workspaceSlug: string, + projectId: string, + data: IIssue, + moduleId?: string | undefined + ) => Promise; + removeIssueFromModule: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + issueId: string, + issueBridgeId: string + ) => Promise; +} + +export class ModuleIssuesStore extends IssueBaseStore implements IModuleIssuesStore { + loader: TLoader = "init-loader"; + issues: { [module_id: string]: IIssueResponse } | undefined = undefined; + // root store + rootStore; + // service + moduleService; + issueService; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // observable + loader: observable.ref, + issues: observable.ref, + // computed + getIssues: computed, + getIssuesIds: computed, + // action + fetchIssues: action, + createIssue: action, + updateIssue: action, + removeIssue: action, + quickAddIssue: action, + removeIssueFromModule: action, + }); + + this.rootStore = _rootStore; + this.issueService = new IssueService(); + this.moduleService = new ModuleService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + const moduleId = this.rootStore.module.moduleId; + if (!workspaceSlug || !projectId || !moduleId) return; + + const userFilters = this.rootStore?.moduleIssuesFilter?.issueFilters?.filters; + if (userFilters) this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); + }); + } + + get getIssues() { + const moduleId = this.rootStore?.module?.moduleId; + if (!moduleId || !this.issues || !this.issues[moduleId]) return undefined; + + return this.issues[moduleId]; + } + + get getIssuesIds() { + const moduleId = this.rootStore?.module?.moduleId; + const displayFilters = this.rootStore?.moduleIssuesFilter?.issueFilters?.displayFilters; + + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; + const orderBy = displayFilters?.order_by; + const layout = displayFilters?.layout; + + if (!moduleId || !this.issues || !this.issues[moduleId]) return undefined; + + let issues: IIssueResponse | IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined = undefined; + + if (layout === "list" && orderBy) { + if (groupBy) issues = this.groupedIssues(groupBy, orderBy, this.issues[moduleId]); + else issues = this.unGroupedIssues(orderBy, this.issues[moduleId]); + } else if (layout === "kanban" && groupBy && orderBy) { + if (subGroupBy) issues = this.subGroupedIssues(subGroupBy, groupBy, orderBy, this.issues[moduleId]); + else issues = this.groupedIssues(groupBy, orderBy, this.issues[moduleId]); + } else if (layout === "calendar") + issues = this.groupedIssues("target_date" as TIssueGroupByOptions, "target_date", this.issues[moduleId], true); + else if (layout === "spreadsheet") issues = this.unGroupedIssues(orderBy ?? "-created_at", this.issues[moduleId]); + else if (layout === "gantt_chart") issues = this.unGroupedIssues(orderBy ?? "sort_order", this.issues[moduleId]); + + return issues; + } + + fetchIssues = async ( + workspaceSlug: string, + projectId: string, + loadType: TLoader = "init-loader", + moduleId: string | undefined = undefined + ) => { + if (!moduleId) return undefined; + + try { + this.loader = loadType; + + const params = this.rootStore?.moduleIssuesFilter?.appliedFilters; + const response = await this.moduleService.getV3ModuleIssues(workspaceSlug, projectId, moduleId, params); + + const _issues = { ...this.issues, [moduleId]: { ...response } }; + + runInAction(() => { + this.issues = _issues; + this.loader = undefined; + }); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); + this.loader = undefined; + throw error; + } + }; + + createIssue = async ( + workspaceSlug: string, + projectId: string, + data: Partial, + moduleId: string | undefined = undefined + ) => { + if (!moduleId) return undefined; + + try { + const response = await this.rootStore.projectIssues.createIssue(workspaceSlug, projectId, data); + const issueToModule = await this.moduleService.addIssuesToModule(workspaceSlug, projectId, moduleId, { + issues: [response.id], + }); + + let _issues = this.issues; + if (!_issues) _issues = {}; + if (!_issues[moduleId]) _issues[moduleId] = {}; + _issues[moduleId] = { ..._issues[moduleId], ...{ [response.id]: response } }; + + runInAction(() => { + this.issues = _issues; + }); + + return issueToModule; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); + throw error; + } + }; + + updateIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + data: Partial, + moduleId: string | undefined = undefined + ) => { + if (!moduleId) return undefined; + + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[moduleId]) _issues[moduleId] = {}; + _issues[moduleId][issueId] = { ..._issues[moduleId][issueId], ...data }; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.rootStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); + throw error; + } + }; + + removeIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + moduleId: string | undefined = undefined + ) => { + if (!moduleId) return undefined; + + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[moduleId]) _issues[moduleId] = {}; + delete _issues?.[moduleId]?.[issueId]; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.rootStore.projectIssues.removeIssue(workspaceSlug, projectId, issueId); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); + throw error; + } + }; + + quickAddIssue = async ( + workspaceSlug: string, + projectId: string, + data: IIssue, + moduleId: string | undefined = undefined + ) => { + if (!moduleId) return undefined; + + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[moduleId]) _issues[moduleId] = {}; + _issues[moduleId] = { ..._issues[moduleId], ...{ [data.id as keyof IIssue]: data } }; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.createIssue(workspaceSlug, projectId, data, moduleId); + + if (this.issues) { + delete this.issues[moduleId][data.id as keyof IIssue]; + + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[moduleId]) _issues[moduleId] = {}; + _issues[moduleId] = { ..._issues[moduleId], ...{ [response.id as keyof IIssue]: response } }; + + runInAction(() => { + this.issues = _issues; + }); + } + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); + throw error; + } + }; + + removeIssueFromModule = async ( + workspaceSlug: string, + projectId: string, + moduleId: string, + issueId: string, + issueBridgeId: string + ) => { + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[moduleId]) _issues[moduleId] = {}; + delete _issues?.[moduleId]?.[issueId]; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.moduleService.removeIssueFromModule( + workspaceSlug, + projectId, + moduleId, + issueBridgeId + ); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation", moduleId); + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/project-view/filter.store.ts b/web/store/issues/project-issues/project-view/filter.store.ts new file mode 100644 index 000000000..5aa25f604 --- /dev/null +++ b/web/store/issues/project-issues/project-view/filter.store.ts @@ -0,0 +1,255 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// base class +import { IssueFilterBaseStore } from "store/issues"; +// services +import { ProjectService, ProjectMemberService } from "services/project"; +import { IssueService } from "services/issue"; +import { ViewService } from "services/view.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "store/root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; +import { EFilterType } from "store/issues/types"; + +interface IViewIssuesFilterOptions { + filters: IIssueFilterOptions; +} + +interface IProjectIssuesFilters { + filters: IIssueFilterOptions | undefined; + displayFilters: IIssueDisplayFilterOptions | undefined; + displayProperties: IIssueDisplayProperties | undefined; +} + +export interface IViewIssuesFilterStore { + // observable + loader: boolean; + filters: { [view_id: string]: IViewIssuesFilterOptions } | undefined; + // computed + issueFilters: IProjectIssuesFilters | undefined; + appliedFilters: TIssueParams[] | undefined; + // actions + fetchViewFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + updateViewFilters: ( + workspaceSlug: string, + projectId: string, + viewId: string, + type: EFilterType, + filters: IIssueFilterOptions + ) => Promise; + + fetchFilters: (workspaceSlug: string, projectId: string, viewId: string) => Promise; + updateFilters: ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties, + viewId?: string | undefined + ) => Promise; +} + +export class ViewIssuesFilterStore extends IssueFilterBaseStore implements IViewIssuesFilterStore { + // observables + loader: boolean = false; + filters: { [projectId: string]: IViewIssuesFilterOptions } | undefined = undefined; + // root store + rootStore; + // services + projectService; + projectMemberService; + issueService; + viewService; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // observables + loader: observable.ref, + filters: observable.ref, + // computed + issueFilters: computed, + appliedFilters: computed, + // actions + fetchViewFilters: action, + updateViewFilters: action, + fetchFilters: action, + updateFilters: action, + }); + + this.rootStore = _rootStore; + + this.projectService = new ProjectService(); + this.projectMemberService = new ProjectMemberService(); + this.issueService = new IssueService(); + this.viewService = new ViewService(); + } + + get issueFilters() { + const projectId = this.rootStore.project.projectId; + const viewId = this.rootStore.projectViews.viewId; + if (!projectId || !viewId) return undefined; + + const displayFilters = this.rootStore.issuesFilter.issueDisplayFilters(projectId); + const viewFilters = this.filters?.[viewId]; + + const _filters: IProjectIssuesFilters = { + filters: viewFilters?.filters, + displayFilters: displayFilters?.displayFilters, + displayProperties: displayFilters?.displayProperties, + }; + + return _filters; + } + + get appliedFilters() { + const userFilters = this.issueFilters; + if (!userFilters) return undefined; + + let filteredRouteParams: any = { + priority: userFilters?.filters?.priority || undefined, + state_group: userFilters?.filters?.state_group || undefined, + state: userFilters?.filters?.state || undefined, + assignees: userFilters?.filters?.assignees || undefined, + mentions: userFilters?.filters?.mentions || undefined, + created_by: userFilters?.filters?.created_by || undefined, + labels: userFilters?.filters?.labels || undefined, + start_date: userFilters?.filters?.start_date || undefined, + target_date: userFilters?.filters?.target_date || undefined, + type: userFilters?.displayFilters?.type || undefined, + sub_issue: userFilters?.displayFilters?.sub_issue || true, + show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, + start_target_date: userFilters?.displayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (userFilters?.displayFilters?.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } + + fetchViewFilters = async (workspaceSlug: string, projectId: string, viewId: string) => { + try { + const viewFilters = await this.viewService.getViewDetails(workspaceSlug, projectId, viewId); + + const filters: IIssueFilterOptions = { + assignees: viewFilters?.query_data?.assignees || null, + mentions: viewFilters?.query_data?.mentions || null, + created_by: viewFilters?.query_data?.created_by || null, + labels: viewFilters?.query_data?.labels || null, + priority: viewFilters?.query_data?.priority || null, + project: viewFilters?.query_data?.project || null, + start_date: viewFilters?.query_data?.start_date || null, + state: viewFilters?.query_data?.state || null, + state_group: viewFilters?.query_data?.state_group || null, + subscriber: viewFilters?.query_data?.subscriber || null, + target_date: viewFilters?.query_data?.target_date || null, + }; + + const issueFilters: IViewIssuesFilterOptions = { + filters: filters, + }; + + let _filters = { ...this.filters }; + if (!_filters) _filters = {}; + if (!_filters[viewId]) _filters[viewId] = { filters: {} }; + _filters[viewId] = issueFilters; + + runInAction(() => { + this.filters = _filters; + }); + + return filters; + } catch (error) { + this.fetchFilters(workspaceSlug, projectId, viewId); + throw error; + } + }; + + updateViewFilters = async ( + workspaceSlug: string, + projectId: string, + viewId: string, + type: EFilterType, + filters: IIssueFilterOptions + ) => { + if (!viewId) return; + try { + let _moduleIssueFilters = { ...this.filters }; + if (!_moduleIssueFilters) _moduleIssueFilters = {}; + if (!_moduleIssueFilters[viewId]) _moduleIssueFilters[viewId] = { filters: {} }; + + const _filters = { filters: { ..._moduleIssueFilters[viewId].filters } }; + + if (type === EFilterType.FILTERS) _filters.filters = { ..._filters.filters, ...filters }; + + _moduleIssueFilters[viewId] = { filters: _filters.filters }; + + runInAction(() => { + this.filters = _moduleIssueFilters; + }); + + await this.viewService.patchView(workspaceSlug, projectId, viewId, { + query_data: { ..._filters.filters }, + }); + + return _filters; + } catch (error) { + this.fetchFilters(workspaceSlug, projectId, viewId); + throw error; + } + }; + + fetchFilters = async (workspaceSlug: string, projectId: string, viewId: string) => { + try { + await this.rootStore.issuesFilter.fetchDisplayFilters(workspaceSlug, projectId); + await this.rootStore.issuesFilter.fetchDisplayProperties(workspaceSlug, projectId); + await this.fetchViewFilters(workspaceSlug, projectId, viewId); + return; + } catch (error) { + this.fetchFilters(workspaceSlug, projectId, viewId); + throw error; + } + }; + + updateFilters = async ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties, + viewId?: string | undefined + ) => { + try { + if (!viewId) throw new Error(); + + switch (filterType) { + case EFilterType.FILTERS: + await this.updateViewFilters(workspaceSlug, projectId, viewId, filterType, filters as IIssueFilterOptions); + break; + case EFilterType.DISPLAY_FILTERS: + await this.rootStore.issuesFilter.updateDisplayFilters( + workspaceSlug, + projectId, + filterType, + filters as IIssueDisplayFilterOptions + ); + break; + case EFilterType.DISPLAY_PROPERTIES: + await this.rootStore.issuesFilter.updateDisplayProperties( + workspaceSlug, + projectId, + filters as IIssueDisplayProperties + ); + break; + } + + return; + } catch (error) { + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/project-view/issue.store.ts b/web/store/issues/project-issues/project-view/issue.store.ts new file mode 100644 index 000000000..17d7be030 --- /dev/null +++ b/web/store/issues/project-issues/project-view/issue.store.ts @@ -0,0 +1,215 @@ +import { action, observable, makeObservable, computed, runInAction, autorun } from "mobx"; +// base class +import { IssueBaseStore } from "store/issues"; +// services +import { IssueService } from "services/issue/issue.service"; +// types +import { TIssueGroupByOptions } from "types"; +import { IIssue } from "types/issues"; +import { IIssueResponse, TLoader, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "../../types"; +import { RootStore } from "store/root"; + +export interface IViewIssuesStore { + // observable + loader: TLoader; + issues: { [view_id: string]: IIssueResponse } | undefined; + // computed + getIssues: IIssueResponse | undefined; + getIssuesIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined; + // actions + fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; + createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + quickAddIssue: (workspaceSlug: string, projectId: string, data: IIssue) => Promise; +} + +export class ViewIssuesStore extends IssueBaseStore implements IViewIssuesStore { + loader: TLoader = "init-loader"; + issues: { [view_id: string]: IIssueResponse } | undefined = undefined; + // root store + rootStore; + // service + issueService; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // observable + loader: observable.ref, + issues: observable.ref, + // computed + getIssues: computed, + getIssuesIds: computed, + // action + fetchIssues: action, + createIssue: action, + updateIssue: action, + removeIssue: action, + quickAddIssue: action, + }); + + this.rootStore = _rootStore; + this.issueService = new IssueService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + if (!workspaceSlug || !projectId) return; + + const userFilters = this.rootStore?.viewIssuesFilter?.issueFilters?.filters; + if (userFilters) this.fetchIssues(workspaceSlug, projectId, "mutation"); + }); + } + + get getIssues() { + const projectId = this.rootStore?.project.projectId; + if (!projectId || !this.issues || !this.issues[projectId]) return undefined; + + return this.issues[projectId]; + } + + get getIssuesIds() { + const projectId = this.rootStore?.project.projectId; + const displayFilters = this.rootStore?.viewIssuesFilter?.issueFilters?.displayFilters; + if (!displayFilters) return undefined; + + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; + const orderBy = displayFilters?.order_by; + const layout = displayFilters?.layout; + + if (!projectId || !this.issues || !this.issues[projectId]) return undefined; + + let issues: IIssueResponse | IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined = undefined; + + if (layout === "list" && orderBy) { + if (groupBy) issues = this.groupedIssues(groupBy, orderBy, this.issues[projectId]); + else issues = this.unGroupedIssues(orderBy, this.issues[projectId]); + } else if (layout === "kanban" && groupBy && orderBy) { + if (subGroupBy) issues = this.subGroupedIssues(subGroupBy, groupBy, orderBy, this.issues[projectId]); + else issues = this.groupedIssues(groupBy, orderBy, this.issues[projectId]); + } else if (layout === "calendar") + issues = this.groupedIssues("target_date" as TIssueGroupByOptions, "target_date", this.issues[projectId], true); + else if (layout === "spreadsheet") issues = this.unGroupedIssues(orderBy ?? "-created_at", this.issues[projectId]); + else if (layout === "gantt_chart") issues = this.unGroupedIssues(orderBy ?? "sort_order", this.issues[projectId]); + + return issues; + } + + fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader") => { + try { + this.loader = loadType; + + const params = this.rootStore?.viewIssuesFilter?.appliedFilters; + const response = await this.issueService.getV3Issues(workspaceSlug, projectId, params); + + const _issues = { ...this.issues, [projectId]: { ...response } }; + + runInAction(() => { + this.issues = _issues; + this.loader = undefined; + }); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId); + this.loader = undefined; + throw error; + } + }; + + createIssue = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const response = await this.issueService.createIssue(workspaceSlug, projectId, data); + + let _issues = this.issues; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + _issues[projectId] = { ..._issues[projectId], ...{ [response.id]: response } }; + + runInAction(() => { + this.issues = _issues; + }); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; + + updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + _issues[projectId][issueId] = { ..._issues[projectId][issueId], ...data }; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; + + removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + delete _issues?.[projectId]?.[issueId]; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; + + quickAddIssue = async (workspaceSlug: string, projectId: string, data: IIssue) => { + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + _issues[projectId] = { ..._issues[projectId], ...{ [data.id as keyof IIssue]: data } }; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.issueService.createIssue(workspaceSlug, projectId, data); + + if (this.issues) { + delete this.issues[projectId][data.id as keyof IIssue]; + + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + _issues[projectId] = { ..._issues[projectId], ...{ [response.id as keyof IIssue]: response } }; + + runInAction(() => { + this.issues = _issues; + }); + } + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/project/filter.store.ts b/web/store/issues/project-issues/project/filter.store.ts new file mode 100644 index 000000000..bb64b5784 --- /dev/null +++ b/web/store/issues/project-issues/project/filter.store.ts @@ -0,0 +1,140 @@ +import { computed, makeObservable } from "mobx"; +// base class +import { IssueFilterBaseStore } from "store/issues"; + +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "store/root"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions, TIssueParams } from "types"; +import { EFilterType } from "store/issues/types"; + +interface IProjectIssuesFilters { + filters: IIssueFilterOptions | undefined; + displayFilters: IIssueDisplayFilterOptions | undefined; + displayProperties: IIssueDisplayProperties | undefined; +} + +export interface IProjectIssuesFilterStore { + // computed + issueFilters: IProjectIssuesFilters | undefined; + appliedFilters: TIssueParams[] | undefined; + // action + fetchFilters: (workspaceSlug: string, projectId: string) => Promise; + updateFilters: ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties + ) => Promise; +} + +export class ProjectIssuesFilterStore extends IssueFilterBaseStore implements IProjectIssuesFilterStore { + // root store + rootStore; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // computed + issueFilters: computed, + appliedFilters: computed, + }); + + // root store + this.rootStore = _rootStore; + } + + get issueFilters() { + const projectId = this.rootStore.project.projectId; + if (!projectId) return undefined; + const displayFilters = this.rootStore.issuesFilter.issueDisplayFilters(projectId); + + const _filters: IProjectIssuesFilters = { + filters: displayFilters?.filters, + displayFilters: displayFilters?.displayFilters, + displayProperties: displayFilters?.displayProperties, + }; + + return _filters; + } + + get appliedFilters() { + const userFilters = this.issueFilters; + if (!userFilters) return undefined; + + let filteredRouteParams: any = { + priority: userFilters?.filters?.priority || undefined, + state_group: userFilters?.filters?.state_group || undefined, + state: userFilters?.filters?.state || undefined, + assignees: userFilters?.filters?.assignees || undefined, + mentions: userFilters?.filters?.mentions || undefined, + created_by: userFilters?.filters?.created_by || undefined, + labels: userFilters?.filters?.labels || undefined, + start_date: userFilters?.filters?.start_date || undefined, + target_date: userFilters?.filters?.target_date || undefined, + type: userFilters?.displayFilters?.type || undefined, + sub_issue: userFilters?.displayFilters?.sub_issue || true, + show_empty_groups: userFilters?.displayFilters?.show_empty_groups || true, + start_target_date: userFilters?.displayFilters?.start_target_date || true, + }; + + const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "issues"); + if (filteredParams) filteredRouteParams = this.computedFilter(filteredRouteParams, filteredParams); + + if (userFilters?.displayFilters?.layout === "calendar") filteredRouteParams.group_by = "target_date"; + if (userFilters?.displayFilters?.layout === "gantt_chart") filteredRouteParams.start_target_date = true; + + return filteredRouteParams; + } + + fetchFilters = async (workspaceSlug: string, projectId: string) => { + try { + await this.rootStore.issuesFilter.fetchDisplayFilters(workspaceSlug, projectId); + await this.rootStore.issuesFilter.fetchDisplayProperties(workspaceSlug, projectId); + return; + } catch (error) { + throw Error; + } + }; + + updateFilters = async ( + workspaceSlug: string, + projectId: string, + filterType: EFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties + ) => { + try { + switch (filterType) { + case EFilterType.FILTERS: + await this.rootStore.issuesFilter.updateDisplayFilters( + workspaceSlug, + projectId, + filterType, + filters as IIssueFilterOptions + ); + break; + case EFilterType.DISPLAY_FILTERS: + await this.rootStore.issuesFilter.updateDisplayFilters( + workspaceSlug, + projectId, + filterType, + filters as IIssueDisplayFilterOptions + ); + break; + case EFilterType.DISPLAY_PROPERTIES: + await this.rootStore.issuesFilter.updateDisplayProperties( + workspaceSlug, + projectId, + filters as IIssueDisplayProperties + ); + break; + } + + return; + } catch (error) { + throw error; + } + }; +} diff --git a/web/store/issues/project-issues/project/issue.store.ts b/web/store/issues/project-issues/project/issue.store.ts new file mode 100644 index 000000000..684df6ed3 --- /dev/null +++ b/web/store/issues/project-issues/project/issue.store.ts @@ -0,0 +1,215 @@ +import { action, observable, makeObservable, computed, runInAction, autorun } from "mobx"; +// base class +import { IssueBaseStore } from "store/issues"; +// services +import { IssueService } from "services/issue/issue.service"; +// types +import { TIssueGroupByOptions } from "types"; +import { IIssue } from "types/issues"; +import { IIssueResponse, TLoader, IGroupedIssues, ISubGroupedIssues, TUnGroupedIssues } from "../../types"; +import { RootStore } from "store/root"; + +export interface IProjectIssuesStore { + // observable + loader: TLoader; + issues: { [project_id: string]: IIssueResponse } | undefined; + // computed + getIssues: IIssueResponse | undefined; + getIssuesIds: IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined; + // actions + fetchIssues: (workspaceSlug: string, projectId: string, loadType: TLoader) => Promise; + createIssue: (workspaceSlug: string, projectId: string, data: Partial) => Promise; + updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => Promise; + removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise; + quickAddIssue: (workspaceSlug: string, projectId: string, data: IIssue) => Promise; +} + +export class ProjectIssuesStore extends IssueBaseStore implements IProjectIssuesStore { + loader: TLoader = "init-loader"; + issues: { [project_id: string]: IIssueResponse } | undefined = undefined; + // root store + rootStore; + // service + issueService; + + constructor(_rootStore: RootStore) { + super(_rootStore); + + makeObservable(this, { + // observable + loader: observable.ref, + issues: observable.ref, + // computed + getIssues: computed, + getIssuesIds: computed, + // action + fetchIssues: action, + createIssue: action, + updateIssue: action, + removeIssue: action, + quickAddIssue: action, + }); + + this.rootStore = _rootStore; + this.issueService = new IssueService(); + + autorun(() => { + const workspaceSlug = this.rootStore.workspace.workspaceSlug; + const projectId = this.rootStore.project.projectId; + if (!workspaceSlug || !projectId) return; + + const userFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.filters; + if (userFilters) this.fetchIssues(workspaceSlug, projectId, "mutation"); + }); + } + + get getIssues() { + const projectId = this.rootStore?.project.projectId; + if (!projectId || !this.issues || !this.issues[projectId]) return undefined; + + return this.issues[projectId]; + } + + get getIssuesIds() { + const projectId = this.rootStore?.project.projectId; + const displayFilters = this.rootStore?.projectIssuesFilter?.issueFilters?.displayFilters; + if (!displayFilters) return undefined; + + const subGroupBy = displayFilters?.sub_group_by; + const groupBy = displayFilters?.group_by; + const orderBy = displayFilters?.order_by; + const layout = displayFilters?.layout; + + if (!projectId || !this.issues || !this.issues[projectId]) return undefined; + + let issues: IIssueResponse | IGroupedIssues | ISubGroupedIssues | TUnGroupedIssues | undefined = undefined; + + if (layout === "list" && orderBy) { + if (groupBy) issues = this.groupedIssues(groupBy, orderBy, this.issues[projectId]); + else issues = this.unGroupedIssues(orderBy, this.issues[projectId]); + } else if (layout === "kanban" && groupBy && orderBy) { + if (subGroupBy) issues = this.subGroupedIssues(subGroupBy, groupBy, orderBy, this.issues[projectId]); + else issues = this.groupedIssues(groupBy, orderBy, this.issues[projectId]); + } else if (layout === "calendar") + issues = this.groupedIssues("target_date" as TIssueGroupByOptions, "target_date", this.issues[projectId], true); + else if (layout === "spreadsheet") issues = this.unGroupedIssues(orderBy ?? "-created_at", this.issues[projectId]); + else if (layout === "gantt_chart") issues = this.unGroupedIssues(orderBy ?? "sort_order", this.issues[projectId]); + + return issues; + } + + fetchIssues = async (workspaceSlug: string, projectId: string, loadType: TLoader = "init-loader") => { + try { + this.loader = loadType; + + const params = this.rootStore?.projectIssuesFilter?.appliedFilters; + const response = await this.issueService.getV3Issues(workspaceSlug, projectId, params); + + const _issues = { ...this.issues, [projectId]: { ...response } }; + + runInAction(() => { + this.issues = _issues; + this.loader = undefined; + }); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId); + this.loader = undefined; + throw error; + } + }; + + createIssue = async (workspaceSlug: string, projectId: string, data: Partial) => { + try { + const response = await this.issueService.createIssue(workspaceSlug, projectId, data); + + let _issues = this.issues; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + _issues[projectId] = { ..._issues[projectId], ...{ [response.id]: response } }; + + runInAction(() => { + this.issues = _issues; + }); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; + + updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + _issues[projectId][issueId] = { ..._issues[projectId][issueId], ...data }; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.issueService.patchIssue(workspaceSlug, projectId, issueId, data); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; + + removeIssue = async (workspaceSlug: string, projectId: string, issueId: string) => { + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + delete _issues?.[projectId]?.[issueId]; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.issueService.deleteIssue(workspaceSlug, projectId, issueId); + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; + + quickAddIssue = async (workspaceSlug: string, projectId: string, data: IIssue) => { + try { + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + _issues[projectId] = { ..._issues[projectId], ...{ [data.id as keyof IIssue]: data } }; + + runInAction(() => { + this.issues = _issues; + }); + + const response = await this.issueService.createIssue(workspaceSlug, projectId, data); + + if (this.issues) { + delete this.issues[projectId][data.id as keyof IIssue]; + + let _issues = { ...this.issues }; + if (!_issues) _issues = {}; + if (!_issues[projectId]) _issues[projectId] = {}; + _issues[projectId] = { ..._issues[projectId], ...{ [response.id as keyof IIssue]: response } }; + + runInAction(() => { + this.issues = _issues; + }); + } + + return response; + } catch (error) { + this.fetchIssues(workspaceSlug, projectId, "mutation"); + throw error; + } + }; +} diff --git a/web/store/issues/types.ts b/web/store/issues/types.ts new file mode 100644 index 000000000..b630c9410 --- /dev/null +++ b/web/store/issues/types.ts @@ -0,0 +1,27 @@ +import { IIssue } from "types"; + +// issue filters +export enum EFilterType { + FILTERS = "filters", + DISPLAY_FILTERS = "display_filters", + DISPLAY_PROPERTIES = "display_properties", +} + +// issues +export interface IGroupedIssues { + [group_id: string]: string[]; +} + +export interface ISubGroupedIssues { + [sub_grouped_id: string]: { + [group_id: string]: string[]; + }; +} + +export type TUnGroupedIssues = string[]; + +export interface IIssueResponse { + [issue_id: string]: IIssue; +} + +export type TLoader = "init-loader" | "mutation" | undefined; diff --git a/web/store/module-issues/index.ts b/web/store/module-issues/index.ts new file mode 100644 index 000000000..2b168d86c --- /dev/null +++ b/web/store/module-issues/index.ts @@ -0,0 +1 @@ +export * from "./issue_filters.store"; diff --git a/web/store/module-issues/issue_filters.store.ts b/web/store/module-issues/issue_filters.store.ts new file mode 100644 index 000000000..b7fd2b206 --- /dev/null +++ b/web/store/module-issues/issue_filters.store.ts @@ -0,0 +1,201 @@ +import { observable, action, computed, makeObservable, runInAction } from "mobx"; +// services +import { ModuleService } from "services/module.service"; +// helpers +import { handleIssueQueryParamsByLayout } from "helpers/issue.helper"; +// types +import { RootStore } from "../root"; +import { IIssueFilterOptions, TIssueParams } from "types"; + +export interface IModuleIssueFiltersStore { + loader: boolean; + error: any | null; + + // observables + userModuleFilters: { + [moduleId: string]: { + filters?: IIssueFilterOptions; + }; + }; + + // action + fetchModuleFilters: (workspaceSlug: string, projectId: string, moduleId: string) => Promise; + updateModuleFilters: ( + workspaceSlug: string, + projectId: string, + moduleId: string, + filterToUpdate: Partial + ) => Promise; + + // computed + appliedFilters: TIssueParams[] | undefined; + moduleFilters: + | { + filters: IIssueFilterOptions; + } + | undefined; +} + +export class ModuleIssueFiltersStore implements IModuleIssueFiltersStore { + // observables + loader: boolean = false; + error: any | null = null; + userModuleFilters: { + [moduleId: string]: { + filters?: IIssueFilterOptions; + }; + } = {}; + // root store + rootStore; + // services + moduleService; + + constructor(_rootStore: RootStore) { + makeObservable(this, { + // states + loader: observable.ref, + error: observable.ref, + // observables + userModuleFilters: observable.ref, + // actions + fetchModuleFilters: action, + updateModuleFilters: action, + // computed + appliedFilters: computed, + moduleFilters: computed, + }); + + this.rootStore = _rootStore; + 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[] | undefined { + const userDisplayFilters = this.rootStore?.projectIssuesFilter.issueFilters?.displayFilters; + + const moduleId = this.rootStore.module.moduleId; + + if (!moduleId) return undefined; + + const moduleFilters = this.userModuleFilters[moduleId]?.filters; + + if (!moduleFilters || !userDisplayFilters) return undefined; + + let filteredRouteParams: any = { + priority: moduleFilters?.priority || undefined, + state_group: moduleFilters?.state_group || undefined, + state: moduleFilters?.state || undefined, + assignees: moduleFilters?.assignees || undefined, + created_by: moduleFilters?.created_by || undefined, + labels: moduleFilters?.labels || undefined, + start_date: moduleFilters?.start_date || undefined, + target_date: moduleFilters?.target_date || 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; + } + + get moduleFilters(): + | { + filters: IIssueFilterOptions; + } + | undefined { + const moduleId = this.rootStore.module.moduleId; + + if (!moduleId) return undefined; + + const activeModuleFilters = this.userModuleFilters[moduleId]; + + if (!activeModuleFilters) return undefined; + + return { + filters: activeModuleFilters?.filters ?? {}, + }; + } + + fetchModuleFilters = async (workspaceSlug: string, projectId: string, moduleId: string) => { + try { + const moduleResponse = await this.moduleService.getModuleDetails(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.userModuleFilters = { + ...this.userModuleFilters, + [moduleId]: { + filters: moduleResponse?.view_props?.filters ?? {}, + }, + }; + }); + } catch (error) { + runInAction(() => { + this.error = error; + }); + + console.log("Failed to fetch user filters in issue filter store", error); + } + }; + + updateModuleFilters = async ( + workspaceSlug: string, + projectId: string, + moduleId: string, + properties: Partial + ) => { + const newViewProps = { + filters: { + ...this.userModuleFilters[moduleId]?.filters, + ...properties, + }, + }; + + let updatedModuleFilters = this.userModuleFilters; + if (!updatedModuleFilters) updatedModuleFilters = {}; + if (!updatedModuleFilters[moduleId]) updatedModuleFilters[moduleId] = {}; + + updatedModuleFilters[moduleId] = newViewProps; + + try { + runInAction(() => { + this.userModuleFilters = { ...updatedModuleFilters }; + }); + + const payload = { + view_props: { + filters: newViewProps.filters, + }, + }; + + const user = this.rootStore.user.currentUser ?? undefined; + + await this.moduleService.patchModule(workspaceSlug, projectId, moduleId, payload); + } catch (error) { + this.fetchModuleFilters(workspaceSlug, projectId, moduleId); + + runInAction(() => { + this.error = error; + }); + + console.log("Failed to update user filters in issue filter store", error); + } + }; +} diff --git a/web/store/project/project-label.store.ts b/web/store/project/project-label.store.ts index 2bc7b9794..47e029237 100644 --- a/web/store/project/project-label.store.ts +++ b/web/store/project/project-label.store.ts @@ -16,6 +16,7 @@ export interface IProjectLabelStore { // computed projectLabels: IIssueLabel[] | null; projectLabelsTree: IIssueLabelTree[] | null; + projectLabelIds: (isLayoutRender?: boolean) => string[]; // actions getProjectLabelById: (labelId: string) => IIssueLabel | null; fetchProjectLabels: (workspaceSlug: string, projectId: string) => Promise; @@ -95,6 +96,13 @@ export class ProjectLabelStore implements IProjectLabelStore { return labelInfo; }; + projectLabelIds = (isLayoutRender: boolean = false) => { + if (!this.projectLabels) return []; + let labelIds = (this.projectLabels ?? []).map((label) => label.id); + labelIds = isLayoutRender ? [...labelIds, "None"] : labelIds; + return labelIds; + }; + fetchProjectLabels = async (workspaceSlug: string, projectId: string) => { try { this.loader = true; diff --git a/web/store/project/project-members.store.ts b/web/store/project/project-members.store.ts index cacc2c1ea..3462a15db 100644 --- a/web/store/project/project-members.store.ts +++ b/web/store/project/project-members.store.ts @@ -15,6 +15,7 @@ export interface IProjectMemberStore { }; // computed projectMembers: IProjectMember[] | null; + projectMemberIds: (isLayoutRender?: boolean) => string[]; // actions getProjectMemberById: (memberId: string) => IProjectMember | null; getProjectMemberByUserId: (memberId: string) => IProjectMember | null; @@ -95,6 +96,13 @@ export class ProjectMemberStore implements IProjectMemberStore { return memberInfo; }; + projectMemberIds = (isLayoutRender: boolean = false) => { + if (!this.projectMembers) return []; + let memberIds = (this.projectMembers ?? []).map((member) => member.member.id); + memberIds = isLayoutRender ? [...memberIds, "None"] : memberIds; + return memberIds; + }; + /** * fetch the project members info using workspace id and project id * @param workspaceSlug diff --git a/web/store/project/project-state.store.ts b/web/store/project/project-state.store.ts index 2c4247b75..2ac6be8ff 100644 --- a/web/store/project/project-state.store.ts +++ b/web/store/project/project-state.store.ts @@ -16,6 +16,8 @@ export interface IProjectStateStore { groupedProjectStates: { [groupId: string]: IState[] } | null; projectStates: IState[] | null; + projectStateIds: () => string[]; + fetchProjectStates: (workspaceSlug: string, projectId: string) => Promise; createState: (workspaceSlug: string, projectId: string, data: Partial) => Promise; updateState: (workspaceSlug: string, projectId: string, stateId: string, data: Partial) => Promise; @@ -78,6 +80,11 @@ export class ProjectStateStore implements IProjectStateStore { return states; } + projectStateIds = () => { + if (!this.projectStates) return []; + return (this.projectStates ?? []).map((state) => state.id); + }; + fetchProjectStates = async (workspaceSlug: string, projectId: string) => { try { const states = await this.stateService.getStates(workspaceSlug, projectId); diff --git a/web/store/project/project.store.ts b/web/store/project/project.store.ts index bfe82e1ce..3d228d856 100644 --- a/web/store/project/project.store.ts +++ b/web/store/project/project.store.ts @@ -24,6 +24,8 @@ export interface IProjectStore { favoriteProjects: IProject[]; currentProjectDetails: IProject | undefined; + workspaceProjectIds: () => string[]; + // actions setProjectId: (projectId: string | null) => void; setSearchQuery: (query: string) => void; @@ -152,6 +154,11 @@ export class ProjectStore implements IProjectStore { this.searchQuery = query; }; + workspaceProjectIds = () => { + if (!this.workspaceProjects) return []; + return (this.workspaceProjects ?? []).map((workspace) => workspace.id); + }; + /** * get Workspace projects using workspace slug * @param workspaceSlug diff --git a/web/store/root.ts b/web/store/root.ts index bcfe04453..fd2aaf2aa 100644 --- a/web/store/root.ts +++ b/web/store/root.ts @@ -112,6 +112,51 @@ import { } from "store/inbox"; import { IWebhookStore, WebhookStore } from "./webhook.store"; +import { + // global + IIssuesFilterStore, + IssuesFilterStore, + // project issues + IProjectIssuesStore, + ProjectIssuesStore, + // project issues filter + IProjectIssuesFilterStore, + ProjectIssuesFilterStore, + // module issues + IModuleIssuesStore, + ModuleIssuesStore, + // module issues filter + IModuleIssuesFilterStore, + ModuleIssuesFilterStore, + // cycle issues + ICycleIssuesStore, + CycleIssuesStore, + // cycle issues filter + ICycleIssuesFilterStore, + CycleIssuesFilterStore, + // project view issues + IViewIssuesStore, + ViewIssuesStore, + // project view issues filter + IViewIssuesFilterStore, + ViewIssuesFilterStore, + // archived issues + IProjectArchivedIssuesStore, + ProjectArchivedIssuesStore, + // archived issues filter + IProjectArchivedIssuesFilterStore, + ProjectArchivedIssuesFilterStore, + // draft issues + IProjectDraftIssuesStore, + ProjectDraftIssuesStore, + // draft issues filter + IProjectDraftIssuesFilterStore, + ProjectDraftIssuesFilterStore, +} from "store/issues"; + +import { CycleIssueFiltersStore, ICycleIssueFiltersStore } from "store/cycle-issues"; +import { ModuleIssueFiltersStore, IModuleIssueFiltersStore } from "store/module-issues"; + import { IMentionsStore, MentionsStore } from "store/editor"; // pages import { PageStore, IPageStore } from "store/page.store"; @@ -157,6 +202,7 @@ export class RootStore { projectViewIssueCalendarView: IProjectViewIssueCalendarViewStore; issueFilter: IIssueFilterStore; + issueDetail: IIssueDetailStore; issueKanBanView: IIssueKanBanViewStore; issueCalendarView: IIssueCalendarViewStore; @@ -188,6 +234,31 @@ export class RootStore { mentionsStore: IMentionsStore; + // project v3 issue and issue-filters starts + issuesFilter: IIssuesFilterStore; + + projectIssues: IProjectIssuesStore; + projectIssuesFilter: IProjectIssuesFilterStore; + + moduleIssues: IModuleIssuesStore; + moduleIssuesFilter: IModuleIssuesFilterStore; + + cycleIssues: ICycleIssuesStore; + cycleIssuesFilter: ICycleIssuesFilterStore; + + viewIssues: IViewIssuesStore; + viewIssuesFilter: IViewIssuesFilterStore; + + projectArchivedIssues: IProjectArchivedIssuesStore; + projectArchivedIssuesFilter: IProjectArchivedIssuesFilterStore; + + projectDraftIssues: IProjectDraftIssuesStore; + projectDraftIssuesFilter: IProjectDraftIssuesFilterStore; + // project v3 issue and issue-filters ends + + cycleIssueFilters: ICycleIssueFiltersStore; + moduleIssueFilters: IModuleIssueFiltersStore; + page: IPageStore; constructor() { @@ -259,6 +330,32 @@ export class RootStore { this.mentionsStore = new MentionsStore(this); + // project v3 issue and issue-filters starts + this.issuesFilter = new IssuesFilterStore(this); + + this.projectIssues = new ProjectIssuesStore(this); + this.projectIssuesFilter = new ProjectIssuesFilterStore(this); + + this.moduleIssues = new ModuleIssuesStore(this); + this.moduleIssuesFilter = new ModuleIssuesFilterStore(this); + + this.cycleIssues = new CycleIssuesStore(this); + this.cycleIssuesFilter = new CycleIssuesFilterStore(this); + + this.viewIssues = new ViewIssuesStore(this); + this.viewIssuesFilter = new ViewIssuesFilterStore(this); + + this.projectArchivedIssues = new ProjectArchivedIssuesStore(this); + this.projectArchivedIssuesFilter = new ProjectArchivedIssuesFilterStore(this); + + this.projectDraftIssues = new ProjectDraftIssuesStore(this); + this.projectDraftIssuesFilter = new ProjectDraftIssuesFilterStore(this); + // project v3 issue and issue-filters ends + + this.cycleIssueFilters = new CycleIssueFiltersStore(this); + + this.moduleIssueFilters = new ModuleIssueFiltersStore(this); + this.page = new PageStore(this); } }