From 9326fb07622787379c0d3393964444c7ac82a2ab Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 29 Feb 2024 15:31:03 +0530 Subject: [PATCH] [WEB-601] feat: enhanced display filters grouping by cycles and modules in project issues (#3834) * feat: implemented cycle and module for display filters groupBy and sunGroupBy in project issues list and kanban layouts * chore: Enabled drag ability for cycle and handled prepopulated data for quick add * chore: disbaled drag ability for cycle * chore: updated preloaded data * chore: updated module and cycle store router dependancy to prop dependancy --- packages/types/src/issues.d.ts | 2 + packages/types/src/view-props.d.ts | 2 + web/components/cycles/cycle-mobile-header.tsx | 1 + web/components/headers/cycle-issues.tsx | 1 + web/components/headers/module-issues.tsx | 1 + .../display-filters-selection.tsx | 6 +- .../header/display-filters/group-by.tsx | 5 +- .../header/display-filters/sub-group-by.tsx | 5 +- .../issues/issue-layouts/kanban/default.tsx | 17 +- .../issue-layouts/kanban/kanban-group.tsx | 8 + .../issues/issue-layouts/kanban/swimlanes.tsx | 24 +- .../issues/issue-layouts/list/default.tsx | 15 +- web/components/issues/issue-layouts/utils.tsx | 85 +++++- .../issues/issues-mobile-header.tsx | 284 ++++++++--------- .../modules/module-mobile-header.tsx | 287 +++++++++--------- web/constants/issue.ts | 115 ++----- web/store/issue/helpers/issue-helper.store.ts | 6 + 17 files changed, 464 insertions(+), 400 deletions(-) diff --git a/packages/types/src/issues.d.ts b/packages/types/src/issues.d.ts index 754d8df8f..ebe537138 100644 --- a/packages/types/src/issues.d.ts +++ b/packages/types/src/issues.d.ts @@ -203,6 +203,8 @@ export interface ViewFlags { export type GroupByColumnTypes = | "project" + | "cycle" + | "module" | "state" | "state_detail.group" | "priority" diff --git a/packages/types/src/view-props.d.ts b/packages/types/src/view-props.d.ts index 8e11d9cea..c2c98def3 100644 --- a/packages/types/src/view-props.d.ts +++ b/packages/types/src/view-props.d.ts @@ -14,6 +14,8 @@ export type TIssueGroupByOptions = | "project" | "assignees" | "mentions" + | "cycle" + | "module" | null; export type TIssueOrderByOptions = diff --git a/web/components/cycles/cycle-mobile-header.tsx b/web/components/cycles/cycle-mobile-header.tsx index b893bf993..624334ec4 100644 --- a/web/components/cycles/cycle-mobile-header.tsx +++ b/web/components/cycles/cycle-mobile-header.tsx @@ -152,6 +152,7 @@ export const CycleMobileHeader = () => { handleDisplayFiltersUpdate={handleDisplayFilters} displayProperties={issueFilters?.displayProperties ?? {}} handleDisplayPropertiesUpdate={handleDisplayProperties} + ignoreGroupedFilters={["cycle"]} /> diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 0a36c133b..7cdc23133 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -244,6 +244,7 @@ export const CycleIssuesHeader: React.FC = observer(() => { handleDisplayFiltersUpdate={handleDisplayFilters} displayProperties={issueFilters?.displayProperties ?? {}} handleDisplayPropertiesUpdate={handleDisplayProperties} + ignoreGroupedFilters={["cycle"]} /> diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index f722b506f..b84504ee2 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -248,6 +248,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { handleDisplayFiltersUpdate={handleDisplayFilters} displayProperties={issueFilters?.displayProperties ?? {}} handleDisplayPropertiesUpdate={handleDisplayProperties} + ignoreGroupedFilters={["module"]} /> diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx index 3c94b4f3f..131bea46b 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/display-filters-selection.tsx @@ -11,7 +11,7 @@ import { FilterSubGroupBy, } from "components/issues"; // types -import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueGroupByOptions } from "@plane/types"; import { ILayoutDisplayFiltersOptions } from "constants/issue"; type Props = { @@ -20,6 +20,7 @@ type Props = { handleDisplayFiltersUpdate: (updatedDisplayFilter: Partial) => void; handleDisplayPropertiesUpdate: (updatedDisplayProperties: Partial) => void; layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined; + ignoreGroupedFilters?: Partial[]; }; export const DisplayFiltersSelection: React.FC = observer((props) => { @@ -29,6 +30,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { handleDisplayFiltersUpdate, handleDisplayPropertiesUpdate, layoutDisplayFiltersOptions, + ignoreGroupedFilters = [], } = props; const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) => @@ -54,6 +56,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { group_by: val, }) } + ignoreGroupedFilters={ignoreGroupedFilters} /> )} @@ -71,6 +74,7 @@ export const DisplayFiltersSelection: React.FC = observer((props) => { }) } subGroupByOptions={layoutDisplayFiltersOptions?.display_filters.sub_group_by ?? []} + ignoreGroupedFilters={ignoreGroupedFilters} /> )} diff --git a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx index 659d86d08..a4478e834 100644 --- a/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx +++ b/web/components/issues/issue-layouts/filters/header/display-filters/group-by.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; - // components import { FilterHeader, FilterOption } from "components/issues"; // types @@ -12,10 +11,11 @@ type Props = { displayFilters: IIssueDisplayFilterOptions; groupByOptions: TIssueGroupByOptions[]; handleUpdate: (val: TIssueGroupByOptions) => void; + ignoreGroupedFilters: Partial[]; }; export const FilterGroupBy: React.FC = observer((props) => { - const { displayFilters, groupByOptions, handleUpdate } = props; + const { displayFilters, groupByOptions, handleUpdate, ignoreGroupedFilters } = props; const [previewEnabled, setPreviewEnabled] = useState(true); @@ -34,6 +34,7 @@ export const FilterGroupBy: React.FC = observer((props) => { {ISSUE_GROUP_BY_OPTIONS.filter((option) => groupByOptions.includes(option.key)).map((groupBy) => { if (displayFilters.layout === "kanban" && selectedSubGroupBy !== null && groupBy.key === selectedSubGroupBy) return null; + if (ignoreGroupedFilters.includes(groupBy?.key)) return null; return ( void; subGroupByOptions: TIssueGroupByOptions[]; + ignoreGroupedFilters: Partial[]; }; export const FilterSubGroupBy: React.FC = observer((props) => { - const { displayFilters, handleUpdate, subGroupByOptions } = props; + const { displayFilters, handleUpdate, subGroupByOptions, ignoreGroupedFilters } = props; const [previewEnabled, setPreviewEnabled] = useState(true); @@ -33,6 +33,7 @@ export const FilterSubGroupBy: React.FC = observer((props) => {
{ISSUE_GROUP_BY_OPTIONS.filter((option) => subGroupByOptions.includes(option.key)).map((subGroupBy) => { if (selectedGroupBy !== null && subGroupBy.key === selectedGroupBy) return null; + if (ignoreGroupedFilters.includes(subGroupBy?.key)) return null; return ( = observer((props) => { const member = useMember(); const project = useProject(); const label = useLabel(); + const cycle = useCycle(); + const _module = useModule(); const projectState = useProjectState(); const { peekIssue } = useIssueDetail(); - const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member); + const list = getGroupByColumns(group_by as GroupByColumnTypes, project, cycle, _module, label, projectState, member); if (!list) return null; - const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)[_list.id]?.length > 0); + const groupWithIssues = list.filter((_list) => (issueIds as TGroupedIssues)?.[_list.id]?.length > 0); const groupList = showEmptyGroup ? list : groupWithIssues; diff --git a/web/components/issues/issue-layouts/kanban/kanban-group.tsx b/web/components/issues/issue-layouts/kanban/kanban-group.tsx index 7cbda05e1..a2f9ec4fb 100644 --- a/web/components/issues/issue-layouts/kanban/kanban-group.tsx +++ b/web/components/issues/issue-layouts/kanban/kanban-group.tsx @@ -80,6 +80,10 @@ export const KanbanGroup = (props: IKanbanGroup) => { preloadedData = { ...preloadedData, state_id: groupValue }; } else if (groupByKey === "priority") { preloadedData = { ...preloadedData, priority: groupValue }; + } else if (groupByKey === "cycle") { + preloadedData = { ...preloadedData, cycle_id: groupValue }; + } else if (groupByKey === "module") { + preloadedData = { ...preloadedData, module_ids: [groupValue] }; } else if (groupByKey === "labels" && groupValue != "None") { preloadedData = { ...preloadedData, label_ids: [groupValue] }; } else if (groupByKey === "assignees" && groupValue != "None") { @@ -96,6 +100,10 @@ export const KanbanGroup = (props: IKanbanGroup) => { preloadedData = { ...preloadedData, state_id: subGroupValue }; } else if (subGroupByKey === "priority") { preloadedData = { ...preloadedData, priority: subGroupValue }; + } else if (groupByKey === "cycle") { + preloadedData = { ...preloadedData, cycle_id: subGroupValue }; + } else if (groupByKey === "module") { + preloadedData = { ...preloadedData, module_ids: [subGroupValue] }; } else if (subGroupByKey === "labels" && subGroupValue != "None") { preloadedData = { ...preloadedData, label_ids: [subGroupValue] }; } else if (subGroupByKey === "assignees" && subGroupValue != "None") { diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 5fdb58ef0..7b57752f2 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -18,7 +18,7 @@ import { } from "@plane/types"; // constants import { EIssueActions } from "../types"; -import { useLabel, useMember, useProject, useProjectState } from "hooks/store"; +import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store"; import { getGroupByColumns } from "../utils"; import { TCreateModalStoreTypes } from "constants/issue"; @@ -217,10 +217,28 @@ export const KanBanSwimLanes: React.FC = observer((props) => { const member = useMember(); const project = useProject(); const label = useLabel(); + const cycle = useCycle(); + const _module = useModule(); const projectState = useProjectState(); - const groupByList = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member); - const subGroupByList = getGroupByColumns(sub_group_by as GroupByColumnTypes, project, label, projectState, member); + const groupByList = getGroupByColumns( + group_by as GroupByColumnTypes, + project, + cycle, + _module, + label, + projectState, + member + ); + const subGroupByList = getGroupByColumns( + sub_group_by as GroupByColumnTypes, + project, + cycle, + _module, + label, + projectState, + member + ); if (!groupByList || !subGroupByList) return null; diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index e148d5131..c6f82c2be 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -3,7 +3,7 @@ import { useRef } from "react"; import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues"; import { HeaderGroupByCard } from "./headers/group-by-card"; // hooks -import { useLabel, useMember, useProject, useProjectState } from "hooks/store"; +import { useCycle, useLabel, useMember, useModule, useProject, useProjectState } from "hooks/store"; // types import { GroupByColumnTypes, @@ -65,10 +65,21 @@ const GroupByList: React.FC = (props) => { const project = useProject(); const label = useLabel(); const projectState = useProjectState(); + const cycle = useCycle(); + const _module = useModule(); const containerRef = useRef(null); - const groups = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true); + const groups = getGroupByColumns( + group_by as GroupByColumnTypes, + project, + cycle, + _module, + label, + projectState, + member, + true + ); if (!groups) return null; diff --git a/web/components/issues/issue-layouts/utils.tsx b/web/components/issues/issue-layouts/utils.tsx index 0c3367dc1..ce49d774d 100644 --- a/web/components/issues/issue-layouts/utils.tsx +++ b/web/components/issues/issue-layouts/utils.tsx @@ -1,16 +1,25 @@ -import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui"; -import { EIssueListRow, ISSUE_PRIORITIES } from "constants/issue"; -import { renderEmoji } from "helpers/emoji.helper"; +import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui"; +// stores import { IMemberRootStore } from "store/member"; import { IProjectStore } from "store/project/project.store"; import { IStateStore } from "store/state.store"; -import { GroupByColumnTypes, IGroupByColumn, IIssueListRow, TGroupedIssues, TUnGroupedIssues } from "@plane/types"; -import { STATE_GROUPS } from "constants/state"; import { ILabelStore } from "store/label.store"; +import { ICycleStore } from "store/cycle.store"; +import { IModuleStore } from "store/module.store"; +// helpers +import { renderEmoji } from "helpers/emoji.helper"; +// constants +import { STATE_GROUPS } from "constants/state"; +import { ISSUE_PRIORITIES } from "constants/issue"; +// types +import { GroupByColumnTypes, IGroupByColumn, TCycleGroups } from "@plane/types"; +import { ContrastIcon } from "lucide-react"; export const getGroupByColumns = ( groupBy: GroupByColumnTypes | null, project: IProjectStore, + cycle: ICycleStore, + module: IModuleStore, label: ILabelStore, projectState: IStateStore, member: IMemberRootStore, @@ -19,6 +28,10 @@ export const getGroupByColumns = ( switch (groupBy) { case "project": return getProjectColumns(project); + case "cycle": + return getCycleColumns(project, cycle); + case "module": + return getModuleColumns(project, module); case "state": return getStateColumns(projectState); case "state_detail.group": @@ -55,6 +68,68 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined }) as any; }; +const getCycleColumns = (projectStore: IProjectStore, cycleStore: ICycleStore): IGroupByColumn[] | undefined => { + const { currentProjectDetails } = projectStore; + const { getProjectCycleIds, getCycleById } = cycleStore; + + if (!currentProjectDetails || !currentProjectDetails?.id) return; + + const cycleIds = currentProjectDetails?.id ? getProjectCycleIds(currentProjectDetails?.id) : undefined; + if (!cycleIds) return; + + const cycles = []; + + cycleIds.map((cycleId) => { + const cycle = getCycleById(cycleId); + if (cycle) { + const cycleStatus = cycle.status ? (cycle.status.toLocaleLowerCase() as TCycleGroups) : "draft"; + cycles.push({ + id: cycle.id, + name: cycle.name, + icon: , + payload: { cycle_id: cycle.id }, + }); + } + }); + cycles.push({ + id: "None", + name: "None", + icon: , + }); + + return cycles as any; +}; + +const getModuleColumns = (projectStore: IProjectStore, moduleStore: IModuleStore): IGroupByColumn[] | undefined => { + const { currentProjectDetails } = projectStore; + const { getProjectModuleIds, getModuleById } = moduleStore; + + if (!currentProjectDetails || !currentProjectDetails?.id) return; + + const moduleIds = currentProjectDetails?.id ? getProjectModuleIds(currentProjectDetails?.id) : undefined; + if (!moduleIds) return; + + const modules = []; + + moduleIds.map((moduleId) => { + const _module = getModuleById(moduleId); + if (_module) + modules.push({ + id: _module.id, + name: _module.name, + icon: , + payload: { module_ids: [_module.id] }, + }); + }) as any; + modules.push({ + id: "None", + name: "None", + icon: , + }); + + return modules as any; +}; + const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => { const { projectStates } = projectState; if (!projectStates) return; diff --git a/web/components/issues/issues-mobile-header.tsx b/web/components/issues/issues-mobile-header.tsx index 2338e1848..56b88f70c 100644 --- a/web/components/issues/issues-mobile-header.tsx +++ b/web/components/issues/issues-mobile-header.tsx @@ -14,153 +14,153 @@ import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOption import { ProjectAnalyticsModal } from "components/analytics"; export const IssuesMobileHeader = () => { - const layouts = [ - { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Kanban", icon: Kanban }, - { key: "calendar", title: "Calendar", icon: Calendar }, - ]; - const [analyticsModal, setAnalyticsModal] = useState(false); - const { workspaceSlug, projectId } = router.query as { - workspaceSlug: string; - projectId: string; - }; - const { currentProjectDetails } = useProject(); - const { projectStates } = useProjectState(); - const { projectLabels } = useLabel(); + const layouts = [ + { key: "list", title: "List", icon: List }, + { key: "kanban", title: "Kanban", icon: Kanban }, + { key: "calendar", title: "Calendar", icon: Calendar }, + ]; + const [analyticsModal, setAnalyticsModal] = useState(false); + const { workspaceSlug, projectId } = router.query as { + workspaceSlug: string; + projectId: string; + }; + const { currentProjectDetails } = useProject(); + const { projectStates } = useProjectState(); + const { projectLabels } = useLabel(); - // store hooks - const { - issuesFilter: { issueFilters, updateFilters }, - } = useIssues(EIssuesStoreType.PROJECT); - const { - project: { projectMemberIds }, - } = useMember(); - const activeLayout = issueFilters?.displayFilters?.layout; + // store hooks + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.PROJECT); + const { + project: { projectMemberIds }, + } = useMember(); + const activeLayout = issueFilters?.displayFilters?.layout; - const handleLayoutChange = useCallback( - (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); - }, - [workspaceSlug, projectId, updateFilters] - ); + const handleLayoutChange = useCallback( + (layout: TIssueLayouts) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }); + }, + [workspaceSlug, projectId, updateFilters] + ); - const handleFiltersUpdate = useCallback( - (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + 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 (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); + if (Array.isArray(value)) { + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + }); + } else { + if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }); + }, + [workspaceSlug, projectId, issueFilters, updateFilters] + ); + + const handleDisplayFilters = useCallback( + (updatedDisplayFilter: Partial) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter); + }, + [workspaceSlug, projectId, updateFilters] + ); + + const handleDisplayProperties = useCallback( + (property: Partial) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property); + }, + [workspaceSlug, projectId, updateFilters] + ); + + return ( + <> + setAnalyticsModal(false)} + projectDetails={currentProjectDetails ?? undefined} + /> +
+ Layout} + customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" + closeOnSelect + > + {layouts.map((layout, index) => ( + { + handleLayoutChange(ISSUE_LAYOUTS[index].key); + }} + className="flex items-center gap-2" + > + +
{layout.title}
+
+ ))} +
+
+ + Filters + + } - - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }); - }, - [workspaceSlug, projectId, issueFilters, updateFilters] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter); - }, - [workspaceSlug, projectId, updateFilters] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property); - }, - [workspaceSlug, projectId, updateFilters] - ); - - return ( - <> - setAnalyticsModal(false)} - projectDetails={currentProjectDetails ?? undefined} + > + -
- Layout} - customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" - closeOnSelect - > - {layouts.map((layout, index) => ( - { - handleLayoutChange(ISSUE_LAYOUTS[index].key); - }} - className="flex items-center gap-2" - > - -
{layout.title}
-
- ))} -
-
- - Filters - - - } - > - - -
-
- - Display - - - } - > - - -
+ +
+
+ + Display + + + } + > + + +
- -
- - ); + +
+ + ); }; diff --git a/web/components/modules/module-mobile-header.tsx b/web/components/modules/module-mobile-header.tsx index e9ed56a8d..e3f504479 100644 --- a/web/components/modules/module-mobile-header.tsx +++ b/web/components/modules/module-mobile-header.tsx @@ -9,154 +9,155 @@ import router from "next/router"; import { useCallback, useState } from "react"; export const ModuleMobileHeader = () => { - const [analyticsModal, setAnalyticsModal] = useState(false); - const { getModuleById } = useModule(); - const layouts = [ - { key: "list", title: "List", icon: List }, - { key: "kanban", title: "Kanban", icon: Kanban }, - { key: "calendar", title: "Calendar", icon: Calendar }, - ]; - const { workspaceSlug, projectId, moduleId } = router.query as { - workspaceSlug: string; - projectId: string; - moduleId: string; - }; - const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; + const [analyticsModal, setAnalyticsModal] = useState(false); + const { getModuleById } = useModule(); + const layouts = [ + { key: "list", title: "List", icon: List }, + { key: "kanban", title: "Kanban", icon: Kanban }, + { key: "calendar", title: "Calendar", icon: Calendar }, + ]; + const { workspaceSlug, projectId, moduleId } = router.query as { + workspaceSlug: string; + projectId: string; + moduleId: string; + }; + const moduleDetails = moduleId ? getModuleById(moduleId.toString()) : undefined; - const { - issuesFilter: { issueFilters, updateFilters }, - } = useIssues(EIssuesStoreType.MODULE); - const activeLayout = issueFilters?.displayFilters?.layout; - const { projectStates } = useProjectState(); - const { projectLabels } = useLabel(); - const { - project: { projectMemberIds }, - } = useMember(); + const { + issuesFilter: { issueFilters, updateFilters }, + } = useIssues(EIssuesStoreType.MODULE); + const activeLayout = issueFilters?.displayFilters?.layout; + const { projectStates } = useProjectState(); + const { projectLabels } = useLabel(); + const { + project: { projectMemberIds }, + } = useMember(); - const handleLayoutChange = useCallback( - (layout: TIssueLayouts) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); + const handleLayoutChange = useCallback( + (layout: TIssueLayouts) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout }, moduleId); + }, + [workspaceSlug, projectId, moduleId, updateFilters] + ); - const handleFiltersUpdate = useCallback( - (key: keyof IIssueFilterOptions, value: string | string[]) => { - if (!workspaceSlug || !projectId) return; - const newValues = issueFilters?.filters?.[key] ?? []; + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + 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 (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); - else newValues.push(value); + if (Array.isArray(value)) { + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + }); + } else { + if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId); + }, + [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] + ); + + const handleDisplayFilters = useCallback( + (updatedDisplayFilter: Partial) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); + }, + [workspaceSlug, projectId, moduleId, updateFilters] + ); + + const handleDisplayProperties = useCallback( + (property: Partial) => { + if (!workspaceSlug || !projectId) return; + updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); + }, + [workspaceSlug, projectId, moduleId, updateFilters] + ); + + return ( +
+ setAnalyticsModal(false)} + moduleDetails={moduleDetails ?? undefined} + /> +
+ Layout} + customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" + closeOnSelect + > + {layouts.map((layout, index) => ( + { + handleLayoutChange(ISSUE_LAYOUTS[index].key); + }} + className="flex items-center gap-2" + > + +
{layout.title}
+
+ ))} +
+
+ + Filters + + } - - updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues }, moduleId); - }, - [workspaceSlug, projectId, moduleId, issueFilters, updateFilters] - ); - - const handleDisplayFilters = useCallback( - (updatedDisplayFilter: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); - - const handleDisplayProperties = useCallback( - (property: Partial) => { - if (!workspaceSlug || !projectId) return; - updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property, moduleId); - }, - [workspaceSlug, projectId, moduleId, updateFilters] - ); - - return ( -
- setAnalyticsModal(false)} - moduleDetails={moduleDetails ?? undefined} + > + -
- Layout} - customButtonClassName="flex flex-grow justify-center text-custom-text-200 text-sm" - closeOnSelect - > - {layouts.map((layout, index) => ( - { - handleLayoutChange(ISSUE_LAYOUTS[index].key); - }} - className="flex items-center gap-2" - > - -
{layout.title}
-
- ))} -
-
- - Filters - - - } - > - - -
-
- - Display - - - } - > - - -
- - -
+
- ); +
+ + Display + + + } + > + + +
+ + +
+
+ ); }; diff --git a/web/constants/issue.ts b/web/constants/issue.ts index f1bcc3a06..7e0fd7a06 100644 --- a/web/constants/issue.ts +++ b/web/constants/issue.ts @@ -49,22 +49,6 @@ export const ISSUE_PRIORITIES: { { key: "none", title: "None" }, ]; -export const ISSUE_START_DATE_OPTIONS = [ - { key: "last_week", title: "Last Week" }, - { key: "2_weeks_from_now", title: "2 weeks from now" }, - { key: "1_month_from_now", title: "1 month from now" }, - { key: "2_months_from_now", title: "2 months from now" }, - { key: "custom", title: "Custom" }, -]; - -export const ISSUE_DUE_DATE_OPTIONS = [ - { key: "last_week", title: "Last Week" }, - { key: "2_weeks_from_now", title: "2 weeks from now" }, - { key: "1_month_from_now", title: "1 month from now" }, - { key: "2_months_from_now", title: "2 months from now" }, - { key: "custom", title: "Custom" }, -]; - export const ISSUE_GROUP_BY_OPTIONS: { key: TIssueGroupByOptions; title: string; @@ -73,6 +57,8 @@ export const ISSUE_GROUP_BY_OPTIONS: { { key: "state_detail.group", title: "State Groups" }, { key: "priority", title: "Priority" }, { key: "project", title: "Project" }, // required this on my issues + { key: "cycle", title: "Cycle" }, // required this on my issues + { key: "module", title: "Module" }, // required this on my issues { key: "labels", title: "Labels" }, { key: "assignees", title: "Assignees" }, { key: "created_by", title: "Created By" }, @@ -140,81 +126,6 @@ export const ISSUE_LAYOUTS: { { key: "gantt_chart", title: "Gantt Chart Layout", icon: GanttChartSquare }, ]; -export const ISSUE_LIST_FILTERS = [ - { key: "mentions", title: "Mentions" }, - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_KANBAN_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_CALENDER_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, -]; - -export const ISSUE_SPREADSHEET_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_GANTT_FILTERS = [ - { key: "priority", title: "Priority" }, - { key: "state", title: "State" }, - { key: "assignees", title: "Assignees" }, - { key: "created_by", title: "Created By" }, - { key: "labels", title: "Labels" }, - { key: "start_date", title: "Start Date" }, - { key: "due_date", title: "Due Date" }, -]; - -export const ISSUE_LIST_DISPLAY_FILTERS = [ - { key: "group_by", title: "Group By" }, - { key: "order_by", title: "Order By" }, - { key: "issue_type", title: "Issue Type" }, - { key: "sub_issue", title: "Sub Issue" }, - { key: "show_empty_groups", title: "Show Empty Groups" }, -]; - -export const ISSUE_KANBAN_DISPLAY_FILTERS = [ - { key: "group_by", title: "Group By" }, - { key: "order_by", title: "Order By" }, - { key: "issue_type", title: "Issue Type" }, - { key: "sub_issue", title: "Sub Issue" }, - { key: "show_empty_groups", title: "Show Empty Groups" }, -]; - -export const ISSUE_CALENDER_DISPLAY_FILTERS = [{ key: "issue_type", title: "Issue Type" }]; - -export const ISSUE_SPREADSHEET_DISPLAY_FILTERS = [{ key: "issue_type", title: "Issue Type" }]; - -export const ISSUE_GANTT_DISPLAY_FILTERS = [ - { key: "order_by", title: "Order By" }, - { key: "issue_type", title: "Issue Type" }, - { key: "sub_issue", title: "Sub Issue" }, -]; - export interface ILayoutDisplayFiltersOptions { filters: (keyof IIssueFilterOptions)[]; display_properties: boolean; @@ -276,7 +187,17 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { ], display_properties: true, display_filters: { - group_by: ["state", "state_detail.group", "priority", "labels", "assignees", "created_by", null], + group_by: [ + "state", + "cycle", + "module", + "state_detail.group", + "priority", + "labels", + "assignees", + "created_by", + null, + ], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -291,7 +212,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date"], display_properties: true, display_filters: { - group_by: ["state_detail.group", "priority", "project", "labels", null], + group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels", null], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -304,7 +225,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date"], display_properties: true, display_filters: { - group_by: ["state_detail.group", "priority", "project", "labels"], + group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels"], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -374,7 +295,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { ], display_properties: true, display_filters: { - group_by: ["state", "priority", "labels", "assignees", "created_by", null], + group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"], type: [null, "active", "backlog"], }, @@ -398,8 +319,8 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: { ], display_properties: true, display_filters: { - group_by: ["state", "priority", "labels", "assignees", "created_by"], - sub_group_by: ["state", "priority", "labels", "assignees", "created_by", null], + group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by"], + sub_group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null], order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority", "target_date"], type: [null, "active", "backlog"], }, diff --git a/web/store/issue/helpers/issue-helper.store.ts b/web/store/issue/helpers/issue-helper.store.ts index 9d498e900..a267ac9c8 100644 --- a/web/store/issue/helpers/issue-helper.store.ts +++ b/web/store/issue/helpers/issue-helper.store.ts @@ -36,6 +36,8 @@ export type TIssueHelperStore = { const ISSUE_FILTER_DEFAULT_DATA: Record = { project: "project_id", + cycle: "cycle_id", + module: "module_ids", state: "state_id", "state_detail.group": "state_group" as keyof TIssue, // state_detail.group is only being used for state_group display, priority: "priority", @@ -157,6 +159,10 @@ export class IssueHelperStore implements TIssueHelperStore { return Object.keys(this.rootStore?.workSpaceMemberRolesMap || {}); case "project": return Object.keys(this.rootStore?.projectMap || {}); + case "cycle": + return Object.keys(this.rootStore?.cycleMap || {}); + case "module": + return Object.keys(this.rootStore?.moduleMap || {}); default: return []; }