Skip to content

[Relations component] Add filters - #3979

Merged
ValeriaMaltseva merged 29 commits into
2026.xfrom
feature/2801-relation-column-header-filters
Aug 19, 2026
Merged

[Relations component] Add filters#3979
ValeriaMaltseva merged 29 commits into
2026.xfrom
feature/2801-relation-column-header-filters

Conversation

@ValeriaMaltseva

Copy link
Copy Markdown
Contributor

Changes in this pull request

Adds the "via table header" part of #2801: the relation grid gets a filter dropdown in the header of every filterable column, next to the existing search in the toolbar.

How a column becomes filterable

The column type comes from the column configuration (SelectedColumns) and is resolved through the field filter and object data registries, adapter types included — so a column offers a filter only when its type resolves to a field filter that can be evaluated on the rows in memory (number, date, boolean, string). Relations, quantity values, classification stores and the like get no filter, and no list of known field types is hardcoded anywhere.

How the filters are applied

The rows of a relation live in the component (unsaved ones included), so the filters do not compose into a backend request but into a row matcher. It is applied next to the search term in useValue, which keeps originalIndex intact — delete and inline edit still hit the right row, and row drag is disabled while rows are filtered. The declarative filter framework (defineFilter / useFilterQuery) is used as-is; only the "query" is a predicate instead of a request.

The value is edited locally in the dropdown and written to the store on apply, so an abandoned edit in one column cannot leak into another. Whether a value is empty is decided by the field filter type itself (shouldApply).

Shared code

  • The setting enums of the date and number editors move to field-filters/utils/filter-setting-values.ts, so a filter value can be interpreted without pulling in a React component. Both stay re-exported from their editor component.
  • transformNumberFilter is added next to the existing transformDateFilter, so "is / less / more / between" is defined once. modules/reports still carries its own copy and can adopt it separately.

Fixes found while testing

  • The setting select of the date and number editors was uncontrolled (defaultValue), so on every remount it fell back to the first option while the stored value kept its setting. Now bound to the current setting, the way the file size editor already does. This one also affects the listing and report sidebars, wherever a filter editor is remounted.
  • In the header dropdown the longer settings were cut off in the list, because Ant Design gives the dropdown the width of its select. Fixed in the dropdown's own styles; the editors are untouched, so the sidebars keep their layout.

Verification

  • jest — 33 suites / 332 tests, including new unit tests for the filter columns, the row matchers and a regression test for the uncontrolled setting select (verified to fail without the fix).
  • eslint and tsc --noEmit clean.
  • Not clicked through in a browser by Claude — the feature was tested manually by @ValeriaMaltseva, the two fixes above came out of that round. The dropdown width fix is purely visual and is not covered by a test.

🤖 Generated with Claude Code

ValeriaMaltseva and others added 5 commits August 11, 2026 13:25
Resolves the "via table header" part of #2801: the relation grid gets a
filter dropdown in the header of every filterable column, next to the
existing search in the toolbar.

A column is filterable when its type resolves to a field filter that can
be evaluated in memory. The type comes from the column configuration
(SelectedColumns) and is resolved through the field filter and object
data registries, including the adapter types - so relations, quantity
values or classification stores offer no filter, without a hardcoded
list of field types.

The rows of a relation are held in the component (unsaved ones
included), so the filters do not compose into a backend query but into a
row matcher that is applied next to the search term in useValue. The
value is edited locally in the dropdown and only written to the store on
apply; emptiness is decided by the field filter type itself.

The setting enums of the date and number editors move to
field-filters/utils so a value can be interpreted without pulling in a
React component, and the number editor gets a transformNumberFilter next
to the existing transformDateFilter. Both stay re-exported from their
editor component.

Co-Authored-By: Claude <noreply@anthropic.com>
The setting select of both editors was uncontrolled - it took its value
from defaultValue, so on every remount it fell back to the first option
("Is" / "On") while the stored value kept its setting. With a "between"
filter the two inputs were rendered and filled correctly, but the select
claimed a different setting.

Binding it to the current setting fixes it, the way the file size filter
already does. Visible wherever a filter editor is remounted, most
obviously in the new relation column header dropdown, which mounts its
editor on every open.

Co-Authored-By: Claude <noreply@anthropic.com>
The filter editors pin their setting select to a width that fits the
shortest setting, and Ant Design gives the dropdown the width of its
select - so the longer settings were cut off in the list.

Scoped to the column header dropdown, which has the room: the select may
grow, and the inputs of a "between" filter wrap onto their own line
instead of being squeezed next to it. The editors themselves are left
untouched, so the listing and report sidebars keep their layout.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings August 11, 2026 13:59
@ValeriaMaltseva ValeriaMaltseva changed the title [Relations] Column header filters for the relation grid [Relations component] Add filters Aug 11, 2026
@ValeriaMaltseva ValeriaMaltseva added this to the 2026.3.0 milestone Aug 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds column-header filtering to many-to-many relation grids using dynamic field-filter types and in-memory row matching.

Changes:

  • Adds filter dropdowns, state providers, column resolution, and row matchers.
  • Integrates filters with search while preserving original row indexes.
  • Shares date/number settings, fixes controlled selectors, and adds tests, stories, styles, and translations.

Reviewed changes

Copilot reviewed 30 out of 31 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
translations/studio.sv.yaml Adds Swedish filter-label translation.
translations/studio.no.yaml Adds Norwegian filter-label translation.
translations/studio.it.yaml Adds Italian filter-label translation.
translations/studio.fr.yaml Adds French filter-label translation.
translations/studio.es.yaml Adds Spanish filter-label translation.
translations/studio.en.yaml Adds English filter-label translation.
translations/studio.de.yaml Adds German filter-label translation.
field-filters/utils/number-filter-transform.ts Adds reusable numeric filter transformation.
field-filters/utils/filter-setting-values.ts Centralizes date and number setting enums.
field-filters/utils/date-filter-transform.ts Uses the shared date setting enum.
field-filters/components/dynamic-type-field-filter-setting.test.tsx Tests controlled filter settings.
field-filters/components/dynamic-type-field-filter-number-component.tsx Makes numeric setting selection controlled.
field-filters/components/dynamic-type-field-filter-date-component.tsx Makes date setting selection controlled.
many-to-many-relation/many-to-many-relation.tsx Provides and applies relation filters.
many-to-many-relation/many-to-many-relation.stories.tsx Adds filtering stories.
many-to-many-relation/index.ts Exports filtering APIs.
many-to-many-relation/hooks/use-value.ts Combines search and column predicates.
many-to-many-relation/hooks/use-columns.tsx Adds filters to eligible headers.
filters/utils/row-matchers.ts Implements in-memory value matchers.
filters/utils/row-matchers.test.ts Tests row-matching behavior.
filters/utils/filter-columns.ts Resolves filter-column metadata and values.
filters/utils/filter-columns.test.ts Tests column resolution.
filters/types.ts Defines relation-filter contracts.
filters/provider/relation-filters-provider.tsx Composes required filter providers.
filters/provider/relation-filter-columns-context.tsx Supplies filterable columns and row data.
filters/index.ts Exports relation-filter modules.
filters/hooks/use-filterable-columns.ts Selects dynamically supported filters.
filters/filters.ts Defines the filter store and query adapter.
filters/components/column-header-filter.tsx Implements the filter popover.
filters/components/column-header-filter.styles.ts Styles header filters and their panel.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread assets/js/src/core/components/many-to-many-relation/many-to-many-relation.tsx Outdated
ValeriaMaltseva and others added 16 commits August 18, 2026 11:20
The relation grid toolbar now offers an icon button next to the existing
actions that resets every applied column header filter at once. It is only
rendered while at least one filter is applied and leaves the search term
untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The grid component already sorts its rows, so the relation grid only turns
the feature on. The actions column opts out, it carries buttons instead of
a value to sort by.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ader

The rows of the grid are memoized on their props, and with the row virtualizer
enabled every one of them stays stable while a column is resized - the rows
kept the width of the previous size while the header cells already moved.
Without the virtualizer the row style is a new object on every render, which
hid the missing dependency.

The rows now receive the column sizing of the table, so a resize invalidates
them like any other state they render.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The applied filter of a relation grid column now carries the highlight the
object listing puts on its search and filter sidebar tab, instead of only
turning the icon into the primary color.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The button that clears the applied column filters carries its own icon now,
instead of reusing the one of the column filter dropdown.

The icons that came along got their hard coded fill colors replaced by
currentColor so they follow the theme. Three of them referenced a clip path
by an id that no longer exists after that change - the clip covered the full
view box, so the reference and the definition are dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ne line

Drops the flex wrapping the dropdown panel applied to the filter editors,
the widened setting select of the panel stays.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The check, close outline, info outline, ownership and question mark outline
icons are back to the state they had before the icon index was generated -
only the new clear filter icon and its registration remain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two fixes for the same stale widths landed next to each other: PEES-1355
passed the serialized column sizing as columnSizingKey while virtualizing,
a later fix passed the very same string as columnSizing unconditionally.

The row keeps columnSizingKey, now required and always passed. Non-virtualized
rows are re-rendered on every render of the grid anyway, as their row style is
a new object each time, so passing it to them costs nothing and the rows no
longer rely on that detail. A test covers the non-virtualized resize as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ValeriaMaltseva
ValeriaMaltseva force-pushed the feature/2801-relation-column-header-filters branch from 3bb2dc4 to 7047490 Compare August 18, 2026 13:47
@ValeriaMaltseva
ValeriaMaltseva force-pushed the feature/2801-relation-column-header-filters branch from ecd0bd9 to a77205e Compare August 18, 2026 14:01
ValeriaMaltseva and others added 3 commits August 18, 2026 14:07
The rows were only re-matched when the stored filter values changed, but the
matchers also depend on the context they resolve against: a filterable column
that is gone drops its matcher, and refreshed visible field data changes the
values the matchers and the search read. The grid kept showing what the previous
context matched - filtering a visible field column and then adding a relation
left the rows matched against the data from before the refetch.

The refresh now runs on a serialized key of the filter values, the filterable
column keys and the visible fields. Serialized because the hosts build the
column definition and the visible fields anew on every render, so depending on
their identity would refresh the rows in a loop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

One or more custom setup steps configured for this repository failed during this Copilot code review run:

Install dependencies with Composer

Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review.

Note

You can configure setup steps for Copilot code review separately from Copilot cloud agent with a copilot-code-review.yml file. Read the docs for details.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 37 out of 39 changed files in this pull request and generated 2 comments.

Suppressed comments (3)

assets/js/src/core/components/many-to-many-relation/filters/components/column-header-filter.styles.ts:53

  • These rules do not widen the setting select: the shared Select applies a concrete width from the editor (90/100px), while this only removes max-width and sets the same 90px minimum. Because Ant Design matches the popup to the trigger by default, the localized options remain clipped—the visual fix described by the PR is ineffective. Give the scoped trigger a sufficiently larger width/minimum or configure the popup not to match the trigger width.
      .ant-select {
        min-width: 90px;
        max-width: none !important;
      }

assets/js/src/core/components/many-to-many-relation/filters/utils/row-matchers.ts:138

  • BEFORE and AFTER are currently inclusive because both To and From use <=/>=. The existing transforms map those settings to strict less-than/greater-than semantics (and the UI says “before”/“after”); only BETWEEN should include its endpoints. Branch on filterValue.setting so a row on the selected day is excluded for before/after.
      case DateFilterOperator.From:
        return day >= conditionDay

      case DateFilterOperator.To:
        return day <= conditionDay

assets/js/src/core/components/many-to-many-relation/many-to-many-relation.tsx:118

  • This serialization records only column keys, although the matcher also depends on each column's valueKey, type, frontend type, and config. If a host replaces a same-key column with a different field definition, matchRow changes but this effect does not rerun, leaving the rows filtered with the old matcher result until another event refreshes them. Include the matcher-relevant column metadata in the serialized key.
  const filterContextKey = useMemo(() => JSON.stringify({
    filters: appliedFilters,
    columns: filterableColumns.map((column) => column.key),
    visibleFields: props.visibleFieldsValue
  }), [appliedFilters, filterableColumns, props.visibleFieldsValue])

Comment thread assets/js/src/core/components/many-to-many-relation/grid.tsx Outdated
ValeriaMaltseva and others added 2 commits August 18, 2026 17:31
A row value is looked up by the plain field key, and the values the API returns
per row carry no group either, so a key that more than one configured column
claims cannot be resolved to a single value. Such a column now offers no filter
at all instead of filtering against the value of its namesake.

The columns of the relation grid itself are unaffected: the endpoint behind them
already collapses the configurations to one per key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sorting the rows of a relation grid conflicts with dragging them: the rows on
screen follow the sorted row model, while a drop resolves the position of the
dragged row in the value, so the persisted order is unrelated to the visible
target and the active sort masks the change.

Taken out until it can be reworked and tested properly - tracked separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

One or more custom setup steps configured for this repository failed during this Copilot code review run:

Install dependencies with Composer

Setup steps run before each review. If the review above is missing context, or no review was posted at all, the failing step above may be the cause. See the workflow run for failure details, fix your setup steps configuration, and re-request a review.

Note

You can configure setup steps for Copilot code review separately from Copilot cloud agent with a copilot-code-review.yml file. Read the docs for details.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 36 out of 38 changed files in this pull request and generated no new comments.

Suppressed comments (2)

assets/js/src/core/components/many-to-many-relation/many-to-many-relation.tsx:126

  • This can overwrite a simultaneous external value update with stale rows. If props.value and visibleFieldsValue change in the same commit, the preceding [props.value] effect refreshes from the new prop, then this effect runs with the old value state captured by that render and refreshes from it last; after setValue rerenders, neither dependency changes again, so the grid remains stale. Coordinate these effects (or track the latest value explicitly) so only the new relation value is used when both inputs change together.
    updateDisplayValue(value)

assets/js/src/core/components/many-to-many-relation/filters/utils/row-matchers.ts:138

  • From and To are evaluated inclusively here, so “After 2026-04-15” still matches rows on 2026-04-15 and “Before” does likewise. This differs from the established filter semantics: both reports and notes map these same DateFilterOperator values to GREATER_THAN and LESS_THAN (reports/.../field-filters/helpers.tsx:77-80, notes-and-events/filters/helpers.ts:15-18). Use strict comparisons so the in-memory relation filter behaves like the existing date filters; the boundary expectation in row-matchers.test.ts should be updated too.
      case DateFilterOperator.From:
        return day >= conditionDay

      case DateFilterOperator.To:
        return day <= conditionDay

@sonarqubecloud

Copy link
Copy Markdown

@ValeriaMaltseva
ValeriaMaltseva merged commit 37a4c9d into 2026.x Aug 19, 2026
21 checks passed
@ValeriaMaltseva
ValeriaMaltseva deleted the feature/2801-relation-column-header-filters branch August 19, 2026 13:25
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 19, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants