@salesforce/webapp-template-app-react-template-b2e-experimental 1.112.6 → 1.112.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/package.json +3 -3
  3. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/api/graphqlClient.ts +25 -0
  4. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/pages/AccountSearch.tsx +82 -54
  5. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/FilterContext.tsx +73 -0
  6. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/PaginationControls.tsx +45 -87
  7. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/BooleanFilter.tsx +16 -36
  8. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/DateFilter.tsx +33 -77
  9. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/DateRangeFilter.tsx +14 -23
  10. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/MultiSelectFilter.tsx +18 -26
  11. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/NumericRangeFilter.tsx +22 -39
  12. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/SearchFilter.tsx +12 -15
  13. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/SelectFilter.tsx +30 -34
  14. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/TextFilter.tsx +27 -30
  15. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useAsyncData.ts +1 -0
  16. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useCachedAsyncData.ts +1 -0
  17. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useObjectSearchParams.ts +22 -0
  18. package/dist/package-lock.json +2 -2
  19. package/dist/package.json +1 -1
  20. package/package.json +1 -1
  21. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/FilterPanel.tsx +0 -127
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [1.112.8](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.112.7...v1.112.8) (2026-03-24)
7
+
8
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
9
+
10
+
11
+
12
+
13
+
14
+ ## [1.112.7](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.112.6...v1.112.7) (2026-03-23)
15
+
16
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
17
+
18
+
19
+
20
+
21
+
6
22
  ## [1.112.6](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.112.5...v1.112.6) (2026-03-23)
7
23
 
8
24
  **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
@@ -15,8 +15,8 @@
15
15
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@salesforce/sdk-data": "^1.112.6",
19
- "@salesforce/webapp-experimental": "^1.112.6",
18
+ "@salesforce/sdk-data": "^1.112.8",
19
+ "@salesforce/webapp-experimental": "^1.112.8",
20
20
  "@tailwindcss/vite": "^4.1.17",
21
21
  "class-variance-authority": "^0.7.1",
22
22
  "clsx": "^2.1.1",
@@ -42,7 +42,7 @@
42
42
  "@graphql-eslint/eslint-plugin": "^4.1.0",
43
43
  "@graphql-tools/utils": "^11.0.0",
44
44
  "@playwright/test": "^1.49.0",
45
- "@salesforce/vite-plugin-webapp-experimental": "^1.112.6",
45
+ "@salesforce/vite-plugin-webapp-experimental": "^1.112.8",
46
46
  "@testing-library/jest-dom": "^6.6.3",
47
47
  "@testing-library/react": "^16.1.0",
48
48
  "@testing-library/user-event": "^14.5.2",
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Thin GraphQL client: createDataSDK + data.graphql with centralized error handling.
3
+ * Use with gql-tagged queries and generated operation types for type-safe calls.
4
+ */
5
+ import { createDataSDK } from '@salesforce/sdk-data';
6
+
7
+ export async function executeGraphQL<TData, TVariables>(
8
+ query: string,
9
+ variables?: TVariables
10
+ ): Promise<TData> {
11
+ const data = await createDataSDK();
12
+ // SDK types graphql() first param as string; at runtime it may accept gql DocumentNode too
13
+ const response = await data.graphql?.<TData, TVariables>(query, variables);
14
+
15
+ if (!response) {
16
+ throw new Error('GraphQL response is undefined');
17
+ }
18
+
19
+ if (response?.errors?.length) {
20
+ const msg = response.errors.map(e => e.message).join('; ');
21
+ throw new Error(`GraphQL Error: ${msg}`);
22
+ }
23
+
24
+ return response.data;
25
+ }
@@ -1,6 +1,6 @@
1
- import { useMemo } from "react";
1
+ import { useMemo, useState } from "react";
2
2
  import { Link } from "react-router";
3
- import { AlertCircle, SearchX } from "lucide-react";
3
+ import { AlertCircle, ChevronDown, SearchX } from "lucide-react";
4
4
  import {
5
5
  searchAccounts,
6
6
  fetchDistinctIndustries,
@@ -10,8 +10,27 @@ import { useCachedAsyncData } from "../../hooks/useCachedAsyncData";
10
10
  import { fieldValue } from "../../utils/fieldUtils";
11
11
  import { useObjectSearchParams } from "../../hooks/useObjectSearchParams";
12
12
  import { Alert, AlertTitle, AlertDescription } from "../../../../components/ui/alert";
13
+ import {
14
+ Card,
15
+ CardContent,
16
+ CardHeader,
17
+ CardTitle,
18
+ } from "../../../../components/ui/card";
19
+ import { Button } from "../../../../components/ui/button";
20
+ import {
21
+ Collapsible,
22
+ CollapsibleContent,
23
+ CollapsibleTrigger,
24
+ } from "../../../../components/ui/collapsible";
13
25
  import { Skeleton } from "../../../../components/ui/skeleton";
14
- import { FilterPanel } from "../../components/FilterPanel";
26
+ import { FilterProvider, FilterResetButton } from "../../components/FilterContext";
27
+ import { SearchFilter } from "../../components/filters/SearchFilter";
28
+ import { TextFilter } from "../../components/filters/TextFilter";
29
+ import { SelectFilter } from "../../components/filters/SelectFilter";
30
+ import { MultiSelectFilter } from "../../components/filters/MultiSelectFilter";
31
+ import { NumericRangeFilter } from "../../components/filters/NumericRangeFilter";
32
+ import { DateFilter } from "../../components/filters/DateFilter";
33
+ import { DateRangeFilter } from "../../components/filters/DateRangeFilter";
15
34
  import { ActiveFilters } from "../../components/ActiveFilters";
16
35
  import { SortControl } from "../../components/SortControl";
17
36
  import type { FilterFieldConfig } from "../../utils/filterUtils";
@@ -31,48 +50,21 @@ type AccountNode = NonNullable<
31
50
  NonNullable<NonNullable<AccountSearchResult["edges"]>[number]>["node"]
32
51
  >;
33
52
 
34
- // -- Configuration ----------------------------------------------------------
35
- // Adding a new filterable field = adding one entry here. No component changes needed.
36
- // Picklist options are fetched dynamically via aggregate groupBy queries.
37
-
38
- function buildAccountFilterConfigs(
39
- industryOptions: Array<{ value: string; label: string }>,
40
- typeOptions: Array<{ value: string; label: string }>,
41
- ): FilterFieldConfig[] {
42
- return [
43
- {
44
- field: "search",
45
- label: "Search",
46
- type: "search",
47
- searchFields: ["Name", "Phone", "Industry"],
48
- placeholder: "Search by name, phone, or industry...",
49
- },
50
- {
51
- field: "Name",
52
- label: "Account Name",
53
- type: "text",
54
- placeholder: "Search by name...",
55
- },
56
- {
57
- field: "Industry",
58
- label: "Industry",
59
- type: "picklist",
60
- options: industryOptions,
61
- },
62
- { field: "Type", label: "Type", type: "multipicklist", options: typeOptions },
63
- { field: "AnnualRevenue", label: "Annual Revenue", type: "numeric" },
64
- {
65
- field: "CreatedDate",
66
- label: "Created Date",
67
- type: "date",
68
- },
69
- {
70
- field: "LastModifiedDate",
71
- label: "Last Modified Date",
72
- type: "daterange",
73
- },
74
- ];
75
- }
53
+ const FILTER_CONFIGS: FilterFieldConfig[] = [
54
+ {
55
+ field: "search",
56
+ label: "Search",
57
+ type: "search",
58
+ searchFields: ["Name", "Phone", "Industry"],
59
+ placeholder: "Search by name, phone, or industry...",
60
+ },
61
+ { field: "Name", label: "Account Name", type: "text", placeholder: "Search by name..." },
62
+ { field: "Industry", label: "Industry", type: "picklist" },
63
+ { field: "Type", label: "Type", type: "multipicklist" },
64
+ { field: "AnnualRevenue", label: "Annual Revenue", type: "numeric" },
65
+ { field: "CreatedDate", label: "Created Date", type: "date" },
66
+ { field: "LastModifiedDate", label: "Last Modified Date", type: "daterange" },
67
+ ];
76
68
 
77
69
  const ACCOUNT_SORT_CONFIGS: SortFieldConfig<keyof Account_OrderBy>[] = [
78
70
  { field: "Name", label: "Name" },
@@ -84,6 +76,7 @@ const ACCOUNT_SORT_CONFIGS: SortFieldConfig<keyof Account_OrderBy>[] = [
84
76
  // -- Component --------------------------------------------------------------
85
77
 
86
78
  export default function AccountSearch() {
79
+ const [filtersOpen, setFiltersOpen] = useState(true);
87
80
  const { data: industryOptions } = useCachedAsyncData(fetchDistinctIndustries, [], {
88
81
  key: "distinctIndustries",
89
82
  ttl: 300_000,
@@ -93,15 +86,10 @@ export default function AccountSearch() {
93
86
  ttl: 300_000,
94
87
  });
95
88
 
96
- const filterConfigs = useMemo(
97
- () => buildAccountFilterConfigs(industryOptions ?? [], typeOptions ?? []),
98
- [industryOptions, typeOptions],
99
- );
100
-
101
89
  const { filters, sort, query, pagination, resetAll } = useObjectSearchParams<
102
90
  Account_Filter,
103
91
  Account_OrderBy
104
- >(filterConfigs, ACCOUNT_SORT_CONFIGS, PAGINATION_CONFIG);
92
+ >(FILTER_CONFIGS, ACCOUNT_SORT_CONFIGS, PAGINATION_CONFIG);
105
93
 
106
94
  const searchKey = `accounts:${JSON.stringify({ where: query.where, orderBy: query.orderBy, first: pagination.pageSize, after: pagination.afterCursor })}`;
107
95
  const { data, loading, error } = useCachedAsyncData(
@@ -139,12 +127,52 @@ export default function AccountSearch() {
139
127
  <div className="flex flex-col lg:flex-row gap-6">
140
128
  {/* Sidebar — Filter Panel */}
141
129
  <aside className="w-full lg:w-80 shrink-0">
142
- <FilterPanel
143
- configs={filterConfigs}
130
+ <FilterProvider
144
131
  filters={filters.active}
145
132
  onFilterChange={filters.set}
133
+ onFilterRemove={filters.remove}
146
134
  onReset={resetAll}
147
- />
135
+ >
136
+ <Card>
137
+ <Collapsible open={filtersOpen} onOpenChange={setFiltersOpen}>
138
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
139
+ <CardTitle className="text-base font-semibold">
140
+ <h2>Filters</h2>
141
+ </CardTitle>
142
+ <div className="flex items-center gap-1">
143
+ <FilterResetButton variant="destructive" size="sm" />
144
+ <CollapsibleTrigger asChild>
145
+ <Button variant="ghost" size="icon">
146
+ <ChevronDown
147
+ className={`h-4 w-4 transition-transform ${filtersOpen ? "" : "-rotate-90"}`}
148
+ />
149
+ <span className="sr-only">Toggle filters</span>
150
+ </Button>
151
+ </CollapsibleTrigger>
152
+ </div>
153
+ </CardHeader>
154
+ <CollapsibleContent>
155
+ <CardContent className="space-y-4 pt-0">
156
+ <SearchFilter
157
+ field="search"
158
+ label="Search"
159
+ placeholder="Search by name, phone, or industry..."
160
+ />
161
+ <TextFilter field="Name" label="Account Name" placeholder="Search by name..." />
162
+ <SelectFilter
163
+ field="Industry"
164
+ label="Industry"
165
+ options={industryOptions ?? []}
166
+ />
167
+ <MultiSelectFilter field="Type" label="Type" options={typeOptions ?? []} />
168
+ <NumericRangeFilter field="AnnualRevenue" label="Annual Revenue" />
169
+ <DateFilter field="CreatedDate" label="Created Date" />
170
+ <DateRangeFilter field="LastModifiedDate" label="Last Modified Date" />
171
+ </CardContent>
172
+ </CollapsibleContent>
173
+ </Collapsible>
174
+ </Card>
175
+ </FilterProvider>
148
176
  </aside>
149
177
 
150
178
  {/* Main area — Sort + Results */}
@@ -0,0 +1,73 @@
1
+ import { createContext, useContext, useCallback, type ReactNode } from "react";
2
+ import { Button } from "../../../components/ui/button";
3
+ import type { ActiveFilterValue } from "../utils/filterUtils";
4
+
5
+ interface FilterContextValue {
6
+ filters: ActiveFilterValue[];
7
+ onFilterChange: (field: string, value: ActiveFilterValue | undefined) => void;
8
+ onFilterRemove: (field: string) => void;
9
+ onReset: () => void;
10
+ }
11
+
12
+ const FilterContext = createContext<FilterContextValue | null>(null);
13
+
14
+ interface FilterProviderProps {
15
+ filters: ActiveFilterValue[];
16
+ onFilterChange: (field: string, value: ActiveFilterValue | undefined) => void;
17
+ onFilterRemove: (field: string) => void;
18
+ onReset: () => void;
19
+ children: ReactNode;
20
+ }
21
+
22
+ export function FilterProvider({
23
+ filters,
24
+ onFilterChange,
25
+ onFilterRemove,
26
+ onReset,
27
+ children,
28
+ }: FilterProviderProps) {
29
+ return (
30
+ <FilterContext.Provider value={{ filters, onFilterChange, onFilterRemove, onReset }}>
31
+ {children}
32
+ </FilterContext.Provider>
33
+ );
34
+ }
35
+
36
+ function useFilterContext() {
37
+ const ctx = useContext(FilterContext);
38
+ if (!ctx) throw new Error("useFilterField must be used within a FilterProvider");
39
+ return ctx;
40
+ }
41
+
42
+ export function useFilterField(field: string) {
43
+ const { filters, onFilterChange, onFilterRemove } = useFilterContext();
44
+ const value = filters.find((f) => f.field === field);
45
+ const onChange = useCallback(
46
+ (next: ActiveFilterValue | undefined) => {
47
+ if (next) {
48
+ onFilterChange(field, next);
49
+ } else {
50
+ onFilterRemove(field);
51
+ }
52
+ },
53
+ [field, onFilterChange, onFilterRemove],
54
+ );
55
+ return { value, onChange };
56
+ }
57
+
58
+ export function useFilterPanel() {
59
+ const { filters, onReset } = useFilterContext();
60
+ return { hasActiveFilters: filters.length > 0, resetAll: onReset };
61
+ }
62
+
63
+ interface FilterResetButtonProps extends Omit<React.ComponentProps<typeof Button>, "onClick"> {}
64
+
65
+ export function FilterResetButton({ children, ...props }: FilterResetButtonProps) {
66
+ const { hasActiveFilters, resetAll } = useFilterPanel();
67
+ if (!hasActiveFilters) return null;
68
+ return (
69
+ <Button onClick={resetAll} aria-label="Reset filters" variant="destructive" {...props}>
70
+ {children ?? "Reset"}
71
+ </Button>
72
+ );
73
+ }
@@ -1,4 +1,3 @@
1
- import type { ReactNode } from "react";
2
1
  import {
3
2
  Pagination,
4
3
  PaginationContent,
@@ -14,112 +13,71 @@ import {
14
13
  SelectValue,
15
14
  } from "../../../components/ui/select";
16
15
  import { Label } from "../../../components/ui/label";
17
- import { Button } from "../../../components/ui/button";
18
16
 
19
- /** Shared props: pagination state is from useObjectSearchParams (synced to URL). */
20
- interface PaginationControlsBase {
21
- pageSize: number;
22
- pageSizeOptions: readonly number[];
23
- onPageSizeChange: (newPageSize: number) => void;
24
- disabled?: boolean;
25
- }
26
-
27
- /** Default mode: Previous, Page N, Next (cursor-based, state in URL). */
28
- interface PaginationControlsDefaultProps extends PaginationControlsBase {
29
- variant?: "default";
17
+ interface PaginationControlsProps {
30
18
  pageIndex: number;
31
19
  hasNextPage: boolean;
32
20
  hasPreviousPage: boolean;
21
+ pageSize: number;
22
+ pageSizeOptions: readonly number[];
33
23
  onNextPage: () => void;
34
24
  onPreviousPage: () => void;
25
+ onPageSizeChange: (newPageSize: number) => void;
26
+ disabled?: boolean;
35
27
  }
36
28
 
37
- /** Load More mode: optional page size + Load More button (or custom slot). */
38
- interface PaginationControlsLoadMoreProps extends PaginationControlsBase {
39
- variant: "loadMore";
40
- hasNextPage: boolean;
41
- onLoadMore: () => void;
42
- loadMoreLoading?: boolean;
43
- /** Custom content for load-more (e.g. custom button). If not set, renders default Load More button. */
44
- loadMoreSlot?: ReactNode;
45
- }
46
-
47
- export type PaginationControlsProps =
48
- | PaginationControlsDefaultProps
49
- | PaginationControlsLoadMoreProps;
50
-
51
- function isLoadMoreProps(props: PaginationControlsProps): props is PaginationControlsLoadMoreProps {
52
- return props.variant === "loadMore";
53
- }
54
-
55
- export default function PaginationControls(props: PaginationControlsProps) {
56
- const { pageSize, pageSizeOptions, onPageSizeChange, disabled = false } = props;
57
-
29
+ export default function PaginationControls({
30
+ pageIndex,
31
+ hasNextPage,
32
+ hasPreviousPage,
33
+ pageSize,
34
+ pageSizeOptions,
35
+ onNextPage,
36
+ onPreviousPage,
37
+ onPageSizeChange,
38
+ disabled = false,
39
+ }: PaginationControlsProps) {
58
40
  const handlePageSizeChange = (newValue: string) => {
59
41
  const newSize = parseInt(newValue, 10);
60
42
  if (!isNaN(newSize) && newSize !== pageSize) {
61
43
  onPageSizeChange(newSize);
62
44
  }
63
45
  };
64
-
65
- const pageSizeBlock = (
66
- <div
67
- className="flex justify-center sm:justify-start items-center gap-2 shrink-0"
68
- role="group"
69
- aria-label="Page size selector"
70
- >
71
- <Label htmlFor="page-size-select" className="text-sm font-normal whitespace-nowrap">
72
- Results per page:
73
- </Label>
74
- <Select value={pageSize.toString()} onValueChange={handlePageSizeChange} disabled={disabled}>
75
- <SelectTrigger
76
- id="page-size-select"
77
- className="w-16"
78
- aria-label="Select number of results per page"
79
- >
80
- <SelectValue />
81
- </SelectTrigger>
82
- <SelectContent>
83
- {pageSizeOptions.map((size) => (
84
- <SelectItem key={size} value={size.toString()}>
85
- {size}
86
- </SelectItem>
87
- ))}
88
- </SelectContent>
89
- </Select>
90
- </div>
91
- );
92
-
93
- if (isLoadMoreProps(props)) {
94
- const { hasNextPage, onLoadMore, loadMoreLoading = false, loadMoreSlot } = props;
95
- return (
96
- <div className="w-full grid grid-cols-1 sm:grid-cols-2 items-center justify-center gap-4 py-2">
97
- {pageSizeBlock}
98
- <div className="flex justify-center sm:justify-end">
99
- {loadMoreSlot !== undefined ? (
100
- loadMoreSlot
101
- ) : hasNextPage ? (
102
- <Button
103
- onClick={onLoadMore}
104
- disabled={loadMoreLoading}
105
- aria-label={loadMoreLoading ? "Loading..." : "Load More"}
106
- >
107
- {loadMoreLoading ? "Loading..." : "Load More"}
108
- </Button>
109
- ) : null}
110
- </div>
111
- </div>
112
- );
113
- }
114
-
115
- const { pageIndex, hasNextPage, hasPreviousPage, onNextPage, onPreviousPage } = props;
116
46
  const currentPage = pageIndex + 1;
117
47
  const prevDisabled = disabled || !hasPreviousPage;
118
48
  const nextDisabled = disabled || !hasNextPage;
119
49
 
120
50
  return (
121
51
  <div className="w-full grid grid-cols-1 sm:grid-cols-2 items-center justify-center gap-4 py-2">
122
- {pageSizeBlock}
52
+ <div
53
+ className="flex justify-center sm:justify-start items-center gap-2 shrink-0 row-2 sm:row-1"
54
+ role="group"
55
+ aria-label="Page size selector"
56
+ >
57
+ <Label htmlFor="page-size-select" className="text-sm font-normal whitespace-nowrap">
58
+ Results per page:
59
+ </Label>
60
+ <Select
61
+ value={pageSize.toString()}
62
+ onValueChange={handlePageSizeChange}
63
+ disabled={disabled}
64
+ >
65
+ <SelectTrigger
66
+ id="page-size-select"
67
+ className="w-16"
68
+ aria-label="Select number of results per page"
69
+ >
70
+ <SelectValue />
71
+ </SelectTrigger>
72
+ <SelectContent>
73
+ {pageSizeOptions.map((size) => (
74
+ <SelectItem key={size} value={size.toString()}>
75
+ {size}
76
+ </SelectItem>
77
+ ))}
78
+ </SelectContent>
79
+ </Select>
80
+ </div>
123
81
  <Pagination className="w-full mx-0 sm:justify-end">
124
82
  <PaginationContent>
125
83
  <PaginationItem>
@@ -7,52 +7,31 @@ import {
7
7
  } from "../../../../components/ui/select";
8
8
  import { Label } from "../../../../components/ui/label";
9
9
  import { cn } from "../../../../lib/utils";
10
- import type { FilterFieldConfig, ActiveFilterValue } from "../../utils/filterUtils";
10
+ import { useFilterField } from "../FilterContext";
11
+ import type { ActiveFilterValue } from "../../utils/filterUtils";
11
12
 
12
13
  const ALL_VALUE = "__all__";
13
14
 
14
15
  interface BooleanFilterProps extends Omit<React.ComponentProps<"div">, "onChange"> {
15
- config: FilterFieldConfig;
16
- value: ActiveFilterValue | undefined;
17
- onChange: (value: ActiveFilterValue | undefined) => void;
18
- labelProps?: React.ComponentProps<typeof Label>;
19
- controlProps?: Omit<
20
- React.ComponentProps<typeof BooleanFilterSelect>,
21
- "config" | "value" | "onChange"
22
- >;
23
- helpTextProps?: React.ComponentProps<"p">;
16
+ field: string;
17
+ label: string;
18
+ helpText?: string;
24
19
  }
25
20
 
26
- export function BooleanFilter({
27
- config,
28
- value,
29
- onChange,
30
- className,
31
- labelProps,
32
- controlProps,
33
- helpTextProps,
34
- ...props
35
- }: BooleanFilterProps) {
21
+ export function BooleanFilter({ field, label, helpText, className, ...props }: BooleanFilterProps) {
22
+ const { value, onChange } = useFilterField(field);
36
23
  return (
37
24
  <div className={cn("space-y-1.5", className)} {...props}>
38
- <Label htmlFor={`filter-${config.field}`} {...labelProps}>
39
- {labelProps?.children ?? config.label}
40
- </Label>
41
- <BooleanFilterSelect config={config} value={value} onChange={onChange} {...controlProps} />
42
- {config.helpText && (
43
- <p
44
- {...helpTextProps}
45
- className={cn("text-xs text-muted-foreground", helpTextProps?.className)}
46
- >
47
- {helpTextProps?.children ?? config.helpText}
48
- </p>
49
- )}
25
+ <Label htmlFor={`filter-${field}`}>{label}</Label>
26
+ <BooleanFilterSelect field={field} label={label} value={value} onChange={onChange} />
27
+ {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
50
28
  </div>
51
29
  );
52
30
  }
53
31
 
54
32
  interface BooleanFilterSelectProps {
55
- config: FilterFieldConfig;
33
+ field: string;
34
+ label: string;
56
35
  value: ActiveFilterValue | undefined;
57
36
  onChange: (value: ActiveFilterValue | undefined) => void;
58
37
  triggerProps?: React.ComponentProps<typeof SelectTrigger>;
@@ -60,7 +39,8 @@ interface BooleanFilterSelectProps {
60
39
  }
61
40
 
62
41
  export function BooleanFilterSelect({
63
- config,
42
+ field,
43
+ label,
64
44
  value,
65
45
  onChange,
66
46
  triggerProps,
@@ -73,12 +53,12 @@ export function BooleanFilterSelect({
73
53
  if (v === ALL_VALUE) {
74
54
  onChange(undefined);
75
55
  } else {
76
- onChange({ field: config.field, label: config.label, type: "boolean", value: v });
56
+ onChange({ field, label, type: "boolean", value: v });
77
57
  }
78
58
  }}
79
59
  >
80
60
  <SelectTrigger
81
- id={`filter-${config.field}`}
61
+ id={`filter-${field}`}
82
62
  {...triggerProps}
83
63
  className={cn("w-full", triggerProps?.className)}
84
64
  >