From e67c553876e6977e374edd3fa1d61e7aec10d3f4 Mon Sep 17 00:00:00 2001 From: Vamsi Krishna <46787868+vamsikrishnamathala@users.noreply.github.com> Date: Fri, 9 May 2025 18:24:35 +0530 Subject: [PATCH] [WEB-3980]feat:epic sub work item filters (#3117) * feat: added filters for sub issues * feat: added list groups for sub issues * drat: epics groupby and filters * feat: sorting, ordering and filtering for sub work items * chore: reverted filter name change * chore: reverted filter change * feat: added sort and filter for initiatives * fix: build errors --- .../constants/src/issue/filter-extended.ts | 20 ++- packages/types/src/issues/issue.d.ts | 1 + .../empty-state/section-empty-state-root.tsx | 2 - .../sub-issues/display-filters.tsx | 1 - .../sub-issues/filters.tsx | 1 - .../sub-issues/title-actions.tsx | 4 +- .../issue/issue-details/sub_issues.store.ts | 1 - .../issue-details/sub_issues_filter.store.ts | 3 +- .../details/main/overview-section-root.tsx | 2 +- .../main/overview-section/issues-root.tsx | 7 +- .../overview-section/work-items-actions.tsx | 70 +++++++--- .../collapsible-section/epics/content.tsx | 57 +++++++-- .../epics/epic-list-item/epics-list-group.tsx | 67 ++++++++++ .../epics/title-actions.tsx | 55 ++++++-- .../sidebar/project-navigation-root.tsx | 2 + .../initiative-epics-filter.store.ts | 121 +++++++++--------- .../initiatives/initiative-epics.store.ts | 5 +- 17 files changed, 298 insertions(+), 121 deletions(-) create mode 100644 web/ee/components/initiatives/details/main/collapsible-section/epics/epic-list-item/epics-list-group.tsx diff --git a/packages/constants/src/issue/filter-extended.ts b/packages/constants/src/issue/filter-extended.ts index 8256bb896b..9db7dcea83 100644 --- a/packages/constants/src/issue/filter-extended.ts +++ b/packages/constants/src/issue/filter-extended.ts @@ -1,5 +1,9 @@ import { ILayoutDisplayFiltersOptions, TIssueActivityComment } from "@plane/types"; -import { ISSUE_DISPLAY_PROPERTIES_KEYS, EPICS_DISPLAY_PROPERTIES_KEYS } from "./common"; +import { + ISSUE_DISPLAY_PROPERTIES_KEYS, + EPICS_DISPLAY_PROPERTIES_KEYS, + SUB_ISSUES_DISPLAY_PROPERTIES_KEYS, +} from "./common"; import { TActivityFilters } from "./filter"; import { EActivityFilterType } from "./filter"; @@ -170,6 +174,20 @@ export const ADDITIONAL_ISSUE_DISPLAY_FILTERS_BY_PAGE: { }, }, }, + initiatives: { + list: { + display_properties: SUB_ISSUES_DISPLAY_PROPERTIES_KEYS, + filters: ["priority", "state_group", "project", "issue_type", "assignees", "start_date", "target_date"], + display_filters: { + order_by: ["-created_at", "-updated_at", "start_date", "-priority"], + group_by: ["state_detail.group", "priority", "assignees", null], + }, + extra_options: { + access: true, + values: ["sub_issue"], + }, + }, + }, }; export enum EActivityFilterTypeEE { diff --git a/packages/types/src/issues/issue.d.ts b/packages/types/src/issues/issue.d.ts index f99349e916..14986e079d 100644 --- a/packages/types/src/issues/issue.d.ts +++ b/packages/types/src/issues/issue.d.ts @@ -70,6 +70,7 @@ export type TIssue = TBaseIssue & sourceIssueId?: string; state__group?: string | null; update_status?: EUpdateStatus | undefined; + state__group?: string | null; }; export type TIssueMap = { diff --git a/web/core/components/empty-state/section-empty-state-root.tsx b/web/core/components/empty-state/section-empty-state-root.tsx index e415db53c6..8cebfb7836 100644 --- a/web/core/components/empty-state/section-empty-state-root.tsx +++ b/web/core/components/empty-state/section-empty-state-root.tsx @@ -1,8 +1,6 @@ "use client"; - import { FC } from "react"; import { cn } from "@plane/utils"; - type Props = { icon: React.ReactNode; title: string; diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/display-filters.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/display-filters.tsx index 46d1f8d229..b6320d98b3 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/display-filters.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/display-filters.tsx @@ -6,7 +6,6 @@ import { IIssueDisplayFilterOptions, ILayoutDisplayFiltersOptions, IIssueDisplay import { cn } from "@plane/utils"; import { FilterDisplayProperties, FilterGroupBy, FilterOrderBy, FiltersDropdown } from "@/components/issues"; import { isDisplayFiltersApplied } from "@/components/issues/issue-layouts/utils"; - type TSubIssueDisplayFiltersProps = { displayProperties: IIssueDisplayProperties; displayFilters: IIssueDisplayFilterOptions; diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/filters.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/filters.tsx index 8ae6195ea8..2e06aa3eec 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/filters.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/filters.tsx @@ -16,7 +16,6 @@ import { } from "@/components/issues"; import { isFiltersApplied } from "@/components/issues/issue-layouts/utils"; import { FilterIssueTypes } from "@/plane-web/components/issues/filters/issue-types"; - type TSubIssueFiltersProps = { handleFiltersUpdate: (key: keyof IIssueFilterOptions, value: string | string[]) => void; filters: IIssueFilterOptions; diff --git a/web/core/components/issues/issue-detail-widgets/sub-issues/title-actions.tsx b/web/core/components/issues/issue-detail-widgets/sub-issues/title-actions.tsx index e9e4393ed7..862ae0f8a9 100644 --- a/web/core/components/issues/issue-detail-widgets/sub-issues/title-actions.tsx +++ b/web/core/components/issues/issue-detail-widgets/sub-issues/title-actions.tsx @@ -35,10 +35,9 @@ export const SubWorkItemTitleActions: FC = observ } = useMember(); // derived values - const subIssueFilters = getSubIssueFilters(parentId); const projectStates = getProjectStates(projectId); const projectMemberIds = getProjectMemberIds(projectId, false); - + const subIssueFilters = getSubIssueFilters(parentId); const layoutDisplayFiltersOptions = ISSUE_DISPLAY_FILTERS_BY_PAGE["sub_work_items"].list; const handleDisplayFilters = useCallback( @@ -69,7 +68,6 @@ export const SubWorkItemTitleActions: FC = observ if (subIssueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); else newValues.push(value); } - updateSubWorkItemFilters(EIssueFilterType.FILTERS, { [key]: newValues }, parentId); }, [subIssueFilters?.filters, updateSubWorkItemFilters, parentId] diff --git a/web/core/store/issue/issue-details/sub_issues.store.ts b/web/core/store/issue/issue-details/sub_issues.store.ts index a8be50f886..a0d5922afd 100644 --- a/web/core/store/issue/issue-details/sub_issues.store.ts +++ b/web/core/store/issue/issue-details/sub_issues.store.ts @@ -126,7 +126,6 @@ export class IssueSubIssuesStore implements IIssueSubIssuesStore { fetchSubIssues = async (workspaceSlug: string, projectId: string, parentIssueId: string) => { this.loader = "init-loader"; - const response = await this.issueService.subIssues(workspaceSlug, projectId, parentIssueId); const subIssuesStateDistribution = response?.state_distribution ?? {}; diff --git a/web/core/store/issue/issue-details/sub_issues_filter.store.ts b/web/core/store/issue/issue-details/sub_issues_filter.store.ts index d68583b683..641115e6c9 100644 --- a/web/core/store/issue/issue-details/sub_issues_filter.store.ts +++ b/web/core/store/issue/issue-details/sub_issues_filter.store.ts @@ -23,7 +23,6 @@ export const DEFAULT_DISPLAY_PROPERTIES = { priority: true, state: true, }; - export interface IWorkItemSubIssueFiltersStore { subIssueFilters: Record>; // helpers methods @@ -50,6 +49,8 @@ export class WorkItemSubIssueFiltersStore implements IWorkItemSubIssueFiltersSto subIssueFilters: observable, updateSubWorkItemFilters: action, getSubIssueFilters: action, + getFilteredSubWorkItems: action, + resetFilters: action, }); // root store diff --git a/web/ee/components/epics/details/main/overview-section-root.tsx b/web/ee/components/epics/details/main/overview-section-root.tsx index e47447bbe0..b519f58d3f 100644 --- a/web/ee/components/epics/details/main/overview-section-root.tsx +++ b/web/ee/components/epics/details/main/overview-section-root.tsx @@ -7,7 +7,7 @@ import { EIssueServiceType } from "@plane/constants"; import { useLocalStorage } from "@plane/hooks"; import { Tabs } from "@plane/ui"; // components -import { RelationActionButton, SubIssuesActionButton } from "@/components/issues"; +import { RelationActionButton } from "@/components/issues"; // plane web import { OverviewSection } from "@/plane-web/components/common/layout/main/sections/overview-root"; // local components diff --git a/web/ee/components/epics/details/main/overview-section/issues-root.tsx b/web/ee/components/epics/details/main/overview-section/issues-root.tsx index df4cbb5653..4091cf9e20 100644 --- a/web/ee/components/epics/details/main/overview-section/issues-root.tsx +++ b/web/ee/components/epics/details/main/overview-section/issues-root.tsx @@ -3,7 +3,7 @@ import React, { FC, useEffect, useState, useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // plane imports -import { EIssueServiceType } from "@plane/constants"; +import { EIssueServiceType, EIssuesStoreType } from "@plane/constants"; import { TIssue, TSubIssueOperations } from "@plane/types"; import { getButtonStyling, LayersIcon } from "@plane/ui"; // components @@ -119,9 +119,6 @@ export const EpicIssuesOverviewRoot: FC = observer((props) => { useEffect(() => { handleFetchSubIssues(); - return () => { - handleFetchSubIssues(); - }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [epicId]); @@ -185,7 +182,6 @@ export const EpicIssuesOverviewRoot: FC = observer((props) => { return ( <> - {/* Display list when group is none */} = observer((props) => { handleIssueCrudState={handleIssueCrudState} subIssueOperations={epicSubIssuesOperation} issueServiceType={EIssueServiceType.EPICS} + storeType={EIssuesStoreType.EPIC} /> {shouldRenderDeleteIssueModal && ( diff --git a/web/ee/components/epics/details/main/overview-section/work-items-actions.tsx b/web/ee/components/epics/details/main/overview-section/work-items-actions.tsx index f846fd4382..5deec7a654 100644 --- a/web/ee/components/epics/details/main/overview-section/work-items-actions.tsx +++ b/web/ee/components/epics/details/main/overview-section/work-items-actions.tsx @@ -3,10 +3,16 @@ import { FC, useCallback } from "react"; import { observer } from "mobx-react"; import { EIssueFilterType, EIssueServiceType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; // types -import { IIssueDisplayFilterOptions, TIssueServiceType, IIssueDisplayProperties } from "@plane/types"; +import { + IIssueDisplayFilterOptions, + TIssueServiceType, + IIssueDisplayProperties, + IIssueFilterOptions, +} from "@plane/types"; // components import { SubIssuesActionButton, SubIssueDisplayFilters } from "@/components/issues/issue-detail-widgets/sub-issues"; -import { useIssueDetail } from "@/hooks/store"; +import { SubIssueFilters } from "@/components/issues/issue-detail-widgets/sub-issues/filters"; +import { useIssueDetail, useMember, useProjectState } from "@/hooks/store"; type TSubWorkItemsActionsProps = { workItemId: string; @@ -22,40 +28,55 @@ export const SubWorkItemsActions: FC = observer((prop // store hooks const { subIssues: { - filters: { getSubIssueFilters, updateSubIssueFilters }, + filters: { getSubIssueFilters, updateSubWorkItemFilters }, }, } = useIssueDetail(workItemServiceType); - // derived values - const subIssueFilters = getSubIssueFilters(workItemId); + const { getProjectStates } = useProjectState(); + const { + project: { getProjectMemberIds }, + } = useMember(); + // derived values + const projectMemberIds = getProjectMemberIds(projectId, false); + const projectStates = getProjectStates(projectId); + const subIssueFilters = getSubIssueFilters(workItemId); // handlers const handleDisplayFilters = useCallback( (updatedDisplayFilter: Partial) => { if (!workspaceSlug || !projectId) return; - updateSubIssueFilters( - workspaceSlug, - projectId, - EIssueFilterType.DISPLAY_FILTERS, - updatedDisplayFilter, - workItemId - ); + updateSubWorkItemFilters(EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter, workItemId); }, - [workspaceSlug, projectId, updateSubIssueFilters, workItemId] + [workspaceSlug, projectId, updateSubWorkItemFilters, workItemId] ); const handleDisplayPropertiesUpdate = useCallback( (updatedDisplayProperties: Partial) => { if (!workspaceSlug || !projectId) return; - updateSubIssueFilters( - workspaceSlug, - projectId, - EIssueFilterType.DISPLAY_PROPERTIES, - updatedDisplayProperties, - workItemId - ); + updateSubWorkItemFilters(EIssueFilterType.DISPLAY_PROPERTIES, updatedDisplayProperties, workItemId); }, - [workspaceSlug, projectId, updateSubIssueFilters, workItemId] + [workspaceSlug, projectId, updateSubWorkItemFilters, workItemId] + ); + + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + if (!workspaceSlug || !projectId) return; + const newValues = subIssueFilters?.filters?.[key] ?? []; + + if (Array.isArray(value)) { + // this validation is majorly for the filter start_date, target_date custom + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + else newValues.splice(newValues.indexOf(val), 1); + }); + } else { + if (subIssueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + + updateSubWorkItemFilters(EIssueFilterType.FILTERS, { [key]: newValues }, workItemId); + }, + [workspaceSlug, projectId, subIssueFilters?.filters, updateSubWorkItemFilters, workItemId] ); const layoutDisplayFiltersOptions = ISSUE_DISPLAY_FILTERS_BY_PAGE["sub_work_items"]?.["list"]; @@ -70,6 +91,13 @@ export const SubWorkItemsActions: FC = observer((prop handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate} handleDisplayFiltersUpdate={handleDisplayFilters} /> + ); diff --git a/web/ee/components/initiatives/details/main/collapsible-section/epics/content.tsx b/web/ee/components/initiatives/details/main/collapsible-section/epics/content.tsx index f77ec8c845..745fa07323 100644 --- a/web/ee/components/initiatives/details/main/collapsible-section/epics/content.tsx +++ b/web/ee/components/initiatives/details/main/collapsible-section/epics/content.tsx @@ -3,15 +3,18 @@ import React, { FC } from "react"; import { observer } from "mobx-react"; // plane imports +import { ListFilter } from "lucide-react"; import { useTranslation } from "@plane/i18n"; -import { EpicIcon, getButtonStyling } from "@plane/ui"; +import { GroupByColumnTypes } from "@plane/types"; +import { Button, EpicIcon, getButtonStyling } from "@plane/ui"; // helpers +import { getGroupByColumns } from "@/components/issues/issue-layouts/utils"; import { cn } from "@/helpers/common.helper"; // plane-web import { SectionEmptyState } from "@/plane-web/components/common"; import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives"; // local components -import { EpicListItem } from "./epic-list-item/root"; +import { EpicsGroup } from "./epic-list-item/epics-list-group"; type Props = { workspaceSlug: string; @@ -24,26 +27,58 @@ export const InitiativeEpicsCollapsibleContent: FC = observer((props) => const { workspaceSlug, initiativeId, toggleEpicModal, disabled = false } = props; // store hooks const { - initiative: { epics: {getInitiativeEpicsById} }, + initiative: { + epics: { + getInitiativeEpicsById, + filters: { getInitiativeEpicsFiltersById, getGroupedEpics, getFilteredEpics, resetFilters }, + }, + }, } = useInitiatives(); const { t } = useTranslation(); + // derived values + const filters = getInitiativeEpicsFiltersById(initiativeId); + const groupBy = filters?.displayFilters?.group_by; + const groupedEpicIds = getGroupedEpics(initiativeId); + const filteredEpicsCount = getFilteredEpics(initiativeId, filters?.filters ?? {}).length; + + const groups = getGroupByColumns({ + groupBy: groupBy as GroupByColumnTypes, + includeNone: true, + isWorkspaceLevel: true, + isEpic: false, + }); + // derived values const initiativeEpicIds = getInitiativeEpicsById(initiativeId) ?? []; return (
{initiativeEpicIds.length > 0 ? ( - initiativeEpicIds.map((epicId) => ( - 0 ? ( + groups?.map((group) => ( + + )) + ) : ( + } + actionElement={ + + } /> - )) + ) ) : ( = observer((props) => { + const { group, disabled, initiativeId, workspaceSlug, epicIds } = props; + + const isAllIssues = group.id === ALL_ISSUES; + + // states + const [isCollapsibleOpen, setIsCollapsibleOpen] = useState(isAllIssues); + + if (!epicIds.length) return null; + + return ( + <> + setIsCollapsibleOpen(!isCollapsibleOpen)} + title={ + !isAllIssues && ( +
+ +
+ {group.icon ?? } +
+ {group.name} + {epicIds.length} +
+ ) + } + buttonClassName={cn("hidden", !isAllIssues && "block")} + > + {/* Epics list */} +
+ {epicIds?.map((epicId) => ( + + ))} +
+
+ + ); +}); diff --git a/web/ee/components/initiatives/details/main/collapsible-section/epics/title-actions.tsx b/web/ee/components/initiatives/details/main/collapsible-section/epics/title-actions.tsx index 60f9401052..18587dd36e 100644 --- a/web/ee/components/initiatives/details/main/collapsible-section/epics/title-actions.tsx +++ b/web/ee/components/initiatives/details/main/collapsible-section/epics/title-actions.tsx @@ -1,12 +1,15 @@ -import { FC } from "react"; +import { FC, useCallback } from "react"; +import cloneDeep from "lodash/cloneDeep"; import { observer } from "mobx-react"; import { Plus } from "lucide-react"; // plane imports import { EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants"; -import { IIssueDisplayFilterOptions, IIssueDisplayProperties } from "@plane/types"; +import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "@plane/types"; // components import { SubIssueDisplayFilters } from "@/components/issues/issue-detail-widgets/sub-issues"; +import { SubIssueFilters } from "@/components/issues/issue-detail-widgets/sub-issues/filters"; // hooks +import { useMember } from "@/hooks/store"; import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives"; type TEpicsTitleActionsProps = { @@ -23,25 +26,51 @@ export const EpicsTitleActions: FC = observer((props) = const { initiative: { epics: { - filters: { getInitiativeEpicsFiltersById, updateSubIssueFilters }, + filters: { getInitiativeEpicsFiltersById, updateEpicsFilters }, }, }, } = useInitiatives(); + const { + workspace: { getWorkspaceMemberIds }, + } = useMember(); + + const memberIds = getWorkspaceMemberIds(workspaceSlug); + // derived values const epicsFilters = getInitiativeEpicsFiltersById(initiativeId); const displayProperties = epicsFilters?.displayProperties; const displayFilters = epicsFilters?.displayFilters; const handleDisplayPropertiesUpdate = (updatedDisplayProperties: Partial) => { - updateSubIssueFilters(workspaceSlug, EIssueFilterType.DISPLAY_PROPERTIES, updatedDisplayProperties, initiativeId); + updateEpicsFilters(workspaceSlug, EIssueFilterType.DISPLAY_PROPERTIES, updatedDisplayProperties, initiativeId); }; const handleDisplayFiltersUpdate = (updatedDisplayFilters: Partial) => { - updateSubIssueFilters(workspaceSlug, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilters, initiativeId); + updateEpicsFilters(workspaceSlug, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilters, initiativeId); }; - const layoutDisplayFiltersOptions = ISSUE_DISPLAY_FILTERS_BY_PAGE.sub_work_items.list; + const handleFiltersUpdate = useCallback( + (key: keyof IIssueFilterOptions, value: string | string[]) => { + if (!workspaceSlug) return; + const newValues = cloneDeep(epicsFilters?.filters?.[key]) ?? []; + + if (Array.isArray(value)) { + // this validation is majorly for the filter start_date, target_date custom + value.forEach((val) => { + if (!newValues.includes(val)) newValues.push(val); + else newValues.splice(newValues.indexOf(val), 1); + }); + } else { + if (epicsFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1); + else newValues.push(value); + } + updateEpicsFilters(workspaceSlug, EIssueFilterType.FILTERS, { [key]: newValues }, initiativeId); + }, + [workspaceSlug, epicsFilters?.filters, updateEpicsFilters, initiativeId] + ); + + const layoutDisplayFiltersOptions = ISSUE_DISPLAY_FILTERS_BY_PAGE.initiatives.list; const handlePropagation = (e: React.MouseEvent) => { e.stopPropagation(); @@ -58,10 +87,18 @@ export const EpicsTitleActions: FC = observer((props) = handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate} handleDisplayFiltersUpdate={handleDisplayFiltersUpdate} /> + {!disabled && ( - +
+ +
)}
); diff --git a/web/ee/components/sidebar/project-navigation-root.tsx b/web/ee/components/sidebar/project-navigation-root.tsx index 0313623386..ed2df08600 100644 --- a/web/ee/components/sidebar/project-navigation-root.tsx +++ b/web/ee/components/sidebar/project-navigation-root.tsx @@ -45,6 +45,7 @@ export const ProjectNavigationRoot: FC = observer((props shouldRender: !!isProjectOverviewEnabled, sortOrder: -2, i18n_key: "common.overview", + key: "overview", }, { name: "Epics", @@ -54,6 +55,7 @@ export const ProjectNavigationRoot: FC = observer((props shouldRender: !!isEpicsEnabled, sortOrder: -1, i18n_key: "sidebar.epics", + key: "epics", }, ]; diff --git a/web/ee/store/initiatives/initiative-epics-filter.store.ts b/web/ee/store/initiatives/initiative-epics-filter.store.ts index b4355f2360..f58a0ec579 100644 --- a/web/ee/store/initiatives/initiative-epics-filter.store.ts +++ b/web/ee/store/initiatives/initiative-epics-filter.store.ts @@ -1,22 +1,30 @@ import set from "lodash/set"; -import { action, makeObservable, observable } from "mobx"; +import { action, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; import { EIssueFilterType } from "@plane/constants"; -import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilters, TIssueParams } from "@plane/types"; +import { + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + IIssueFilterOptions, + IIssueFilters, + TIssue, +} from "@plane/types"; +import { getFilteredWorkItems, getGroupedWorkItemIds, updateFilters } from "@/store/issue/helpers/base-issues-utils"; +import { DEFAULT_DISPLAY_PROPERTIES } from "@/store/issue/issue-details/sub_issues_filter.store"; import { InitiativeEpicStore } from "./initiative-epics.store"; export interface IInitiativeEpicsFilterStore { initiativeEpicsFiltersMap: Record>; getInitiativeEpicsFiltersById: (initiativeId: string) => Partial | undefined; - updateSubIssueFilters: ( + updateEpicsFilters: ( workspaceSlug: string, filterType: EIssueFilterType, filters: IIssueDisplayFilterOptions | IIssueDisplayProperties, initiativeId: string ) => Promise; - - // helpers - computedFilterParams: (initiativeId: string) => Partial>; - + getGroupedEpics: (initiativeId: string) => Record; + getFilteredEpics: (initiativeId: string, filters: IIssueFilterOptions) => TIssue[]; + resetFilters: (workItemId: string) => void; // store initiativeEpicStore: InitiativeEpicStore; } @@ -28,8 +36,11 @@ export class InitiativeEpicsFilterStore implements IInitiativeEpicsFilterStore { constructor(initiativeEpicStore: InitiativeEpicStore) { makeObservable(this, { initiativeEpicsFiltersMap: observable, - updateSubIssueFilters: action, + updateEpicsFilters: action, getInitiativeEpicsFiltersById: action, + getGroupedEpics: action, + getFilteredEpics: action, + resetFilters: action, }); this.initiativeEpicStore = initiativeEpicStore; @@ -39,20 +50,10 @@ export class InitiativeEpicsFilterStore implements IInitiativeEpicsFilterStore { * Initialize the initiative epics filters * @param initiativeId - The initiative id */ - initInitiativeEpicsFilters = (initiativeId: string) => { - set(this.initiativeEpicsFiltersMap, [initiativeId], { - displayFilters: {}, - displayProperties: { - key: true, - issue_type: true, - assignee: true, - start_date: true, - due_date: true, - labels: true, - priority: true, - state: true, - }, - }); + initializeFilters = (initiativeId: string) => { + set(this.initiativeEpicsFiltersMap, [initiativeId, "displayProperties"], DEFAULT_DISPLAY_PROPERTIES); + set(this.initiativeEpicsFiltersMap, [initiativeId, "filters"], {}); + set(this.initiativeEpicsFiltersMap, [initiativeId, "displayFilters"], {}); }; /** @@ -63,7 +64,7 @@ export class InitiativeEpicsFilterStore implements IInitiativeEpicsFilterStore { getInitiativeEpicsFiltersById = (initiativeId: string) => { // initialize the filters if no exists before if (!this.initiativeEpicsFiltersMap?.[initiativeId]) { - this.initInitiativeEpicsFilters(initiativeId); + this.initializeFilters(initiativeId); } return this.initiativeEpicsFiltersMap?.[initiativeId]; }; @@ -75,54 +76,54 @@ export class InitiativeEpicsFilterStore implements IInitiativeEpicsFilterStore { * @param filters - The filters * @param initiativeId - The initiative id */ - updateSubIssueFilters = async ( + updateEpicsFilters = async ( workspaceSlug: string, filterType: EIssueFilterType, filters: IIssueDisplayFilterOptions | IIssueDisplayProperties, initiativeId: string ) => { - const _filters = this.getInitiativeEpicsFiltersById(initiativeId); - switch (filterType) { - case EIssueFilterType.DISPLAY_FILTERS: { - set(this.initiativeEpicsFiltersMap, [initiativeId, "displayFilters"], { - ..._filters.displayFilters, - ...filters, - }); - this.initiativeEpicStore.fetchInitiativeEpics(workspaceSlug, initiativeId); - break; - } - case EIssueFilterType.DISPLAY_PROPERTIES: - set(this.initiativeEpicsFiltersMap, [initiativeId, "displayProperties"], { - ..._filters.displayProperties, - ...filters, - }); - break; - } + runInAction(() => { + updateFilters(this.initiativeEpicsFiltersMap, filterType, filters, initiativeId); + }); }; /** - * Compute the filter params for the initiative epics - * @param initiativeId - The initiative id - * @returns The computed filter params + * @description This method is used to get the grouped epics + * @param parentWorkItemId + * @returns */ - computedFilterParams = (initiativeId: string) => { - const displayFilters = this.getInitiativeEpicsFiltersById(initiativeId).displayFilters; + getGroupedEpics = computedFn((parentWorkItemId: string) => { + const epicsFilters = this.getInitiativeEpicsFiltersById(parentWorkItemId); + // get group by and order by + const orderByKey = epicsFilters.displayFilters?.order_by; + const groupByKey = epicsFilters.displayFilters?.group_by; - const computedFilters: Partial> = { - order_by: displayFilters?.order_by || undefined, - }; + const filteredEpics = this.getFilteredEpics(parentWorkItemId, epicsFilters.filters ?? {}); - const issueFiltersParams: Partial> = {}; - Object.keys(computedFilters).forEach((key) => { - const _key = key as TIssueParams; - const _value: string | boolean | string[] | undefined = computedFilters[_key]; - const nonEmptyArrayValue = Array.isArray(_value) && _value.length === 0 ? undefined : _value; - if (nonEmptyArrayValue != undefined) - issueFiltersParams[_key] = Array.isArray(nonEmptyArrayValue) - ? nonEmptyArrayValue.join(",") - : nonEmptyArrayValue; - }); + const groupedEpics = getGroupedWorkItemIds(filteredEpics, groupByKey, orderByKey); - return issueFiltersParams; + return groupedEpics; + }); + + /** + * @description This method is used to get the filtered epics + * @param initiativeId + * @returns + */ + getFilteredEpics = computedFn((initiativeId: string, filters: IIssueFilterOptions) => { + const epicIds = this.initiativeEpicStore.getInitiativeEpicsById(initiativeId); + const epics = this.initiativeEpicStore.rootStore.issue.issues.getIssuesByIds(epicIds, "un-archived"); + + const filteredEpics = getFilteredWorkItems(epics, filters); + + return filteredEpics; + }); + + /** + * @description This method is used to reset the filters + * @param initiativeId + */ + resetFilters = (initiativeId: string) => { + this.initializeFilters(initiativeId); }; } diff --git a/web/ee/store/initiatives/initiative-epics.store.ts b/web/ee/store/initiatives/initiative-epics.store.ts index db7df77b10..7189519513 100644 --- a/web/ee/store/initiatives/initiative-epics.store.ts +++ b/web/ee/store/initiatives/initiative-epics.store.ts @@ -158,11 +158,8 @@ export class InitiativeEpicStore implements IInitiativeEpicStore { }; }); - // get computed filters - const queries = this.filters.computedFilterParams(initiativeId); - // fetch the initiative epics - const response = await this.initiativeService.fetchInitiativeEpics(workspaceSlug, initiativeId, queries); + const response = await this.initiativeService.fetchInitiativeEpics(workspaceSlug, initiativeId); const transformedResponse = response.map((epic) => ({ ...epic,