[WEB-4951] [WEB-4884] feat: work item filters revamp (#7810)

This commit is contained in:
Prateek Shourya 2025-09-19 18:27:36 +05:30 committed by GitHub
parent e6a7ca4c72
commit 9aef5d4aa9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
160 changed files with 5879 additions and 4881 deletions

View file

@ -0,0 +1,16 @@
import { TWorkItemFiltersEntityProps } from "@/plane-web/hooks/work-item-filters/use-work-item-filters-config";
export type TGetAdditionalPropsForProjectLevelFiltersHOCParams = {
workspaceSlug: string;
projectId: string;
};
export type TGetAdditionalPropsForProjectLevelFiltersHOC = (
params: TGetAdditionalPropsForProjectLevelFiltersHOCParams
) => TWorkItemFiltersEntityProps;
export const getAdditionalProjectLevelFiltersHOCProps: TGetAdditionalPropsForProjectLevelFiltersHOC = ({
workspaceSlug,
}) => ({
workspaceSlug,
});

View file

@ -0,0 +1,15 @@
import { CORE_OPERATORS, TSupportedOperators } from "@plane/types";
export type TFiltersOperatorConfigs = {
allowedOperators: Set<TSupportedOperators>;
allowNegative: boolean;
};
export type TUseFiltersOperatorConfigsProps = {
workspaceSlug: string;
};
export const useFiltersOperatorConfigs = (_props: TUseFiltersOperatorConfigsProps): TFiltersOperatorConfigs => ({
allowedOperators: new Set(Object.values(CORE_OPERATORS)),
allowNegative: false,
});

View file

@ -0,0 +1,366 @@
import { useCallback, useMemo } from "react";
import {
AtSign,
Briefcase,
CalendarCheck2,
CalendarClock,
CircleUserRound,
SignalHigh,
Tag,
Users,
} from "lucide-react";
// plane imports
import {
ContrastIcon,
CycleGroupIcon,
DiceIcon,
DoubleCircleIcon,
PriorityIcon,
StateGroupIcon,
} from "@plane/propel/icons";
import {
ICycle,
IState,
IUserLite,
TFilterConfig,
TFilterValue,
IIssueLabel,
IModule,
IProject,
TWorkItemFilterProperty,
} from "@plane/types";
import { Avatar, Logo } from "@plane/ui";
import {
getAssigneeFilterConfig,
getCreatedByFilterConfig,
getCycleFilterConfig,
getFileURL,
getLabelFilterConfig,
getMentionFilterConfig,
getModuleFilterConfig,
getPriorityFilterConfig,
getProjectFilterConfig,
getStartDateFilterConfig,
getStateFilterConfig,
getStateGroupFilterConfig,
getSubscriberFilterConfig,
getTargetDateFilterConfig,
} from "@plane/utils";
// store hooks
import { useCycle } from "@/hooks/store/use-cycle";
import { useLabel } from "@/hooks/store/use-label";
import { useMember } from "@/hooks/store/use-member";
import { useModule } from "@/hooks/store/use-module";
import { useProject } from "@/hooks/store/use-project";
import { useProjectState } from "@/hooks/store/use-project-state";
// plane web imports
import { useFiltersOperatorConfigs } from "@/plane-web/hooks/rich-filters/use-filters-operator-configs";
export type TWorkItemFiltersEntityProps = {
workspaceSlug: string;
cycleIds?: string[];
labelIds?: string[];
memberIds?: string[];
moduleIds?: string[];
projectId?: string;
projectIds?: string[];
stateIds?: string[];
};
export type TUseWorkItemFiltersConfigProps = {
allowedFilters: TWorkItemFilterProperty[];
} & TWorkItemFiltersEntityProps;
export type TWorkItemFiltersConfig = {
configs: TFilterConfig<TWorkItemFilterProperty, TFilterValue>[];
configMap: {
[key in TWorkItemFilterProperty]?: TFilterConfig<TWorkItemFilterProperty, TFilterValue>;
};
isFilterEnabled: (key: TWorkItemFilterProperty) => boolean;
};
export const useWorkItemFiltersConfig = (props: TUseWorkItemFiltersConfigProps): TWorkItemFiltersConfig => {
const { allowedFilters, cycleIds, labelIds, memberIds, moduleIds, projectId, projectIds, stateIds, workspaceSlug } =
props;
// store hooks
const { getProjectById } = useProject();
const { getCycleById } = useCycle();
const { getLabelById } = useLabel();
const { getModuleById } = useModule();
const { getStateById } = useProjectState();
const { getUserDetails } = useMember();
// derived values
const operatorConfigs = useFiltersOperatorConfigs({ workspaceSlug });
const filtersToShow = useMemo(() => new Set(allowedFilters), [allowedFilters]);
const project = useMemo(() => getProjectById(projectId), [projectId, getProjectById]);
const members: IUserLite[] | undefined = useMemo(
() =>
memberIds
? (memberIds.map((memberId) => getUserDetails(memberId)).filter((member) => member) as IUserLite[])
: undefined,
[memberIds, getUserDetails]
);
const workItemStates: IState[] | undefined = useMemo(
() =>
stateIds ? (stateIds.map((stateId) => getStateById(stateId)).filter((state) => state) as IState[]) : undefined,
[stateIds, getStateById]
);
const workItemLabels: IIssueLabel[] | undefined = useMemo(
() =>
labelIds
? (labelIds.map((labelId) => getLabelById(labelId)).filter((label) => label) as IIssueLabel[])
: undefined,
[labelIds, getLabelById]
);
const cycles = useMemo(
() => (cycleIds ? (cycleIds.map((cycleId) => getCycleById(cycleId)).filter((cycle) => cycle) as ICycle[]) : []),
[cycleIds, getCycleById]
);
const modules = useMemo(
() =>
moduleIds ? (moduleIds.map((moduleId) => getModuleById(moduleId)).filter((module) => module) as IModule[]) : [],
[moduleIds, getModuleById]
);
const projects = useMemo(
() =>
projectIds
? (projectIds.map((projectId) => getProjectById(projectId)).filter((project) => project) as IProject[])
: [],
[projectIds, getProjectById]
);
/**
* Checks if a filter is enabled based on the filters to show.
* @param key - The filter key.
* @param level - The level of the filter.
* @returns True if the filter is enabled, false otherwise.
*/
const isFilterEnabled = useCallback((key: TWorkItemFilterProperty) => filtersToShow.has(key), [filtersToShow]);
// state group filter config
const stateGroupFilterConfig = useMemo(
() =>
getStateGroupFilterConfig<TWorkItemFilterProperty>("state_group")({
isEnabled: isFilterEnabled("state_group"),
filterIcon: DoubleCircleIcon,
getOptionIcon: (stateGroupKey) => <StateGroupIcon stateGroup={stateGroupKey} />,
...operatorConfigs,
}),
[isFilterEnabled, operatorConfigs]
);
// state filter config
const stateFilterConfig = useMemo(
() =>
getStateFilterConfig<TWorkItemFilterProperty>("state_id")({
isEnabled: isFilterEnabled("state_id") && workItemStates !== undefined,
filterIcon: DoubleCircleIcon,
getOptionIcon: (state) => <StateGroupIcon stateGroup={state.group} color={state.color} />,
states: workItemStates ?? [],
...operatorConfigs,
}),
[isFilterEnabled, workItemStates, operatorConfigs]
);
// label filter config
const labelFilterConfig = useMemo(
() =>
getLabelFilterConfig<TWorkItemFilterProperty>("label_id")({
isEnabled: isFilterEnabled("label_id") && workItemLabels !== undefined,
filterIcon: Tag,
labels: workItemLabels ?? [],
getOptionIcon: (color) => (
<span className="flex flex-shrink-0 size-2.5 rounded-full" style={{ backgroundColor: color }} />
),
...operatorConfigs,
}),
[isFilterEnabled, workItemLabels, operatorConfigs]
);
// cycle filter config
const cycleFilterConfig = useMemo(
() =>
getCycleFilterConfig<TWorkItemFilterProperty>("cycle_id")({
isEnabled: isFilterEnabled("cycle_id") && project?.cycle_view === true && cycles !== undefined,
filterIcon: ContrastIcon,
getOptionIcon: (cycleGroup) => <CycleGroupIcon cycleGroup={cycleGroup} className="h-3.5 w-3.5 flex-shrink-0" />,
cycles: cycles ?? [],
...operatorConfigs,
}),
[isFilterEnabled, project?.cycle_view, cycles, operatorConfigs]
);
// module filter config
const moduleFilterConfig = useMemo(
() =>
getModuleFilterConfig<TWorkItemFilterProperty>("module_id")({
isEnabled: isFilterEnabled("module_id") && project?.module_view === true && modules !== undefined,
filterIcon: DiceIcon,
getOptionIcon: () => <DiceIcon className="h-3 w-3 flex-shrink-0" />,
modules: modules ?? [],
...operatorConfigs,
}),
[isFilterEnabled, project?.module_view, modules, operatorConfigs]
);
// assignee filter config
const assigneeFilterConfig = useMemo(
() =>
getAssigneeFilterConfig<TWorkItemFilterProperty>("assignee_id")({
isEnabled: isFilterEnabled("assignee_id") && members !== undefined,
filterIcon: Users,
members: members ?? [],
getOptionIcon: (memberDetails) => (
<Avatar
name={memberDetails.display_name}
src={getFileURL(memberDetails.avatar_url)}
showTooltip={false}
size="sm"
/>
),
...operatorConfigs,
}),
[isFilterEnabled, members, operatorConfigs]
);
// mention filter config
const mentionFilterConfig = useMemo(
() =>
getMentionFilterConfig<TWorkItemFilterProperty>("mention_id")({
isEnabled: isFilterEnabled("mention_id") && members !== undefined,
filterIcon: AtSign,
members: members ?? [],
getOptionIcon: (memberDetails) => (
<Avatar
name={memberDetails.display_name}
src={getFileURL(memberDetails.avatar_url)}
showTooltip={false}
size="sm"
/>
),
...operatorConfigs,
}),
[isFilterEnabled, members, operatorConfigs]
);
// created by filter config
const createdByFilterConfig = useMemo(
() =>
getCreatedByFilterConfig<TWorkItemFilterProperty>("created_by_id")({
isEnabled: isFilterEnabled("created_by_id") && members !== undefined,
filterIcon: CircleUserRound,
members: members ?? [],
getOptionIcon: (memberDetails) => (
<Avatar
name={memberDetails.display_name}
src={getFileURL(memberDetails.avatar_url)}
showTooltip={false}
size="sm"
/>
),
...operatorConfigs,
}),
[isFilterEnabled, members, operatorConfigs]
);
// subscriber filter config
const subscriberFilterConfig = useMemo(
() =>
getSubscriberFilterConfig<TWorkItemFilterProperty>("subscriber_id")({
isEnabled: isFilterEnabled("subscriber_id") && members !== undefined,
filterIcon: Users,
members: members ?? [],
getOptionIcon: (memberDetails) => (
<Avatar
name={memberDetails.display_name}
src={getFileURL(memberDetails.avatar_url)}
showTooltip={false}
size="sm"
/>
),
...operatorConfigs,
}),
[isFilterEnabled, members, operatorConfigs]
);
// priority filter config
const priorityFilterConfig = useMemo(
() =>
getPriorityFilterConfig<TWorkItemFilterProperty>("priority")({
isEnabled: isFilterEnabled("priority"),
filterIcon: SignalHigh,
getOptionIcon: (priority) => <PriorityIcon priority={priority} />,
...operatorConfigs,
}),
[isFilterEnabled, operatorConfigs]
);
// start date filter config
const startDateFilterConfig = useMemo(
() =>
getStartDateFilterConfig<TWorkItemFilterProperty>("start_date")({
isEnabled: true,
filterIcon: CalendarClock,
...operatorConfigs,
}),
[operatorConfigs]
);
// target date filter config
const targetDateFilterConfig = useMemo(
() =>
getTargetDateFilterConfig<TWorkItemFilterProperty>("target_date")({
isEnabled: true,
filterIcon: CalendarCheck2,
...operatorConfigs,
}),
[operatorConfigs]
);
// project filter config
const projectFilterConfig = useMemo(
() =>
getProjectFilterConfig<TWorkItemFilterProperty>("project_id")({
isEnabled: isFilterEnabled("project_id") && projects !== undefined,
filterIcon: Briefcase,
projects: projects,
getOptionIcon: (project) => <Logo logo={project.logo_props} size={12} />,
...operatorConfigs,
}),
[isFilterEnabled, projects, operatorConfigs]
);
return {
configs: [
stateFilterConfig,
stateGroupFilterConfig,
assigneeFilterConfig,
priorityFilterConfig,
projectFilterConfig,
mentionFilterConfig,
labelFilterConfig,
cycleFilterConfig,
moduleFilterConfig,
startDateFilterConfig,
targetDateFilterConfig,
createdByFilterConfig,
subscriberFilterConfig,
],
configMap: {
project_id: projectFilterConfig,
state_group: stateGroupFilterConfig,
state_id: stateFilterConfig,
label_id: labelFilterConfig,
cycle_id: cycleFilterConfig,
module_id: moduleFilterConfig,
assignee_id: assigneeFilterConfig,
mention_id: mentionFilterConfig,
created_by_id: createdByFilterConfig,
subscriber_id: subscriberFilterConfig,
priority: priorityFilterConfig,
start_date: startDateFilterConfig,
target_date: targetDateFilterConfig,
},
isFilterEnabled,
};
};