Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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.urlParamKey) as string | string[] | undefined

const selectedCount = Array.isArray(selectedValue)
? selectedValue.length
: selectedValue != null
? 1
: 0

return (
<HookedInputToggleButton
label={
item.render.props.mode === "multi"
? computeFilterLabel({
label: item.label,
selectedCount,
totalCount: visibleOptions.length,
})
: item.label
}
name={item.urlParamKey}
mode={item.render.props.mode}
options={visibleOptions.map(({ label, value }) => ({ label, value }))}
/>
)
}

FieldGroupedPredicates.displayName = "FieldGroupedPredicates"
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -107,6 +108,14 @@ function FiltersForm({
)
}

if (item.type === "groupedPredicates") {
return (
<Spacer bottom="10" key={item.label}>
<FieldGroupedPredicates item={item} />
</Spacer>
)
}

return null
})}
<div className="w-full sticky bottom-0 bg-gray-50 pb-8">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type FiltersInstructionItem,
type FiltersInstructions,
type FormFullValues,
getInstructionKey,
isTextSearch,
type UiFilterName,
type UiFilterValue,
Expand Down Expand Up @@ -140,7 +141,7 @@ export function FiltersNav({
() =>
instructions
.filter((item) => item.hidden === true)
.map((item) => item.sdk.predicate),
.map((item) => getInstructionKey(item)),
[instructions],
)

Expand Down Expand Up @@ -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,
)
}

/**
Expand Down Expand Up @@ -439,6 +442,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}`
}
Expand Down Expand Up @@ -477,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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SearchBarProps, "placeholder" | "debounceMs"> {
Expand Down Expand Up @@ -53,7 +57,7 @@ function FiltersSearchBar({
})

const textPredicate = instructions.find(
(item) =>
(item): item is FilterItemTextSearch =>
item.type === "textSearch" && item.render.component === "searchBar",
)?.sdk.predicate

Expand All @@ -70,7 +74,7 @@ function FiltersSearchBar({
formValues: {
...currentFilters,
[textPredicate]: isEmpty(hint?.trim()) ? undefined : hint,
},
} as FormFullValues,
})

onUpdate(newQueryString)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -224,6 +268,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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import { makeSdkFilterTime } from "./timeUtils"
import {
type CurrencyRangeFieldValue,
type FilterItemCurrencyRange,
type FilterItemGroupedPredicates,
type FilterItemOptions,
type FilterItemTextSearch,
type FiltersInstructions,
getInstructionKey,
isCurrencyRange,
isGroupedPredicates,
isItemOptions,
isTextSearch,
type TimeRangePreset,
Expand Down Expand Up @@ -49,17 +52,23 @@ 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),
([] as string[])
.concat(getInstructionKey(item))
.concat(predicateWhitelist),
)

const sdkFilters = formFieldNames.reduce<Partial<QueryFilter>>(
(acc, key) => {
const instructionItem = instructions.find(
(item) => item.sdk.predicate === key,
(item) => getInstructionKey(item) === key,
)

if (instructionItem == null) {
Expand All @@ -75,6 +84,7 @@ export function adaptFormValuesToSdk<

// user custom defined parseFormValue function
if (
instructionItem.type !== "groupedPredicates" &&
"parseFormValue" in instructionItem.sdk &&
instructionItem.sdk.parseFormValue != null
) {
Expand Down Expand Up @@ -110,6 +120,24 @@ 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(
(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,
)
if (option == null) return innerAcc
return {
...innerAcc,
[option.sdk.predicate]: option.sdk.value,
}
Comment thread
gciotola marked this conversation as resolved.
}, acc)
Comment thread
gciotola marked this conversation as resolved.
}

return acc
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"] },
},
})
})
})
Loading
Loading