[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

@ -1,4 +1,10 @@
import { EIssuesStoreType, ILayoutDisplayFiltersOptions, TIssueActivityComment } from "@plane/types";
import {
EIssuesStoreType,
IIssueFilterOptions,
ILayoutDisplayFiltersOptions,
TIssueActivityComment,
TWorkItemFilterProperty,
} from "@plane/types";
import {
TIssueFilterPriorityObject,
ISSUE_DISPLAY_PROPERTIES_KEYS,
@ -23,12 +29,17 @@ export enum EServerGroupByToFilterOptions {
}
export enum EIssueFilterType {
FILTERS = "filters",
FILTERS = "rich_filters",
DISPLAY_FILTERS = "display_filters",
DISPLAY_PROPERTIES = "display_properties",
KANBAN_FILTERS = "kanban_filters",
}
export type TSupportedFilterTypeForUpdate =
| EIssueFilterType.DISPLAY_FILTERS
| EIssueFilterType.DISPLAY_PROPERTIES
| EIssueFilterType.KANBAN_FILTERS;
export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
[key in TIssueLayout]: Record<"filters", TIssueFilterKeys[]>;
} = {
@ -82,257 +93,218 @@ export const ISSUE_PRIORITY_FILTERS: TIssueFilterPriorityObject[] = [
},
];
export type TFiltersByLayout = {
export type TFiltersLayoutOptions = {
[layoutType: string]: ILayoutDisplayFiltersOptions;
};
export type TFilterPropertiesByPageType = {
filters: TWorkItemFilterProperty[];
layoutOptions: TFiltersLayoutOptions;
};
export type TIssueFiltersToDisplayByPageType = {
[pageType: string]: TFiltersByLayout;
[pageType: string]: TFilterPropertiesByPageType;
};
export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
profile_issues: {
list: {
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
filters: ["priority", "state_group", "label_id", "start_date", "target_date"],
layoutOptions: {
list: {
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: ["active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
kanban: {
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
kanban: {
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: ["active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
},
archived_issues: {
list: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state", "cycle", "module", "priority", "labels", "assignees", "created_by", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
filters: [
"priority",
"state_group",
"state_id",
"cycle_id",
"module_id",
"assignee_id",
"created_by_id",
"label_id",
"start_date",
"target_date",
],
layoutOptions: {
list: {
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state", "cycle", "module", "priority", "labels", "assignees", "created_by", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: ["active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups"],
},
},
},
},
my_issues: {
spreadsheet: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: [],
type: [null, "active", "backlog"],
filters: [
"priority",
"state_group",
"label_id",
"assignee_id",
"created_by_id",
"subscriber_id",
"project_id",
"start_date",
"target_date",
],
layoutOptions: {
spreadsheet: {
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: [],
type: ["active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
list: {
filters: [
"priority",
"state_group",
"labels",
"assignees",
"created_by",
"subscriber",
"project",
"start_date",
"target_date",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: false,
values: [],
list: {
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
type: ["active", "backlog"],
},
extra_options: {
access: false,
values: [],
},
},
},
},
issues: {
list: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
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"],
filters: [
"priority",
"state_group",
"state_id",
"cycle_id",
"module_id",
"assignee_id",
"mention_id",
"created_by_id",
"label_id",
"start_date",
"target_date",
],
layoutOptions: {
list: {
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
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: ["active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
kanban: {
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
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: ["active", "backlog"],
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
},
},
},
kanban: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
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"],
calendar: {
display_properties: ["key", "issue_type"],
display_filters: {
type: ["active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
extra_options: {
access: true,
values: ["show_empty_groups", "sub_issue"],
spreadsheet: {
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: ["active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
},
calendar: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"issue_type",
],
display_properties: ["key", "issue_type"],
display_filters: {
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
spreadsheet: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
gantt_chart: {
filters: [
"priority",
"state",
"cycle",
"module",
"assignees",
"mentions",
"created_by",
"labels",
"start_date",
"target_date",
"issue_type",
],
display_properties: ["key", "issue_type"],
display_filters: {
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
gantt_chart: {
display_properties: ["key", "issue_type"],
display_filters: {
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: ["active", "backlog"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
},
},
sub_work_items: {
list: {
display_properties: SUB_ISSUES_DISPLAY_PROPERTIES_KEYS,
filters: ["priority", "state", "issue_type", "assignees", "start_date", "target_date"],
display_filters: {
order_by: ["-created_at", "-updated_at", "start_date", "-priority"],
group_by: ["state", "priority", "assignees", null],
},
extra_options: {
access: true,
values: ["sub_issue"],
filters: ["priority", "state_id", "assignee_id", "start_date", "target_date"],
layoutOptions: {
list: {
display_properties: SUB_ISSUES_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: ["-created_at", "-updated_at", "start_date", "-priority"],
group_by: ["state", "priority", "assignees", null],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
},
},
};
export const ISSUE_STORE_TO_FILTERS_MAP: Partial<Record<EIssuesStoreType, TFiltersByLayout>> = {
export const ISSUE_STORE_TO_FILTERS_MAP: Partial<Record<EIssuesStoreType, TFilterPropertiesByPageType>> = {
[EIssuesStoreType.PROJECT]: ISSUE_DISPLAY_FILTERS_BY_PAGE.issues,
};
export const SUB_WORK_ITEM_AVAILABLE_FILTERS_FOR_WORK_ITEM_PAGE: (keyof IIssueFilterOptions)[] = [
"priority",
"state",
"issue_type",
"assignees",
"start_date",
"target_date",
];
export enum EActivityFilterType {
ACTIVITY = "ACTIVITY",
COMMENT = "COMMENT",

View file

@ -1 +1,2 @@
export * from "./rich-filters";
export * from "./work-item-filters";

View file

@ -28,8 +28,7 @@ type TOperatorOptionForDisplay = {
export interface IFilterConfig<P extends TFilterProperty, V extends TFilterValue = TFilterValue>
extends TFilterConfig<P, V> {
// computed
allSupportedOperators: TSupportedOperators[];
allSupportedOperatorConfigs: TOperatorSpecificConfigs<V>[keyof TOperatorSpecificConfigs<V>][];
allEnabledSupportedOperators: TSupportedOperators[];
firstOperator: TSupportedOperators | undefined;
// computed functions
getOperatorConfig: (
@ -76,8 +75,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
supportedOperatorConfigsMap: observable,
allowMultipleFilters: observable,
// computed
allSupportedOperators: computed,
allSupportedOperatorConfigs: computed,
allEnabledSupportedOperators: computed,
firstOperator: computed,
// actions
mutate: action,
@ -90,16 +88,10 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
* Returns all supported operators.
* @returns All supported operators.
*/
get allSupportedOperators(): IFilterConfig<P, V>["allSupportedOperators"] {
return Array.from(this.supportedOperatorConfigsMap.keys());
}
/**
* Returns all supported operator configs.
* @returns All supported operator configs.
*/
get allSupportedOperatorConfigs(): IFilterConfig<P, V>["allSupportedOperatorConfigs"] {
return Array.from(this.supportedOperatorConfigsMap.values());
get allEnabledSupportedOperators(): IFilterConfig<P, V>["allEnabledSupportedOperators"] {
return Array.from(this.supportedOperatorConfigsMap.entries())
.filter(([, operatorConfig]) => operatorConfig.isOperatorEnabled)
.map(([operator]) => operator);
}
/**
@ -107,7 +99,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
* @returns The first operator.
*/
get firstOperator(): IFilterConfig<P, V>["firstOperator"] {
return this.allSupportedOperators[0];
return this.allEnabledSupportedOperators[0];
}
// ------------ computed functions ------------
@ -168,7 +160,7 @@ export class FilterConfig<P extends TFilterProperty, V extends TFilterValue = TF
const operatorOptions: TOperatorOptionForDisplay[] = [];
// Process each supported operator to build display options
for (const operator of this.allSupportedOperators) {
for (const operator of this.allEnabledSupportedOperators) {
const displayOperator = this.getDisplayOperatorByValue(operator, value);
const displayOperatorLabel = this.getLabelForOperator(displayOperator);
operatorOptions.push({

View file

@ -0,0 +1,259 @@
// plane imports
import isEmpty from "lodash/isEmpty";
import {
LOGICAL_OPERATOR,
SingleOrArray,
TFilterExpression,
TFilterValue,
TSupportedOperators,
TWorkItemFilterConditionData,
TWorkItemFilterConditionKey,
TWorkItemFilterExpression,
TWorkItemFilterExpressionData,
TWorkItemFilterProperty,
WORK_ITEM_FILTER_PROPERTY_KEYS,
} from "@plane/types";
import { createConditionNode, createAndGroupNode, isAndGroupNode, isConditionNode } from "@plane/utils";
// local imports
import { FilterAdapter } from "../rich-filters/adapter";
class WorkItemFiltersAdapter extends FilterAdapter<TWorkItemFilterProperty, TWorkItemFilterExpression> {
/**
* Converts external work item filter expression to internal filter tree
* @param externalFilter - The external filter expression
* @returns Internal filter expression or null
*/
toInternal(externalFilter: TWorkItemFilterExpression): TFilterExpression<TWorkItemFilterProperty> | null {
if (!externalFilter || isEmpty(externalFilter)) return null;
try {
return this._convertExpressionToInternal(externalFilter);
} catch (error) {
console.error("Failed to convert external filter to internal:", error);
return null;
}
}
/**
* Recursively converts external expression data to internal filter tree
* @param expression - The external expression data
* @returns Internal filter expression
*/
private _convertExpressionToInternal(
expression: TWorkItemFilterExpressionData
): TFilterExpression<TWorkItemFilterProperty> {
if (!expression || isEmpty(expression)) {
throw new Error("Invalid expression: empty or null data");
}
// Check if it's a simple condition (has field property)
if (this._isWorkItemFilterConditionData(expression)) {
const conditionResult = this._extractWorkItemFilterConditionData(expression);
if (!conditionResult) {
throw new Error("Failed to extract condition data");
}
const [property, operator, value] = conditionResult;
return createConditionNode({
property,
operator,
value,
});
}
// It's a logical group - check which type
const expressionKeys = Object.keys(expression);
if (LOGICAL_OPERATOR.AND in expression) {
const andExpression = expression as { [LOGICAL_OPERATOR.AND]: TWorkItemFilterExpressionData[] };
const andConditions = andExpression[LOGICAL_OPERATOR.AND];
if (!Array.isArray(andConditions) || andConditions.length === 0) {
throw new Error("AND group must contain at least one condition");
}
const convertedConditions = andConditions.map((item) => this._convertExpressionToInternal(item));
return createAndGroupNode(convertedConditions);
}
throw new Error(`Invalid expression: unknown structure with keys [${expressionKeys.join(", ")}]`);
}
/**
* Converts internal filter expression to external format
* @param internalFilter - The internal filter expression
* @returns External filter expression
*/
toExternal(internalFilter: TFilterExpression<TWorkItemFilterProperty>): TWorkItemFilterExpression {
if (!internalFilter) {
return {};
}
try {
return this._convertExpressionToExternal(internalFilter);
} catch (error) {
console.error("Failed to convert internal filter to external:", error);
return {};
}
}
/**
* Recursively converts internal expression to external format
* @param expression - The internal filter expression
* @returns External expression data
*/
private _convertExpressionToExternal(
expression: TFilterExpression<TWorkItemFilterProperty>
): TWorkItemFilterExpressionData {
if (isConditionNode(expression)) {
return this._createWorkItemFilterConditionData(expression.property, expression.operator, expression.value);
}
// It's a group node
if (isAndGroupNode(expression)) {
return {
[LOGICAL_OPERATOR.AND]: expression.children.map((child) => this._convertExpressionToExternal(child)),
} as TWorkItemFilterExpressionData;
}
throw new Error(`Unknown group node type for expression`);
}
/**
* Type guard to check if data is of type TWorkItemFilterConditionData
* @param data - The data to check
* @returns True if data is TWorkItemFilterConditionData, false otherwise
*/
private _isWorkItemFilterConditionData = (data: unknown): data is TWorkItemFilterConditionData => {
if (!data || typeof data !== "object" || isEmpty(data)) return false;
const keys = Object.keys(data);
if (keys.length === 0) return false;
// Check if any key contains logical operators (would indicate it's a group)
const hasLogicalOperators = keys.some((key) => key === LOGICAL_OPERATOR.AND);
if (hasLogicalOperators) return false;
// All keys must match the work item filter condition key pattern
return keys.every((key) => this._isValidWorkItemFilterConditionKey(key));
};
/**
* Validates if a key is a valid work item filter condition key
* @param key - The key to validate
* @returns True if the key is valid
*/
private _isValidWorkItemFilterConditionKey = (key: string): key is TWorkItemFilterConditionKey => {
if (typeof key !== "string" || key.length === 0) return false;
// Find the last occurrence of '__' to separate property from operator
const lastDoubleUnderscoreIndex = key.lastIndexOf("__");
if (
lastDoubleUnderscoreIndex === -1 ||
lastDoubleUnderscoreIndex === 0 ||
lastDoubleUnderscoreIndex === key.length - 2
) {
return false;
}
const property = key.substring(0, lastDoubleUnderscoreIndex);
const operator = key.substring(lastDoubleUnderscoreIndex + 2);
// Validate property is in allowed list
if (!WORK_ITEM_FILTER_PROPERTY_KEYS.includes(property as TWorkItemFilterProperty)) {
return false;
}
// Validate operator is not empty
return operator.length > 0;
};
/**
* Extracts property, operator and value from work item filter condition data
* @param data - The condition data
* @returns Tuple of property, operator and value, or null if invalid
*/
private _extractWorkItemFilterConditionData = (
data: TWorkItemFilterConditionData
): [TWorkItemFilterProperty, TSupportedOperators, SingleOrArray<TFilterValue>] | null => {
const keys = Object.keys(data);
if (keys.length !== 1) {
console.error("Work item filter condition data must have exactly one key");
return null;
}
const key = keys[0];
if (!this._isValidWorkItemFilterConditionKey(key)) {
console.error(`Invalid work item filter condition key: ${key}`);
return null;
}
// Find the last occurrence of '__' to separate property from operator
const lastDoubleUnderscoreIndex = key.lastIndexOf("__");
const property = key.substring(0, lastDoubleUnderscoreIndex);
const operator = key.substring(lastDoubleUnderscoreIndex + 2);
const rawValue = data[key as TWorkItemFilterConditionKey];
if (typeof rawValue !== "string") {
console.error(`Filter value must be a string, got: ${typeof rawValue}`);
return null;
}
// Parse comma-separated values
const parsedValue = this._parseFilterValue(rawValue);
return [property as TWorkItemFilterProperty, operator as TSupportedOperators, parsedValue];
};
/**
* Parses filter value from string format
* @param value - The string value to parse
* @returns Parsed value as string or array of strings
*/
private _parseFilterValue = (value: string): SingleOrArray<TFilterValue> => {
if (typeof value !== "string") return value;
// Handle empty string
if (value === "") return value;
// Split by comma if contains comma, otherwise return as single value
if (value.includes(",")) {
// Split and trim each value, filter out empty strings
const splitValues = value
.split(",")
.map((v) => v.trim())
.filter((v) => v.length > 0);
// Return single value if only one non-empty value after split
return splitValues.length === 1 ? splitValues[0] : splitValues;
}
return value;
};
/**
* Creates TWorkItemFilterConditionData from property, operator and value
* @param property - The filter property key
* @param operator - The filter operator
* @param value - The filter value
* @returns The condition data object
*/
private _createWorkItemFilterConditionData = (
property: TWorkItemFilterProperty,
operator: TSupportedOperators,
value: SingleOrArray<TFilterValue>
): TWorkItemFilterConditionData => {
const conditionKey = `${property}__${operator}` as TWorkItemFilterConditionKey;
// Convert value to string format
const stringValue = Array.isArray(value) ? value.join(",") : value;
return {
[conditionKey]: stringValue,
} as TWorkItemFilterConditionData;
};
}
export const workItemFiltersAdapter = new WorkItemFiltersAdapter();

View file

@ -0,0 +1,215 @@
import { action, makeObservable, observable } from "mobx";
import { computedFn } from "mobx-utils";
// plane imports
import { TExpressionOptions } from "@plane/constants";
import { EIssuesStoreType, LOGICAL_OPERATOR, TWorkItemFilterExpression, TWorkItemFilterProperty } from "@plane/types";
import { getOperatorForPayload } from "@plane/utils";
// local imports
import { buildWorkItemFilterExpressionFromConditions, TWorkItemFilterCondition } from "../../utils";
import { FilterInstance, IFilterInstance } from "../rich-filters/filter";
import { workItemFiltersAdapter } from "./adapter";
type TGetOrCreateFilterParams = {
entityId: string;
entityType: EIssuesStoreType;
expressionOptions?: TExpressionOptions<TWorkItemFilterExpression>;
initialExpression?: TWorkItemFilterExpression;
onExpressionChange?: (expression: TWorkItemFilterExpression) => void;
};
type TWorkItemFilterKey = `${EIssuesStoreType}-${string}`;
export interface IWorkItemFilterStore {
filters: Map<TWorkItemFilterKey, IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression>>; // key is the entity id (project, cycle, workspace, teamspace, etc)
getFilter: (
entityType: EIssuesStoreType,
entityId: string
) => IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression> | undefined;
getOrCreateFilter: (
params: TGetOrCreateFilterParams
) => IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression>;
resetExpression: (entityType: EIssuesStoreType, entityId: string, expression: TWorkItemFilterExpression) => void;
updateFilterExpressionFromConditions: (
entityType: EIssuesStoreType,
entityId: string,
conditions: TWorkItemFilterCondition[],
fallbackFn: (expression: TWorkItemFilterExpression) => Promise<void>
) => Promise<void>;
updateFilterValueFromSidebar: (
entityType: EIssuesStoreType,
entityId: string,
condition: TWorkItemFilterCondition
) => void;
deleteFilter: (entityType: EIssuesStoreType, entityId: string) => void;
}
export class WorkItemFilterStore implements IWorkItemFilterStore {
// observable
filters: IWorkItemFilterStore["filters"];
constructor() {
this.filters = new Map<TWorkItemFilterKey, IFilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression>>();
makeObservable(this, {
filters: observable,
getOrCreateFilter: action,
resetExpression: action,
updateFilterExpressionFromConditions: action,
deleteFilter: action,
});
}
// ------------ computed functions ------------
/**
* Returns a filter instance.
* @param entityType - The entity type.
* @param entityId - The entity id.
* @returns The filter instance.
*/
getFilter: IWorkItemFilterStore["getFilter"] = computedFn((entityType, entityId) =>
this.filters.get(this._getFilterKey(entityType, entityId))
);
// ------------ actions ------------
/**
* Gets or creates a new filter instance.
* If the instance already exists, updates its expression options to ensure they're current.
*/
getOrCreateFilter: IWorkItemFilterStore["getOrCreateFilter"] = action((params) => {
const existingFilter = this.getFilter(params.entityType, params.entityId);
if (existingFilter) {
// Update expression options on existing filter to ensure they're current
if (params.expressionOptions) {
existingFilter.updateExpressionOptions(params.expressionOptions);
}
// Update callback if provided
if (params.onExpressionChange) {
existingFilter.onExpressionChange = params.onExpressionChange;
}
return existingFilter;
}
// create new filter instance
const newFilter = this._initializeFilterInstance(params);
this.filters.set(this._getFilterKey(params.entityType, params.entityId), newFilter);
return newFilter;
});
/**
* Resets the initial expression for a filter instance.
* @param entityType - The entity type.
* @param entityId - The entity id.
* @param expression - The expression to update.
*/
resetExpression: IWorkItemFilterStore["resetExpression"] = action((entityType, entityId, expression) => {
const filter = this.getFilter(entityType, entityId);
if (filter) {
filter.resetExpression(expression);
}
});
/**
* Updates the filter expression from conditions.
* @param entityType - The entity type.
* @param entityId - The entity id.
* @param conditions - The conditions to update.
* @param fallbackFn - The fallback function to update the expression if the filter instance does not exist.
*/
updateFilterExpressionFromConditions: IWorkItemFilterStore["updateFilterExpressionFromConditions"] = action(
async (entityType, entityId, conditions, fallbackFn) => {
const filter = this.getFilter(entityType, entityId);
const newFilterExpression = buildWorkItemFilterExpressionFromConditions({
conditions,
});
if (!newFilterExpression) return;
// Update the filter expression using the filter instance if it exists, otherwise use the fallback function
if (filter) {
filter.resetExpression(newFilterExpression, false);
} else {
await fallbackFn(newFilterExpression);
}
}
);
/**
* Handles sidebar filter updates by adding new conditions or updating existing ones.
* This method processes filter conditions from the sidebar UI and applies them to the
* appropriate filter instance, handling both positive and negative operators correctly.
*
* @param entityType - The entity type (e.g., project, cycle, module)
* @param entityId - The unique identifier for the entity
* @param condition - The filter condition containing property, operator, and value
*/
updateFilterValueFromSidebar: IWorkItemFilterStore["updateFilterValueFromSidebar"] = action(
(entityType, entityId, condition) => {
// Retrieve the filter instance for the specified entity
const filter = this.getFilter(entityType, entityId);
// Early return if filter instance doesn't exist
if (!filter) {
console.warn(
`Cannot handle sidebar filters update: filter instance not found for entity type "${entityType}" with ID "${entityId}"`
);
return;
}
// Check for existing conditions with the same property and operator
const conditionNode = filter.findFirstConditionByPropertyAndOperator(condition.property, condition.operator);
// No existing condition found - add new condition with AND logic
if (!conditionNode) {
const { operator, isNegation } = getOperatorForPayload(condition.operator);
// Create the condition payload with normalized operator
const conditionPayload = {
property: condition.property,
operator,
value: condition.value,
};
filter.addCondition(LOGICAL_OPERATOR.AND, conditionPayload, isNegation);
return;
}
// Update existing condition (assuming single condition per property-operator pair)
filter.updateConditionValue(conditionNode.id, condition.value);
}
);
/**
* Deletes a filter instance.
* @param entityType - The entity type.
* @param entityId - The entity id.
*/
deleteFilter: IWorkItemFilterStore["deleteFilter"] = action((entityType, entityId) => {
this.filters.delete(this._getFilterKey(entityType, entityId));
});
// ------------ private helpers ------------
/**
* Returns a filter key.
* @param entityType - The entity type.
* @param entityId - The entity id.s
* @returns The filter key.
*/
_getFilterKey = (entityType: EIssuesStoreType, entityId: string): TWorkItemFilterKey => `${entityType}-${entityId}`;
/**
* Initializes a filter instance.
* @param params - The parameters for the filter instance.
* @returns The filter instance.
*/
_initializeFilterInstance = (params: TGetOrCreateFilterParams) =>
new FilterInstance<TWorkItemFilterProperty, TWorkItemFilterExpression>({
adapter: workItemFiltersAdapter,
initialExpression: params.initialExpression,
onExpressionChange: params.onExpressionChange,
options: {
expression: params.expressionOptions,
},
});
}

View file

@ -0,0 +1,2 @@
export * from "./adapter";
export * from "./filter.store";

View file

@ -1 +1,2 @@
export * from "./rich-filter.helper";
export * from "./work-item-filters.helper";

View file

@ -0,0 +1,32 @@
// plane imports
import {
TBuildFilterExpressionParams,
TFilterConditionForBuild,
TFilterValue,
TWorkItemFilterExpression,
TWorkItemFilterProperty,
} from "@plane/types";
// local imports
import { workItemFiltersAdapter } from "../store/work-item-filters/adapter";
import { buildTempFilterExpressionFromConditions } from "./rich-filter.helper";
export type TWorkItemFilterCondition = TFilterConditionForBuild<TWorkItemFilterProperty, TFilterValue>;
/**
* Builds a work item filter expression from conditions.
* @param params.conditions - The conditions for building the filter expression.
* @returns The work item filter expression.
*/
export const buildWorkItemFilterExpressionFromConditions = (
params: Omit<
TBuildFilterExpressionParams<TWorkItemFilterProperty, TFilterValue, TWorkItemFilterExpression>,
"adapter"
>
): TWorkItemFilterExpression | undefined => {
const workItemFilterExpression = buildTempFilterExpressionFromConditions({
...params,
adapter: workItemFiltersAdapter,
});
if (!workItemFilterExpression) console.error("Failed to build work item filter expression from conditions");
return workItemFilterExpression;
};

View file

@ -6,7 +6,6 @@ import { IStateLite } from "./state";
import { IUserLite } from "./users";
import {
IIssueDisplayProperties,
IIssueFilterOptions,
TIssueExtraOptions,
TIssueGroupByOptions,
TIssueGroupingFilters,
@ -219,7 +218,6 @@ export interface IIssueListRow {
}
export interface ILayoutDisplayFiltersOptions {
filters: (keyof IIssueFilterOptions)[];
display_properties: (keyof IIssueDisplayProperties)[];
display_filters: {
group_by?: TIssueGroupByOptions[];

View file

@ -13,6 +13,7 @@ export type TNegativeOperatorConfig = { allowNegative: true; negOperatorLabel?:
* - negativeOperatorConfig: Configuration for negative operators
*/
export type TBaseFilterFieldConfig = {
isOperatorEnabled?: boolean;
operatorLabel?: string;
} & TNegativeOperatorConfig;

View file

@ -26,13 +26,16 @@ export const CORE_COMPARISON_OPERATOR = {
RANGE: "range",
} as const;
// -------- TYPE EXPORTS --------
type TCoreEqualityOperator = (typeof CORE_EQUALITY_OPERATOR)[keyof typeof CORE_EQUALITY_OPERATOR];
type TCoreCollectionOperator = (typeof CORE_COLLECTION_OPERATOR)[keyof typeof CORE_COLLECTION_OPERATOR];
type TCoreComparisonOperator = (typeof CORE_COMPARISON_OPERATOR)[keyof typeof CORE_COMPARISON_OPERATOR];
/**
* All core operators
*/
export const CORE_OPERATORS = {
...CORE_EQUALITY_OPERATOR,
...CORE_COLLECTION_OPERATOR,
...CORE_COMPARISON_OPERATOR,
} as const;
/**
* All core operators that can be used in filter conditions
*/
export type TCoreSupportedOperators = TCoreEqualityOperator | TCoreCollectionOperator | TCoreComparisonOperator;
export type TCoreSupportedOperators = (typeof CORE_OPERATORS)[keyof typeof CORE_OPERATORS];

View file

@ -18,16 +18,15 @@ export const EXTENDED_COLLECTION_OPERATOR = {} as const;
*/
export const EXTENDED_COMPARISON_OPERATOR = {} as const;
// -------- TYPE EXPORTS --------
type TExtendedEqualityOperator = (typeof EXTENDED_EQUALITY_OPERATOR)[keyof typeof EXTENDED_EQUALITY_OPERATOR];
type TExtendedCollectionOperator = (typeof EXTENDED_COLLECTION_OPERATOR)[keyof typeof EXTENDED_COLLECTION_OPERATOR];
type TExtendedComparisonOperator = (typeof EXTENDED_COMPARISON_OPERATOR)[keyof typeof EXTENDED_COMPARISON_OPERATOR];
/**
* All extended operators
*/
export const EXTENDED_OPERATORS = {
...EXTENDED_EQUALITY_OPERATOR,
...EXTENDED_COLLECTION_OPERATOR,
...EXTENDED_COMPARISON_OPERATOR,
} as const;
/**
* All extended operators that can be used in filter conditions
*/
export type TExtendedSupportedOperators =
| TExtendedEqualityOperator
| TExtendedCollectionOperator
| TExtendedComparisonOperator;
export type TExtendedSupportedOperators = (typeof EXTENDED_OPERATORS)[keyof typeof EXTENDED_OPERATORS];

View file

@ -1,4 +1,6 @@
import { TIssue } from "./issues/issue";
import { LOGICAL_OPERATOR, TSupportedOperators } from "./rich-filters";
import { CompleteOrEmpty } from "./utils";
export type TIssueLayouts = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
@ -47,7 +49,7 @@ export type TIssueOrderByOptions =
| "sub_issues_count"
| "-sub_issues_count";
export type TIssueGroupingFilters = "active" | "backlog" | null;
export type TIssueGroupingFilters = "active" | "backlog";
export type TIssueExtraOptions = "show_empty_groups" | "sub_issue";
@ -76,10 +78,47 @@ export type TIssueParams =
| "per_page"
| "issue_type"
| "layout"
| "expand";
| "expand"
| "filters";
export type TCalendarLayouts = "month" | "week";
/**
* Keys for the work item filter properties
*/
export const WORK_ITEM_FILTER_PROPERTY_KEYS = [
"state_group",
"priority",
"start_date",
"target_date",
"assignee_id",
"mention_id",
"created_by_id",
"subscriber_id",
"label_id",
"state_id",
"cycle_id",
"module_id",
"project_id",
] as const;
export type TWorkItemFilterProperty = (typeof WORK_ITEM_FILTER_PROPERTY_KEYS)[number];
export type TWorkItemFilterConditionKey = `${TWorkItemFilterProperty}__${TSupportedOperators}`;
export type TWorkItemFilterConditionData = Partial<{
[K in TWorkItemFilterConditionKey]: string;
}>;
export type TWorkItemFilterAndGroup = {
[LOGICAL_OPERATOR.AND]: TWorkItemFilterConditionData[];
};
export type TWorkItemFilterGroup = TWorkItemFilterAndGroup;
export type TWorkItemFilterExpressionData = TWorkItemFilterConditionData | TWorkItemFilterGroup;
export type TWorkItemFilterExpression = CompleteOrEmpty<TWorkItemFilterExpressionData>;
export interface IIssueFilterOptions {
assignees?: string[] | null;
mentions?: string[] | null;
@ -109,7 +148,6 @@ export interface IIssueDisplayFilterOptions {
order_by?: TIssueOrderByOptions;
show_empty_groups?: boolean;
sub_issue?: boolean;
type?: TIssueGroupingFilters;
}
export interface IIssueDisplayProperties {
assignee?: boolean;
@ -136,14 +174,20 @@ export type TIssueKanbanFilters = {
};
export interface IIssueFilters {
filters: IIssueFilterOptions | undefined;
richFilters: TWorkItemFilterExpression;
displayFilters: IIssueDisplayFilterOptions | undefined;
displayProperties: IIssueDisplayProperties | undefined;
kanbanFilters: TIssueKanbanFilters | undefined;
}
export interface IIssueFiltersResponse {
export type TSupportedFilterForUpdate = IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters;
export interface ISubWorkItemFilters extends Omit<IIssueFilters, "richFilters"> {
filters: IIssueFilterOptions;
}
export interface IIssueFiltersResponse {
rich_filters: TWorkItemFilterExpression;
display_filters: IIssueDisplayFilterOptions;
display_properties: IIssueDisplayProperties;
}
@ -172,17 +216,16 @@ export interface IWorkspaceViewIssuesParams {
target_date?: string | undefined;
project?: string | undefined;
order_by?: string | undefined;
type?: "active" | "backlog" | undefined;
sub_issue?: boolean;
}
export interface IProjectViewProps {
rich_filters: TWorkItemFilterExpression;
display_filters: IIssueDisplayFilterOptions | undefined;
filters: IIssueFilterOptions;
}
export interface IWorkspaceViewProps {
filters: IIssueFilterOptions;
rich_filters: TWorkItemFilterExpression;
display_filters: IIssueDisplayFilterOptions | undefined;
display_properties: IIssueDisplayProperties;
}

View file

@ -1,5 +1,10 @@
import { TLogoProps } from "./common";
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, IIssueFilterOptions } from "./view-props";
import {
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
IIssueFilterOptions,
TWorkItemFilterExpression,
} from "./view-props";
export enum EViewAccess {
PRIVATE,
@ -16,7 +21,7 @@ export interface IProjectView {
updated_by: string;
name: string;
description: string;
filters: IIssueFilterOptions;
rich_filters: TWorkItemFilterExpression;
display_filters: IIssueDisplayFilterOptions;
display_properties: IIssueDisplayProperties;
query: IIssueFilterOptions;
@ -29,6 +34,10 @@ export interface IProjectView {
owned_by: string;
}
export interface IPublishedProjectView extends Omit<IProjectView, "rich_filters"> {
filters: IIssueFilterOptions;
}
export type TPublishViewSettings = {
is_comments_enabled: boolean;
is_reactions_enabled: boolean;

View file

@ -2,7 +2,7 @@ import {
IWorkspaceViewProps,
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
IIssueFilterOptions,
TWorkItemFilterExpression,
} from "./view-props";
import { EViewAccess } from "./views";
@ -16,7 +16,7 @@ export interface IWorkspaceView {
updated_by: string;
name: string;
description: string;
filters: IIssueFilterOptions;
rich_filters: TWorkItemFilterExpression;
display_filters: IIssueDisplayFilterOptions;
display_properties: IIssueDisplayProperties;
query: any;
@ -32,4 +32,6 @@ export interface IWorkspaceView {
};
}
export type TStaticViewTypes = "all-issues" | "assigned" | "created" | "subscribed";
export const STATIC_VIEW_TYPES = ["all-issues", "assigned", "created", "subscribed"];
export type TStaticViewTypes = (typeof STATIC_VIEW_TYPES)[number];

View file

@ -1,6 +1,4 @@
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
// plane imports
import { IIssueFilters } from "@plane/types";
// local imports
import { getDate } from "./datetime";
@ -63,17 +61,3 @@ export const satisfiesDateFilter = (date: Date, filter: string): boolean => {
return false;
};
/**
* @description checks if the issue filter is active
* @param {IIssueFilters} issueFilters
* @returns {boolean}
*/
export const isIssueFilterActive = (issueFilters: IIssueFilters | undefined): boolean => {
if (!issueFilters) return false;
const issueType = issueFilters?.displayFilters?.type;
const isFiltersApplied = calculateTotalFilters(issueFilters?.filters ?? {}) !== 0 || !!issueType;
return isFiltersApplied;
};

View file

@ -28,5 +28,6 @@ export * from "./subscription";
export * from "./tab-indices";
export * from "./theme";
export * from "./url";
export * from "./work-item-filters";
export * from "./work-item";
export * from "./workspace";

View file

@ -1,30 +1,7 @@
// plane imports
import {
FILTER_FIELD_TYPE,
TFilterValue,
TFilterProperty,
TFilterConfig,
TSupportedOperators,
TBaseFilterFieldConfig,
} from "@plane/types";
import { FILTER_FIELD_TYPE, TFilterValue, TSupportedOperators, TBaseFilterFieldConfig } from "@plane/types";
// local imports
import {
createFilterFieldConfig,
DEFAULT_DATE_FILTER_TYPE_CONFIG,
DEFAULT_DATE_RANGE_FILTER_TYPE_CONFIG,
DEFAULT_MULTI_SELECT_FILTER_TYPE_CONFIG,
DEFAULT_SINGLE_SELECT_FILTER_TYPE_CONFIG,
IFilterIconConfig,
} from "./shared";
/**
* Helper to create a type-safe filter config
* @param config - The filter config to create
* @returns The created filter config
*/
export const createFilterConfig = <P extends TFilterProperty, V extends TFilterValue>(
config: TFilterConfig<P, V>
): TFilterConfig<P, V> => config;
import { createFilterFieldConfig, IFilterIconConfig } from "./shared";
// ------------ Selection filters ------------
@ -59,12 +36,11 @@ export const getSingleSelectConfig = <
TIconData extends string | number | boolean | object | undefined = undefined,
>(
transforms: TOptionTransforms<TItem, TValue, TIconData>,
config?: TSingleSelectConfig<TValue>,
config: TSingleSelectConfig<TValue>,
iconConfig?: IFilterIconConfig<TIconData>
) =>
createFilterFieldConfig<typeof FILTER_FIELD_TYPE.SINGLE_SELECT, TValue>({
type: FILTER_FIELD_TYPE.SINGLE_SELECT,
...DEFAULT_SINGLE_SELECT_FILTER_TYPE_CONFIG,
...config,
getOptions: () =>
transforms.items.map((item) => ({
@ -101,7 +77,6 @@ export const getMultiSelectConfig = <
) =>
createFilterFieldConfig<typeof FILTER_FIELD_TYPE.MULTI_SELECT, TValue>({
type: FILTER_FIELD_TYPE.MULTI_SELECT,
...DEFAULT_MULTI_SELECT_FILTER_TYPE_CONFIG,
...config,
operatorLabel: config?.operatorLabel,
getOptions: () =>
@ -136,10 +111,9 @@ export type TDateRangeConfig = TBaseFilterFieldConfig & {
* @param config - Date-specific configuration
* @returns The date picker config
*/
export const getDatePickerConfig = (config?: TDateConfig) =>
export const getDatePickerConfig = (config: TDateConfig) =>
createFilterFieldConfig<typeof FILTER_FIELD_TYPE.DATE, Date>({
type: FILTER_FIELD_TYPE.DATE,
...DEFAULT_DATE_FILTER_TYPE_CONFIG,
...config,
});
@ -148,9 +122,8 @@ export const getDatePickerConfig = (config?: TDateConfig) =>
* @param config - Date range-specific configuration
* @returns The date range picker config
*/
export const getDateRangePickerConfig = (config?: TDateRangeConfig) =>
export const getDateRangePickerConfig = (config: TDateRangeConfig) =>
createFilterFieldConfig<typeof FILTER_FIELD_TYPE.DATE_RANGE, Date>({
type: FILTER_FIELD_TYPE.DATE_RANGE,
...DEFAULT_DATE_RANGE_FILTER_TYPE_CONFIG,
...config,
});

View file

@ -10,8 +10,59 @@ import {
TMultiSelectFilterFieldConfig,
TSingleSelectFilterFieldConfig,
TSupportedFilterFieldConfigs,
TSupportedOperators,
} from "@plane/types";
/**
* Helper to create a type-safe filter config
* @param config - The filter config to create
* @returns The created filter config
*/
export const createFilterConfig = <P extends TFilterProperty, V extends TFilterValue>(
config: TFilterConfig<P, V>
): TFilterConfig<P, V> => config;
/**
* Base parameters for filter type config factory functions.
* - operator: The operator to use for the filter.
*/
export type TCreateFilterConfigParams = Omit<TBaseFilterFieldConfig, "isOperatorEnabled"> & {
isEnabled: boolean;
allowedOperators: Set<TSupportedOperators>;
};
/**
* Icon configuration for filters and their options.
* - filterIcon: Optional icon for the filter
* - getOptionIcon: Function to get icon for specific option values
*/
export interface IFilterIconConfig<T extends string | number | boolean | object | undefined = undefined> {
filterIcon?: React.FC<React.SVGAttributes<SVGElement>>;
getOptionIcon?: (value: T) => React.ReactNode;
}
/**
* Date filter config params
*/
export type TCreateDateFilterParams = TCreateFilterConfigParams & IFilterIconConfig<Date>;
/**
* Helper to create an operator entry for the supported operators map.
* This ensures consistency between the operator key and the operator passed to the config function.
* @param operator - The operator to use as both key and parameter
* @param createParams - The base filter configuration parameters
* @param configFn - Function that creates the operator config using base configuration
* @returns A tuple of operator and its config
*/
export const createOperatorConfigEntry = <T, P extends TCreateFilterConfigParams>(
operator: TSupportedOperators,
createParams: P,
configFn: (updatedParams: P) => T
): [TSupportedOperators, T] => [
operator,
configFn({ isOperatorEnabled: createParams.allowedOperators.has(operator), ...createParams }),
];
/**
* Factory function signature for creating filter configurations.
*/
@ -33,44 +84,3 @@ export const createFilterFieldConfig = <T extends TFilterFieldType, V extends TF
? TDateRangeFilterFieldConfig<V>
: never
): TSupportedFilterFieldConfigs<V> => config as TSupportedFilterFieldConfigs<V>;
/**
* Base parameters for filter type config factory functions.
* - operator: The operator to use for the filter.
*/
export type TCreateFilterConfigParams = TBaseFilterFieldConfig & {
isEnabled: boolean;
};
/**
* Icon configuration for filters and their options.
* - filterIcon: Optional icon for the filter
* - getOptionIcon: Function to get icon for specific option values
*/
export interface IFilterIconConfig<T extends string | number | boolean | object | undefined = undefined> {
filterIcon?: React.FC<React.SVGAttributes<SVGElement>>;
getOptionIcon?: (value: T) => React.ReactNode;
}
/**
* Date filter config params
*/
export type TCreateDateFilterParams = TCreateFilterConfigParams & IFilterIconConfig<Date>;
// ------------ Default filter type configs ------------
export const DEFAULT_SINGLE_SELECT_FILTER_TYPE_CONFIG = {
allowNegative: false,
};
export const DEFAULT_MULTI_SELECT_FILTER_TYPE_CONFIG = {
allowNegative: false,
};
export const DEFAULT_DATE_FILTER_TYPE_CONFIG = {
allowNegative: false,
};
export const DEFAULT_DATE_RANGE_FILTER_TYPE_CONFIG = {
allowNegative: false,
};

View file

@ -0,0 +1,70 @@
// plane imports
import {
EQUALITY_OPERATOR,
ICycle,
TCycleGroups,
TFilterProperty,
COLLECTION_OPERATOR,
TSupportedOperators,
} from "@plane/types";
// local imports
import {
createFilterConfig,
TCreateFilterConfigParams,
IFilterIconConfig,
TCreateFilterConfig,
getMultiSelectConfig,
createOperatorConfigEntry,
} from "../../../rich-filters";
/**
* Cycle filter specific params
*/
export type TCreateCycleFilterParams = TCreateFilterConfigParams &
IFilterIconConfig<TCycleGroups> & {
cycles: ICycle[];
};
/**
* Helper to get the cycle multi select config
* @param params - The filter params
* @returns The cycle multi select config
*/
export const getCycleMultiSelectConfig = (params: TCreateCycleFilterParams, singleValueOperator: TSupportedOperators) =>
getMultiSelectConfig<ICycle, string, TCycleGroups>(
{
items: params.cycles,
getId: (cycle) => cycle.id,
getLabel: (cycle) => cycle.name,
getValue: (cycle) => cycle.id,
getIconData: (cycle) => cycle.status || "draft",
},
{
singleValueOperator,
...params,
},
{
...params,
}
);
/**
* Get the cycle filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the cycle filter config
*/
export const getCycleFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateCycleFilterParams> =>
(params: TCreateCycleFilterParams) =>
createFilterConfig<P, string>({
id: key,
label: "Cycle",
icon: params.filterIcon,
isEnabled: params.isEnabled,
supportedOperatorConfigsMap: new Map([
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
getCycleMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
),
]),
});

View file

@ -0,0 +1,43 @@
// plane imports
import { TFilterProperty } from "@plane/types";
// local imports
import { createFilterConfig, TCreateFilterConfig, TCreateDateFilterParams } from "../../../rich-filters";
import { getSupportedDateOperators } from "./shared";
// ------------ Date filters ------------
/**
* Get the start date filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the start date filter config
*/
export const getStartDateFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateDateFilterParams> =>
(params: TCreateDateFilterParams) =>
createFilterConfig<P, Date>({
id: key,
label: "Start date",
icon: params.filterIcon,
isEnabled: params.isEnabled,
allowMultipleFilters: true,
supportedOperatorConfigsMap: getSupportedDateOperators(params),
});
/**
* Get the target date filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the target date filter config
*/
export const getTargetDateFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateDateFilterParams> =>
(params: TCreateDateFilterParams) =>
createFilterConfig<P, Date>({
id: key,
label: "Target date",
icon: params.filterIcon,
isEnabled: params.isEnabled,
allowMultipleFilters: true,
supportedOperatorConfigsMap: getSupportedDateOperators(params),
});

View file

@ -0,0 +1,8 @@
export * from "./cycle";
export * from "./date";
export * from "./label";
export * from "./module";
export * from "./priority";
export * from "./project";
export * from "./state";
export * from "./user";

View file

@ -0,0 +1,69 @@
// plane imports
import {
EQUALITY_OPERATOR,
IIssueLabel,
TFilterProperty,
COLLECTION_OPERATOR,
TSupportedOperators,
} from "@plane/types";
// local imports
import {
createFilterConfig,
TCreateFilterConfigParams,
IFilterIconConfig,
TCreateFilterConfig,
getMultiSelectConfig,
createOperatorConfigEntry,
} from "../../../rich-filters";
/**
* Label filter specific params
*/
export type TCreateLabelFilterParams = TCreateFilterConfigParams &
IFilterIconConfig<string> & {
labels: IIssueLabel[];
};
/**
* Helper to get the label multi select config
* @param params - The filter params
* @returns The label multi select config
*/
export const getLabelMultiSelectConfig = (params: TCreateLabelFilterParams, singleValueOperator: TSupportedOperators) =>
getMultiSelectConfig<IIssueLabel, string, string>(
{
items: params.labels,
getId: (label) => label.id,
getLabel: (label) => label.name,
getValue: (label) => label.id,
getIconData: (label) => label.color,
},
{
singleValueOperator,
...params,
},
{
getOptionIcon: params.getOptionIcon,
}
);
/**
* Get the label filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the label filter config
*/
export const getLabelFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateLabelFilterParams> =>
(params: TCreateLabelFilterParams) =>
createFilterConfig<P, string>({
id: key,
label: "Label",
icon: params.filterIcon,
isEnabled: params.isEnabled,
supportedOperatorConfigsMap: new Map([
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
getLabelMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
),
]),
});

View file

@ -0,0 +1,63 @@
// plane imports
import { EQUALITY_OPERATOR, IModule, TFilterProperty, COLLECTION_OPERATOR } from "@plane/types";
// local imports
import {
createFilterConfig,
TCreateFilterConfigParams,
IFilterIconConfig,
TCreateFilterConfig,
getMultiSelectConfig,
createOperatorConfigEntry,
} from "../../../rich-filters";
/**
* Module filter specific params
*/
export type TCreateModuleFilterParams = TCreateFilterConfigParams &
IFilterIconConfig<undefined> & {
modules: IModule[];
};
/**
* Helper to get the module multi select config
* @param params - The filter params
* @returns The module multi select config
*/
export const getModuleMultiSelectConfig = (params: TCreateModuleFilterParams) =>
getMultiSelectConfig<IModule, string, undefined>(
{
items: params.modules,
getId: (module) => module.id,
getLabel: (module) => module.name,
getValue: (module) => module.id,
getIconData: () => undefined,
},
{
singleValueOperator: EQUALITY_OPERATOR.EXACT,
...params,
},
{
...params,
}
);
/**
* Get the module filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the module filter config
*/
export const getModuleFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateModuleFilterParams> =>
(params: TCreateModuleFilterParams) =>
createFilterConfig<P, string>({
id: key,
label: "Module",
icon: params.filterIcon,
isEnabled: params.isEnabled,
supportedOperatorConfigsMap: new Map([
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
getModuleMultiSelectConfig(updatedParams)
),
]),
});

View file

@ -0,0 +1,66 @@
// plane imports
import { ISSUE_PRIORITIES, TIssuePriorities } from "@plane/constants";
import { EQUALITY_OPERATOR, TFilterProperty, COLLECTION_OPERATOR, TSupportedOperators } from "@plane/types";
// local imports
import {
createFilterConfig,
TCreateFilterConfigParams,
IFilterIconConfig,
TCreateFilterConfig,
getMultiSelectConfig,
createOperatorConfigEntry,
} from "../../../rich-filters";
// ------------ Priority filter ------------
/**
* Priority filter specific params
*/
export type TCreatePriorityFilterParams = TCreateFilterConfigParams & IFilterIconConfig<TIssuePriorities>;
/**
* Helper to get the priority multi select config
* @param params - The filter params
* @returns The priority multi select config
*/
export const getPriorityMultiSelectConfig = (
params: TCreatePriorityFilterParams,
singleValueOperator: TSupportedOperators
) =>
getMultiSelectConfig<{ key: TIssuePriorities; title: string }, TIssuePriorities, TIssuePriorities>(
{
items: ISSUE_PRIORITIES,
getId: (priority) => priority.key,
getLabel: (priority) => priority.title,
getValue: (priority) => priority.key,
getIconData: (priority) => priority.key,
},
{
singleValueOperator,
...params,
},
{
getOptionIcon: params.getOptionIcon,
}
);
/**
* Get the priority filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the priority filter config
*/
export const getPriorityFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreatePriorityFilterParams> =>
(params: TCreatePriorityFilterParams) =>
createFilterConfig<P, TIssuePriorities>({
id: key,
label: "Priority",
icon: params.filterIcon,
isEnabled: params.isEnabled,
supportedOperatorConfigsMap: new Map([
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
getPriorityMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
),
]),
});

View file

@ -0,0 +1,28 @@
// plane imports
import { EQUALITY_OPERATOR, TFilterProperty, COLLECTION_OPERATOR } from "@plane/types";
// local imports
import { createFilterConfig, createOperatorConfigEntry, TCreateFilterConfig } from "../../../rich-filters";
import { getProjectMultiSelectConfig, TCreateProjectFilterParams } from "./shared";
// ------------ Project filter ------------
/**
* Get the project filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the project filter config
*/
export const getProjectFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateProjectFilterParams> =>
(params: TCreateProjectFilterParams) =>
createFilterConfig<P, string>({
id: key,
label: "Projects",
icon: params.filterIcon,
isEnabled: params.isEnabled,
supportedOperatorConfigsMap: new Map([
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
getProjectMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
),
]),
});

View file

@ -0,0 +1,64 @@
// plane imports
import {
COMPARISON_OPERATOR,
EQUALITY_OPERATOR,
IProject,
TOperatorConfigMap,
TSupportedOperators,
} from "@plane/types";
// local imports
import {
createOperatorConfigEntry,
getDatePickerConfig,
getDateRangePickerConfig,
getMultiSelectConfig,
IFilterIconConfig,
TCreateDateFilterParams,
TCreateFilterConfigParams,
} from "../../../rich-filters";
// ------------ Date filter ------------
export const getSupportedDateOperators = (params: TCreateDateFilterParams): TOperatorConfigMap<Date> =>
new Map([
createOperatorConfigEntry(EQUALITY_OPERATOR.EXACT, params, (updatedParams) => getDatePickerConfig(updatedParams)),
createOperatorConfigEntry(COMPARISON_OPERATOR.RANGE, params, (updatedParams) =>
getDateRangePickerConfig(updatedParams)
),
]);
// ------------ Project filter ------------
/**
* Project filter specific params
*/
export type TCreateProjectFilterParams = TCreateFilterConfigParams &
IFilterIconConfig<IProject> & {
projects: IProject[];
};
/**
* Helper to get the project multi select config
* @param params - The filter params
* @returns The member multi select config
*/
export const getProjectMultiSelectConfig = (
params: TCreateProjectFilterParams,
singleValueOperator: TSupportedOperators
) =>
getMultiSelectConfig<IProject, string, IProject>(
{
items: params.projects,
getId: (project) => project.id,
getLabel: (project) => project.name,
getValue: (project) => project.id,
getIconData: (project) => project,
},
{
singleValueOperator,
...params,
},
{
...params,
}
);

View file

@ -0,0 +1,127 @@
// plane imports
import { STATE_GROUPS } from "@plane/constants";
import {
COLLECTION_OPERATOR,
EQUALITY_OPERATOR,
IState,
TFilterProperty,
TStateGroups,
TSupportedOperators,
} from "@plane/types";
// local imports
import {
createFilterConfig,
getMultiSelectConfig,
IFilterIconConfig,
TCreateFilterConfig,
TCreateFilterConfigParams,
createOperatorConfigEntry,
} from "../../../rich-filters";
// ------------ State group filter ------------
/**
* State group filter specific params
*/
export type TCreateStateGroupFilterParams = TCreateFilterConfigParams & IFilterIconConfig<TStateGroups>;
/**
* Helper to get the state group multi select config
* @param params - The filter params
* @returns The state group multi select config
*/
export const getStateGroupMultiSelectConfig = (
params: TCreateStateGroupFilterParams,
singleValueOperator: TSupportedOperators
) =>
getMultiSelectConfig<{ key: TStateGroups; label: string }, TStateGroups, TStateGroups>(
{
items: Object.values(STATE_GROUPS),
getId: (state) => state.key,
getLabel: (state) => state.label,
getValue: (state) => state.key,
getIconData: (state) => state.key,
},
{
singleValueOperator,
...params,
},
{
...params,
}
);
/**
* Get the state group filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the state group filter config
*/
export const getStateGroupFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateStateGroupFilterParams> =>
(params: TCreateStateGroupFilterParams) =>
createFilterConfig<P, TStateGroups>({
id: key,
label: "State Group",
icon: params.filterIcon,
isEnabled: params.isEnabled,
supportedOperatorConfigsMap: new Map([
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
getStateGroupMultiSelectConfig(updatedParams, EQUALITY_OPERATOR.EXACT)
),
]),
});
// ------------ State filter ------------
/**
* State filter specific params
*/
export type TCreateStateFilterParams = TCreateFilterConfigParams &
IFilterIconConfig<IState> & {
states: IState[];
};
/**
* Helper to get the state multi select config
* @param params - The filter params
* @returns The state multi select config
*/
export const getStateMultiSelectConfig = (params: TCreateStateFilterParams) =>
getMultiSelectConfig<IState, string, IState>(
{
items: params.states,
getId: (state) => state.id,
getLabel: (state) => state.name,
getValue: (state) => state.id,
getIconData: (state) => state,
},
{
singleValueOperator: EQUALITY_OPERATOR.EXACT,
...params,
},
{
...params,
}
);
/**
* Get the state filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the state filter config
*/
export const getStateFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateStateFilterParams> =>
(params: TCreateStateFilterParams) =>
createFilterConfig<P, string>({
id: key,
label: "State",
icon: params.filterIcon,
isEnabled: params.isEnabled,
supportedOperatorConfigsMap: new Map([
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
getStateMultiSelectConfig(updatedParams)
),
]),
});

View file

@ -0,0 +1,156 @@
// plane imports
import { EQUALITY_OPERATOR, IUserLite, TFilterProperty, COLLECTION_OPERATOR } from "@plane/types";
// local imports
import {
createFilterConfig,
TCreateFilterConfigParams,
IFilterIconConfig,
TCreateFilterConfig,
getMultiSelectConfig,
createOperatorConfigEntry,
} from "../../../rich-filters";
// ------------ Base User Filter Types ------------
/**
* User filter specific params
*/
export type TCreateUserFilterParams = TCreateFilterConfigParams &
IFilterIconConfig<IUserLite> & {
members: IUserLite[];
};
/**
* Helper to get the member multi select config
* @param params - The filter params
* @returns The member multi select config
*/
export const getMemberMultiSelectConfig = (params: TCreateUserFilterParams) =>
getMultiSelectConfig<IUserLite, string, IUserLite>(
{
items: params.members,
getId: (member) => member.id,
getLabel: (member) => member.display_name,
getValue: (member) => member.id,
getIconData: (member) => member,
},
{
singleValueOperator: EQUALITY_OPERATOR.EXACT,
...params,
},
{
...params,
}
);
// ------------ Assignee filter ------------
/**
* Assignee filter specific params
*/
export type TCreateAssigneeFilterParams = TCreateUserFilterParams;
/**
* Get the assignee filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the assignee filter config
*/
export const getAssigneeFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateAssigneeFilterParams> =>
(params: TCreateAssigneeFilterParams) =>
createFilterConfig<P, string>({
id: key,
label: "Assignees",
icon: params.filterIcon,
isEnabled: params.isEnabled,
supportedOperatorConfigsMap: new Map([
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
getMemberMultiSelectConfig(updatedParams)
),
]),
});
// ------------ Mention filter ------------
/**
* Mention filter specific params
*/
export type TCreateMentionFilterParams = TCreateUserFilterParams;
/**
* Get the mention filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the mention filter config
*/
export const getMentionFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateMentionFilterParams> =>
(params: TCreateMentionFilterParams) =>
createFilterConfig<P, string>({
id: key,
label: "Mentions",
icon: params.filterIcon,
isEnabled: params.isEnabled,
supportedOperatorConfigsMap: new Map([
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
getMemberMultiSelectConfig(updatedParams)
),
]),
});
// ------------ Created by filter ------------
/**
* Created by filter specific params
*/
export type TCreateCreatedByFilterParams = TCreateUserFilterParams;
/**
* Get the created by filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the created by filter config
*/
export const getCreatedByFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateCreatedByFilterParams> =>
(params: TCreateCreatedByFilterParams) =>
createFilterConfig<P, string>({
id: key,
label: "Created by",
icon: params.filterIcon,
isEnabled: params.isEnabled,
supportedOperatorConfigsMap: new Map([
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
getMemberMultiSelectConfig(updatedParams)
),
]),
});
// ------------ Subscriber filter ------------
/**
* Subscriber filter specific params
*/
export type TCreateSubscriberFilterParams = TCreateUserFilterParams;
/**
* Get the subscriber filter config
* @template K - The filter key
* @param key - The filter key to use
* @returns A function that takes parameters and returns the subscriber filter config
*/
export const getSubscriberFilterConfig =
<P extends TFilterProperty>(key: P): TCreateFilterConfig<P, TCreateSubscriberFilterParams> =>
(params: TCreateSubscriberFilterParams) =>
createFilterConfig<P, string>({
id: key,
label: "Subscriber",
icon: params.filterIcon,
isEnabled: params.isEnabled,
supportedOperatorConfigsMap: new Map([
createOperatorConfigEntry(COLLECTION_OPERATOR.IN, params, (updatedParams) =>
getMemberMultiSelectConfig(updatedParams)
),
]),
});

View file

@ -0,0 +1 @@
export * from "./filters";

View file

@ -0,0 +1 @@
export * from "./configs";

View file

@ -4,15 +4,16 @@ import { v4 as uuidv4 } from "uuid";
// plane imports
import {
ISSUE_DISPLAY_FILTERS_BY_PAGE,
STATE_GROUPS,
TIssuePriorities,
ISSUE_PRIORITY_FILTERS,
STATE_GROUPS,
TIssueFilterPriorityObject,
TIssuePriorities,
} from "@plane/constants";
import {
EIssueLayoutTypes,
IGanttBlock,
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
IGanttBlock,
TGroupedIssues,
TIssue,
TIssueGroupByOptions,
@ -21,7 +22,6 @@ import {
TStateGroups,
TSubGroupedIssues,
TUnGroupedIssues,
EIssueLayoutTypes,
} from "@plane/types";
// local imports
import { orderArrayBy } from "../array";
@ -111,25 +111,20 @@ export const handleIssueQueryParamsByLayout = (
| "team_issues"
| "team_project_work_items"
): TIssueParams[] | null => {
const queryParams: TIssueParams[] = [];
const queryParams: TIssueParams[] = ["filters"];
if (!layout) return null;
const layoutOptions = ISSUE_DISPLAY_FILTERS_BY_PAGE[viewType][layout];
// add filters query params
layoutOptions.filters.forEach((option) => {
queryParams.push(option);
});
const currentViewLayoutOptions = ISSUE_DISPLAY_FILTERS_BY_PAGE[viewType].layoutOptions[layout];
// add display filters query params
Object.keys(layoutOptions.display_filters).forEach((option) => {
Object.keys(currentViewLayoutOptions.display_filters).forEach((option) => {
queryParams.push(option as TIssueParams);
});
// add extra options query params
if (layoutOptions.extra_options.access) {
layoutOptions.extra_options.values.forEach((option) => {
if (currentViewLayoutOptions.extra_options.access) {
currentViewLayoutOptions.extra_options.values.forEach((option) => {
queryParams.push(option);
});
}
@ -286,7 +281,6 @@ export const getComputedDisplayFilters = (
order_by: filters?.order_by || "sort_order",
group_by: filters?.group_by || null,
sub_group_by: filters?.sub_group_by || null,
type: filters?.type || null,
sub_issue: filters?.sub_issue || false,
show_empty_groups: filters?.show_empty_groups || false,
};