Skip to content

[WEB-4885] feat: new filters architecture and UI components#7802

Merged
sriramveeraghanta merged 8 commits intopreviewfrom
feat-rich-filters
Sep 16, 2025
Merged

[WEB-4885] feat: new filters architecture and UI components#7802
sriramveeraghanta merged 8 commits intopreviewfrom
feat-rich-filters

Conversation

@prateekshourya29
Copy link
Member

@prateekshourya29 prateekshourya29 commented Sep 15, 2025

Description

Type of Change

  • Feature (non-breaking change which adds functionality)

Summary by CodeRabbit

  • New Features
    • Rich Filters UI: Add Filter button, per-condition editor, and Filters Row with Clear / Save / Update actions.
    • New filter inputs: single-select, multi-select, single-date, and date-range (including compact selected-options display).
    • Date pickers: can open by default; date-range popover can render in a portal for correct layering.
    • Dropdowns: support full-height option lists.
    • Time display: sub-second durations now show milliseconds.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 15, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a rich-filters subsystem (types, utils, factories, operations, validators, UI) and a shared-state rich-filters store; extends dropdowns (defaultOpen, portal rendering) and UI helpers; reorganizes many type/constant barrels and adds related package dependencies and exports.

Changes

Cohort / File(s) Summary
Web dropdowns
apps/web/core/components/dropdowns/date.tsx, apps/web/core/components/dropdowns/date-range.tsx
Add defaultOpen?: boolean prop to Date and DateRange; add renderInPortal?: boolean to DateRange; use createPortal for popover when enabled; initial open state derived from defaultOpen; conditional arrow rendering; popover still positioned with Popper.
Rich-filters — web UI components
apps/web/core/components/rich-filters/*
Add AddFilterButton, FilterItem, FiltersRow, FilterValueInput (root) and specialized inputs (date single/range, select single/multi), SelectedOptionsDisplay, shared CSS constant and select helpers; integrate CustomSearchSelect, auto-open and portal behaviors, and wiring to IFilterInstance APIs.
Shared-state — rich-filters store
packages/shared-state/src/store/rich-filters/*, packages/shared-state/src/index.ts
Add FilterInstance store, FilterConfig and FilterConfigManager, FilterAdapter base class, FilterInstanceHelper, and re-exports; add utils/helpers and builder integration; expose store and utils via barrels.
Utils — rich-filters runtime & factories
packages/utils/src/rich-filters/**/*, packages/utils/src/index.ts
Add factories (configs/nodes), operations (manipulation/transformation/traversal/comparison), operators helpers, validators, values helpers, and multiple barrels to expose rich-filters utilities via packages/utils.
Types — rich-filters model & barrels
packages/types/src/rich-filters/**/*, packages/types/src/index.ts
Add rich-filters type surface: expression AST, operators (core/extended/aggregate), operator-configs, field-types, adapter, builder types, SingleOrArray utility, derived operator types, and new barrels; reorganize top-level types exports.
Constants — export reorg & rich-filters
packages/constants/src/index.ts, packages/constants/src/rich-filters/*
Reorder and consolidate barrel exports; add rich-filters re-exports and operator-label/option constants.
UI dropdown props/support
packages/ui/src/dropdowns/helper.tsx, packages/ui/src/dropdowns/custom-search-select.tsx
Extend maxHeight prop to include "full" and map it to max-h-full.
Datetime util
packages/utils/src/datetime.ts
formatDuration: add sub-second (ms) formatting and simplify hour label; add isValidDate type-guard.
Workspace and deps
apps/web/package.json, packages/shared-state/package.json, pnpm-workspace.yaml
Add @plane/shared-state dependency and workspace deps for constants/types/utils; add runtime deps (lodash, mobx-utils, uuid) and @types/uuid to workspace catalog.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as AddFilterButton / FilterItem
  participant CM as FilterConfigManager
  participant FI as FilterInstance
  participant Ops as operators (utils)
  participant Ext as External consumer

  UI->>CM: read allAvailableConfigs
  UI->>UI: render add-filter options
  UI-->>UI: user selects property
  UI->>CM: getConfigByProperty(property)
  CM-->>UI: config (with firstOperator)
  UI->>Ops: getOperatorForPayload(firstOperator)
  Ops-->>UI: { operator, isNegation }
  UI->>FI: addCondition(LOGICAL_OPERATOR.AND, payload, isNegation)
  FI->>FI: sanitize/stabilize expression (utils/operations)
  FI->>Ext: onExpressionChange(externalExpression)
Loading
sequenceDiagram
  autonumber
  participant DateUI as DateRangeDropdown
  participant Portal as document.body
  participant Popper as usePopper

  DateUI->>DateUI: mount (defaultOpen?)
  alt renderInPortal true
    DateUI->>Portal: createPortal(Options)
  else
    DateUI->>DateUI: render Options inline
  end
  DateUI->>Popper: compute popover position
  DateUI-->>User: user selects range -> onSelect -> update condition
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

✨feature, 🌐frontend, 🛠️refactor

Poem

I nibble on logic by moonlight and sun,
stitching filters and barrels till all is one.
Dates leap through portals, ANDs find their nest—
tokens and types in a tidy, warm chest.
Hop happy, dear devs; ship it with zest! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description is effectively the template placeholder with only the "Feature" box checked and contains no detailed "Description", Test Scenarios, screenshots, or references; given the extensive, cross-cutting changes (new packages, public exports, stores, types, and UI components) the current description does not provide reviewers with sufficient context, verification steps, or risk/migration notes. Please complete the repository template: add a clear "Description" summarizing goals and high-level changes (notable files/packages and public API impacts), include Test Scenarios and steps to verify (unit/integration/manual), attach screenshots or media for UI changes, and list any related issues or migration notes so reviewers can assess risk and validate behavior.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "[WEB-4885] feat: new filters architecture and UI components" is concise, includes the ticket reference, and accurately summarizes the primary change set (new filters architecture and UI components) reflected across the added rich-filters types, store, utilities, and UI components in the diff.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat-rich-filters

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@makeplane
Copy link

makeplane bot commented Sep 15, 2025

Pull Request Linked with Plane Work Items

Comment Automatically Generated by Plane

@prateekshourya29 prateekshourya29 marked this pull request as ready for review September 16, 2025 11:31
cursor[bot]

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/utils/src/datetime.ts (1)

583-590: isValidDate() is incorrect for Date inputs and misaligned with its type guard.

It returns true only for strings; valid Date objects return false. Leverage date‑fns isValid and parseISO.

-export const isValidDate = (date: unknown): date is string | Date =>
-  typeof date === "string" && !isNaN(Date.parse(date));
+export const isValidDate = (date: unknown): date is string | Date => {
+  if (date instanceof Date) return isValid(date);
+  if (typeof date === "string") {
+    const parsed = parseISO(date);
+    return isValid(parsed);
+  }
+  return false;
+}

Optional: consider a less confusing name (e.g., isParsableDate) to avoid ambiguity with date‑fns isValid in call sites.

apps/web/package.json (1)

80-80: Align @types/uuid to workspace catalog (use 9.0.8)

pnpm-lock.yaml shows both @types/uuid@8.3.4 and @types/uuid@9.0.8; update apps/web/package.json (line 80) from "^8.3.4" to the workspace-pinned 9.0.8 (or remove if the types are unnecessary) to avoid duplicate typings.

packages/constants/src/rich-filters/operator-labels/extended.ts (1)

1-22: Empty constant maps need to be populated.

All four operator label maps are initialized as empty objects. This appears to be incomplete implementation, as the maps are typed to hold operator labels but contain no actual mappings. The empty Record<never, string> types for negated maps are also suspicious.

These maps should contain the actual label mappings for extended operators. For example:

-export const EXTENDED_OPERATOR_LABELS_MAP: Record<TExtendedSupportedOperators, string> = {} as const;
+export const EXTENDED_OPERATOR_LABELS_MAP: Record<TExtendedSupportedOperators, string> = {
+  // Add actual operator mappings here
+  // Example: [SOME_OPERATOR]: "Operator Label",
+} as const;

The Record<never, string> type for negated maps indicates they can never have any properties, which seems incorrect if these are meant to be populated later.

🧹 Nitpick comments (86)
packages/utils/src/datetime.ts (1)

569-569: Unit copy uses "hr" for all counts. Confirm consistency.

Elsewhere you use "min"/"sec". If product copy expects "hrs" for plurals, adjust here.

packages/types/src/utils.ts (1)

9-9: Useful alias; semantics LGTM.

SingleOrArray<T> correctly preserves null | undefined and widens others to T | T[].

Consider adding a brief doc comment and/or alias MaybeArray<T> for discoverability.

packages/ui/src/dropdowns/helper.tsx (1)

1-1: Remove or action the FIXME.

Top-level FIXME without context creates noise; either replace with a specific TODO/issue link or drop it.

-// FIXME: fix this!!!
+// TODO(UX): track dropdown helper cleanup in issue WEB-XXXX if needed.
packages/constants/src/rich-filters/option.ts (1)

60-63: Consider optional config and stronger typing.

If config is optional at call-sites, make it optional; also allow narrowing via a generic when needed.

-export type TFilterOptions<E extends TExternalFilter> = {
-  expression: Partial<TExpressionOptions<E>>;
-  config: Partial<TConfigOptions>;
-};
+export type TFilterOptions<
+  E extends TExternalFilter,
+  C extends TConfigOptions = TConfigOptions
+> = {
+  expression: Partial<TExpressionOptions<E>>;
+  config?: Partial<C>;
+};
packages/ui/src/dropdowns/custom-search-select.tsx (4)

160-166: Clamp “full” height to the viewport (not parent).

In a portal, max-h-full can exceed the viewport if no ancestor has an explicit height. Prefer viewport‑based caps.

Apply this diff:

-                        "max-h-full": maxHeight === "full",
+                        // Use viewport-based cap to avoid overflow in portals
+                        "max-h-screen max-h-[100dvh]": maxHeight === "full",

Confirm Tailwind arbitrary values are enabled; otherwise use only max-h-screen.


54-61: Close on selection for keyboard users (single‑select).

onClick on Option won’t fire for keyboard selection; close via onChange when !multiple.

Apply this diff (moves comboboxProps after closeDropdown and wraps onChange):

-  const comboboxProps: any = {
-    value,
-    onChange,
-    disabled,
-  };
-
-  if (multiple) comboboxProps.multiple = true;
+  // after closeDropdown()
+  const handleChange = (val: any) => {
+    onChange?.(val);
+    if (!multiple) closeDropdown();
+  };
+  const comboboxProps: any = {
+    value,
+    onChange: handleChange,
+    disabled,
+    ...(multiple ? { multiple: true } : {}),
+  };

Also applies to: 68-71


90-93: Avoid duplicate onOpen calls.

You call onOpen inside openDropdown() and also when Combobox render‑prop open flips. This can double‑fire.

Apply this diff to rely on the controlled isOpen path:

-      {({ open }: { open: boolean }) => {
-        if (open && onOpen) onOpen();
+      {() => {

141-144: Prevent nested scrollbars.

Outer container already wraps a scrollable list; overflow-y-scroll here causes double scrollbars.

Apply this diff:

-                    className={cn(
-                      "my-1 overflow-y-scroll rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-48 whitespace-nowrap z-30",
+                    className={cn(
+                      "my-1 overflow-hidden rounded-md border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none min-w-48 whitespace-nowrap z-30",
apps/web/core/components/dropdowns/date.tsx (2)

45-46: Optional: focus the trigger when opened by default.

Call onOpen after mount if defaultOpen to keep behavior consistent with manual open.

React.useEffect(() => {
  if (defaultOpen) onOpen?.();
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultOpen]);

45-46: Prop is “default” only; changes after mount are ignored.

If you expect defaultOpen to react to prop changes, sync it into isOpen via an effect (as above) or document that it’s initial‑only.

Also applies to: 65-65

packages/utils/src/rich-filters/operations/transformation/shared.ts (1)

5-11: Docstring claims OR/NOT support that doesn’t exist in types

Current types limit TFilterGroupNode to AND groups and processGroupNode only handles onAndGroup. Please align the doc to avoid confusion.

Apply:

- * Handles AND/OR groups with children and NOT groups with single child.
+ * Handles AND groups by transforming their children.
packages/utils/src/rich-filters/validators/shared.ts (1)

12-22: Unused parameter and misleading docs; simplify to the actual behavior

_preserveNotGroups is unused and the code simply unwraps when there is exactly one child. Either implement the param or remove it and tighten the docs.

Apply:

-export const shouldUnwrapGroup = <P extends TFilterProperty>(group: TFilterGroupNode<P>, _preserveNotGroups = true) => {
+export const shouldUnwrapGroup = <P extends TFilterProperty>(group: TFilterGroupNode<P>): boolean => {
   const children = getGroupChildren(group);
@@
-  // Unwrap AND/OR groups with single children, and NOT groups if preserveNotGroups is false
+  // Unwrap groups with a single child
   return true;
 };

And update the docstring accordingly (AND-only).

packages/types/src/rich-filters/field-types/extended.ts (2)

1-1: Use type‑only import to avoid accidental value import

Minor hygiene tweak.

-import { TFilterValue } from "../expression";
+import type { TFilterValue } from "../expression";

6-13: Placeholder surface is fine; consider a short rationale comment

Using an empty const and never config is an acceptable placeholder. Add a note so future contributors understand why it’s empty.

-export const EXTENDED_FILTER_FIELD_TYPE = {} as const;
+// Intentionally empty: extended field types are wired by consumers or future releases.
+export const EXTENDED_FILTER_FIELD_TYPE = {} as const;
packages/types/src/rich-filters/operator-configs/extended.ts (1)

1-14: Use type-only import and document unknown intent

Switch the import to a type-only import and add a short comment explaining that unknown is intentional so Core & Extended preserves Core keys. Confirmed TOperatorSpecificConfigs uses intersection (&) in packages/types/src/rich-filters/operator-configs/index.ts.

-import { TFilterValue } from "../expression";
+import type { TFilterValue } from "../expression";
@@
-// ----------------------------- Extended Operator Specific Configs -----------------------------
-export type TExtendedOperatorSpecificConfigs<_V extends TFilterValue> = unknown;
+// ----------------------------- Extended Operator Specific Configs -----------------------------
+// Keep as `unknown` so that `Core & Extended` preserves Core keys until Extended is defined.
+export type TExtendedOperatorSpecificConfigs<_V extends TFilterValue> = unknown;
packages/utils/src/rich-filters/operations/traversal/shared.ts (1)

9-23: Docstring promises NOT-group handling that isn’t implemented.

Either implement NOT-context inversion or clarify the JSDoc to avoid misleading callers. For now, it’s a pass-through which can surface incorrect operators in display when a condition is under a NOT group.

Apply this doc tweak until negation is implemented:

- * Helper function to get the display operator for a condition.
- * This checks for NOT group context and applies negation if needed.
+ * Helper function to get the display operator for a condition.
+ * NOTE: Placeholder — currently returns operator as-is. TODO: Apply NOT-group negation when present.
packages/shared-state/src/utils/rich-filter.helper.ts (1)

35-44: Operator negation is hardcoded off.

getOperatorForPayload currently returns isNegation = false, so negated conditions passed into this helper will be lost. If negation isn’t supported yet, gate the feature and/or document it on the builder types to avoid silent drops.

packages/shared-state/src/store/rich-filters/adapter.ts (1)

4-10: Doc claims “Provides common utilities” but class exposes no utilities.

Tighten the comment to match reality or add protected helpers in a follow-up.

- * Provides common utilities for creating and manipulating filter nodes.
+ * Base contract for converting between external filter formats and internal expressions.
packages/types/src/rich-filters/field-types/index.ts (1)

19-21: Optional: export the key union for ergonomics.

Handy when switching on keys without importing the constant.

+export type TFilterFieldTypeKey = keyof typeof FILTER_FIELD_TYPE;
apps/web/core/components/dropdowns/date-range.tsx (2)

99-104: defaultOpen only seeds state; no focus sync.

If you expect focus on mount when defaultOpen = true, onOpen won’t run. Consider a mount-effect that calls onOpen when defaultOpen is true.

+ useEffect(() => {
+   if (defaultOpen) onOpen();
+   // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);

269-278: Consider closing on complete selection (optional).

Add a closeOnSelect?: boolean (default false) to align with common datepickers; when true and range is complete, setIsOpen(false).

- onSelect={(val: DateRange | undefined) => {
-   onSelect?.(val);
- }}
+ onSelect={(val: DateRange | undefined) => {
+   onSelect?.(val);
+   if (val?.from && val?.to && closeOnSelect) setIsOpen(false);
+ }}
apps/web/core/components/rich-filters/filter-value-input/root.tsx (3)

50-55: Type cast mismatch for condition in MULTI_SELECT branch.

Use the same “ForDisplay” condition type as other branches; current cast drops display-operator typing.

Apply this diff:

-        <MultiSelectFilterValueInput<P>
-          config={filterFieldConfig as TMultiSelectFilterFieldConfig<string>}
-          condition={condition as TFilterConditionNode<P, string>}
+        <MultiSelectFilterValueInput<P>
+          config={filterFieldConfig as TMultiSelectFilterFieldConfig<string>}
+          condition={condition as TFilterConditionNodeForDisplay<P, string>}
           isDisabled={isDisabled}
           onChange={(value) => onChange(value as SingleOrArray<V>)}
         />

35-45: Remove unsafe casts via typed type guards.

Define field-config type guards so each branch narrows without assertions. This also keeps V aligned with value kinds.

Additional code (new helpers; place in a shared types/util file):

export const isSingleSelect = <V extends TFilterValue>(
  c: TSupportedFilterFieldConfigs<V>
): c is TSingleSelectFilterFieldConfig<string> => c?.type === FILTER_FIELD_TYPE.SINGLE_SELECT;

export const isMultiSelect = <V extends TFilterValue>(
  c: TSupportedFilterFieldConfigs<V>
): c is TMultiSelectFilterFieldConfig<string> => c?.type === FILTER_FIELD_TYPE.MULTI_SELECT;

export const isDate = <V extends TFilterValue>(
  c: TSupportedFilterFieldConfigs<V>
): c is TDateFilterFieldConfig<string> => c?.type === FILTER_FIELD_TYPE.DATE;

export const isDateRange = <V extends TFilterValue>(
  c: TSupportedFilterFieldConfigs<V>
): c is TDateRangeFilterFieldConfig<string> => c?.type === FILTER_FIELD_TYPE.DATE_RANGE;

Then:

if (isSingleSelect(filterFieldConfig)) { /* no casts */ }
if (isMultiSelect(filterFieldConfig)) { /* no casts */ }
if (isDate(filterFieldConfig)) { /* no casts */ }
if (isDateRange(filterFieldConfig)) { /* no casts */ }

83-88: Fallback: add a11y hint.

Consider role="status" and aria-disabled to clarify non-interactive state.

-  <div className="h-full flex items-center px-4 text-xs text-custom-text-400 transition-opacity duration-200 cursor-not-allowed">
+  <div role="status" aria-disabled className="h-full flex items-center px-4 text-xs text-custom-text-400 transition-opacity duration-200 cursor-not-allowed">
packages/constants/src/rich-filters/operator-labels/index.ts (1)

18-23: Provide a safe accessor and trim redundant typing.

  • Add getOperatorLabel/getDateOperatorLabel with EMPTY_OPERATOR_LABEL fallback.
  • Optional: either keep explicit Record<…> or drop the trailing “as const” (both is redundant).

Additional code (new exports):

export const getOperatorLabel = (op: TAllAvailableOperatorsForDisplay) =>
  OPERATOR_LABELS_MAP[op] ?? EMPTY_OPERATOR_LABEL;

export const getDateOperatorLabel = (op: TAllAvailableDateFilterOperatorsForDisplay) =>
  DATE_OPERATOR_LABELS_MAP[op] ?? EMPTY_OPERATOR_LABEL;

Also applies to: 27-31

apps/web/core/components/rich-filters/filter-value-input/date/range.tsx (1)

47-55: Optional UX: indicate invalid state to screen readers.

If DateRangeDropdown supports it, pass aria-invalid/isInvalid when isIncomplete is true.

packages/utils/src/rich-filters/validators/core.ts (1)

33-52: Consider adding a default case for unknown node types.

While the current implementation handles condition and group nodes, it silently returns false for any unknown node types. Consider adding explicit handling or logging for unexpected node types.

 export const shouldNotifyChangeForExpression = <P extends TFilterProperty>(
   expression: TFilterExpression<P> | null
 ): boolean => {
   if (!expression) {
     return false;
   }
 
   // If it's a condition, check its value
   if (isConditionNode(expression)) {
     return hasValidValue(expression.value);
   }
 
   // If it's a group, check if any of its children have meaningful values
   if (isGroupNode(expression)) {
     const children = getGroupChildren(expression);
     return children.some((child) => shouldNotifyChangeForExpression(child));
   }
 
+  // Log unexpected node types in development
+  if (process.env.NODE_ENV === 'development') {
+    console.warn('Unexpected filter expression node type:', expression);
+  }
   return false;
 };
apps/web/core/components/rich-filters/filters-row.tsx (3)

54-58: Consider extracting magic number to a constant.

The 240ms timeout value should be extracted to a named constant for better maintainability and clarity about its purpose.

+const UPDATE_CONFIRMATION_DELAY_MS = 240; // Prevents UI flickering during update
+
 const handleUpdate = useCallback(async () => {
   setIsUpdating(true);
   await filter.updateView();
-  setTimeout(() => setIsUpdating(false), 240); // To avoid flickering
+  setTimeout(() => setIsUpdating(false), UPDATE_CONFIRMATION_DELAY_MS);
 }, [filter]);

83-83: Undefined constant COMMON_VISIBILITY_BUTTON_CLASSNAME.

The constant COMMON_VISIBILITY_BUTTON_CLASSNAME is used here but defined later at line 165. This works due to JavaScript hoisting but violates the principle of defining before use.

Move the constant definitions before the component:

+const COMMON_VISIBILITY_BUTTON_CLASSNAME = "py-0.5 px-2 text-custom-text-300 hover:text-custom-text-100 rounded-full";
+const COMMON_OPERATION_BUTTON_CLASSNAME = "py-1";
+
 export const FiltersRow = observer(
   <K extends TFilterProperty, E extends TExternalFilter>(props: TFiltersRowProps<K, E>) => {

168-185: Consider memoizing ElementTransition component.

The ElementTransition component could benefit from memoization since it only depends on its props and doesn't need to re-render when parent state changes.

-const ElementTransition = observer((props: TElementTransitionProps) => (
+const ElementTransition = React.memo(observer((props: TElementTransitionProps) => (
   <Transition
     show={props.show}
     enter="transition ease-out duration-200"
     enterFrom="opacity-0 scale-95"
     enterTo="opacity-100 scale-100"
     leave="transition ease-in duration-150"
     leaveFrom="opacity-100 scale-100"
     leaveTo="opacity-0 scale-95"
   >
     {props.children}
   </Transition>
-));
+)));
apps/web/core/components/rich-filters/filter-value-input/select/single.tsx (2)

31-33: Consider handling config changes more robustly.

The effect reloads options whenever config changes, but doesn't handle the case where options might already be an array (not a function). Also consider clearing options when config is undefined.

 useEffect(() => {
+  // If config has static options (array), set them directly
+  if ("getOptions" in config && Array.isArray(config.getOptions)) {
+    setOptions(config.getOptions);
+    return;
+  }
+  
+  // Otherwise load async options
   loadOptions({ config, setOptions, setLoading });
 }, [config]);

54-54: Verify defaultOpen behavior — avoid auto-opening every empty select

filters-row -> FilterItem -> FilterValueInput -> SingleSelectFilterValueInput; defaultOpen={!condition.value} will open every empty select when multiple filters render. Remove defaultOpen or gate it behind an explicit autoOpen prop (only true for newly-added conditions) to prevent multiple dropdowns opening at once. Files: apps/web/core/components/rich-filters/filter-value-input/select/single.tsx and the filters renderer (apps/web/core/components/rich-filters/filters-row.tsx / filter-item.tsx).

apps/web/core/components/rich-filters/filter-item.tsx (4)

66-67: Avoid using void as an expression statement.

The void operator is being used to force a reflow, but this is unconventional and the comment doesn't align with the actual mechanism.

Apply this diff to use a more conventional approach:

-        // Force a reflow to ensure the initial state is applied
-        void element.offsetWidth;
+        // Force a reflow to ensure the initial state is applied
+        element.offsetWidth; // eslint-disable-line @typescript-eslint/no-unused-expressions

75-77: Cleanup function modifies DOM state unexpectedly.

The cleanup function resets opacity and transform when the component unmounts or dependencies change, which could cause unwanted visual artifacts if the component is being removed from the DOM anyway.

Consider removing the cleanup function or only applying it when the component remains mounted:

-      return () => {
-        applyInitialStyles();
-      };
+      // No cleanup needed when unmounting

131-134: Consider semantic HTML for better accessibility.

Using a div with aria-disabled instead of a button element reduces accessibility for screen reader users.

The CustomSearchSelect's customButton should ideally render a semantic button element that properly conveys its interactive nature and disabled state to assistive technologies. Consider passing a button element or ensuring CustomSearchSelect handles the disabled state properly.


153-153: Consider a more specific aria-label.

The aria-label could be more descriptive by including the filter property being removed.

-            aria-label="Remove filter"
+            aria-label={`Remove ${filterConfig?.label || 'filter'}`}
packages/utils/src/rich-filters/types/core.ts (2)

45-50: Redundant type guard implementation.

The hasChildrenProperty function essentially duplicates the logic that isAndGroupNode already provides, since AND groups are defined to have a children property.

Consider removing this function or documenting why it's needed separately from isAndGroupNode if there's a specific use case.


48-49: Unnecessary type assertion.

The type assertion to { children?: unknown } is redundant since you're already checking the property exists on the original group parameter.

-  const groupWithChildren = group as { children?: unknown };
-  return "children" in group && Array.isArray(groupWithChildren.children);
+  return "children" in group && Array.isArray(group.children);
packages/utils/src/rich-filters/types/shared.ts (2)

28-28: Error message could be more helpful.

The error message doesn't properly stringify the group object, which would make debugging difficult.

-  throw new Error(`Invalid group node: unknown logical operator ${group}`);
+  throw new Error(`Invalid group node: unknown logical operator ${JSON.stringify(group.logicalOperator)}`);

32-32: Misleading comment about OR/NOT groups.

The comment mentions handling "AND/OR groups (children array) and NOT groups (single child)", but the implementation only handles AND groups.

Update the comment to accurately reflect the current implementation:

-/**
- * Gets the children of a group node, handling AND/OR groups (children array) and NOT groups (single child).
- * Uses processGroupNode for consistent group type handling.
- * @param group - The group node to get children from
- * @returns Array of child expressions
- */
+/**
+ * Gets the children of a group node. Currently only supports AND groups.
+ * Uses processGroupNode for consistent group type handling.
+ * @param group - The group node to get children from
+ * @returns Array of child expressions
+ */
packages/types/src/rich-filters/operator-configs/index.ts (1)

48-51: Consider documenting Map type usage.

Using a Map type in TypeScript type definitions is less common and might benefit from additional documentation about its intended usage pattern.

Consider adding a comment explaining when and why a Map is preferred over a regular object type here, or consider using a more conventional object type if the Map semantics aren't specifically required:

// Alternative object-based approach:
export type TOperatorConfigRecord<V extends TFilterValue> = {
  [K in keyof TOperatorSpecificConfigs<V>]: TOperatorSpecificConfigs<V>[K];
};
apps/web/core/components/rich-filters/filter-value-input/select/multi.tsx (2)

33-35: Add proper cleanup and dependency tracking for loadOptions effect.

The effect doesn't include config in its dependency array, which could lead to stale closures or missed updates when config changes. Additionally, consider handling cleanup for any pending async operations.

Apply this diff to fix the effect dependencies:

 useEffect(() => {
   loadOptions({ config, setOptions, setLoading });
-}, [config]);
+}, [config]);

Actually, the dependency is already correct. However, consider adding cleanup for pending async operations:

 useEffect(() => {
+  let isMounted = true;
+  const loadOptionsWithCleanup = async () => {
+    if ("getOptions" in config && typeof config.getOptions === "function") {
+      setLoading(true);
+      try {
+        const result = await config.getOptions();
+        if (isMounted) {
+          setOptions(result);
+        }
+      } catch (error) {
+        if (isMounted) {
+          console.error("Failed to load options:", error);
+        }
+      } finally {
+        if (isMounted) {
+          setLoading(false);
+        }
+      }
+    }
+  };
+  loadOptionsWithCleanup();
+  return () => {
+    isMounted = false;
+  };
-  loadOptions({ config, setOptions, setLoading });
 }, [config]);

37-39: Consider memoizing the handleSelectChange callback.

The handleSelectChange function is recreated on every render, which could potentially cause unnecessary re-renders of child components.

Apply this diff to optimize the callback:

+import React, { useState, useEffect, useMemo, useCallback } from "react";

-const handleSelectChange = (values: string[]) => {
-  onChange(values);
-};
+const handleSelectChange = useCallback((values: string[]) => {
+  onChange(values);
+}, [onChange]);
packages/utils/src/rich-filters/operators/core.ts (1)

1-1: Consider using direct property access instead of lodash get.

Since you're accessing simple object properties with known keys, using lodash's get function adds unnecessary overhead and an additional dependency.

Apply this diff to simplify the implementation:

-import get from "lodash/get";
 // plane imports
 import { DATE_OPERATOR_LABELS_MAP, EMPTY_OPERATOR_LABEL, OPERATOR_LABELS_MAP } from "@plane/constants";

Then update the functions:

 export const getOperatorLabel = (operator: TAllAvailableOperatorsForDisplay | undefined): string => {
   if (!operator) return EMPTY_OPERATOR_LABEL;
-  return get(OPERATOR_LABELS_MAP, operator, EMPTY_OPERATOR_LABEL);
+  return OPERATOR_LABELS_MAP[operator] ?? EMPTY_OPERATOR_LABEL;
 };

 export const getDateOperatorLabel = (operator: TAllAvailableDateFilterOperatorsForDisplay | undefined): string => {
   if (!operator) return EMPTY_OPERATOR_LABEL;
-  return get(DATE_OPERATOR_LABELS_MAP, operator, EMPTY_OPERATOR_LABEL);
+  return DATE_OPERATOR_LABELS_MAP[operator] ?? EMPTY_OPERATOR_LABEL;
 };
apps/web/core/components/rich-filters/filter-value-input/date/single.tsx (1)

22-44: Consider memoizing the onChange callback to prevent unnecessary re-renders.

The inline onChange handler creates a new function on every render, which could cause the DateDropdown to re-render unnecessarily.

Apply this diff to optimize the component:

+import React, { useCallback } from "react";

 export const SingleDateFilterValueInput = observer(
   <P extends TFilterProperty>(props: TSingleDateFilterValueInputProps<P>) => {
     const { config, condition, isDisabled, onChange } = props;
     // derived values
     const conditionValue = typeof condition.value === "string" ? condition.value : null;

+    const handleDateChange = useCallback((value: Date | null) => {
+      const formattedDate = value ? renderFormattedPayloadDate(value) : null;
+      onChange(formattedDate);
+    }, [onChange]);

     return (
       <DateDropdown
         value={conditionValue}
-        onChange={(value: Date | null) => {
-          const formattedDate = value ? renderFormattedPayloadDate(value) : null;
-          onChange(formattedDate);
-        }}
+        onChange={handleDateChange}
         buttonClassName={cn("rounded-none", {
apps/web/core/components/rich-filters/filter-value-input/select/selected-options-display.tsx (2)

36-46: Use option.id as key instead of array index.

Using array indices as React keys can lead to rendering issues when the list order changes. Since options have unique IDs, use them instead.

Apply this diff to use stable keys:

-{selectedOptions.slice(0, displayCount).map((option, index) => (
-  <React.Fragment key={index}>
+{selectedOptions.slice(0, displayCount).map((option, index) => (
+  <React.Fragment key={option.id}>

48-58: Transition component's show prop is redundant.

The show prop is hardcoded to true but the component is already conditionally rendered with remainingCount > 0. Consider removing the redundant prop.

Apply this diff to simplify:

 {remainingCount > 0 && (
   <Transition
-    show
     appear
     enter="transition-opacity duration-300"
     enterFrom="opacity-0"
     enterTo="opacity-100"
     className="text-custom-text-300 whitespace-nowrap ml-1"
   >
     +{remainingCount} more
   </Transition>
 )}
apps/web/core/components/rich-filters/add-filters-button.tsx (1)

39-50: Consider memoizing filterOptions to avoid recalculation on every render.

The transformation of allAvailableConfigs to options format happens on every render. Since this depends only on filter.configManager.allAvailableConfigs, it could be memoized.

Apply this diff to optimize:

+import React, { useMemo } from "react";

 export const AddFilterButton = observer(
   <P extends TFilterProperty, E extends TExternalFilter>(props: TAddFilterButtonProps<P, E>) => {
     // ... other code ...

-    // Transform available filter configs to CustomSearchSelect options format
-    const filterOptions = filter.configManager.allAvailableConfigs.map((config) => ({
-      value: config.id,
-      content: (
-        <div className="flex items-center gap-2 text-custom-text-200 transition-all duration-200 ease-in-out">
-          {config.icon && (
-            <config.icon className="size-4 text-custom-text-300 transition-transform duration-200 ease-in-out" />
-          )}
-          <span>{config.label}</span>
-        </div>
-      ),
-      query: config.label.toLowerCase(),
-    }));
+    // Transform available filter configs to CustomSearchSelect options format
+    const filterOptions = useMemo(() => 
+      filter.configManager.allAvailableConfigs.map((config) => ({
+        value: config.id,
+        content: (
+          <div className="flex items-center gap-2 text-custom-text-200 transition-all duration-200 ease-in-out">
+            {config.icon && (
+              <config.icon className="size-4 text-custom-text-300 transition-transform duration-200 ease-in-out" />
+            )}
+            <span>{config.label}</span>
+          </div>
+        ),
+        query: config.label.toLowerCase(),
+      })), [filter.configManager.allAvailableConfigs]);
packages/utils/src/rich-filters/operations/manipulation/core.ts (3)

35-38: Avoid in-place mutation of AND-group children

Pushing into children mutates the existing node, which can break memoization/identity checks in consumers. Return a new group with updated children.

-  if (isGroupNode(expression) && isAndGroupNode(expression)) {
-    expression.children.push(condition);
-    return expression;
-  }
+  if (isGroupNode(expression) && isAndGroupNode(expression)) {
+    // avoid in-place mutation to preserve referential stability
+    return { ...expression, children: [...getGroupChildren(expression), condition] };
+  }

61-71: Consider propagating shouldNotify intent (or remove it here)

You always return shouldNotify: false. If the transform pipeline uses this flag for downstream updates, make it a parameter or set true when a replacement occurs. Otherwise, remove the flag from this call-site to avoid confusion.


111-120: Annotate explicit return type

Make the return type clear for callers.

-export const unwrapGroupIfNeeded = <P extends TFilterProperty>(
+export const unwrapGroupIfNeeded = <P extends TFilterProperty>(
   group: TFilterGroupNode<P>,
   preserveNotGroups = true
-) => {
+): TFilterExpression<P> => {
apps/web/core/components/rich-filters/filter-value-input/select/shared.tsx (2)

13-28: Ensure deterministic state on load failure (and consider cancellation)

On error, options remain stale. Set an empty options list to keep UI consistent. Also consider a simple stale‑request guard or AbortController to avoid setting state after unmount.

     try {
       const result = await config.getOptions();
       setOptions(result);
     } catch (error) {
       console.error("Failed to load options:", error);
+      // reset options on failure
+      setOptions([]);
     } finally {
       setLoading?.(false);
     }

30-44: Minor: simplify query derivation

label is already a string; toString() is redundant.

-    query: option.label.toString().toLowerCase(),
+    query: option.label.toLowerCase(),
packages/types/src/rich-filters/operators/core.ts (1)

31-39: Export individual operator types (and logical) for completeness

Downstream code may need specific operator categories. Export these types and include logical as well.

-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];
+export type TCoreLogicalOperator = (typeof CORE_LOGICAL_OPERATOR)[keyof typeof CORE_LOGICAL_OPERATOR];
+export type TCoreEqualityOperator = (typeof CORE_EQUALITY_OPERATOR)[keyof typeof CORE_EQUALITY_OPERATOR];
+export type TCoreCollectionOperator = (typeof CORE_COLLECTION_OPERATOR)[keyof typeof CORE_COLLECTION_OPERATOR];
+export type TCoreComparisonOperator = (typeof CORE_COMPARISON_OPERATOR)[keyof typeof CORE_COMPARISON_OPERATOR];
 
 /**
  * All core operators that can be used in filter conditions
  */
 export type TCoreSupportedOperators = TCoreEqualityOperator | TCoreCollectionOperator | TCoreComparisonOperator;
packages/types/src/rich-filters/operators/index.ts (1)

40-43: Consider extracting operator-related types to a dedicated types file.

The operator type definitions are cleanly extracted from the const objects, providing good type safety. However, as this architecture grows, you might want to separate type definitions from the operator constant definitions for better organization.

packages/shared-state/src/store/rich-filters/config.ts (3)

150-156: Fix potential type safety issue in getDisplayOperatorByValue.

The type assertion as typeof operator on Line 153 could be unsafe. When operatorConfig.singleValueOperator is returned, its type might not match the generic parameter T passed to the function.

Consider adding a type guard or validation:

 getDisplayOperatorByValue: IFilterConfig<P, V>["getDisplayOperatorByValue"] = computedFn((operator, value) => {
   const operatorConfig = this.getOperatorConfig(operator);
   if (operatorConfig?.type === FILTER_FIELD_TYPE.MULTI_SELECT && (Array.isArray(value) ? value.length : 0) <= 1) {
-    return operatorConfig.singleValueOperator as typeof operator;
+    // Ensure the singleValueOperator is compatible with the expected return type
+    const singleOp = operatorConfig.singleValueOperator;
+    return singleOp as T;
   }
   return operator;
 });

195-204: Replace hasOwnProperty with Object.hasOwn for better safety.

The use of hasOwnProperty on Line 198 could be problematic if the object has a custom hasOwnProperty method. Use Object.hasOwn for better safety.

 mutate: IFilterConfig<P, V>["mutate"] = action((updates) => {
   runInAction(() => {
     for (const key in updates) {
-      if (updates.hasOwnProperty(key)) {
+      if (Object.hasOwn(updates, key)) {
         const configKey = key as keyof TFilterConfig<P, V>;
         set(this, configKey, updates[configKey]);
       }
     }
   });
 });

208-211: Document the purpose of _getAdditionalOperatorOptions hook.

The private _getAdditionalOperatorOptions method appears to be a hook for subclasses but currently returns undefined. Add documentation explaining its intended use case.

+  /**
+   * Hook method for subclasses to provide additional operator options.
+   * Override this method to add custom operator variants (e.g., negation options).
+   * @param operator - The base operator
+   * @param value - The current filter value
+   * @returns Additional operator option or undefined
+   */
   private _getAdditionalOperatorOptions = (
     _operator: TSupportedOperators,
     _value: V
   ): TOperatorOptionForDisplay | undefined => undefined;
packages/types/src/rich-filters/field-types/core.ts (2)

17-20: Extract TBaseDateFilterFieldConfig to shared types.

TBaseDateFilterFieldConfig is defined as a local type but might be useful in other contexts. Consider exporting it for reusability.

-type TBaseDateFilterFieldConfig = TBaseFilterFieldConfig & {
+export type TBaseDateFilterFieldConfig = TBaseFilterFieldConfig & {
   min?: Date;
   max?: Date;
 };

51-55: Document async behavior for getOptions function.

The getOptions property can be either a static array or an async function. Document the expected behavior when the promise is rejected and how loading states should be handled in the UI.

Also applies to: 63-68

packages/utils/src/rich-filters/factories/configs/shared.ts (2)

25-35: Type assertion in createFilterFieldConfig may hide type errors.

The function uses a type assertion (as TSupportedFilterFieldConfigs<V>) which bypasses TypeScript's type checking. This could hide type mismatches at compile time.

Consider using a type guard or validation function instead:

-): TSupportedFilterFieldConfigs<V> => config as TSupportedFilterFieldConfigs<V>;
+): TSupportedFilterFieldConfigs<V> => {
+  // Runtime validation could be added here if needed
+  return config as TSupportedFilterFieldConfigs<V>;
+};

50-53: Generic constraint on IFilterIconConfig is overly broad.

The generic constraint T extends string | number | boolean | object | undefined includes object which might be too permissive. Consider using a more specific constraint that matches the actual icon value types used in the application.

-export interface IFilterIconConfig<T extends string | number | boolean | object | undefined = undefined> {
+export interface IFilterIconConfig<T extends string | number | boolean | Date | undefined = undefined> {
   filterIcon?: React.FC<React.SVGAttributes<SVGElement>>;
   getOptionIcon?: (value: T) => React.ReactNode;
 }
packages/shared-state/src/store/rich-filters/filter.ts (3)

345-377: Add null check for config retrieval in updateConditionOperator.

The method retrieves configs using optional chaining but doesn't handle the case where the config might be null before calling getOperatorConfig.

 updateConditionOperator: IFilterInstance<P, E>["updateConditionOperator"] = action(
   (conditionId: string, operator: TSupportedOperators, isNegation: boolean) => {
     if (!this.expression) return;
     const conditionBeforeUpdate = cloneDeep(findNodeById(this.expression, conditionId));
     if (!conditionBeforeUpdate || conditionBeforeUpdate.type !== FILTER_NODE_TYPE.CONDITION) return;

+    const propertyConfig = this.configManager.getConfigByProperty(conditionBeforeUpdate.property);
+    if (!propertyConfig) {
+      console.warn(`No config found for property: ${conditionBeforeUpdate.property}`);
+      return;
+    }
+
     // Get the operator configs for the current and new operators
-    const currentOperatorConfig = this.configManager
-      .getConfigByProperty(conditionBeforeUpdate.property)
-      ?.getOperatorConfig(conditionBeforeUpdate.operator);
-    const newOperatorConfig = this.configManager
-      .getConfigByProperty(conditionBeforeUpdate.property)
-      ?.getOperatorConfig(operator);
+    const currentOperatorConfig = propertyConfig.getOperatorConfig(conditionBeforeUpdate.operator);
+    const newOperatorConfig = propertyConfig.getOperatorConfig(operator);

421-432: Improve error handling in clearFilters method.

The method uses console.warn for error cases but doesn't provide enough context about why the operation failed. Consider throwing an error or returning a result object.

 clearFilters: IFilterInstance<P, E>["clearFilters"] = action(async () => {
   if (this.canClearFilters) {
     const shouldNotify = shouldNotifyChangeForExpression(this.expression);
     this.expression = null;
-    await this.clearFilterOptions?.onFilterClear();
+    try {
+      await this.clearFilterOptions?.onFilterClear();
+    } catch (error) {
+      console.error("Failed to clear filters:", error);
+      // Restore expression on failure
+      throw error;
+    }
     if (shouldNotify) {
       this._notifyExpressionChange();
     }
   } else {
-    console.warn("Cannot clear filters: invalid expression or missing options.");
+    const reasons = [];
+    if (!this.expression) reasons.push("no active expression");
+    if (this.allConditionsForDisplay.length === 0) reasons.push("no conditions");
+    if (this.clearFilterOptions?.isDisabled) reasons.push("option disabled");
+    console.warn(`Cannot clear filters: ${reasons.join(", ")}`);
   }
 });

480-482: computedFn already memoizes; only optimize the deep-clone if profiling shows it’s necessary

  private _getExternalExpression = computedFn(() =>
    this.adapter.toExternal(sanitizeAndStabilizeExpression(toJS(this.expression)))
  );

Location: packages/shared-state/src/store/rich-filters/filter.ts:480-482

computedFn caches the result by tracking MobX observables accessed inside the function. _getExternalExpression is used in saveView, updateView and _notifyExpressionChange; the costly part is toJS (deep clone) which only runs when the expression observables change. Suggested optional refactors (only if you observe perf issues): avoid full toJS traversal (read only needed fields), memoize sanitizeAndStabilizeExpression/adapter.toExternal, or compute/cache the external representation once when the expression changes.

packages/types/src/rich-filters/expression.ts (3)

5-13: Docs claim OR/NOT, but the model only supports AND groups.

Comments mention AND/OR/NOT, while types alias groups to AND-only. Align docs or add OR/NOT node types before exposing them.

Also applies to: 75-80


66-73: Consider enforcing “≥2 children” at the type level for groups.

The comment states minimum 2, but the type allows any length. Optional but helps correctness.

Add a helper and tighten the children type:

// add near other shared types
type AtLeastTwo<T> = [T, T, ...T[]];
-  children: TFilterExpression<P>[];
+  children: AtLeastTwo<TFilterExpression<P>>;

31-34: Make base node identity immutable.

Prevent accidental mutation of id/type; payload updates already exclude them.

-type TBaseFilterNode = {
-  id: string;
-  type: TFilterNodeType;
-};
+type TBaseFilterNode = {
+  readonly id: string;
+  readonly type: TFilterNodeType;
+};
packages/utils/src/rich-filters/operations/comparison.ts (2)

44-56: Fix childrenCount logic; avoid referencing non-existent child property.

child.child is never set in comparables; use children?.length only.

-    if (child?.type === FILTER_NODE_TYPE.GROUP) {
-      const childrenCount = child.child ? 1 : Array.isArray(child.children) ? child.children.length : 0;
-      return `group_${child.logicalOperator}_${childrenCount}_${JSON.stringify(child)}`;
-    }
+    if (child?.type === FILTER_NODE_TYPE.GROUP) {
+      const childrenCount = Array.isArray(child.children) ? child.children.length : 0;
+      return `group_${child.logicalOperator}_${childrenCount}_${JSON.stringify(child)}`;
+    }

Also consider replacing the JSON.stringify(child) portion with a stable key generator to reduce sort cost on large trees.


35-37: Comment nit: only AND groups are supported today.

Avoid mentioning OR in this helper to prevent confusion.

- * Helper function to create comparable children for AND/OR groups.
+ * Helper function to create comparable children for AND groups.
packages/utils/src/rich-filters/factories/configs/core.ts (1)

102-114: Remove redundant operatorLabel reassignment.

...config already carries operatorLabel; re‑setting it is a no‑op.

   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: () =>
       transforms.items.map((item) => ({
packages/shared-state/src/store/rich-filters/filter-helpers.ts (4)

118-133: Restructure helper ignores isNegation and does not restructure groups.

Doc says it handles positive/negative operator changes; current impl only updates operator (and optionally value). Clarify doc or implement the restructuring (e.g., wrap in NOT group if/when supported).


75-78: Avoid double cloning (cloneDeep + toJS).

toJS already deep‑clones MobX observables and plain objects. Drop cloneDeep to cut overhead.

-    return this.adapter.toInternal(toJS(cloneDeep(initialExpression)));
+    return this.adapter.toInternal(toJS(initialExpression));

164-171: Tighten unsupported-operator path.

Warn‑and‑return can hide bugs. Either narrow accepted operators at the type boundary or make the default branch unmistakably unreachable.

-      default:
-        console.warn(`Unsupported logical operator: ${groupOperator}`);
-        return expression;
+      default: {
+        // Future: support OR/NOT. For now, return unchanged but surface loudly in dev.
+        if (process.env.NODE_ENV !== "production") {
+          // eslint-disable-next-line no-console
+          console.warn(`Unsupported logical operator: ${String(groupOperator)}`);
+        }
+        return expression;
+      }

51-54: JSDoc generic tag mismatch (K vs P).

Use P to match the class generic.

- * @template K - The filter property type extending TFilterProperty
+ * @template P - The filter property type extending TFilterProperty
packages/utils/src/rich-filters/operations/traversal/core.ts (2)

146-158: Remove impossible null check.

expression is non‑nullable in this signature.

 export const findImmediateParent = <P extends TFilterProperty>(
   expression: TFilterExpression<P>,
   targetId: string
 ): TFilterGroupNode<P> | null => {
-  // if the expression is null, return null
-  if (!expression) return null;
-
   // find the parent chain
   const parentChain = findParentChain(expression, targetId);

91-103: Optional: switch to early‑exit search to avoid full-tree allocation.

findNodeById currently walks the whole tree and allocates an array. A DFS with early return is simpler and faster.

 export const findNodeById = <P extends TFilterProperty>(
   expression: TFilterExpression<P>,
   targetId: string
 ): TFilterExpression<P> | null => {
-  const results = traverseExpressionTree(
-    expression,
-    (node) => (node.id === targetId ? node : null),
-    TreeTraversalMode.ALL
-  );
-
-  // Return the first match (there should only be one with unique IDs)
-  return results.length > 0 ? results[0] : null;
+  if (expression.id === targetId) return expression;
+  if (isGroupNode(expression)) {
+    const children = getGroupChildren(expression);
+    for (const child of children) {
+      const found = findNodeById(child, targetId);
+      if (found) return found;
+    }
+  }
+  return null;
 };
packages/types/src/rich-filters/derived/core.ts (2)

16-22: Consider simplifying the complex mapped type for better readability.

The nested mapped type with immediate indexing pattern could be more readable. Consider extracting the mapped type into a separate type alias for clarity.

You could improve readability with:

+type MappedOperatorHelpers<V extends TFilterValue, TConfig> = {
+  [K in keyof TCoreOperatorSpecificConfigs<V>]: TFilterOperatorHelper<
+    TCoreOperatorSpecificConfigs<V>,
+    K,
+    TConfig
+  >;
+};
+
 export type TCoreSupportedSingleDateFilterOperators<V extends TFilterValue = TFilterValue> = 
-  {
-    [K in keyof TCoreOperatorSpecificConfigs<V>]: TFilterOperatorHelper<
-      TCoreOperatorSpecificConfigs<V>,
-      K,
-      TDateFilterFieldConfig<V>
-    >;
-  }[keyof TCoreOperatorSpecificConfigs<V>];
+  MappedOperatorHelpers<V, TDateFilterFieldConfig<V>>[keyof TCoreOperatorSpecificConfigs<V>];

42-43: Consider removing redundant type aliases.

The ForDisplay type aliases (TCoreAllAvailableDateFilterOperatorsForDisplay and TCoreAllAvailableSelectFilterOperatorsForDisplay) are just re-exports without any transformation. Unless there's a specific semantic reason or future extension plan, these could be removed to reduce the API surface.

Also applies to: 76-77

packages/shared-state/src/store/rich-filters/config-manager.ts (3)

108-110: Unnecessary type cast in getConfigByProperty.

The cast to IFilterConfig<P, TFilterValue> appears redundant since the Map is already typed correctly.

Remove the unnecessary cast:

 getConfigByProperty: IFilterConfigManager<P>["getConfigByProperty"] = computedFn(
-  (property) => this.filterConfigs.get(property) as IFilterConfig<P, TFilterValue>
+  (property) => this.filterConfigs.get(property)
 );

170-172: Improve null safety in config options initialization.

The conditional check for DEFAULT_FILTER_CONFIG_OPTIONS seems unusual. If it's falsy, you return potentially undefined options, which might not match TConfigOptions type.

Consider this safer approach:

 private _initializeConfigOptions(options?: Partial<TConfigOptions>): TConfigOptions {
-  return DEFAULT_FILTER_CONFIG_OPTIONS ? { ...DEFAULT_FILTER_CONFIG_OPTIONS, ...options } : options;
+  return { ...DEFAULT_FILTER_CONFIG_OPTIONS, ...options } ?? {};
 }

120-129: Consider adding validation for config updates.

The register method mutates existing configs without validation. Consider adding checks to ensure critical properties like id aren't changed during updates.

Add validation to prevent id changes:

 register: IFilterConfigManager<P>["register"] = action((configUpdates) => {
   if (this.filterConfigs.has(configUpdates.id)) {
     // Update existing config if it has differences
     const existingConfig = this.filterConfigs.get(configUpdates.id)!;
+    // Ensure id cannot be changed
+    const { id, ...safeUpdates } = configUpdates;
+    if (id !== existingConfig.id) {
+      console.warn(`Cannot change config id from ${existingConfig.id} to ${id}`);
+    }
-    existingConfig.mutate(configUpdates);
+    existingConfig.mutate(safeUpdates);
   } else {
     // Create new config if it doesn't exist
     this.filterConfigs.set(configUpdates.id, new FilterConfig(configUpdates));
   }
 });
packages/utils/src/rich-filters/operations/transformation/core.ts (3)

32-42: Remove duplicate JSDoc comment.

There's a duplicate JSDoc block for the createGroupTransformResult function (lines 24-31 and 32-35).

Remove the duplicate comment block:

-/**
- * Generic recursive tree transformer that handles common tree manipulation logic.
- * This function provides a reusable way to transform expression trees while maintaining
- * tree integrity, handling group restructuring, and applying stabilization.
- *
- * @param expression - The expression to transform
- * @param transformFn - Function that defines the transformation logic for each node
- * @returns The transformation result with expression and metadata
- */
 /**
  * Helper function to create a consistent transformation result for group nodes.
  * Centralizes the logic for wrapping group expressions and tracking notifications.
  */

75-78: Type assertion could be avoided with better typing.

The type assertion as TFilterGroupNode<P> on line 78 seems necessary due to the spread operator, but could potentially be avoided with more specific typing.

Consider using a type-safe helper function instead:

-  const updatedGroup: TFilterGroupNode<P> = {
-    ...group,
-    children: transformedChildren,
-  } as TFilterGroupNode<P>;
+  // Add a helper function to create type-safe group updates
+  const updateGroupChildren = <P extends TFilterProperty>(
+    group: TFilterGroupNode<P>, 
+    children: TFilterExpression<P>[]
+  ): TFilterGroupNode<P> => ({
+    ...group,
+    children
+  } as TFilterGroupNode<P>);
+  
+  const updatedGroup = updateGroupChildren(group, transformedChildren);

119-119: Consider more specific error message.

The error message could be more helpful by including the actual node type encountered.

Improve the error message:

-  throw new Error("Unknown expression type in transformExpressionTree");
+  throw new Error(`Unknown expression type in transformExpressionTree: ${(expression as any).type}`);
packages/types/src/index.ts (1)

27-27: TODO comment needs attention.

The TODO comment on line 27 indicates temporary exports from "./issues/base" that should be removed after stabilization.

Would you like me to help track this technical debt by creating an issue to ensure this TODO is addressed after the refactor/mobx-store-issue branch is stable?

cursor[bot]

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
packages/utils/src/rich-filters/operations/manipulation/core.ts (1)

92-97: Resolved: safeguard prevents corrupting group nodes.

The guard against applying condition updates to groups addresses the prior concern.

packages/utils/src/rich-filters/values/core.ts (1)

7-15: Fix unsafe cast; filter null/undefined while preserving falsy values (0, false, "")

Return type promises NonNullable[] but current casts can leave null/undefined when the input is an array — explicitly filter null/undefined.

File: packages/utils/src/rich-filters/values/core.ts (lines ~7-15)

-export const toFilterArray = <V extends TFilterValue>(value: SingleOrArray<V>): NonNullable<V>[] => {
-  if (value === null || value === undefined) {
-    return [];
-  }
-
-  return Array.isArray(value)
-    ? (value as NonNullable<V>[])
-    : ([value] as NonNullable<V>[]);
-};
+export const toFilterArray = <V extends TFilterValue>(value: SingleOrArray<V>): NonNullable<V>[] => {
+  if (value == null) return [];
+  const arr = Array.isArray(value) ? value : [value];
+  return arr.filter((v): v is NonNullable<V> => v != null);
+};

Add unit tests for: 0, false, "", null, undefined, [0, 1], [false, true], ["", "x"].

🧹 Nitpick comments (13)
packages/utils/src/rich-filters/operations/manipulation/core.ts (7)

2-8: Use type-only imports to avoid runtime side effects.

These are types only; switch to import type to keep bundles lean and prevent accidental value imports.

Apply:

-import {
+import type {
   TFilterConditionPayload,
   TFilterExpression,
   TFilterGroupNode,
   TFilterProperty,
   TFilterValue,
 } from "@plane/types";

35-38: Avoid in-place mutation of group children (preserve referential integrity).

push mutates the existing node; prefer returning a new object/array for consistent immutability with other helpers.

Apply:

-    expression.children.push(condition);
-    return expression;
+    return {
+      ...expression,
+      children: [...expression.children, condition],
+    };

43-46: Replace unreachable/weak error path with a thrown error.

Given current unions, this branch shouldn’t be hit. If it is, fail fast.

Apply:

-  console.error("Invalid expression type", expression);
-  return expression;
+  throw new Error("addAndCondition: invalid expression type");

22-25: Parameter naming/type mismatch with behavior.

You accept condition: TFilterExpression, which can be a group. Either rename to node or narrow to a condition node.


100-103: Tiny tidy-up: inline the children loop.

Removes the temporary variable without changing behavior.

Apply:

-      const children = getGroupChildren(node);
-      children.forEach((child) => updateNode(child));
+      getGroupChildren(node).forEach(updateNode);

77-87: Document mutation semantics (in-place).

This function mutates the passed expression tree. Callers need to know for state/update semantics.

Add to JSDoc: “Mutates the expression tree in place; does not return a new reference.”


115-124: preserveNotGroups is ignored downstream; unwrapping may be too aggressive.

shouldUnwrapGroup currently ignores its preserveNotGroups flag (see validators/shared.ts snippet), so NOT groups would unwrap when they shouldn’t.

Proposed fix in packages/utils/src/rich-filters/validators/shared.ts:

-export const shouldUnwrapGroup = <P extends TFilterProperty>(group: TFilterGroupNode<P>, _preserveNotGroups = true) => {
+export const shouldUnwrapGroup = <P extends TFilterProperty>(group: TFilterGroupNode<P>, preserveNotGroups = true) => {
   const children = getGroupChildren(group);

   if (children.length !== 1) return false;

-  // Unwrap AND/OR groups with single children, and NOT groups if preserveNotGroups is false
-  return true;
+  // If this is a NOT group and preservation is requested, do not unwrap
+  // (Pseudo: adjust predicate to your group shape/helpers)
+  // if (isNotGroupNode(group) && preserveNotGroups) return false;
+  return true;
}

If NOT groups aren’t modeled yet (types re-export AND only), either wire the flag through now or remove the param to avoid misleading APIs.

packages/utils/src/rich-filters/values/core.ts (1)

20-26: Make length calculation consistent with filtered values.

Currently counts null/undefined; can mis-drive UI (e.g., defaultOpen). Use toFilterArray length.

-export const getFilterValueLength = <V extends TFilterValue>(value: SingleOrArray<V>): number => {
-  if (value === null || value === undefined) {
-    return 0;
-  }
-
-  return Array.isArray(value) ? value.length : 1;
-};
+export const getFilterValueLength = <V extends TFilterValue>(value: SingleOrArray<V>): number => {
+  return toFilterArray(value).length;
+};
packages/shared-state/src/store/rich-filters/config-manager.ts (4)

95-99: Consider a more descriptive variable name for _allEnabledConfigs.

The underscore prefix _allEnabledConfigs suggests it's private, but it's actually referenced in a public computed property. Consider renaming to better reflect its filtered nature.

-    return this._allEnabledConfigs.filter((config) => config.allowMultipleFilters || !appliedProperties.has(config.id));
+    return this.enabledConfigs.filter((config) => config.allowMultipleFilters || !appliedProperties.has(config.id));

Also update the corresponding private getter:

-  private get _allEnabledConfigs(): IFilterConfig<P, TFilterValue>[] {
+  private get enabledConfigs(): IFilterConfig<P, TFilterValue>[] {

108-110: Consider adding null-safety check for type assertion.

The type assertion as IFilterConfig<P, TFilterValue> could be unsafe if the map doesn't contain the expected type. While the current implementation ensures type consistency, it's better to be explicit.

   getConfigByProperty: IFilterConfigManager<P>["getConfigByProperty"] = computedFn(
-    (property) => this.filterConfigs.get(property) as IFilterConfig<P, TFilterValue>
+    (property) => this.filterConfigs.get(property)
   );

The return type is already defined in the interface, so the assertion is redundant.


144-147: Add null check before mutating config.

The optional chaining prevConfig?.mutate silently fails if the config doesn't exist. Consider explicitly handling this case to help with debugging.

   updateConfigByProperty: IFilterConfigManager<P>["updateConfigByProperty"] = action((property, configUpdates) => {
     const prevConfig = this.filterConfigs.get(property);
-    prevConfig?.mutate(configUpdates);
+    if (!prevConfig) {
+      console.warn(`Config for property "${property}" not found. Register it first.`);
+      return;
+    }
+    prevConfig.mutate(configUpdates);
   });

170-172: Potential edge case with empty DEFAULT_FILTER_CONFIG_OPTIONS.

The ternary operator logic seems inverted - if DEFAULT_FILTER_CONFIG_OPTIONS is truthy (even if empty object), it spreads both defaults and options. But if it's falsy, it returns options || {}. This could lead to unexpected behavior.

   private _initializeConfigOptions(options?: Partial<TConfigOptions>): TConfigOptions {
-    return DEFAULT_FILTER_CONFIG_OPTIONS ? { ...DEFAULT_FILTER_CONFIG_OPTIONS, ...options } : options || {};
+    return { ...DEFAULT_FILTER_CONFIG_OPTIONS, ...(options || {}) };
   }
packages/types/src/index.ts (1)

26-26: Remove TODO comment after completing the refactor.

The TODO comment indicates temporary technical debt. Track this for removal once the refactor/mobx-store-issue branch is stable.

Would you like me to create an issue to track the removal of this temporary export once the refactor is complete?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8586a31 and 8bbe04c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • apps/web/core/components/dropdowns/date.tsx (4 hunks)
  • packages/constants/src/rich-filters/option.ts (1 hunks)
  • packages/shared-state/package.json (1 hunks)
  • packages/shared-state/src/store/rich-filters/config-manager.ts (1 hunks)
  • packages/types/src/index.ts (1 hunks)
  • packages/utils/src/datetime.ts (3 hunks)
  • packages/utils/src/rich-filters/operations/manipulation/core.ts (1 hunks)
  • packages/utils/src/rich-filters/values/core.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/web/core/components/dropdowns/date.tsx
  • packages/shared-state/package.json
  • packages/constants/src/rich-filters/option.ts
  • packages/utils/src/datetime.ts
🧰 Additional context used
🧬 Code graph analysis (3)
packages/utils/src/rich-filters/values/core.ts (2)
packages/types/src/rich-filters/expression.ts (1)
  • TFilterValue (24-24)
packages/types/src/utils.ts (1)
  • SingleOrArray (9-9)
packages/shared-state/src/store/rich-filters/config-manager.ts (6)
packages/types/src/rich-filters/expression.ts (2)
  • TFilterProperty (19-19)
  • TFilterValue (24-24)
packages/shared-state/src/store/rich-filters/config.ts (2)
  • IFilterConfig (28-46)
  • FilterConfig (48-212)
packages/constants/src/rich-filters/option.ts (2)
  • TConfigOptions (6-6)
  • DEFAULT_FILTER_CONFIG_OPTIONS (11-11)
packages/types/src/rich-filters/config/filter-config.ts (1)
  • TFilterConfig (11-18)
packages/types/src/rich-filters/adapter.ts (1)
  • TExternalFilter (7-7)
packages/shared-state/src/store/rich-filters/filter.ts (1)
  • IFilterInstance (63-109)
packages/utils/src/rich-filters/operations/manipulation/core.ts (6)
packages/types/src/rich-filters/expression.ts (5)
  • TFilterProperty (19-19)
  • TFilterExpression (86-88)
  • TFilterConditionPayload (95-98)
  • TFilterValue (24-24)
  • TFilterGroupNode (79-79)
packages/utils/src/rich-filters/types/core.ts (3)
  • isConditionNode (19-21)
  • isGroupNode (28-29)
  • isAndGroupNode (36-38)
packages/utils/src/rich-filters/factories/nodes/core.ts (1)
  • createAndGroupNode (32-39)
packages/utils/src/rich-filters/operations/transformation/core.ts (1)
  • transformExpressionTree (92-120)
packages/utils/src/rich-filters/types/shared.ts (1)
  • getGroupChildren (37-40)
packages/utils/src/rich-filters/validators/shared.ts (1)
  • shouldUnwrapGroup (12-22)
🪛 GitHub Actions: Build and lint web apps
packages/utils/src/rich-filters/values/core.ts

[warning] 1-1: Prettier formatting issue detected in this file. Run 'prettier --write' to fix.

🔇 Additional comments (4)
packages/utils/src/rich-filters/operations/manipulation/core.ts (1)

61-75: Confirm dropping shouldNotify is intentional.

transformExpressionTree produces shouldNotify, but it’s discarded here. If callers rely on notifications, ensure they’re handled upstream.

packages/utils/src/rich-filters/values/core.ts (1)

1-27: Fix Prettier to unblock CI.

The job reports a formatting issue. Run Prettier on this file (or the repo).

Example:

  • pnpm: pnpm prettier --write packages/utils/src/rich-filters/values/core.ts
  • or: prettier --write packages/utils/src/rich-filters/values/core.ts
packages/shared-state/src/store/rich-filters/config-manager.ts (1)

120-129: LGTM! Proper use of MobX action for state mutations.

The implementation correctly uses MobX action decorator and properly handles both update and creation scenarios.

packages/types/src/index.ts (1)

1-50: Export structure looks good - comprehensive coverage of new rich-filters module.

The reorganization properly exports all necessary modules including the new rich-filters feature. The alphabetical ordering aids in maintainability.

}

return Array.isArray(value) ? (value as NonNullable<V>[]) : ([value] as NonNullable<V>[]);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Function Fails to Filter Null Values

The toFilterArray function incorrectly asserts its return type as NonNullable<V>[]. When the input value is an array, it doesn't filter out null or undefined elements, allowing them to remain in the returned array. This violates the type contract and can lead to runtime errors for consumers expecting non-nullable values.

Fix in Cursor Fix in Web

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
packages/utils/src/rich-filters/types/shared.ts (4)

27-31: Docstring claims OR/NOT support that isn’t implemented.

The comment mentions AND/OR and NOT, but code (and types) only handle AND groups. Please correct to avoid misleading consumers.

- * Gets the children of a group node, handling AND/OR groups (children array) and NOT groups (single child).
- * Uses processGroupNode for consistent group type handling.
+ * Gets the children of a group node.
+ * Currently supports AND groups only. Extend when OR/NOT groups are introduced.
+ * Uses processGroupNode for consistent group type handling.

16-24: Improve error context; current template prints “[object Object]”.

Interpolate the logical operator (and optionally a brief shape) instead of the whole object so errors are actionable.

 export const processGroupNode = <P extends TFilterProperty, T>(
   group: TFilterGroupNode<P>,
   handlers: TProcessGroupNodeHandlers<P, T>
 ): T => {
   if (isAndGroupNode(group)) {
     return handlers.onAndGroup(group);
   }
-  throw new Error(`Invalid group node: unknown logical operator ${group}`);
+  const op = (group as any)?.logicalOperator ?? 'unknown';
+  throw new Error(`Invalid group node: unknown logical operator: ${String(op)}`);
 };

Follow‑up: when OR/NOT are added, consider an exhaustive check pattern (assertNever) to make unhandled cases a compile‑time error.


6-8: Clarify handler surface and future‑proof the type.

Add a short doc that today only AND is required; more handlers will be added as new group kinds land.

+/**
+ * Handlers for group node processing.
+ * Note: currently only AND groups exist; extend this type when OR/NOT are introduced.
+ */
 type TProcessGroupNodeHandlers<P extends TFilterProperty, T> = {
   onAndGroup: (group: TFilterAndGroupNode<P>) => T;
 };

32-35: Tiny tidy + optional immutability.

  • Pass function ref directly.
  • Optionally return a defensive copy or a readonly type to prevent accidental mutation by callers.
-export const getGroupChildren = <P extends TFilterProperty>(group: TFilterGroupNode<P>): TFilterExpression<P>[] =>
-  processGroupNode(group, {
-    onAndGroup: (andGroup) => getAndGroupChildren(andGroup),
-  });
+export const getGroupChildren = <P extends TFilterProperty>(
+  group: TFilterGroupNode<P>
+): TFilterExpression<P>[] =>
+  processGroupNode(group, {
+    onAndGroup: getAndGroupChildren,
+  });

If you prefer immutability:

-export const getGroupChildren = <P extends TFilterProperty>(group: TFilterGroupNode<P>): TFilterExpression<P>[] =>
+export const getGroupChildren = <P extends TFilterProperty>(group: TFilterGroupNode<P>): ReadonlyArray<TFilterExpression<P>> =>
   processGroupNode(group, {
-    onAndGroup: getAndGroupChildren,
+    onAndGroup: (andGroup) => [...getAndGroupChildren(andGroup)],
   });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8bbe04c and 3ce6789.

📒 Files selected for processing (2)
  • packages/utils/src/rich-filters/types/shared.ts (1 hunks)
  • packages/utils/src/rich-filters/values/core.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/utils/src/rich-filters/values/core.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/utils/src/rich-filters/types/shared.ts (2)
packages/types/src/rich-filters/expression.ts (4)
  • TFilterProperty (19-19)
  • TFilterAndGroupNode (69-73)
  • TFilterGroupNode (79-79)
  • TFilterExpression (86-88)
packages/utils/src/rich-filters/types/core.ts (2)
  • isAndGroupNode (36-38)
  • getAndGroupChildren (57-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Analyze (javascript)
  • GitHub Check: Build and lint web apps

@sriramveeraghanta sriramveeraghanta merged commit d521eab into preview Sep 16, 2025
7 of 8 checks passed
@sriramveeraghanta sriramveeraghanta deleted the feat-rich-filters branch September 16, 2025 15:45
yarikoptic pushed a commit to yarikoptic/plane that referenced this pull request Oct 1, 2025
…e#7802)

* feat: add rich filters types

* feat: add rich filters constants

* feat: add rich filters utils

* feat: add rich filters store in shared state package

* feat: add rich filters UI components

* fix: make setLoading optional in loadOptions function for improved flexibility

* chore: minor improvements to rich filters

* fix: formatting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants