From d526417d93feabd6396f95133d5441fcc8fbe4fc Mon Sep 17 00:00:00 2001 From: gciotola <30926550+gciotola@users.noreply.github.com> Date: Tue, 12 May 2026 18:17:04 +0200 Subject: [PATCH 1/6] feat: add support for groupedPredicates filter type in resource filters --- .../FieldGroupedPredicates.tsx | 49 ++++++++++++++ .../useResourceFilters/FiltersForm.tsx | 9 +++ .../useResourceFilters/FiltersNav.tsx | 11 ++++ .../adaptFormValuesToSdk.ts | 27 +++++++- .../adaptUrlQueryToFormValues.test.ts | 8 +++ .../adaptUrlQueryToFormValues.ts | 41 ++++++++++++ .../useResourceFilters/mockedInstructions.ts | 25 ++++++++ .../ui/resources/useResourceFilters/types.ts | 64 +++++++++++++++++++ 8 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 packages/app-elements/src/ui/resources/useResourceFilters/FieldGroupedPredicates.tsx diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/FieldGroupedPredicates.tsx b/packages/app-elements/src/ui/resources/useResourceFilters/FieldGroupedPredicates.tsx new file mode 100644 index 000000000..17836e0b7 --- /dev/null +++ b/packages/app-elements/src/ui/resources/useResourceFilters/FieldGroupedPredicates.tsx @@ -0,0 +1,49 @@ +import type { JSX } from "react" +import { useFormContext } from "react-hook-form" +import { HookedInputToggleButton } from "#ui/forms/InputToggleButton" +import type { FilterItemGroupedPredicates } from "./types" +import { computeFilterLabel } from "./utils" + +interface FieldGroupedPredicatesProps { + item: FilterItemGroupedPredicates +} + +export function FieldGroupedPredicates({ + item, +}: FieldGroupedPredicatesProps): JSX.Element { + const { watch } = useFormContext() + + const visibleOptions = item.render.props.options.filter( + (opt) => opt.isHidden !== true, + ) + + const selectedValue = watch(item.sdk.predicate) as + | string + | string[] + | undefined + + const selectedCount = Array.isArray(selectedValue) + ? selectedValue.length + : selectedValue != null + ? 1 + : 0 + + return ( + ({ label, value }))} + /> + ) +} + +FieldGroupedPredicates.displayName = "FieldGroupedPredicates" diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/FiltersForm.tsx b/packages/app-elements/src/ui/resources/useResourceFilters/FiltersForm.tsx index 8a2f2a165..6132e4e4a 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/FiltersForm.tsx +++ b/packages/app-elements/src/ui/resources/useResourceFilters/FiltersForm.tsx @@ -7,6 +7,7 @@ import { Spacer } from "#ui/atoms/Spacer" import { HookedForm } from "#ui/forms/Form" import { makeFilterAdapters } from "./adapters" import { FieldCurrencyRange } from "./FieldCurrencyRange" +import { FieldGroupedPredicates } from "./FieldGroupedPredicates" import { FieldOptions } from "./FieldOptions" import { FieldTextSearch } from "./FieldTextSearch" import { FieldTimeRange } from "./FieldTimeRange" @@ -107,6 +108,14 @@ function FiltersForm({ ) } + if (item.type === "groupedPredicates") { + return ( + + + + ) + } + return null })}
diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/FiltersNav.tsx b/packages/app-elements/src/ui/resources/useResourceFilters/FiltersNav.tsx index 1e86e7672..a92bea7f5 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/FiltersNav.tsx +++ b/packages/app-elements/src/ui/resources/useResourceFilters/FiltersNav.tsx @@ -439,6 +439,17 @@ function getButtonFilterLabel({ ) } + if ( + instructionItem.type === "groupedPredicates" && + (isSingleElementArray || isString) + ) { + return ( + instructionItem.render.props.options.find( + ({ value }) => value === optionValue, + )?.label ?? instructionItem.label + ) + } + if (instructionItem.type === "textSearch") { return `${instructionItem.label} · ${optionValue}` } diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts index 656174809..5c3f05d1a 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts @@ -8,10 +8,12 @@ import { makeSdkFilterTime } from "./timeUtils" import { type CurrencyRangeFieldValue, type FilterItemCurrencyRange, + type FilterItemGroupedPredicates, type FilterItemOptions, type FilterItemTextSearch, type FiltersInstructions, isCurrencyRange, + isGroupedPredicates, isItemOptions, isTextSearch, type TimeRangePreset, @@ -49,8 +51,12 @@ export function adaptFormValuesToSdk< ): item is | FilterItemOptions | FilterItemTextSearch - | FilterItemCurrencyRange => - isItemOptions(item) || isTextSearch(item) || isCurrencyRange(item), + | FilterItemCurrencyRange + | FilterItemGroupedPredicates => + isItemOptions(item) || + isTextSearch(item) || + isCurrencyRange(item) || + isGroupedPredicates(item), ) .flatMap((item) => ([] as string[]).concat(item.sdk.predicate).concat(predicateWhitelist), @@ -110,6 +116,23 @@ export function adaptFormValuesToSdk< } } + if (instructionItem.type === "groupedPredicates") { + // Each selected option value maps to its own distinct SDK predicate + value + const selectedValues = castArray(formValues[key]).filter( + Boolean, + ) as string[] + return selectedValues.reduce((innerAcc, selectedValue) => { + const option = instructionItem.render.props.options.find( + (o) => o.value === selectedValue, + ) + if (option == null) return innerAcc + return { + ...innerAcc, + [option.sdk.predicate]: option.sdk.value, + } + }, acc) + } + return acc }, { diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.test.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.test.ts index e4faf21f7..bdf1b1768 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.test.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.test.ts @@ -14,6 +14,7 @@ describe("adaptUrlQueryToFormValues", () => { status_in: ["cancelled"], payment_status_eq: undefined, fulfillment_status_in: [], + quantity_filter: undefined, archived_at_null: undefined, timePreset: undefined, timeFrom: undefined, @@ -40,6 +41,7 @@ describe("adaptUrlQueryToFormValues", () => { status_in: [], payment_status_eq: undefined, fulfillment_status_in: [], + quantity_filter: undefined, archived_at_null: undefined, timePreset: undefined, timeFrom: undefined, @@ -68,6 +70,7 @@ describe("adaptUrlQueryToFormValues", () => { status_in: [], payment_status_eq: undefined, fulfillment_status_in: [], + quantity_filter: undefined, archived_at_null: undefined, timePreset: undefined, timeFrom: undefined, @@ -94,6 +97,7 @@ describe("adaptUrlQueryToFormValues", () => { status_in: ["approved"], payment_status_eq: undefined, fulfillment_status_in: [], + quantity_filter: undefined, archived_at_null: undefined, timePreset: undefined, timeFrom: undefined, @@ -120,6 +124,7 @@ describe("adaptUrlQueryToFormValues", () => { status_in: [], payment_status_eq: undefined, fulfillment_status_in: [], + quantity_filter: undefined, archived_at_null: undefined, timePreset: undefined, timeFrom: undefined, @@ -147,6 +152,7 @@ describe("adaptUrlQueryToFormValues", () => { status_in: ["placed"], payment_status_eq: undefined, fulfillment_status_in: [], + quantity_filter: undefined, archived_at_null: undefined, timePreset: undefined, timeFrom: undefined, @@ -174,6 +180,7 @@ describe("adaptUrlQueryToFormValues", () => { status_in: ["placed"], payment_status_eq: undefined, fulfillment_status_in: [], + quantity_filter: undefined, archived_at_null: undefined, timePreset: undefined, timeFrom: undefined, @@ -201,6 +208,7 @@ describe("adaptUrlQueryToFormValues", () => { status_in: ["placed"], payment_status_eq: undefined, fulfillment_status_in: [], + quantity_filter: undefined, archived_at_null: undefined, timePreset: undefined, timeFrom: undefined, diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.ts index dbf8320d1..df8550209 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.ts @@ -163,6 +163,47 @@ export function adaptUrlQueryToFormValues< } } + // single grouped predicates option: store one option value in URL + if ( + instructionItem.type === "groupedPredicates" && + instructionItem.render.props.mode === "single" + ) { + const allowedValues = instructionItem.render.props.options.map( + (o) => o.value, + ) + return parsedQuery[key] != null + ? { + ...formValues, + [key]: parseQueryStringValueAsArray( + parsedQuery[key], + allowedValues, + )[0], + } + : { + ...formValues, + [key]: undefined, + } + } + + // multi grouped predicates options: store array of option values in URL + if (instructionItem.type === "groupedPredicates") { + const allowedValues = instructionItem.render.props.options.map( + (o) => o.value, + ) + return parsedQuery[key] != null + ? { + ...formValues, + [key]: parseQueryStringValueAsArray( + parsedQuery[key], + allowedValues, + ), + } + : { + ...formValues, + [key]: [], + } + } + return formValues }, { diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts b/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts index 209749fff..7d5e7a87a 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts @@ -148,6 +148,31 @@ export const instructions: FiltersInstructions = [ }, }, }, + { + label: "Items quantity", + type: "groupedPredicates", + sdk: { + predicate: "quantity_filter", + }, + render: { + component: "inputToggleButton", + props: { + mode: "single", + options: [ + { + label: "Has items", + value: "has_items", + sdk: { predicate: "quantity_gte", value: "1" }, + }, + { + label: "Empty", + value: "empty", + sdk: { predicate: "quantity_eq", value: "0" }, + }, + ], + }, + }, + }, { label: "Time range", type: "timeRange", diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/types.ts b/packages/app-elements/src/ui/resources/useResourceFilters/types.ts index 3d1766784..2031c53d2 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/types.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/types.ts @@ -73,6 +73,63 @@ export interface BaseFilterItem { } } +export interface FilterItemGroupedPredicates { + /** + * Label of the filter field in form component + */ + label: string + /** + * Flag to hide the filter field in form component + */ + hidden?: boolean + type: "groupedPredicates" + /** + * A virtual/synthetic field name used as the URL query param and react-hook-form field name. + * The actual SDK predicates are defined individually per option. + */ + sdk: { + /** + * Virtual predicate used in the URL query string and form state. + * Example: `quantity_filter` — the real SDK predicates are defined inside each option. + */ + predicate: string + } + render: { + /** + * UI component to render + */ + component: "inputToggleButton" + /** + * Props required for the UI component + */ + props: { + mode: "single" | "multi" + /** + * Each option maps to a specific SDK predicate + value pair when selected. + * An option can be hidden from the UI but still be used in the query. + * @example + * ```ts + * [ + * { label: 'Has items', value: 'has_items', sdk: { predicate: 'quantity_gte', value: '1' } }, + * { label: 'Empty', value: 'empty', sdk: { predicate: 'quantity_eq', value: '0' } }, + * ] + * ``` + */ + options: Array<{ + label: string + /** The value stored in the URL query string and form state */ + value: string + isHidden?: boolean + /** The actual SDK predicate + value this option maps to when selected */ + sdk: { + predicate: string + value: string + } + }> + } + } +} + export type FilterItemOptions = BaseFilterItem & { type: "options" render: @@ -161,6 +218,7 @@ export type FiltersInstructionItem = | FilterItemTextSearch | FilterItemTime | FilterItemCurrencyRange + | FilterItemGroupedPredicates export type FiltersInstructions = FiltersInstructionItem[] @@ -181,3 +239,9 @@ export function isCurrencyRange( ): item is FilterItemCurrencyRange { return item.type === "currencyRange" } + +export function isGroupedPredicates( + item: FiltersInstructionItem, +): item is FilterItemGroupedPredicates { + return item.type === "groupedPredicates" +} From 805379408e1484e880aab6ec68f6a3fbe0c6bd04 Mon Sep 17 00:00:00 2001 From: gciotola <30926550+gciotola@users.noreply.github.com> Date: Wed, 13 May 2026 15:18:35 +0200 Subject: [PATCH 2/6] fix: replace sdk.predicate key for groupedPredicates filter item with a flat urlParamKey --- .../FieldGroupedPredicates.tsx | 7 ++-- .../useResourceFilters/FiltersNav.tsx | 9 +++-- .../adaptFormValuesToSdk.test.ts | 1 + .../adaptFormValuesToSdk.ts | 8 +++-- .../useResourceFilters/adaptSdkToMetrics.ts | 3 +- .../adaptUrlQueryToFormValues.ts | 5 +-- .../resources/useResourceFilters/adapters.ts | 8 +++-- .../useResourceFilters/mockedInstructions.ts | 4 +-- .../ui/resources/useResourceFilters/types.ts | 34 ++++++++++++++----- .../ui/resources/useResourceFilters/utils.ts | 3 +- 10 files changed, 53 insertions(+), 29 deletions(-) diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/FieldGroupedPredicates.tsx b/packages/app-elements/src/ui/resources/useResourceFilters/FieldGroupedPredicates.tsx index 17836e0b7..76d860c51 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/FieldGroupedPredicates.tsx +++ b/packages/app-elements/src/ui/resources/useResourceFilters/FieldGroupedPredicates.tsx @@ -17,10 +17,7 @@ export function FieldGroupedPredicates({ (opt) => opt.isHidden !== true, ) - const selectedValue = watch(item.sdk.predicate) as - | string - | string[] - | undefined + const selectedValue = watch(item.urlParamKey) as string | string[] | undefined const selectedCount = Array.isArray(selectedValue) ? selectedValue.length @@ -39,7 +36,7 @@ export function FieldGroupedPredicates({ }) : item.label } - name={item.sdk.predicate} + name={item.urlParamKey} mode={item.render.props.mode} options={visibleOptions.map(({ label, value }) => ({ label, value }))} /> diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/FiltersNav.tsx b/packages/app-elements/src/ui/resources/useResourceFilters/FiltersNav.tsx index a92bea7f5..8a918a573 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/FiltersNav.tsx +++ b/packages/app-elements/src/ui/resources/useResourceFilters/FiltersNav.tsx @@ -24,6 +24,7 @@ import { type FiltersInstructionItem, type FiltersInstructions, type FormFullValues, + getInstructionKey, isTextSearch, type UiFilterName, type UiFilterValue, @@ -140,7 +141,7 @@ export function FiltersNav({ () => instructions .filter((item) => item.hidden === true) - .map((item) => item.sdk.predicate), + .map((item) => getInstructionKey(item)), [instructions], ) @@ -366,7 +367,9 @@ function getInstructionItemByFilterPredicate({ if (isTimeRangeFilterUiName(filterPredicate)) { return instructions.find(({ type }) => type === "timeRange") } - return instructions.find((item) => item.sdk.predicate === filterPredicate) + return instructions.find( + (item) => getInstructionKey(item) === filterPredicate, + ) } /** @@ -488,7 +491,7 @@ function predicateBelongsToCurrencyRange({ instructions: FiltersInstructions }): boolean { const instructionItem = instructions.find( - (item) => item.sdk.predicate === filterPredicate, + (item) => getInstructionKey(item) === filterPredicate, ) return instructionItem?.type === "currencyRange" diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.test.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.test.ts index 288144b6c..69b461dbc 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.test.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.test.ts @@ -224,6 +224,7 @@ describe("extractEnforcedValues", () => { test("should return empty object if no enforced values", () => { const instructionsWithoutDefaultOptions = instructions.filter( (item) => + item.type === "groupedPredicates" || !("defaultOptions" in item.sdk && item.sdk.defaultOptions != null), ) expect( diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts index 5c3f05d1a..02341ac35 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts @@ -12,6 +12,7 @@ import { type FilterItemOptions, type FilterItemTextSearch, type FiltersInstructions, + getInstructionKey, isCurrencyRange, isGroupedPredicates, isItemOptions, @@ -59,13 +60,15 @@ export function adaptFormValuesToSdk< isGroupedPredicates(item), ) .flatMap((item) => - ([] as string[]).concat(item.sdk.predicate).concat(predicateWhitelist), + ([] as string[]) + .concat(getInstructionKey(item)) + .concat(predicateWhitelist), ) const sdkFilters = formFieldNames.reduce>( (acc, key) => { const instructionItem = instructions.find( - (item) => item.sdk.predicate === key, + (item) => getInstructionKey(item) === key, ) if (instructionItem == null) { @@ -81,6 +84,7 @@ export function adaptFormValuesToSdk< // user custom defined parseFormValue function if ( + instructionItem.type !== "groupedPredicates" && "parseFormValue" in instructionItem.sdk && instructionItem.sdk.parseFormValue != null ) { diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.ts index 6011af4f2..866ce5dab 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.ts @@ -5,6 +5,7 @@ import { removeMillisecondsFromIsoDate, } from "#helpers/date" import type { FiltersInstructions } from "#ui/resources/useResourceFilters/types" +import { getInstructionKey } from "#ui/resources/useResourceFilters/types" export type CoreResourceEnabledInMetrics = "orders" | "returns" type MetricsResource = "order" | "return" @@ -89,7 +90,7 @@ export function adaptSdkToMetrics({ }>( (acc, [key, value]) => { const instructionItem = instructions.find( - (item) => item.sdk.predicate === key, + (item) => getInstructionKey(item) === key, ) if ( diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.ts index df8550209..1b9a6a71d 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.ts @@ -5,6 +5,7 @@ import { type FiltersInstructions, type FormFullValues, filterableTimeRangePreset, + getInstructionKey, isCurrencyRange, type UiFilterName, type UiFilterValue, @@ -34,7 +35,7 @@ export function adaptUrlQueryToFormValues< const allowedQueryParams = [ ...instructions .filter((item) => !isCurrencyRange(item)) - .map((item) => item.sdk.predicate), + .map((item) => getInstructionKey(item)), ...predicateWhitelist, // ...currencyRangeFieldKeys ] @@ -101,7 +102,7 @@ export function adaptUrlQueryToFormValues< const formValues = allowedQueryParams.reduce( (formValues, key) => { const instructionItem = instructions.find( - (item) => item.sdk.predicate === key, + (item) => getInstructionKey(item) === key, ) if (instructionItem == null) { diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adapters.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adapters.ts index 2eaffbb06..184403bdf 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adapters.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adapters.ts @@ -7,6 +7,7 @@ import { adaptUrlQueryToFormValues as adaptUrlQueryToFormValuesFn } from "./adap import { adaptUrlQueryToSdk as adaptUrlQueryToSdkFn } from "./adaptUrlQueryToSdk" import { adaptUrlQueryToUrlQuery as adaptUrlQueryToUrlQueryFn } from "./adaptUrlQueryToUrlQuery" import type { FiltersInstructions } from "./types" +import { getInstructionKey } from "./types" export const makeFilterAdapters: MakeFiltersAdapters = ({ instructions, @@ -87,9 +88,10 @@ function isValidInstructions(instructions: FiltersInstructions): boolean { const hasReservedTimePresetUiFilterName = instructions.filter( (item) => - (item.sdk.predicate === "timePreset" && item.type !== "timeRange") || - item.sdk.predicate === "timeFrom" || - item.sdk.predicate === "timeTo", + (getInstructionKey(item) === "timePreset" && + item.type !== "timeRange") || + getInstructionKey(item) === "timeFrom" || + getInstructionKey(item) === "timeTo", )?.length > 0 const isInvalid = diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts b/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts index 7d5e7a87a..f1ba01d37 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts @@ -151,9 +151,7 @@ export const instructions: FiltersInstructions = [ { label: "Items quantity", type: "groupedPredicates", - sdk: { - predicate: "quantity_filter", - }, + urlParamKey: "quantity_filter", render: { component: "inputToggleButton", props: { diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/types.ts b/packages/app-elements/src/ui/resources/useResourceFilters/types.ts index 2031c53d2..84fb3deaf 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/types.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/types.ts @@ -84,16 +84,16 @@ export interface FilterItemGroupedPredicates { hidden?: boolean type: "groupedPredicates" /** - * A virtual/synthetic field name used as the URL query param and react-hook-form field name. - * The actual SDK predicates are defined individually per option. + * The URL query string key used to identify this filter in the URL and form state. + * Unlike other filter types, this is NOT an SDK predicate — it is a virtual key used + * only for URL serialization and react-hook-form state management. + * The actual SDK predicates are defined per-option inside `render.props.options`. + * @example + * ```ts + * urlParamKey: "availability" // → ?availability=in_stock + * ``` */ - sdk: { - /** - * Virtual predicate used in the URL query string and form state. - * Example: `quantity_filter` — the real SDK predicates are defined inside each option. - */ - predicate: string - } + urlParamKey: string render: { /** * UI component to render @@ -245,3 +245,19 @@ export function isGroupedPredicates( ): item is FilterItemGroupedPredicates { return item.type === "groupedPredicates" } + +/** + * Returns the URL query string key / form field name for any instruction item. + * + * - For all standard types (`options`, `textSearch`, `timeRange`, `currencyRange`) this is `item.sdk.predicate`. + * - For `groupedPredicates` it is `item.urlParamKey`, because that type has no single SDK predicate + * (each option maps to its own predicate at the option level). + * + * Use this helper everywhere you need to resolve the URL/form key from an unnarrowed `FiltersInstructionItem` + * instead of accessing `item.sdk.predicate` directly. + */ +export function getInstructionKey(item: FiltersInstructionItem): string { + return item.type === "groupedPredicates" + ? item.urlParamKey + : item.sdk.predicate +} diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/utils.ts b/packages/app-elements/src/ui/resources/useResourceFilters/utils.ts index 625452944..b5ab42447 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/utils.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/utils.ts @@ -6,6 +6,7 @@ import type { UiFilterName, UiFilterValue, } from "./types" +import { getInstructionKey } from "./types" /** * Show the filter label with the counter for selected options @@ -55,7 +56,7 @@ export function getActiveFilterCountFromUrl({ }) return instructions.reduce((total, instructionItem) => { - const { predicate } = instructionItem.sdk + const predicate = getInstructionKey(instructionItem) if (instructionItem.hidden === true) { return total From 5cb36aaeb7b370c76d97e5750aee56a6d250a059 Mon Sep 17 00:00:00 2001 From: gciotola <30926550+gciotola@users.noreply.github.com> Date: Wed, 13 May 2026 15:20:50 +0200 Subject: [PATCH 3/6] fix: update types --- .../resources/useResourceFilters/FiltersSearchBar.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/FiltersSearchBar.tsx b/packages/app-elements/src/ui/resources/useResourceFilters/FiltersSearchBar.tsx index 5083b160f..12513c25b 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/FiltersSearchBar.tsx +++ b/packages/app-elements/src/ui/resources/useResourceFilters/FiltersSearchBar.tsx @@ -4,7 +4,11 @@ import type { JSX } from "react" import { t } from "#providers/I18NProvider" import { SearchBar, type SearchBarProps } from "#ui/composite/SearchBar" import { makeFilterAdapters } from "./adapters" -import type { FiltersInstructions } from "./types" +import type { + FilterItemTextSearch, + FiltersInstructions, + FormFullValues, +} from "./types" export interface FilterSearchBarProps extends Pick { @@ -53,7 +57,7 @@ function FiltersSearchBar({ }) const textPredicate = instructions.find( - (item) => + (item): item is FilterItemTextSearch => item.type === "textSearch" && item.render.component === "searchBar", )?.sdk.predicate @@ -70,7 +74,7 @@ function FiltersSearchBar({ formValues: { ...currentFilters, [textPredicate]: isEmpty(hint?.trim()) ? undefined : hint, - }, + } as FormFullValues, }) onUpdate(newQueryString) From 5ccf96a8fe2eb6ec388f77e11be2a36019134bc2 Mon Sep 17 00:00:00 2001 From: gciotola <30926550+gciotola@users.noreply.github.com> Date: Wed, 13 May 2026 15:30:54 +0200 Subject: [PATCH 4/6] fix: support groupedPredicates options in metrics and URL query adapters --- .../adaptSdkToMetrics.test.ts | 35 ++++++++++ .../useResourceFilters/adaptSdkToMetrics.ts | 17 ++++- .../adaptSdkToUrlQuery.test.ts | 24 +++++++ .../useResourceFilters/adaptSdkToUrlQuery.ts | 64 ++++++++++++++++++- .../useResourceFilters/mockedInstructions.ts | 2 +- 5 files changed, 137 insertions(+), 5 deletions(-) diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.test.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.test.ts index 68c21bfc3..afbdb43d8 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.test.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.test.ts @@ -211,4 +211,39 @@ describe("adaptSdkToMetrics", () => { }, }) }) + + test("Should include groupedPredicates option SDK predicates as main resource metrics filters", () => { + expect( + adaptSdkToMetrics({ + sdkFilters: { quantity_eq: "0" }, + resourceType: "orders", + instructions, + }), + ).toStrictEqual({ + order: { + date_from: "2022-04-05T15:20:01Z", + date_to: "2023-04-05T15:20:00Z", + date_field: "updated_at", + quantity: { eq: "0" }, + }, + }) + }) + + test("Should include groupedPredicates option SDK predicates alongside standard filters", () => { + expect( + adaptSdkToMetrics({ + sdkFilters: { quantity_gteq: "1", status_in: "approved,cancelled" }, + resourceType: "orders", + instructions, + }), + ).toStrictEqual({ + order: { + date_from: "2022-04-05T15:20:01Z", + date_to: "2023-04-05T15:20:00Z", + date_field: "updated_at", + quantity: { gte: "1" }, + statuses: { in: ["approved", "cancelled"] }, + }, + }) + }) }) diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.ts index 866ce5dab..c4acf8044 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToMetrics.ts @@ -5,7 +5,10 @@ import { removeMillisecondsFromIsoDate, } from "#helpers/date" import type { FiltersInstructions } from "#ui/resources/useResourceFilters/types" -import { getInstructionKey } from "#ui/resources/useResourceFilters/types" +import { + getInstructionKey, + isGroupedPredicates, +} from "#ui/resources/useResourceFilters/types" export type CoreResourceEnabledInMetrics = "orders" | "returns" type MetricsResource = "order" | "return" @@ -77,6 +80,15 @@ export function adaptSdkToMetrics({ instructions.find((item) => item.type === "timeRange")?.sdk.predicate ?? "created_at" + // Pre-compute the set of all SDK predicates declared inside groupedPredicates options. + // These are not top-level instruction keys, but must be allowed through the drop guard + // so they can be forwarded to the Metrics API as regular field filters. + const groupedPredicatesOptionKeys = new Set( + instructions + .filter(isGroupedPredicates) + .flatMap((item) => item.render.props.options.map((o) => o.sdk.predicate)), + ) + // separate relationships from main resource filters const regroupedFilters = Object.entries(sdkFilters).reduce<{ /** main resource filters */ @@ -96,7 +108,8 @@ export function adaptSdkToMetrics({ if ( instructionItem == null && !predicateWhitelist.includes(key) && - !key.startsWith(defaultDatePredicate) + !key.startsWith(defaultDatePredicate) && + !groupedPredicatesOptionKeys.has(key) ) { return acc } diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.test.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.test.ts index b3f7a2631..e200baeaf 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.test.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.test.ts @@ -53,4 +53,28 @@ describe("adaptSdkToUrlQuery", () => { }), ).toBe("lastname_eq=doe&status_in=approved&status_in=cancelled") }) + + test("should map a groupedPredicates option SDK predicate back to its virtual URL param", () => { + expect( + adaptSdkToUrlQuery({ + sdkFilters: { + quantity_eq: "0", + status_in: "approved", + }, + instructions, + }), + ).toBe("quantity_filter=empty&status_in=approved") + }) + + test("should map multiple groupedPredicates option SDK predicates back to their virtual URL params", () => { + expect( + adaptSdkToUrlQuery({ + sdkFilters: { + quantity_gteq: "1", + status_in: "approved", + }, + instructions, + }), + ).toBe("quantity_filter=has_items&status_in=approved") + }) }) diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.ts index 28628c4a5..44cb4f43c 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.ts @@ -1,6 +1,9 @@ import type { QueryFilter } from "@commercelayer/sdk" import queryString from "query-string" -import type { FiltersInstructions } from "#ui/resources/useResourceFilters/types" +import { + type FiltersInstructions, + isGroupedPredicates, +} from "#ui/resources/useResourceFilters/types" import { adaptUrlQueryToUrlQuery } from "./adaptUrlQueryToUrlQuery" export interface AdaptSdkToUrlQueryParams { @@ -14,7 +17,11 @@ export function adaptSdkToUrlQuery({ predicateWhitelist, instructions, }: AdaptSdkToUrlQueryParams): string { - const sdkFiltersWithArrayValues = Object.entries(sdkFilters).reduce( + // Invert groupedPredicates option predicates back to their virtual URL param keys + // before entering the standard URL pipeline, otherwise they would be dropped as unknown. + const resolvedFilters = invertGroupedPredicates(sdkFilters, instructions) + + const sdkFiltersWithArrayValues = Object.entries(resolvedFilters).reduce( (acc, [key, value]) => { return { ...acc, @@ -35,3 +42,56 @@ export function adaptSdkToUrlQuery({ instructions, }) } + +/** + * Converts SDK predicates produced by `groupedPredicates` options back to the + * corresponding virtual URL param key + option value, so the result can be + * understood by `adaptUrlQueryToUrlQuery`. + * + * Example: an instruction with `urlParamKey: "availability"` has an option + * `{ value: "in_stock", sdk: { predicate: "quantity_gteq", value: "1" } }`. + * When `sdkFilters` contains `{ quantity_gteq: "1" }`, this returns + * `{ availability: "in_stock" }` — removing `quantity_gteq` from the result. + */ +function invertGroupedPredicates( + sdkFilters: QueryFilter, + instructions: FiltersInstructions, +): QueryFilter { + const groupedInstructions = instructions.filter(isGroupedPredicates) + if (groupedInstructions.length === 0) return sdkFilters + + const consumedSdkKeys = new Set() + const virtualParams: Record = {} + + for (const instruction of groupedInstructions) { + const matchedOptionValues: string[] = [] + + for (const option of instruction.render.props.options) { + const sdkValue = sdkFilters[option.sdk.predicate] + if (sdkValue != null && String(sdkValue) === option.sdk.value) { + matchedOptionValues.push(option.value) + consumedSdkKeys.add(option.sdk.predicate) + } + } + + if (matchedOptionValues.length === 0) continue + + const [firstValue] = matchedOptionValues + if (firstValue == null) continue + + // Single mode: only one option can be active at a time + // Multi mode: multiple options can be active simultaneously + virtualParams[instruction.urlParamKey] = + instruction.render.props.mode === "single" + ? firstValue + : matchedOptionValues + } + + if (consumedSdkKeys.size === 0) return sdkFilters + + // Strip the consumed SDK keys and inject the virtual URL params in their place + return Object.entries(sdkFilters).reduce((acc, [key, value]) => { + if (consumedSdkKeys.has(key)) return acc + return { ...acc, [key]: value } + }, virtualParams) +} diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts b/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts index f1ba01d37..415df2c8d 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/mockedInstructions.ts @@ -160,7 +160,7 @@ export const instructions: FiltersInstructions = [ { label: "Has items", value: "has_items", - sdk: { predicate: "quantity_gte", value: "1" }, + sdk: { predicate: "quantity_gteq", value: "1" }, }, { label: "Empty", From bb0762bef96b53e5bb611cdd45bebecadb464d76 Mon Sep 17 00:00:00 2001 From: Giuseppe Ciotola <30926550+gciotola@users.noreply.github.com> Date: Wed, 13 May 2026 15:46:20 +0200 Subject: [PATCH 5/6] fix: apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../adaptFormValuesToSdk.ts | 5 +++-- .../useResourceFilters/adaptSdkToUrlQuery.ts | 20 ++++++++++++++----- .../ui/resources/useResourceFilters/types.ts | 4 ++-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts index 02341ac35..6369f31eb 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.ts @@ -123,8 +123,9 @@ export function adaptFormValuesToSdk< if (instructionItem.type === "groupedPredicates") { // Each selected option value maps to its own distinct SDK predicate + value const selectedValues = castArray(formValues[key]).filter( - Boolean, - ) as string[] + (value): value is string => + typeof value === "string" && value.length > 0, + ) return selectedValues.reduce((innerAcc, selectedValue) => { const option = instructionItem.render.props.options.find( (o) => o.value === selectedValue, diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.ts index 44cb4f43c..24ce2e294 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptSdkToUrlQuery.ts @@ -65,26 +65,36 @@ function invertGroupedPredicates( for (const instruction of groupedInstructions) { const matchedOptionValues: string[] = [] + const matchedSdkPredicates: string[] = [] for (const option of instruction.render.props.options) { const sdkValue = sdkFilters[option.sdk.predicate] if (sdkValue != null && String(sdkValue) === option.sdk.value) { matchedOptionValues.push(option.value) - consumedSdkKeys.add(option.sdk.predicate) + matchedSdkPredicates.push(option.sdk.predicate) } } if (matchedOptionValues.length === 0) continue + const isSingleMode = instruction.render.props.mode === "single" + + // In single mode, multiple matches cannot be represented without losing + // information. Leave the original SDK predicates untouched instead. + if (isSingleMode && matchedOptionValues.length > 1) continue + const [firstValue] = matchedOptionValues if (firstValue == null) continue + for (const predicate of matchedSdkPredicates) { + consumedSdkKeys.add(predicate) + } + // Single mode: only one option can be active at a time // Multi mode: multiple options can be active simultaneously - virtualParams[instruction.urlParamKey] = - instruction.render.props.mode === "single" - ? firstValue - : matchedOptionValues + virtualParams[instruction.urlParamKey] = isSingleMode + ? firstValue + : matchedOptionValues } if (consumedSdkKeys.size === 0) return sdkFilters diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/types.ts b/packages/app-elements/src/ui/resources/useResourceFilters/types.ts index 84fb3deaf..f68b47212 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/types.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/types.ts @@ -110,8 +110,8 @@ export interface FilterItemGroupedPredicates { * @example * ```ts * [ - * { label: 'Has items', value: 'has_items', sdk: { predicate: 'quantity_gte', value: '1' } }, - * { label: 'Empty', value: 'empty', sdk: { predicate: 'quantity_eq', value: '0' } }, + * { label: 'Has items', value: 'has_items', sdk: { predicate: 'quantity_gteq', value: '1' } }, + * { label: 'Empty', value: 'empty', sdk: { predicate: 'quantity_eq', value: '0' } }, * ] * ``` */ From 407cbf5f0dbf941d1ab8fbb2fd0a5d0ab257ac9c Mon Sep 17 00:00:00 2001 From: gciotola <30926550+gciotola@users.noreply.github.com> Date: Wed, 13 May 2026 15:53:21 +0200 Subject: [PATCH 6/6] test: add missing test coverage for groupedPredicates filter item --- .../adaptFormValuesToSdk.test.ts | 44 +++++++++++++++ .../adaptFormValuesToUrlQuery.test.ts | 24 +++++++++ .../adaptUrlQueryToFormValues.test.ts | 54 +++++++++++++++++++ .../adaptUrlQueryToSdk.test.ts | 26 +++++++++ .../adaptUrlQueryToUrlQuery.test.ts | 18 +++++++ 5 files changed, 166 insertions(+) diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.test.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.test.ts index 69b461dbc..094e857b1 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.test.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToSdk.test.ts @@ -197,6 +197,50 @@ describe("adaptFormValuesToSdk", () => { }) }) + test("should map a groupedPredicates option to its sdk predicate", () => { + expect( + adaptFormValuesToSdk({ + formValues: { + quantity_filter: "has_items", + }, + instructions, + }), + ).toStrictEqual({ + quantity_gteq: "1", + archived_at_null: true, + status_in: "placed,approved,cancelled", + }) + }) + + test("should map a different groupedPredicates option to its own sdk predicate", () => { + expect( + adaptFormValuesToSdk({ + formValues: { + quantity_filter: "empty", + }, + instructions, + }), + ).toStrictEqual({ + quantity_eq: "0", + archived_at_null: true, + status_in: "placed,approved,cancelled", + }) + }) + + test("should omit groupedPredicates sdk predicates when no option is selected", () => { + expect( + adaptFormValuesToSdk({ + formValues: { + quantity_filter: undefined, + }, + instructions, + }), + ).toStrictEqual({ + archived_at_null: true, + status_in: "placed,approved,cancelled", + }) + }) + test("should generate `lastname_eq` when whitelisted", () => { expect( adaptFormValuesToSdk({ diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToUrlQuery.test.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToUrlQuery.test.ts index a9efd7838..2e4e0141e 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToUrlQuery.test.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptFormValuesToUrlQuery.test.ts @@ -106,6 +106,30 @@ describe("adaptFormValuesToUrlQuery", () => { ).toBe("archived_at_null=show") }) + test("should serialize a groupedPredicates selection as its urlParamKey value", () => { + expect( + adaptFormValuesToUrlQuery({ + formValues: { + status_in: ["approved"], + quantity_filter: "has_items", + }, + instructions, + }), + ).toBe("quantity_filter=has_items&status_in=approved") + }) + + test("should omit groupedPredicates urlParamKey when no option is selected", () => { + expect( + adaptFormValuesToUrlQuery({ + formValues: { + status_in: ["approved"], + quantity_filter: undefined, + }, + instructions, + }), + ).toBe("status_in=approved") + }) + test("should accept empty values", () => { expect( adaptFormValuesToUrlQuery({ diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.test.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.test.ts index bdf1b1768..6a401f340 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.test.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToFormValues.test.ts @@ -196,6 +196,60 @@ describe("adaptUrlQueryToFormValues", () => { }) }) + test("should parse a groupedPredicates option value from the query string", () => { + expect( + adaptUrlQueryToFormValues({ + queryString: "quantity_filter=has_items", + instructions, + }), + ).toStrictEqual({ + market_id_in: [], + status_in: [], + payment_status_eq: undefined, + fulfillment_status_in: [], + quantity_filter: "has_items", + archived_at_null: undefined, + timePreset: undefined, + timeFrom: undefined, + timeTo: undefined, + name_eq: undefined, + number_or_email_cont: undefined, + viewTitle: undefined, + total_amount_cents: { + from: undefined, + to: undefined, + currencyCode: undefined, + }, + }) + }) + + test("should discard an invalid groupedPredicates option value from the query string", () => { + expect( + adaptUrlQueryToFormValues({ + queryString: "quantity_filter=not_a_valid_option", + instructions, + }), + ).toStrictEqual({ + market_id_in: [], + status_in: [], + payment_status_eq: undefined, + fulfillment_status_in: [], + quantity_filter: undefined, + archived_at_null: undefined, + timePreset: undefined, + timeFrom: undefined, + timeTo: undefined, + name_eq: undefined, + number_or_email_cont: undefined, + viewTitle: undefined, + total_amount_cents: { + from: undefined, + to: undefined, + currencyCode: undefined, + }, + }) + }) + test("should handle partial currency range values", () => { expect( adaptUrlQueryToFormValues({ diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToSdk.test.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToSdk.test.ts index 08752cf41..377293c47 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToSdk.test.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToSdk.test.ts @@ -66,6 +66,32 @@ describe("adaptUrlQueryToSdk", () => { }) }) + test("should resolve a groupedPredicates option to its sdk predicate", () => { + expect( + adaptUrlQueryToSdk({ + queryString: "quantity_filter=has_items", + instructions, + }), + ).toStrictEqual({ + quantity_gteq: "1", + archived_at_null: true, + status_in: "placed,approved,cancelled", + }) + }) + + test("should resolve a different groupedPredicates option to its own sdk predicate", () => { + expect( + adaptUrlQueryToSdk({ + queryString: "quantity_filter=empty", + instructions, + }), + ).toStrictEqual({ + quantity_eq: "0", + archived_at_null: true, + status_in: "placed,approved,cancelled", + }) + }) + test("should consider the query string whitelist", () => { expect( adaptUrlQueryToSdk({ diff --git a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToUrlQuery.test.ts b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToUrlQuery.test.ts index 168da0af5..9d750281a 100644 --- a/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToUrlQuery.test.ts +++ b/packages/app-elements/src/ui/resources/useResourceFilters/adaptUrlQueryToUrlQuery.test.ts @@ -58,6 +58,24 @@ describe("adaptUrlQueryToUrlQuery", () => { ).toBe("status_in=approved&status_in=cancelled") }) + test("should preserve a valid groupedPredicates urlParamKey through the round-trip", () => { + expect( + adaptUrlQueryToUrlQuery({ + queryString: "quantity_filter=has_items&status_in=approved", + instructions, + }), + ).toBe("quantity_filter=has_items&status_in=approved") + }) + + test("should strip a groupedPredicates urlParamKey with an invalid option value", () => { + expect( + adaptUrlQueryToUrlQuery({ + queryString: "quantity_filter=not_a_valid_option&status_in=approved", + instructions, + }), + ).toBe("status_in=approved") + }) + test("should ignore invalid values", () => { expect( adaptUrlQueryToUrlQuery({