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.
Multi-select shared filters on PricingFlowV3 product selection. Selected values merge into one OR-style filter rule; no checked values means “All”.
Filter authoring moves into Admin Attributes for enum and multi_enum SKU attributes, including parent filter selection and child value mapping.
The persisted shape stays inside productSelection.sharedFilters.terms. There is no new tRPC route; saves still use v3Admin.updateFlowSpec.
Admin attribute and filter saves now auto-publish spec versions directly and clear dangling drafts, instead of writing draft rows first.
1. Why this exists
- 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.
- A term can set
multiSelect: true. - A term can point at
parentTermId, and each option can declareparentValue. - 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
The schema change is backward-compatible: old terms parse the same way because every new field is optional.
[] means hidden under specific parent selections.-
Region term
AmericasAMEA
-
Country term with parentTermId = Region
Canada+USAshow underAmericasChina+Indiashow underAMEA
2.2 Admin authoring: filters are projected from enum SKU attributes
sku.filterDraftToTerm converts allowed values into sharedFilters.terms[].productSelection.sharedFilters.terms with a stable empty array.useUpdateFlowSpec.2.3 Runtime: single-select, multi-select, and cascading controls
- 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.
- 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
- ✓
updateFlowSpecnow validates a full flat V3 flowSpec, semantically validates product selection keys, writes a spec version, advances the global version pointer, deletes any flowSpec draft, and best-effort syncs to the V2 runtime record. - ✓
updateAttributeSchemanow writes an attributes spec version directly and clears lingering attributes drafts. - ✓
useUpdateFlowSpecinvalidateslistVersionsbecause every save creates a new published version.
3. How it works
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.
Existing term ids are preserved when editing a filter.
That keeps legacy ids like region alive and avoids breaking child parentTermId references.
Parent candidates exclude the term itself and its descendants.
A cycle would make filters wait on each other’s parent selection and never render.
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.
parentValue: undefinedmeans “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.
- 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
validateProductSelection accepts missing sections as empty and recognizes built-in product keys like id, productId, pricebookId, and categoryId.
resolveSharedTermOptionFilterFor.test.ts covers the “All” sentinel, authored filterFor, and the catalog visibility regression.
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
- No new REST or tRPC route for filters; the implementation continues through
v3Admin.getFlowSpecandv3Admin.updateFlowSpec. - No Prisma migration or new database table; filters remain embedded in the versioned V3 flowSpec body.
- No Dealops 1 product/pricing tables are touched.
- No section-level or per-filter behavior editor is introduced; v1 authoring is page-level/global filters only.
- No quote persistence for multi-select values; only single-select term values mirror into quote terms.
- No automatic creation of parent filters; a child can only choose from existing enum/multi_enum SKU filters.
5. Risks / rollback / open questions
- Auto-publish behavior is broader than filter UI.
updateAttributeSchemaandupdateFlowSpecnow bypass draft creation and publish directly. - Full-flowSpec writes are required. The updated server handler rejects malformed non-legacy partial bodies instead of saving them.
- Native
<details>multi-select UX is simple but not a full Select component. It works offline and avoids extra dependency surface, but keyboard/menu polish may differ fromDealopsSelect. - Parent mappings depend on term ids. Preserving ids is handled, but manual flowSpec edits that rename ids can still break child references.
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.
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.