mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 22:09:12 +02:00
[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
This commit is contained in:
@@ -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 {
|
||||
|
||||
1
packages/types/src/issues/issue.d.ts
vendored
1
packages/types/src/issues/issue.d.ts
vendored
@@ -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 = {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -35,10 +35,9 @@ export const SubWorkItemTitleActions: FC<TSubWorkItemTitleActionsProps> = 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<TSubWorkItemTitleActionsProps> = 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]
|
||||
|
||||
@@ -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 ?? {};
|
||||
|
||||
@@ -23,7 +23,6 @@ export const DEFAULT_DISPLAY_PROPERTIES = {
|
||||
priority: true,
|
||||
state: true,
|
||||
};
|
||||
|
||||
export interface IWorkItemSubIssueFiltersStore {
|
||||
subIssueFilters: Record<string, Partial<IIssueFilters>>;
|
||||
// helpers methods
|
||||
@@ -50,6 +49,8 @@ export class WorkItemSubIssueFiltersStore implements IWorkItemSubIssueFiltersSto
|
||||
subIssueFilters: observable,
|
||||
updateSubWorkItemFilters: action,
|
||||
getSubIssueFilters: action,
|
||||
getFilteredSubWorkItems: action,
|
||||
resetFilters: action,
|
||||
});
|
||||
|
||||
// root store
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Props> = observer((props) => {
|
||||
useEffect(() => {
|
||||
handleFetchSubIssues();
|
||||
|
||||
return () => {
|
||||
handleFetchSubIssues();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [epicId]);
|
||||
|
||||
@@ -185,7 +182,6 @@ export const EpicIssuesOverviewRoot: FC<Props> = observer((props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Display list when group is none */}
|
||||
<SubIssuesListRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
@@ -196,6 +192,7 @@ export const EpicIssuesOverviewRoot: FC<Props> = observer((props) => {
|
||||
handleIssueCrudState={handleIssueCrudState}
|
||||
subIssueOperations={epicSubIssuesOperation}
|
||||
issueServiceType={EIssueServiceType.EPICS}
|
||||
storeType={EIssuesStoreType.EPIC}
|
||||
/>
|
||||
|
||||
{shouldRenderDeleteIssueModal && (
|
||||
|
||||
@@ -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<TSubWorkItemsActionsProps> = 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<IIssueDisplayFilterOptions>) => {
|
||||
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<IIssueDisplayProperties>) => {
|
||||
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<TSubWorkItemsActionsProps> = observer((prop
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
handleDisplayFiltersUpdate={handleDisplayFilters}
|
||||
/>
|
||||
<SubIssueFilters
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
filters={subIssueFilters?.filters ?? {}}
|
||||
memberIds={projectMemberIds ?? undefined}
|
||||
states={projectStates}
|
||||
layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
|
||||
/>
|
||||
<SubIssuesActionButton issueId={workItemId} issueServiceType={workItemServiceType} disabled={disabled} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<Props> = 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 (
|
||||
<div className="mt-3">
|
||||
{initiativeEpicIds.length > 0 ? (
|
||||
initiativeEpicIds.map((epicId) => (
|
||||
<EpicListItem
|
||||
key={epicId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
epicId={epicId}
|
||||
initiativeId={initiativeId}
|
||||
disabled={disabled}
|
||||
filteredEpicsCount > 0 ? (
|
||||
groups?.map((group) => (
|
||||
<EpicsGroup
|
||||
key={group.id}
|
||||
group={group}
|
||||
epicIds={groupedEpicIds?.[group.id] ?? []}
|
||||
disabled={disabled}
|
||||
workspaceSlug={workspaceSlug}
|
||||
initiativeId={initiativeId}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<SectionEmptyState
|
||||
heading="You don’t have epics that match the filters you have applied."
|
||||
subHeading="To see all epics, clear all applied filters."
|
||||
icon={<ListFilter />}
|
||||
actionElement={
|
||||
<Button variant="neutral-primary" size="sm" onClick={() => resetFilters(initiativeId)}>
|
||||
Clear filters
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
))
|
||||
)
|
||||
) : (
|
||||
<SectionEmptyState
|
||||
heading={t("epics.empty_state.section.title")}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ChevronRight, CircleDashed } from "lucide-react";
|
||||
import { ALL_ISSUES } from "@plane/constants";
|
||||
import { IGroupByColumn } from "@plane/types";
|
||||
import { Collapsible } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
import { EpicListItem } from "./root";
|
||||
|
||||
interface TEpicsGroupProps {
|
||||
epicIds: string[];
|
||||
workspaceSlug: string;
|
||||
group: IGroupByColumn;
|
||||
disabled: boolean;
|
||||
initiativeId: string;
|
||||
}
|
||||
|
||||
export const EpicsGroup: FC<TEpicsGroupProps> = 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 (
|
||||
<>
|
||||
<Collapsible
|
||||
isOpen={isCollapsibleOpen}
|
||||
onToggle={() => setIsCollapsibleOpen(!isCollapsibleOpen)}
|
||||
title={
|
||||
!isAllIssues && (
|
||||
<div className="flex items-center gap-2 p-3">
|
||||
<ChevronRight
|
||||
className={cn("size-3.5 transition-all text-custom-text-400", {
|
||||
"rotate-90": isCollapsibleOpen,
|
||||
})}
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
<div className="flex-shrink-0 grid place-items-center overflow-hidden">
|
||||
{group.icon ?? <CircleDashed className="size-3.5" strokeWidth={2} />}
|
||||
</div>
|
||||
<span className="text-sm text-custom-text-100 font-medium">{group.name}</span>
|
||||
<span className="text-sm text-custom-text-400">{epicIds.length}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
buttonClassName={cn("hidden", !isAllIssues && "block")}
|
||||
>
|
||||
{/* Epics list */}
|
||||
<div className="pl-2">
|
||||
{epicIds?.map((epicId) => (
|
||||
<EpicListItem
|
||||
key={epicId}
|
||||
workspaceSlug={workspaceSlug}
|
||||
epicId={epicId}
|
||||
initiativeId={initiativeId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -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<TEpicsTitleActionsProps> = 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<IIssueDisplayProperties>) => {
|
||||
updateSubIssueFilters(workspaceSlug, EIssueFilterType.DISPLAY_PROPERTIES, updatedDisplayProperties, initiativeId);
|
||||
updateEpicsFilters(workspaceSlug, EIssueFilterType.DISPLAY_PROPERTIES, updatedDisplayProperties, initiativeId);
|
||||
};
|
||||
|
||||
const handleDisplayFiltersUpdate = (updatedDisplayFilters: Partial<IIssueDisplayFilterOptions>) => {
|
||||
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<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
@@ -58,10 +87,18 @@ export const EpicsTitleActions: FC<TEpicsTitleActionsProps> = observer((props) =
|
||||
handleDisplayPropertiesUpdate={handleDisplayPropertiesUpdate}
|
||||
handleDisplayFiltersUpdate={handleDisplayFiltersUpdate}
|
||||
/>
|
||||
<SubIssueFilters
|
||||
handleFiltersUpdate={handleFiltersUpdate}
|
||||
filters={epicsFilters?.filters ?? {}}
|
||||
memberIds={memberIds ?? undefined}
|
||||
layoutDisplayFiltersOptions={layoutDisplayFiltersOptions}
|
||||
/>
|
||||
{!disabled && (
|
||||
<button type="button" onClick={() => toggleEpicModal()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
<div>
|
||||
<button type="button" onClick={() => toggleEpicModal()}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -45,6 +45,7 @@ export const ProjectNavigationRoot: FC<TProjectItemsRootProps> = observer((props
|
||||
shouldRender: !!isProjectOverviewEnabled,
|
||||
sortOrder: -2,
|
||||
i18n_key: "common.overview",
|
||||
key: "overview",
|
||||
},
|
||||
{
|
||||
name: "Epics",
|
||||
@@ -54,6 +55,7 @@ export const ProjectNavigationRoot: FC<TProjectItemsRootProps> = observer((props
|
||||
shouldRender: !!isEpicsEnabled,
|
||||
sortOrder: -1,
|
||||
i18n_key: "sidebar.epics",
|
||||
key: "epics",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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<string, Partial<IIssueFilters>>;
|
||||
getInitiativeEpicsFiltersById: (initiativeId: string) => Partial<IIssueFilters> | undefined;
|
||||
updateSubIssueFilters: (
|
||||
updateEpicsFilters: (
|
||||
workspaceSlug: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueDisplayFilterOptions | IIssueDisplayProperties,
|
||||
initiativeId: string
|
||||
) => Promise<void>;
|
||||
|
||||
// helpers
|
||||
computedFilterParams: (initiativeId: string) => Partial<Record<TIssueParams, boolean | string>>;
|
||||
|
||||
getGroupedEpics: (initiativeId: string) => Record<string, string[]>;
|
||||
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<Record<TIssueParams, undefined | string[] | boolean | string>> = {
|
||||
order_by: displayFilters?.order_by || undefined,
|
||||
};
|
||||
const filteredEpics = this.getFilteredEpics(parentWorkItemId, epicsFilters.filters ?? {});
|
||||
|
||||
const issueFiltersParams: Partial<Record<TIssueParams, boolean | string>> = {};
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user