[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
This commit is contained in:
guru_sainath 2024-02-29 15:31:03 +05:30 committed by GitHub
parent 56805203f1
commit 9326fb0762
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 464 additions and 400 deletions

View File

@ -203,6 +203,8 @@ export interface ViewFlags {
export type GroupByColumnTypes =
| "project"
| "cycle"
| "module"
| "state"
| "state_detail.group"
| "priority"

View File

@ -14,6 +14,8 @@ export type TIssueGroupByOptions =
| "project"
| "assignees"
| "mentions"
| "cycle"
| "module"
| null;
export type TIssueOrderByOptions =

View File

@ -152,6 +152,7 @@ export const CycleMobileHeader = () => {
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["cycle"]}
/>
</FiltersDropdown>
</div>

View File

@ -244,6 +244,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["cycle"]}
/>
</FiltersDropdown>

View File

@ -248,6 +248,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["module"]}
/>
</FiltersDropdown>
</div>

View File

@ -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<IIssueDisplayFilterOptions>) => void;
handleDisplayPropertiesUpdate: (updatedDisplayProperties: Partial<IIssueDisplayProperties>) => void;
layoutDisplayFiltersOptions: ILayoutDisplayFiltersOptions | undefined;
ignoreGroupedFilters?: Partial<TIssueGroupByOptions>[];
};
export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
@ -29,6 +30,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
handleDisplayFiltersUpdate,
handleDisplayPropertiesUpdate,
layoutDisplayFiltersOptions,
ignoreGroupedFilters = [],
} = props;
const isDisplayFilterEnabled = (displayFilter: keyof IIssueDisplayFilterOptions) =>
@ -54,6 +56,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
group_by: val,
})
}
ignoreGroupedFilters={ignoreGroupedFilters}
/>
</div>
)}
@ -71,6 +74,7 @@ export const DisplayFiltersSelection: React.FC<Props> = observer((props) => {
})
}
subGroupByOptions={layoutDisplayFiltersOptions?.display_filters.sub_group_by ?? []}
ignoreGroupedFilters={ignoreGroupedFilters}
/>
</div>
)}

View File

@ -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<TIssueGroupByOptions>[];
};
export const FilterGroupBy: React.FC<Props> = 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<Props> = 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 (
<FilterOption

View File

@ -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;
handleUpdate: (val: TIssueGroupByOptions) => void;
subGroupByOptions: TIssueGroupByOptions[];
ignoreGroupedFilters: Partial<TIssueGroupByOptions>[];
};
export const FilterSubGroupBy: React.FC<Props> = 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<Props> = observer((props) => {
<div>
{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 (
<FilterOption

View File

@ -1,6 +1,15 @@
import { observer } from "mobx-react-lite";
// hooks
import { useIssueDetail, useKanbanView, useLabel, useMember, useProject, useProjectState } from "hooks/store";
import {
useCycle,
useIssueDetail,
useKanbanView,
useLabel,
useMember,
useModule,
useProject,
useProjectState,
} from "hooks/store";
// components
import { HeaderGroupByCard } from "./headers/group-by-card";
import { KanbanGroup } from "./kanban-group";
@ -79,14 +88,16 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = 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;

View File

@ -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") {

View File

@ -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<IKanBanSwimLanes> = 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;

View File

@ -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<IGroupByList> = (props) => {
const project = useProject();
const label = useLabel();
const projectState = useProjectState();
const cycle = useCycle();
const _module = useModule();
const containerRef = useRef<HTMLDivElement | null>(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;

View File

@ -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: <CycleGroupIcon cycleGroup={cycleStatus as TCycleGroups} className="h-3.5 w-3.5" />,
payload: { cycle_id: cycle.id },
});
}
});
cycles.push({
id: "None",
name: "None",
icon: <ContrastIcon className="h-3.5 w-3.5" />,
});
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: <DiceIcon className="w-3.5 h-3.5" />,
payload: { module_ids: [_module.id] },
});
}) as any;
modules.push({
id: "None",
name: "None",
icon: <DiceIcon className="w-3.5 h-3.5" />,
});
return modules as any;
};
const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefined => {
const { projectStates } = projectState;
if (!projectStates) return;

View File

@ -146,6 +146,7 @@ export const ModuleMobileHeader = () => {
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
ignoreGroupedFilters={["module"]}
/>
</FiltersDropdown>
</div>

View File

@ -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"],
},

View File

@ -36,6 +36,8 @@ export type TIssueHelperStore = {
const ISSUE_FILTER_DEFAULT_DATA: Record<TIssueDisplayFilterOptions, keyof TIssue> = {
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 [];
}