[Relations component] Add filters - #3979
Conversation
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>
There was a problem hiding this comment.
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.
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>
3bb2dc4 to
7047490
Compare
ecd0bd9 to
a77205e
Compare
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>
|
One or more custom setup steps configured for this repository failed during this Copilot code review run: 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 |
There was a problem hiding this comment.
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
Selectapplies a concretewidthfrom the editor (90/100px), while this only removesmax-widthand 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
BEFOREandAFTERare currently inclusive because bothToandFromuse<=/>=. The existing transforms map those settings to strict less-than/greater-than semantics (and the UI says “before”/“after”); onlyBETWEENshould include its endpoints. Branch onfilterValue.settingso 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,matchRowchanges 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])
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>
53411d3 to
ed5c7e0
Compare
|
One or more custom setup steps configured for this repository failed during this Copilot code review run: 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 |
There was a problem hiding this comment.
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.valueandvisibleFieldsValuechange in the same commit, the preceding[props.value]effect refreshes from the new prop, then this effect runs with the oldvaluestate captured by that render and refreshes from it last; aftersetValuererenders, 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
FromandToare 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 sameDateFilterOperatorvalues toGREATER_THANandLESS_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 inrow-matchers.test.tsshould be updated too.
case DateFilterOperator.From:
return day >= conditionDay
case DateFilterOperator.To:
return day <= conditionDay
|



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 keepsoriginalIndexintact — 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
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.transformNumberFilteris added next to the existingtransformDateFilter, so "is / less / more / between" is defined once.modules/reportsstill carries its own copy and can adopt it separately.Fixes found while testing
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.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).eslintandtsc --noEmitclean.🤖 Generated with Claude Code