AG-10819 advanced filter - #14783
Conversation
Module Size ComparisonExtreme Values🔺 Largest Increase: AllEnterpriseModule
Significant Changes (≥ 0.5KB)
📊 Full Statistics
All Module Changes
Updated: 2026-08-10T19:41:44.883Z |
3c5d7cf to
e69592e
Compare
|
/pr-review |
Live-test this PR in PlunkerPaste these two <script src="https://ag-grid.github.io/ag-grid/pr-14783/ag-grid-community.min.js"></script>
<script src="https://ag-grid.github.io/ag-grid/pr-14783/ag-grid-enterprise.min.js"></script>Bundles are removed automatically when the PR is closed. Updated on every push. |
|
✅ Codex review complete; 6 issues found (P0: 0 | P1: 0 | P2: 6 | P3: 0) View full reviewAG-10819 advanced filterPR: #14783 SummaryThis PR adds custom filter options to Advanced Filter, including multi-operand parsing, Builder support, validation, and related filter infrastructure updates. Adds extensive behavioural coverage and test harnesses for custom filter options, validation, formatting, and floating filters. FindingsP0: 0 | P1: 0 | P2: 6 | P3: 0 6 of 6 finding(s) also posted inline; all findings are listed below. ℹ️ [P2] Formatter validation does not model the actual input constraints
The native-input branch uses Number(formatted), which accepts values such as hexadecimal or padded strings that an HTML number input may reject. The text-input branch receives only a boolean and never checks formatted characters against allowedCharPattern. Consequently invalid formatter output can bypass the fallback, appear blank or be impossible to re-enter, and omit warning 326. ℹ️ [P2] Floating filters bypass the new formatter fallback
NumberFilterModelFormatter returns numberFormatter output unconditionally, and floating filters use this formatter for their displayed value. When a native number floating-filter input cannot hold formatted text such as "1,000", it can become blank even though the main filter now falls back to the unformatted number. Apply the same round-trip validation to floating-filter formatting. ℹ️ [P2] Changing a retained option's arity preserves stale input values
During a filterOptions refresh, inputs are cleared only when the selected key disappears. If the same custom displayKey changes numberOfInputs, newly active inputs retain values previously hidden under the old definition, potentially producing an unintended filterTo value. Clear values whose slots were not active under the previous option definition. ℹ️ [P2] Column changes can resurrect stale operands
When the same custom key exists on both columns with different arities, changing columns updates numOperands and rebuilds the pills without clearing newly introduced operand slots. A stale filterTo supplied on the previous one-input model can therefore become the second operand on the new column. This path should clear slots added by the arity increase, as setOperatorKey already does. ℹ️ [P2] Validation and console mocks leak between tests
This describe block only resets grids, but the text inRange test changes global development-validation settings and mocks console.warn. Those changes remain active for subsequent tests, potentially suppressing unexpected warnings and making results order-dependent. Restore mocks and reset validations in afterEach. ℹ️ [P2] Number-filter case supplies a text-filter model
The parameterised test always sets filterType to "text", including for agNumberColumnFilter. The number branch therefore uses a malformed model and may pass because of filter-type handling rather than because the required second operand is absent. Use the corresponding "number" discriminator for that case. VerdictAssessment: correct The core custom-option implementation is coherent and extensively covered, but several edge cases around formatter validation and operand reuse can expose stale or unusable filter values. The changes are broadly coherent, but two test-isolation and coverage defects weaken the reliability of the new suite. Required Actions:
|
e69592e to
7f6470c
Compare
|
/pr-review |
1 similar comment
|
/pr-review |
740cd30 to
8b9b465
Compare
|
/pr-review |
|
| if (formatted == null || formatted.trim() === '') { | ||
| return false; | ||
| } | ||
| return (usesTextInput ? stringToFloat(numberParser, formatted) : Number(formatted)) === value; |
There was a problem hiding this comment.
ℹ️ [P2] Formatter validation does not model the actual input constraints
The native-input branch uses Number(formatted), which accepts values such as hexadecimal or padded strings that an HTML number input may reject. The text-input branch receives only a boolean and never checks formatted characters against allowedCharPattern. Consequently invalid formatter output can bypass the fallback, appear blank or be impossible to re-enter, and omit warning 326.
|
|
||
| constructor(optionsFactory: OptionsFactory, filterParams: INumberFilterParams) { | ||
| super(optionsFactory, filterParams, filterParams.numberFormatter); | ||
| protected override getValueFormatter(): ((value: number | null) => string | null) | undefined { |
There was a problem hiding this comment.
ℹ️ [P2] Floating filters bypass the new formatter fallback
NumberFilterModelFormatter returns numberFormatter output unconditionally, and floating filters use this formatter for their displayed value. When a native number floating-filter input cannot hold formatted text such as "1,000", it can become blank even though the main filter now falls back to the unformatted number. Apply the same round-trip validation to floating-filter formatting.
| eType.clearOptions(); | ||
| this.putOptionsIntoDropdown(eType); | ||
| const isStillOffered = optionsFactory.hasOption(selectedType); | ||
| eType.setValue(isStillOffered ? selectedType : optionsFactory.defaultOption, true); | ||
| if (!isStillOffered) { | ||
| // Values the withdrawn option collected mean nothing to the one replacing it. |
There was a problem hiding this comment.
ℹ️ [P2] Changing a retained option's arity preserves stale input values
During a filterOptions refresh, inputs are cleared only when the selected key disappears. If the same custom displayKey changes numberOfInputs, newly active inputs retain values previously hidden under the old definition, potentially producing an unintended filterTo value. Clear values whose slots were not active under the previous option definition.
| // The kept operator can take a different number of values on this column. | ||
| this.numOperands = this.getNumOperands(this.filterModel.type); |
There was a problem hiding this comment.
ℹ️ [P2] Column changes can resurrect stale operands
When the same custom key exists on both columns with different arities, changing columns updates numOperands and rebuilds the pills without clearing newly introduced operand slots. A stale filterTo supplied on the previous one-input model can therefore become the second operand on the new column. This path should clear slots added by the arity increase, as setOperatorKey already does.
| }); | ||
| afterAll(() => uninstallFilterLayoutMock()); | ||
| afterEach(() => gridsManager.reset()); | ||
|
|
There was a problem hiding this comment.
ℹ️ [P2] Validation and console mocks leak between tests
This describe block only resets grids, but the text inRange test changes global development-validation settings and mocks console.warn. Those changes remain active for subsequent tests, potentially suppressing unexpected warnings and making results order-dependent. Restore mocks and reset validations in afterEach.
|
|
||
| await new GridRows(api, `incomplete ${_name} model leaves every row`).check(` | ||
| ROOT id:ROOT_NODE_ID | ||
| ├── LEAF id:0 athlete:"Bolt" |
There was a problem hiding this comment.
ℹ️ [P2] Number-filter case supplies a text-filter model
The parameterised test always sets filterType to "text", including for agNumberColumnFilter. The number branch therefore uses a malformed model and may pass because of filter-type handling rather than because the required second operand is absent. Use the corresponding "number" discriminator for that case.
8b9b465 to
4f47e9b
Compare



AG-10819: A Custom Filter Option means the same thing wherever it is used
FIX AG-10819
FIX AG-14491
FIX AG-16738
Custom Filter Options (
colDef.filterParams.filterOptions) work in the Advanced Filter, taking 0, 1 or 2values, the same arities the column filter supports. The other two tickets are bugs found on the way there.
Breaking bug fixes
Both are TypeScript-only: corrections to types that were already wrong. What stops compiling is code the
declared type should never have accepted.
IFilterOptionDef.predicateis required. An option without one was offered and matched no rows.typeaccepts a Custom Filter Option'sdisplayKey, onISimpleFilterModel, the nine*AdvancedFilterModel, andIFilterPlaceholderFunctionParams.filterOptionKey. The declared unions werealready wrong: choosing a custom option has always stored its
displayKeythere, and the source castas ISimpleFilterModelTypeto compile. Only readingtypeinto a variable of the narrow union, or anexhaustive
switchon it, stops compiling.filterOptions,defaultOptionandfilterPlaceholdernarrow only what they suggest: every value theytook before is still accepted, and a wrong one is reported at runtime instead.
What a user could notice
Everything observable in one place, each labelled with what it is.
filterOptionscontaining a custom option now narrow the Advanced Filter's operators. One object entry used to disable narrowing for the whole list, so the column offered every built-in.ANDnor admits them to anOR.inRangeis no longer applied. It reached the model and hid every row; the inputs report it and the previously applied filter stands, as the Date Filter already did.numberParserinstead ofNumber().numberFormatterwhose output the input cannot hold, or cannot read back as the same number, shows the plain number. Output an<input type="number">could not hold left it blank; output it could hold but not read back, such as a formatter that rounds, was shown and then silently changed the model on the next edit.#327), adefaultOptionthe list does not offer (#328, first option used), and anumberFormatterwhose output cannot be read back (#326, naming the column).#72also fires for an option carrying only the removedtest, and now reports on a column filtered solely through the Advanced Filter, which builds noOptionsFactoryto report it.#73is removed, having become unreachable.Two degenerate hand-written models differ, neither reachable from the UI: a combined model with
conditions: []filters on nothing rather than hiding every row underOR, and one that also needs itsfilterTypecorrected keeps its own keys instead of collapsing to{filterType}.The feature
A column's
filterOptionsare offered in the expression editor and the Builder under theirdisplayName,localised through their own
displayKeyas the column filter's dropdown label is, and evaluated with thesame
predicateas the column filter. Operators resolve per column, so two columnscan reuse one
displayKeyfor different options, and an option whosedisplayKeyis a built-in replaces itfor that column. Suggestions carry one entry per key, as the column filter's dropdown does.
Expression syntax. An option is written under its
displayNamefollowed by as many values as it takes.Two are comma separated, brackets optional. Each value is quoted by its Cell Data Type as for the built-ins:
numberandbigintunquoted, the rest quoted. AdisplayKeytyped in place of thedisplayNameisreplaced by it once recognised.
Builder. A two-value option renders two value pills, labelled
Value From/Value Tofrom the existinginRangeStart/inRangeEndlocale text, and held to the same out-of-order check the column filter reports onits inputs, and reported through that filter's own locale key:
strictMaxValueValidationfornumberandbigint,maxDateValidationfor the date types. Changing a condition's column to one of another data typeclears the values.
Model. The Advanced Filter gains a
filterToslot, its condition model having had nowhere to put asecond value.
API added
CustomFilterOptionKeydisplayKey;string & {}FilterOptionKeyCustomFilterOptionKeyTextFilterOptionKey,ScalarFilterOptionKeyDateFilterOptionKey,CommonFilterOptionKeyfilterTo?on all nine*AdvancedFilterModelfilter?onBooleanAdvancedFilterModelPre-existing bugs fixed
30 of them, each covered by a test that fails on unmodified
latest.Range validation
this.statein a
refresh()override after the base had overwritten it; Date had no refresh at all.toinput's error held the condition back oncethe option no longer had a
toinput.fromagainsttowhatever the option.inRange.shouldKeepInvalidInputStatematched thekey rather than the two-value shape.
captured a condition index, and removing one from the middle shifts every one after it.
Custom filter options
entries are skipped and the rest kept, making
#73unreachable, so it is removed.testwas still offered, and matched no rows.FilterOptionDefof the samedisplayKeywere both offered: two dropdown rowsfor one key, and selecting either highlighted the first.
defaultOptionwas returned unchecked, leaving the dropdown on a value it does not list.getCustomOptionresolved againstObject.prototype, so adisplayKeyoftoStringreturned aninherited member. The option map is a
Map.numberOfInputswas not guarded, and anullentry threw ondisplayKey.filterOptionslist swapped at runtime never reached the dropdown. An option added later could notbe chosen and one withdrawn stayed selectable, showing as another option's applied value.
filterPlaceholdernever saw a custom option: placeholders were not recomputed when the optionchanged, and a custom key has no locale entry, so
filterOptionarrivedundefined.builds the
OptionsFactorythat reports it.Number, BigInt and Date
numberFormatterblanked both inputs, its output going into an<input type="number">. It is shownonly where the input can hold it and read it back, and the inputs are re-rendered when a
colDefrefreshreplaces the formatter or parser. Text no parser reads is left as typed, so a refresh does not empty an
input mid-keystroke.
updateParamsreplacesfilterParams.Number(), so with anallowedCharPatterna value onlynumberParsercould read reached the model asNaN.allowedCharPatternchanged, though its own floatingfilter already did.
includeTimeshowed an empty picker.setDatewrote2020-01-01into adatetime-localinput, which the browser blanks.expireswas stored as a duration and tested as anabsolute timestamp, re-running the relative-range function per row. Results were correct; the cost was not.
Advanced Filter
Over Or Equalresolved toOver, silently: the editor shows the expression,getAdvancedFilterModel()returnsnull, and nothingis filtered. Reachable with no custom options by localising
advancedFilterNotContainstocontains not.)failed to parse,)being a terminator. Subsumed by 24.position still to be parsed. Rewrites are queued and applied last-first, against the text as typed.
filterTo: both only looked atfilter.different
displayName.Other
conditionsthrew:validateModelmapped over an absent array.AbstractHeaderCellCtrl.shouldStopEventPropagationdestructuredfocusSvc.focusedHeaderbehind a!,though it is typed
HeaderPosition | null.Two optimisations, with no behaviour to test: the column autocomplete list was never memoised, its cache field
being assigned nowhere, so every keystroke rebuilt and re-sorted the entries; and a column's
filterOptionswereclassified twice per keystroke, once for the operators and once for the keys they narrow to. Both are cached per
column now, and invalidated on the column events they are built from.