@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.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/package.json +3 -3
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/api/graphqlClient.ts +25 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/pages/AccountSearch.tsx +82 -54
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/FilterContext.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/PaginationControls.tsx +45 -87
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/BooleanFilter.tsx +16 -36
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/DateFilter.tsx +33 -77
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/DateRangeFilter.tsx +14 -23
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/MultiSelectFilter.tsx +18 -26
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/NumericRangeFilter.tsx +22 -39
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/SearchFilter.tsx +12 -15
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/SelectFilter.tsx +30 -34
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/TextFilter.tsx +27 -30
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useAsyncData.ts +1 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useCachedAsyncData.ts +1 -0
- package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useObjectSearchParams.ts +22 -0
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/package.json +1 -1
- 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.
|
|
19
|
-
"@salesforce/webapp-experimental": "^1.112.
|
|
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.
|
|
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",
|
package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/api/graphqlClient.ts
ADDED
|
@@ -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 {
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
>(
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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-${
|
|
39
|
-
|
|
40
|
-
</
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
56
|
+
onChange({ field, label, type: "boolean", value: v });
|
|
77
57
|
}
|
|
78
58
|
}}
|
|
79
59
|
>
|
|
80
60
|
<SelectTrigger
|
|
81
|
-
id={`filter-${
|
|
61
|
+
id={`filter-${field}`}
|
|
82
62
|
{...triggerProps}
|
|
83
63
|
className={cn("w-full", triggerProps?.className)}
|
|
84
64
|
>
|