@salesforce/ui-bundle-template-app-react-template-b2x 11.17.1 → 11.19.0

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 (77) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/uiBundles/reactexternalapp/package.json +4 -4
  3. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/{features/object-search/components → components}/ObjectBreadcrumb.tsx +7 -2
  4. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/api/distinctValuesService.ts +84 -0
  5. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/api/searchService.ts +71 -0
  6. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/MergedSearchResults.tsx +263 -0
  7. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/Search.tsx +185 -0
  8. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/SearchResults.tsx +77 -0
  9. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/SourceSection.tsx +166 -0
  10. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/PaginationControls.tsx +185 -0
  11. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/ScopeSelector.tsx +55 -0
  12. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/SearchBar.tsx +55 -0
  13. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/controls/SortControl.tsx +62 -0
  14. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/ActiveFilters.tsx +61 -0
  15. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/DefaultFilterPanel.tsx +125 -0
  16. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/{object-search/components → search/components/filters}/FilterContext.tsx +7 -20
  17. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
  18. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/DateRangeFilter.tsx +160 -0
  19. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
  20. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +88 -0
  21. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
  22. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
  23. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
  24. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/components/results/DefaultResultRow.tsx +109 -0
  25. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/config.json +83 -0
  26. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/{object-search → search}/hooks/useAsyncData.ts +3 -10
  27. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/hooks/useDistinctValues.ts +59 -0
  28. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/hooks/useSearch.ts +703 -0
  29. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/index.ts +90 -0
  30. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/loadConfig.ts +14 -0
  31. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/queryBuilder.ts +156 -0
  32. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/types.ts +307 -0
  33. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/debounce.ts +17 -0
  34. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/fieldUtils.ts +14 -0
  35. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/filterUtils.ts +285 -0
  36. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/search/utils/sortUtils.ts +34 -0
  37. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountObjectDetailPage.tsx +49 -16
  38. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/AccountSearch.tsx +21 -293
  39. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/pages/Home.tsx +22 -7
  40. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/routes.tsx +1 -1
  41. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/{features/object-search/utils/fieldUtils.ts → utils/accountFields.ts} +9 -0
  42. package/dist/force-app/main/default/uiBundles/reactexternalapp/tsconfig.tsbuildinfo +1 -1
  43. package/dist/package-lock.json +2 -2
  44. package/dist/package.json +1 -1
  45. package/package.json +1 -1
  46. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/accountSearchService.ts +0 -46
  47. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/query/distinctAccountIndustries.graphql +0 -19
  48. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/query/distinctAccountTypes.graphql +0 -19
  49. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/account/query/searchAccounts.graphql +0 -51
  50. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/api/graphql-operations-types.ts +0 -11260
  51. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/accountSearchService.ts +0 -46
  52. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
  53. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
  54. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
  55. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
  56. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -355
  57. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +0 -304
  58. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/__examples__/pages/Home.tsx +0 -34
  59. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/api/objectSearchService.ts +0 -82
  60. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/ActiveFilters.tsx +0 -89
  61. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/PaginationControls.tsx +0 -109
  62. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/SearchBar.tsx +0 -41
  63. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/SortControl.tsx +0 -143
  64. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/BooleanFilter.tsx +0 -78
  65. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/DateFilter.tsx +0 -128
  66. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
  67. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -43
  68. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/MultiSelectFilter.tsx +0 -105
  69. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +0 -171
  70. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/SearchFilter.tsx +0 -52
  71. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/SelectFilter.tsx +0 -97
  72. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/components/filters/TextFilter.tsx +0 -93
  73. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/hooks/useDebouncedCallback.ts +0 -34
  74. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/hooks/useObjectSearchParams.ts +0 -254
  75. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/utils/debounce.ts +0 -25
  76. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/utils/filterUtils.ts +0 -404
  77. package/dist/force-app/main/default/uiBundles/reactexternalapp/src/features/object-search/utils/sortUtils.ts +0 -38
@@ -1,304 +0,0 @@
1
- import { useMemo, useState } from "react";
2
- import { Link } from "react-router";
3
- import { AlertCircle, ChevronDown, SearchX } from "lucide-react";
4
- import {
5
- searchAccounts,
6
- fetchDistinctIndustries,
7
- fetchDistinctTypes,
8
- } from "../api/accountSearchService";
9
- import { useAsyncData } from "../../hooks/useAsyncData";
10
- import { fieldValue } from "../../utils/fieldUtils";
11
- import { useObjectSearchParams } from "../../hooks/useObjectSearchParams";
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";
25
- import { Skeleton } from "../../../../components/ui/skeleton";
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";
34
- import { ActiveFilters } from "../../components/ActiveFilters";
35
- import { SortControl } from "../../components/SortControl";
36
- import type { FilterFieldConfig } from "../../utils/filterUtils";
37
- import type { SortFieldConfig } from "../../utils/sortUtils";
38
- import type { Account_Filter, Account_OrderBy } from "../../../../api/graphql-operations-types";
39
- import type { AccountSearchResult } from "../api/accountSearchService";
40
- import { ObjectBreadcrumb } from "../../components/ObjectBreadcrumb";
41
- import PaginationControls from "../../components/PaginationControls";
42
- import type { PaginationConfig } from "../../hooks/useObjectSearchParams";
43
-
44
- const PAGINATION_CONFIG: PaginationConfig = {
45
- defaultPageSize: 7,
46
- validPageSizes: [7, 14, 28, 42],
47
- };
48
-
49
- type AccountNode = NonNullable<
50
- NonNullable<NonNullable<AccountSearchResult["edges"]>[number]>["node"]
51
- >;
52
-
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: "datetime" },
66
- { field: "LastModifiedDate", label: "Last Modified Date", type: "datetimerange" },
67
- ];
68
-
69
- const ACCOUNT_SORT_CONFIGS: SortFieldConfig<keyof Account_OrderBy>[] = [
70
- { field: "Name", label: "Name" },
71
- { field: "AnnualRevenue", label: "Annual Revenue" },
72
- { field: "Industry", label: "Industry" },
73
- { field: "CreatedDate", label: "Created Date" },
74
- ];
75
-
76
- // -- Component --------------------------------------------------------------
77
-
78
- export default function AccountSearch() {
79
- const [filtersOpen, setFiltersOpen] = useState(true);
80
- const { data: industryOptions } = useAsyncData(fetchDistinctIndustries, []);
81
- const { data: typeOptions } = useAsyncData(fetchDistinctTypes, []);
82
-
83
- const { filters, sort, query, pagination, resetAll } = useObjectSearchParams<
84
- Account_Filter,
85
- Account_OrderBy
86
- >(FILTER_CONFIGS, ACCOUNT_SORT_CONFIGS, PAGINATION_CONFIG);
87
-
88
- const { data, loading, error } = useAsyncData(
89
- () =>
90
- searchAccounts({
91
- where: query.where,
92
- orderBy: query.orderBy,
93
- first: pagination.pageSize,
94
- after: pagination.afterCursor,
95
- }),
96
- [query.where, query.orderBy, pagination.pageSize, pagination.afterCursor],
97
- );
98
-
99
- const pageInfo = data?.pageInfo;
100
- const totalCount = data?.totalCount;
101
- const hasNextPage = pageInfo?.hasNextPage ?? false;
102
- const hasPreviousPage = pagination.pageIndex > 0;
103
-
104
- const validAccountNodes = useMemo(
105
- () =>
106
- (data?.edges ?? []).reduce<AccountNode[]>((acc, edge) => {
107
- if (edge?.node) acc.push(edge.node);
108
- return acc;
109
- }, []),
110
- [data?.edges],
111
- );
112
-
113
- return (
114
- <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
115
- <ObjectBreadcrumb listPath="/accounts" listLabel="Accounts" />
116
-
117
- <h1 className="text-2xl font-bold mb-4">Search Accounts</h1>
118
-
119
- <div className="flex flex-col lg:flex-row gap-6">
120
- {/* Sidebar — Filter Panel */}
121
- <aside className="w-full lg:w-80 shrink-0">
122
- <FilterProvider
123
- filters={filters.active}
124
- onFilterChange={filters.set}
125
- onFilterRemove={filters.remove}
126
- onReset={resetAll}
127
- >
128
- <Card>
129
- <Collapsible open={filtersOpen} onOpenChange={setFiltersOpen}>
130
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
131
- <CardTitle className="text-base font-semibold">
132
- <h2>Filters</h2>
133
- </CardTitle>
134
- <div className="flex items-center gap-1">
135
- <FilterResetButton variant="destructive" size="sm" />
136
- <CollapsibleTrigger asChild>
137
- <Button variant="ghost" size="icon">
138
- <ChevronDown
139
- className={`h-4 w-4 transition-transform ${filtersOpen ? "" : "-rotate-90"}`}
140
- />
141
- <span className="sr-only">Toggle filters</span>
142
- </Button>
143
- </CollapsibleTrigger>
144
- </div>
145
- </CardHeader>
146
- <CollapsibleContent>
147
- <CardContent className="space-y-1 pt-0">
148
- <SearchFilter
149
- field="search"
150
- label="Search"
151
- placeholder="Search by name, phone, or industry..."
152
- />
153
- <TextFilter field="Name" label="Account Name" placeholder="Search by name..." />
154
- <SelectFilter
155
- field="Industry"
156
- label="Industry"
157
- options={industryOptions ?? []}
158
- />
159
- <MultiSelectFilter field="Type" label="Type" options={typeOptions ?? []} />
160
- <NumericRangeFilter
161
- field="AnnualRevenue"
162
- label="Annual Revenue"
163
- min={0}
164
- max={1_000_000_000_000}
165
- />
166
- <DateFilter field="CreatedDate" label="Created Date" filterType="datetime" />
167
- <DateRangeFilter
168
- field="LastModifiedDate"
169
- label="Last Modified Date"
170
- filterType="datetimerange"
171
- />
172
- </CardContent>
173
- </CollapsibleContent>
174
- </Collapsible>
175
- </Card>
176
- </FilterProvider>
177
- </aside>
178
-
179
- {/* Main area — Sort + Results */}
180
- <div className="flex-1 min-w-0">
181
- {/* Sort control + active filters */}
182
- <div className="flex flex-wrap items-center gap-2 mb-4">
183
- <SortControl
184
- configs={ACCOUNT_SORT_CONFIGS}
185
- sort={sort.current}
186
- onSortChange={sort.set}
187
- />
188
- <ActiveFilters filters={filters.active} onRemove={filters.remove} />
189
- </div>
190
-
191
- <div className="min-h-132">
192
- {/* Loading state */}
193
- {loading && (
194
- <>
195
- <Skeleton className="h-5 w-30 mb-3" />
196
- <div className="divide-y">
197
- {Array.from({ length: pagination.pageSize }, (_, i) => (
198
- <div key={i} className="flex items-center justify-between py-3">
199
- <div className="space-y-2">
200
- <Skeleton className="h-5 w-40" />
201
- <Skeleton className="h-4 w-28" />
202
- </div>
203
- <div className="space-y-2 flex flex-col items-end">
204
- <Skeleton className="h-4 w-24" />
205
- <Skeleton className="h-4 w-20" />
206
- </div>
207
- </div>
208
- ))}
209
- </div>
210
- </>
211
- )}
212
-
213
- {/* Error state */}
214
- {error && (
215
- <>
216
- <p className="text-sm text-muted-foreground mb-3">0 accounts found</p>
217
- <Alert variant="destructive" role="alert">
218
- <AlertCircle />
219
- <AlertTitle>Failed to load accounts</AlertTitle>
220
- <AlertDescription>
221
- Something went wrong while loading accounts. Please try again later.
222
- </AlertDescription>
223
- </Alert>
224
- </>
225
- )}
226
-
227
- {/* Results list */}
228
- {!loading && !error && validAccountNodes.length > 0 && (
229
- <>
230
- <p className="text-sm text-muted-foreground mb-3">
231
- {totalCount != null && (hasNextPage || hasPreviousPage)
232
- ? `${totalCount} account${totalCount !== 1 ? "s" : ""} found`
233
- : `Showing ${validAccountNodes.length} account${validAccountNodes.length !== 1 ? "s" : ""}`}
234
- </p>
235
- <AccountResultsList nodes={validAccountNodes} />
236
- </>
237
- )}
238
-
239
- {/* No results state */}
240
- {!loading && !error && validAccountNodes.length === 0 && (
241
- <div className="flex flex-col items-center justify-center py-16 text-center">
242
- <SearchX className="size-12 text-muted-foreground mb-4" />
243
- <h2 className="text-lg font-semibold mb-1">No accounts found</h2>
244
- <p className="text-sm text-muted-foreground">
245
- Try adjusting your filters or search criteria.
246
- </p>
247
- </div>
248
- )}
249
- </div>
250
-
251
- {/* Pagination — always visible, disabled while loading or on error */}
252
- <PaginationControls
253
- pageIndex={pagination.pageIndex}
254
- hasNextPage={hasNextPage}
255
- hasPreviousPage={hasPreviousPage}
256
- pageSize={pagination.pageSize}
257
- pageSizeOptions={PAGINATION_CONFIG.validPageSizes}
258
- onNextPage={() => {
259
- if (pageInfo?.endCursor) pagination.goToNextPage(pageInfo.endCursor);
260
- }}
261
- onPreviousPage={pagination.goToPreviousPage}
262
- onPageSizeChange={pagination.setPageSize}
263
- disabled={loading || !!error}
264
- />
265
- </div>
266
- </div>
267
- </div>
268
- );
269
- }
270
-
271
- // -- Result Components ------------------------------------------------------
272
-
273
- function AccountResultsList({ nodes }: { nodes: AccountNode[] }) {
274
- return (
275
- <ul className="divide-y">
276
- {nodes.map((node) => (
277
- <AccountResultItem key={node.Id} node={node} />
278
- ))}
279
- </ul>
280
- );
281
- }
282
-
283
- function AccountResultItem({ node }: { node: AccountNode }) {
284
- return (
285
- <li>
286
- <Link
287
- to={`/accounts/${node.Id}`}
288
- className="flex items-center justify-between py-3 px-3 -mx-3 rounded-md transition-colors hover:bg-accent"
289
- >
290
- <div>
291
- <span className="font-medium">{fieldValue(node.Name) ?? "\u2014"}</span>
292
- <p className="text-sm text-muted-foreground">
293
- {[fieldValue(node.Industry), fieldValue(node.Type)].filter(Boolean).join(" \u00B7 ") ||
294
- "\u2014"}
295
- </p>
296
- </div>
297
- <div className="text-right text-sm">
298
- <p>{fieldValue(node.Phone) ?? ""}</p>
299
- <p className="text-muted-foreground">{fieldValue(node.Owner?.Name) ?? ""}</p>
300
- </div>
301
- </Link>
302
- </li>
303
- );
304
- }
@@ -1,34 +0,0 @@
1
- import { useState } from "react";
2
- import { useNavigate } from "react-router";
3
- import { SearchBar } from "../../components/SearchBar";
4
- import { Button } from "../../../../components/ui/button";
5
-
6
- export default function HomePage() {
7
- const navigate = useNavigate();
8
- const [text, setText] = useState("");
9
-
10
- const handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
11
- e.preventDefault();
12
- const params = text ? `?q=${encodeURIComponent(text)}` : "";
13
- navigate(`/accounts${params}`);
14
- };
15
-
16
- return (
17
- <div className="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
18
- <div className="flex items-center gap-6 mb-6">
19
- <h1 className="text-2xl font-bold">Account Search</h1>
20
- <Button variant="outline" size="sm" onClick={() => navigate("/accounts")}>
21
- Browse All Accounts
22
- </Button>
23
- </div>
24
- <form onSubmit={handleSubmit} className="flex gap-2">
25
- <SearchBar
26
- placeholder="Search by name, phone, or industry..."
27
- value={text}
28
- handleChange={setText}
29
- />
30
- <Button type="submit">Search</Button>
31
- </form>
32
- </div>
33
- );
34
- }
@@ -1,82 +0,0 @@
1
- import { createDataSDK } from "@salesforce/platform-sdk";
2
-
3
- export interface ObjectSearchOptions<TWhere, TOrderBy> {
4
- where?: TWhere;
5
- orderBy?: TOrderBy;
6
- first?: number;
7
- after?: string;
8
- }
9
-
10
- export type PicklistOption = { value: string; label: string };
11
-
12
- /**
13
- * Executes a GraphQL search query and extracts the result for the given object name
14
- * from the standard `uiapi.query.<ObjectName>` response shape.
15
- */
16
- export async function searchObjects<TResult, TQuery, TVariables>(
17
- query: string,
18
- objectName: string,
19
- options: ObjectSearchOptions<unknown, unknown> = {},
20
- ): Promise<TResult> {
21
- const { where, orderBy, first = 20, after } = options;
22
-
23
- const data = await createDataSDK();
24
- const variables = { first, after, where, orderBy } as TVariables;
25
- const result = await data.graphql!.query<TQuery, TVariables>({
26
- query: query,
27
- variables: variables,
28
- });
29
-
30
- if (result.errors?.length) {
31
- throw new Error(result.errors.map((e) => e.message).join("; "));
32
- }
33
-
34
- const uiapi = (result.data as Record<string, unknown> | undefined)?.uiapi as
35
- | Record<string, unknown>
36
- | undefined;
37
- const queryResult = (uiapi?.query as Record<string, unknown> | undefined)?.[objectName] as
38
- | TResult
39
- | undefined;
40
-
41
- if (!queryResult) {
42
- throw new Error(`No ${objectName} data returned`);
43
- }
44
-
45
- return queryResult;
46
- }
47
-
48
- /**
49
- * Executes a GraphQL aggregate/groupBy query and extracts picklist options
50
- * from the standard `uiapi.aggregate.<ObjectName>` response shape.
51
- */
52
- export async function fetchDistinctValues<TQuery>(
53
- query: string,
54
- objectName: string,
55
- fieldName: string,
56
- ): Promise<PicklistOption[]> {
57
- const data = await createDataSDK();
58
- const result = await data.graphql!.query<TQuery>({ query: query });
59
-
60
- if (result.errors?.length) {
61
- throw new Error(result.errors.map((e) => e.message).join("; "));
62
- }
63
-
64
- const uiapi = (result.data as Record<string, unknown> | undefined)?.uiapi as
65
- | Record<string, unknown>
66
- | undefined;
67
- const aggregate = (uiapi?.aggregate as Record<string, unknown> | undefined)?.[objectName] as
68
- | { edges?: Array<{ node?: { aggregate?: Record<string, unknown> } }> }
69
- | undefined;
70
-
71
- const edges = aggregate?.edges ?? [];
72
- return edges
73
- .map((edge) => {
74
- const field = edge?.node?.aggregate?.[fieldName] as
75
- | { value?: string | null; displayValue?: string | null; label?: string | null }
76
- | undefined;
77
- const value = field?.value;
78
- if (!value) return null;
79
- return { value, label: field.label ?? field.displayValue ?? value };
80
- })
81
- .filter((opt): opt is PicklistOption => opt !== null);
82
- }
@@ -1,89 +0,0 @@
1
- import { X } from "lucide-react";
2
- import { Button } from "../../../components/ui/button";
3
- import { cn } from "../../../lib/utils";
4
- import type { ActiveFilterValue } from "../utils/filterUtils";
5
-
6
- function formatFilterLabel(filter: ActiveFilterValue): string {
7
- const { label, type, value, min, max } = filter;
8
-
9
- switch (type) {
10
- case "search":
11
- return `Search: ${value}`;
12
- case "text":
13
- case "picklist":
14
- return `${label}: ${value}`;
15
- case "multipicklist": {
16
- const values = value ? value.split(",") : [];
17
- if (values.length <= 2) return `${label}: ${values.join(", ")}`;
18
- return `${label}: ${values.length} selected`;
19
- }
20
- case "boolean":
21
- return `${label}: ${value === "true" ? "Yes" : "No"}`;
22
- case "numeric": {
23
- if (min && max) return `${label}: ${min} - ${max}`;
24
- if (min) return `${label}: >= ${min}`;
25
- return `${label}: <= ${max}`;
26
- }
27
- case "date": {
28
- if (min && max) return `${label}: ${min} to ${max}`;
29
- if (min) return `${label}: from ${min}`;
30
- return `${label}: until ${max}`;
31
- }
32
- default:
33
- return label;
34
- }
35
- }
36
-
37
- interface ActiveFiltersProps extends React.ComponentProps<"div"> {
38
- filters: ActiveFilterValue[];
39
- onRemove: (field: string) => void;
40
- buttonProps?: Omit<React.ComponentProps<typeof ActiveFilterButton>, "filter" | "onRemove">;
41
- }
42
-
43
- export function ActiveFilters({
44
- filters,
45
- onRemove,
46
- className,
47
- buttonProps,
48
- ...props
49
- }: ActiveFiltersProps) {
50
- if (filters.length === 0) return null;
51
-
52
- return (
53
- <div className={cn("flex flex-wrap gap-2", className)} {...props}>
54
- {filters.map((filter) => (
55
- <ActiveFilterButton
56
- key={filter.field}
57
- filter={filter}
58
- onRemove={onRemove}
59
- {...buttonProps}
60
- />
61
- ))}
62
- </div>
63
- );
64
- }
65
-
66
- interface ActiveFilterButtonProps extends React.ComponentProps<typeof Button> {
67
- filter: ActiveFilterValue;
68
- onRemove: (field: string) => void;
69
- }
70
-
71
- export function ActiveFilterButton({
72
- filter,
73
- onRemove,
74
- className,
75
- ...props
76
- }: ActiveFilterButtonProps) {
77
- return (
78
- <Button
79
- variant="outline"
80
- size="sm"
81
- className={cn("gap-1 h-7 text-xs", className)}
82
- onClick={() => onRemove(filter.field)}
83
- {...props}
84
- >
85
- {formatFilterLabel(filter)}
86
- <X className="h-3 w-3" />
87
- </Button>
88
- );
89
- }
@@ -1,109 +0,0 @@
1
- import {
2
- Pagination,
3
- PaginationContent,
4
- PaginationItem,
5
- PaginationPrevious,
6
- PaginationNext,
7
- } from "../../../components/ui/pagination";
8
- import {
9
- Select,
10
- SelectContent,
11
- SelectItem,
12
- SelectTrigger,
13
- SelectValue,
14
- } from "../../../components/ui/select";
15
- import { Label } from "../../../components/ui/label";
16
-
17
- interface PaginationControlsProps {
18
- pageIndex: number;
19
- hasNextPage: boolean;
20
- hasPreviousPage: boolean;
21
- pageSize: number;
22
- pageSizeOptions: readonly number[];
23
- onNextPage: () => void;
24
- onPreviousPage: () => void;
25
- onPageSizeChange: (newPageSize: number) => void;
26
- disabled?: boolean;
27
- }
28
-
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) {
40
- const handlePageSizeChange = (newValue: string) => {
41
- const newSize = parseInt(newValue, 10);
42
- if (!isNaN(newSize) && newSize !== pageSize) {
43
- onPageSizeChange(newSize);
44
- }
45
- };
46
- const currentPage = pageIndex + 1;
47
- const prevDisabled = disabled || !hasPreviousPage;
48
- const nextDisabled = disabled || !hasNextPage;
49
-
50
- return (
51
- <div className="w-full grid grid-cols-1 sm:grid-cols-2 items-center justify-center gap-4 py-2">
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>
81
- <Pagination className="w-full mx-0 sm:justify-end">
82
- <PaginationContent>
83
- <PaginationItem>
84
- <PaginationPrevious
85
- onClick={prevDisabled ? undefined : onPreviousPage}
86
- aria-disabled={prevDisabled}
87
- className={prevDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
88
- />
89
- </PaginationItem>
90
- <PaginationItem>
91
- <span
92
- className="min-w-16 text-center text-sm text-muted-foreground px-2"
93
- aria-current="page"
94
- >
95
- Page {currentPage}
96
- </span>
97
- </PaginationItem>
98
- <PaginationItem>
99
- <PaginationNext
100
- onClick={nextDisabled ? undefined : onNextPage}
101
- aria-disabled={nextDisabled}
102
- className={nextDisabled ? "pointer-events-none opacity-50" : "cursor-pointer"}
103
- />
104
- </PaginationItem>
105
- </PaginationContent>
106
- </Pagination>
107
- </div>
108
- );
109
- }
@@ -1,41 +0,0 @@
1
- import { Search } from "lucide-react";
2
- import { Input } from "../../../components/ui/input";
3
- import { cn } from "../../../lib/utils";
4
-
5
- interface SearchBarProps extends React.ComponentProps<"div"> {
6
- value: string;
7
- handleChange: (value: string) => void;
8
- placeholder?: string;
9
- iconProps?: React.ComponentProps<typeof Search>;
10
- inputProps?: Omit<React.ComponentProps<typeof Input>, "value">;
11
- }
12
-
13
- export function SearchBar({
14
- value,
15
- handleChange,
16
- placeholder,
17
- className,
18
- iconProps,
19
- inputProps,
20
- ...props
21
- }: SearchBarProps) {
22
- return (
23
- <div className={cn("relative flex-1", className)} title={placeholder} {...props}>
24
- <Search
25
- {...iconProps}
26
- className={cn(
27
- "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground",
28
- iconProps?.className,
29
- )}
30
- />
31
- <Input
32
- type="text"
33
- value={value}
34
- onChange={(e) => handleChange(e.target.value)}
35
- placeholder={placeholder}
36
- {...inputProps}
37
- className={cn("pl-9", inputProps?.className)}
38
- />
39
- </div>
40
- );
41
- }