V3 Admin filters gain multi-select and parent/child cascades

This PR makes page-level PricingFlowV3 shared filter terms richer, authorable from Admin Attributes, and saved through the existing V3 flowSpec publish path.

Author @pk675 PR #6315 State closed Base main Head pk/admin-filters-multiselect-hierarchy Files 21 Diff +1642 / -143 Area V3 Admin + PricingFlowV3

What it adds

Multi-select shared filters on PricingFlowV3 product selection. Selected values merge into one OR-style filter rule; no checked values means “All”.

What it changes

Filter authoring moves into Admin Attributes for enum and multi_enum SKU attributes, including parent filter selection and child value mapping.

What it preserves

The persisted shape stays inside productSelection.sharedFilters.terms. There is no new tRPC route; saves still use v3Admin.updateFlowSpec.

Important side effect

Admin attribute and filter saves now auto-publish spec versions directly and clear dangling drafts, instead of writing draft rows first.

FlowSpec schema
Admin Attributes UI
PricingFlowV3 runtime
Versioning / publish path
Tests

1. Why this exists

Before
  • Page-level shared filters were effectively single-select dropdowns.
  • Admin users could not model “Region → Country” cascades.
  • Hand-authored terms could have an “All” option, but the runtime could incorrectly fall back to { attrKey: "all" }.
  • Filter edits required stitching together flowSpec writes from UI code without a dedicated filter domain model.
After
  • A term can set multiSelect: true.
  • A term can point at parentTermId, and each option can declare parentValue.
  • Runtime filtering OR-merges multiple selected values for the same attribute.
  • Admins configure the filter projection directly while editing an enum SKU attribute.

Concrete demo shape: parent Region has Americas and AMEA; child Country only shows Canada/USA for Americas and China/India for AMEA.

2. What changes

2.1 Persisted model: additive fields on shared filter terms

FlowSpec contract

The schema change is backward-compatible: old terms parse the same way because every new field is optional.

term.multiSelect?
When true, render a checkbox dropdown and treat selected option values as OR alternatives.
term.parentTermId?
References another term id. The child term is hidden until the parent has a specific non-“All” selection.
option.parentValue?
Parent option value or values that make this child option visible. Omitted means “show under every parent value”; [] means hidden under specific parent selections.
Hierarchy example

2.2 Admin authoring: filters are projected from enum SKU attributes

Attribute
Admin edits an enum or multi_enum attribute scoped to sku.
Use as filter
Drawer exposes filter toggles: multi-select, All option, parent filter, default value.
Term projection
filterDraftToTerm converts allowed values into sharedFilters.terms[].
Save
Attribute schema save runs, then filter terms are spliced into the cloned flowSpec.
Runtime
PricingFlowV3 renders the configured controls and derives the product filter rule.
apps/client/src/dashboard/Admin/components/Attributes/AttributesContainer.tsx
Adds the “Use as a filter” section inside the attribute drawer, candidate parent filtering, cascade row controls, save/delete filter sync, and list badges.
apps/client/src/dashboard/Admin/components/Filters/filter.model.ts
New client-side domain model for converting between admin form values and persisted shared filter terms.
apps/client/src/api/hooks/filters/useListFilters.ts
Reads draft-aware V3 flowSpec and extracts productSelection.sharedFilters.terms with a stable empty array.
apps/client/src/api/hooks/filters/useFilterMutations.ts
Creates, updates, deletes, reorders, or bulk-saves terms by read-modify-writing the full flowSpec through useUpdateFlowSpec.

2.3 Runtime: single-select, multi-select, and cascading controls

Single-select path
  • Still renders through DealopsSelect.
  • Stores a string in state.termValues[term.id].
  • If a parent change hides the current child value, the child snaps to the first visible option.
Multi-select path
  • Renders a native <details> dropdown with checkboxes.
  • Stores string[] in shared filter state.
  • The “All” row clears the array; an empty array contributes no product filter.
Runtime helper New responsibility Why it matters
getVisibleOptions(term) Filters child options by the parent’s meaningful selected value(s). Country options can cascade from Region without duplicating term definitions.
isTermVisible(term) Hides child terms until the parent has a specific non-“All” selection. Prevents empty or confusing child dropdowns before a parent choice exists.
toCombinedFilterRule() Merges active option filter rules and accumulates duplicate keys into arrays. Multiple selected values for one attribute become OR matches.
resolveSharedTermOptionFilterFor() Short-circuits unauthored value: "all" to an empty rule. Fixes the regression where “All” could hide all products carrying the attribute.

2.4 Server/versioning: filter saves now publish immediately

Publish semantics

3. How it works

Authoring guardrails

Only enum or multi_enum attributes that apply to sku can become filters.

This avoids creating a product filter for quote/opportunity/user-only data that products do not carry.

Stable ids

Existing term ids are preserved when editing a filter.

That keeps legacy ids like region alive and avoids breaking child parentTermId references.

Cycle prevention

Parent candidates exclude the term itself and its descendants.

A cycle would make filters wait on each other’s parent selection and never render.

Key conversion shape
FilterFormValues
  → toSharedFilterTerm()
  → {
      id,
      label,
      attrKey,
      multiSelect?,
      parentTermId?,
      options: [
        { value, label, filterFor: { [attrKey]: value }, parentValue? }
      ]
    }

The admin model deliberately mirrors the server term shape structurally instead of importing server-only types into the client.

Parent mapping semantics
  • parentValue: undefined means “shown under all parent values”, including parent values added later.
  • parentValue: "americas" means shown under exactly that parent value.
  • parentValue: ["americas", "amea"] means shown under a subset.
  • parentValue: [] means hidden when a specific parent selection is active.
Multi-select filtering semantics
  • Each selected option resolves to a filter rule.
  • Rules with the same key merge via mergeFilterValues.
  • The resulting array is interpreted by product matching as OR.
  • Behavior overrides still merge from active options.

3.1 Array-safe follow-through

File Adjustment
useSharedFilterAutoAdd.ts Handles string and string-array selections when calculating expected auto-added SKU ownership.
useSharedFilterTermPersistence.ts + planSharedFilterTermHydration.ts Skips multi-select terms for quote-term persistence because they cannot be stored as a single selected quote term.
CategoriesSection.tsx Avoids treating multi-select arrays as single tag filter values.
SharedFilters.tsx + UseCaseSelector.tsx Reflows page controls into a responsive 3-column grid and keeps use-case controls width-safe.

3.2 Validation and tests

Server validation

validateProductSelection accepts missing sections as empty and recognizes built-in product keys like id, productId, pricebookId, and categoryId.

Client regression tests

resolveSharedTermOptionFilterFor.test.ts covers the “All” sentinel, authored filterFor, and the catalog visibility regression.

Server publish tests

autoPublishAdminSpecs.test.ts uses Mocha + Sinon stubs to assert direct version writes, global pointer updates, draft deletion, and invalid flowSpec rejection.

4. What it doesn't change

Explicit non-goals

5. Risks / rollback / open questions

Review risks
Rollback shape

Code rollback is straightforward: revert the PR to remove the new runtime state shape, admin projection model, and auto-publish path changes. Data rollback is additive: existing flowSpecs with multiSelect, parentTermId, or parentValue would need those optional fields removed or ignored before older code reads them.

Net effect

V3 Admin can now author the common “pick several values” and “child options depend on parent” filter patterns without adding storage, while PricingFlowV3 gets the runtime logic needed to honor those terms safely.