@salesforce/webapp-template-app-react-template-b2x-experimental 1.109.5 → 1.109.6

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 (102) hide show
  1. package/dist/CHANGELOG.md +8 -0
  2. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/package.json +4 -3
  3. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/api/graphql-operations-types.ts +11260 -0
  4. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/ui/sonner.tsx +20 -0
  5. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/accountSearchService.ts +46 -0
  6. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
  7. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
  8. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
  9. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
  10. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
  11. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/AccountSearch.tsx +275 -0
  12. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/__examples__/pages/Home.tsx +34 -0
  13. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/api/objectSearchService.ts +84 -0
  14. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/ActiveFilters.tsx +89 -0
  15. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/FilterPanel.tsx +127 -0
  16. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
  17. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/PaginationControls.tsx +151 -0
  18. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/SearchBar.tsx +41 -0
  19. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/SortControl.tsx +143 -0
  20. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/BooleanFilter.tsx +94 -0
  21. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/DateFilter.tsx +138 -0
  22. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/DateRangeFilter.tsx +78 -0
  23. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/MultiSelectFilter.tsx +106 -0
  24. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/NumericRangeFilter.tsx +102 -0
  25. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/SearchFilter.tsx +40 -0
  26. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/SelectFilter.tsx +97 -0
  27. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/components/filters/TextFilter.tsx +77 -0
  28. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useAsyncData.ts +53 -0
  29. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useCachedAsyncData.ts +183 -0
  30. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/hooks/useObjectSearchParams.ts +225 -0
  31. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/debounce.ts +22 -0
  32. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/fieldUtils.ts +29 -0
  33. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/filterUtils.ts +372 -0
  34. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/object-search/utils/sortUtils.ts +38 -0
  35. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/pages/Home.tsx +10 -11
  36. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/routes.tsx +8 -20
  37. package/dist/package-lock.json +2 -2
  38. package/dist/package.json +1 -1
  39. package/package.json +1 -1
  40. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectDetailService.ts +0 -102
  41. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectInfoGraphQLService.ts +0 -137
  42. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/objectInfoService.ts +0 -95
  43. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/api/recordListGraphQLService.ts +0 -364
  44. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailFields.tsx +0 -55
  45. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailForm.tsx +0 -146
  46. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailHeader.tsx +0 -34
  47. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/DetailLayoutSections.tsx +0 -80
  48. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/Section.tsx +0 -108
  49. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/SectionRow.tsx +0 -20
  50. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/UiApiDetailForm.tsx +0 -140
  51. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +0 -73
  52. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +0 -29
  53. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +0 -17
  54. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +0 -24
  55. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedText.tsx +0 -11
  56. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +0 -29
  57. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterField.tsx +0 -54
  58. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterInput.tsx +0 -55
  59. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FilterSelect.tsx +0 -72
  60. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/filters/FiltersPanel.tsx +0 -380
  61. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/forms/filters-form.tsx +0 -114
  62. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/forms/submit-button.tsx +0 -47
  63. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/GlobalSearchInput.tsx +0 -114
  64. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/ResultCardFields.tsx +0 -71
  65. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchHeader.tsx +0 -31
  66. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchPagination.tsx +0 -144
  67. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchResultCard.tsx +0 -138
  68. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/search/SearchResultsPanel.tsx +0 -197
  69. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/components/shared/LoadingFallback.tsx +0 -61
  70. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/constants.ts +0 -39
  71. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/filters/FilterInput.tsx +0 -55
  72. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/filters/FilterSelect.tsx +0 -72
  73. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/form.tsx +0 -209
  74. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useObjectInfoBatch.ts +0 -72
  75. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useObjectSearchData.ts +0 -174
  76. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useRecordDetailLayout.ts +0 -137
  77. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/hooks/useRecordListGraphQL.ts +0 -135
  78. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/pages/DetailPage.tsx +0 -109
  79. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/pages/GlobalSearch.tsx +0 -235
  80. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/filters/filters.ts +0 -121
  81. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/filters/picklist.ts +0 -6
  82. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/objectInfo/objectInfo.ts +0 -49
  83. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/recordDetail/recordDetail.ts +0 -61
  84. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/schema.d.ts +0 -200
  85. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/types/search/searchResults.ts +0 -229
  86. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/apiUtils.ts +0 -59
  87. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/cacheUtils.ts +0 -76
  88. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/debounce.ts +0 -90
  89. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/fieldUtils.ts +0 -354
  90. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/fieldValueExtractor.ts +0 -67
  91. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/filterUtils.ts +0 -32
  92. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/formDataTransformUtils.ts +0 -260
  93. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/formUtils.ts +0 -142
  94. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLNodeFieldUtils.ts +0 -186
  95. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +0 -77
  96. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/graphQLRecordAdapter.ts +0 -90
  97. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/layoutTransformUtils.ts +0 -236
  98. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/linkUtils.ts +0 -14
  99. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/paginationUtils.ts +0 -49
  100. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/recordUtils.ts +0 -159
  101. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/utils/sanitizationUtils.ts +0 -50
  102. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/index.ts +0 -120
@@ -0,0 +1,275 @@
1
+ import { useMemo } from "react";
2
+ import { Link } from "react-router";
3
+ import { AlertCircle, SearchX } from "lucide-react";
4
+ import {
5
+ searchAccounts,
6
+ fetchDistinctIndustries,
7
+ fetchDistinctTypes,
8
+ } from "../api/accountSearchService";
9
+ import { useCachedAsyncData } from "../../hooks/useCachedAsyncData";
10
+ import { fieldValue } from "../../utils/fieldUtils";
11
+ import { useObjectSearchParams } from "../../hooks/useObjectSearchParams";
12
+ import { Alert, AlertTitle, AlertDescription } from "../../../../components/ui/alert";
13
+ import { Skeleton } from "../../../../components/ui/skeleton";
14
+ import { FilterPanel } from "../../components/FilterPanel";
15
+ import { ActiveFilters } from "../../components/ActiveFilters";
16
+ import { SortControl } from "../../components/SortControl";
17
+ import type { FilterFieldConfig } from "../../utils/filterUtils";
18
+ import type { SortFieldConfig } from "../../utils/sortUtils";
19
+ import type { Account_Filter, Account_OrderBy } from "../../../../api/graphql-operations-types";
20
+ import type { AccountSearchResult } from "../api/accountSearchService";
21
+ import { ObjectBreadcrumb } from "../../components/ObjectBreadcrumb";
22
+ import PaginationControls from "../../components/PaginationControls";
23
+ import type { PaginationConfig } from "../../hooks/useObjectSearchParams";
24
+
25
+ const PAGINATION_CONFIG: PaginationConfig = {
26
+ defaultPageSize: 6,
27
+ validPageSizes: [6, 12, 24, 48],
28
+ };
29
+
30
+ type AccountNode = NonNullable<
31
+ NonNullable<NonNullable<AccountSearchResult["edges"]>[number]>["node"]
32
+ >;
33
+
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
+ }
76
+
77
+ const ACCOUNT_SORT_CONFIGS: SortFieldConfig<keyof Account_OrderBy>[] = [
78
+ { field: "Name", label: "Name" },
79
+ { field: "AnnualRevenue", label: "Annual Revenue" },
80
+ { field: "Industry", label: "Industry" },
81
+ { field: "CreatedDate", label: "Created Date" },
82
+ ];
83
+
84
+ // -- Component --------------------------------------------------------------
85
+
86
+ export default function AccountSearch() {
87
+ const { data: industryOptions } = useCachedAsyncData(fetchDistinctIndustries, [], {
88
+ key: "distinctIndustries",
89
+ ttl: 300_000,
90
+ });
91
+ const { data: typeOptions } = useCachedAsyncData(fetchDistinctTypes, [], {
92
+ key: "distinctTypes",
93
+ ttl: 300_000,
94
+ });
95
+
96
+ const filterConfigs = useMemo(
97
+ () => buildAccountFilterConfigs(industryOptions ?? [], typeOptions ?? []),
98
+ [industryOptions, typeOptions],
99
+ );
100
+
101
+ const { filters, sort, query, pagination, resetAll } = useObjectSearchParams<
102
+ Account_Filter,
103
+ Account_OrderBy
104
+ >(filterConfigs, ACCOUNT_SORT_CONFIGS, PAGINATION_CONFIG);
105
+
106
+ const searchKey = `accounts:${JSON.stringify({ where: query.where, orderBy: query.orderBy, first: pagination.pageSize, after: pagination.afterCursor })}`;
107
+ const { data, loading, error } = useCachedAsyncData(
108
+ () =>
109
+ searchAccounts({
110
+ where: query.where,
111
+ orderBy: query.orderBy,
112
+ first: pagination.pageSize,
113
+ after: pagination.afterCursor,
114
+ }),
115
+ [query.where, query.orderBy, pagination.pageSize, pagination.afterCursor],
116
+ { key: searchKey },
117
+ );
118
+
119
+ const pageInfo = data?.pageInfo;
120
+ const totalCount = data?.totalCount;
121
+ const hasNextPage = pageInfo?.hasNextPage ?? false;
122
+ const hasPreviousPage = pagination.pageIndex > 0;
123
+
124
+ const validAccountNodes = useMemo(
125
+ () =>
126
+ (data?.edges ?? []).reduce<AccountNode[]>((acc, edge) => {
127
+ if (edge?.node) acc.push(edge.node);
128
+ return acc;
129
+ }, []),
130
+ [data?.edges],
131
+ );
132
+
133
+ return (
134
+ <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
135
+ <ObjectBreadcrumb listPath="/accounts" listLabel="Accounts" />
136
+
137
+ <h1 className="text-2xl font-bold mb-4">Search Accounts</h1>
138
+
139
+ <div className="flex flex-col lg:flex-row gap-6">
140
+ {/* Sidebar — Filter Panel */}
141
+ <aside className="w-full lg:w-80 shrink-0">
142
+ <FilterPanel
143
+ configs={filterConfigs}
144
+ filters={filters.active}
145
+ onFilterChange={filters.set}
146
+ onReset={resetAll}
147
+ />
148
+ </aside>
149
+
150
+ {/* Main area — Sort + Results */}
151
+ <div className="flex-1 min-w-0">
152
+ {/* Sort control + active filters */}
153
+ <div className="flex flex-wrap items-center gap-2 mb-4">
154
+ <SortControl
155
+ configs={ACCOUNT_SORT_CONFIGS}
156
+ sort={sort.current}
157
+ onSortChange={sort.set}
158
+ />
159
+ <ActiveFilters filters={filters.active} onRemove={filters.remove} />
160
+ </div>
161
+
162
+ <div className="min-h-112">
163
+ {/* Loading state */}
164
+ {loading && (
165
+ <>
166
+ <Skeleton className="h-5 w-30 mb-3" />
167
+ <div className="divide-y">
168
+ {Array.from({ length: pagination.pageSize }, (_, i) => (
169
+ <div key={i} className="flex items-center justify-between py-3">
170
+ <div className="space-y-2">
171
+ <Skeleton className="h-5 w-40" />
172
+ <Skeleton className="h-4 w-28" />
173
+ </div>
174
+ <div className="space-y-2 flex flex-col items-end">
175
+ <Skeleton className="h-4 w-24" />
176
+ <Skeleton className="h-4 w-20" />
177
+ </div>
178
+ </div>
179
+ ))}
180
+ </div>
181
+ </>
182
+ )}
183
+
184
+ {/* Error state */}
185
+ {error && (
186
+ <>
187
+ <p className="text-sm text-muted-foreground mb-3">0 accounts found</p>
188
+ <Alert variant="destructive" role="alert">
189
+ <AlertCircle />
190
+ <AlertTitle>Failed to load accounts</AlertTitle>
191
+ <AlertDescription>
192
+ Something went wrong while loading accounts. Please try again later.
193
+ </AlertDescription>
194
+ </Alert>
195
+ </>
196
+ )}
197
+
198
+ {/* Results list */}
199
+ {!loading && !error && validAccountNodes.length > 0 && (
200
+ <>
201
+ <p className="text-sm text-muted-foreground mb-3">
202
+ {totalCount != null && (hasNextPage || hasPreviousPage)
203
+ ? `${totalCount} account${totalCount !== 1 ? "s" : ""} found`
204
+ : `Showing ${validAccountNodes.length} account${validAccountNodes.length !== 1 ? "s" : ""}`}
205
+ </p>
206
+ <AccountResultsList nodes={validAccountNodes} />
207
+ </>
208
+ )}
209
+
210
+ {/* No results state */}
211
+ {!loading && !error && validAccountNodes.length === 0 && (
212
+ <div className="flex flex-col items-center justify-center py-16 text-center">
213
+ <SearchX className="size-12 text-muted-foreground mb-4" />
214
+ <h2 className="text-lg font-semibold mb-1">No accounts found</h2>
215
+ <p className="text-sm text-muted-foreground">
216
+ Try adjusting your filters or search criteria.
217
+ </p>
218
+ </div>
219
+ )}
220
+ </div>
221
+
222
+ {/* Pagination — always visible, disabled while loading or on error */}
223
+ <PaginationControls
224
+ pageIndex={pagination.pageIndex}
225
+ hasNextPage={hasNextPage}
226
+ hasPreviousPage={hasPreviousPage}
227
+ pageSize={pagination.pageSize}
228
+ pageSizeOptions={PAGINATION_CONFIG.validPageSizes}
229
+ onNextPage={() => {
230
+ if (pageInfo?.endCursor) pagination.goToNextPage(pageInfo.endCursor);
231
+ }}
232
+ onPreviousPage={pagination.goToPreviousPage}
233
+ onPageSizeChange={pagination.setPageSize}
234
+ disabled={loading || !!error}
235
+ />
236
+ </div>
237
+ </div>
238
+ </div>
239
+ );
240
+ }
241
+
242
+ // -- Result Components ------------------------------------------------------
243
+
244
+ function AccountResultsList({ nodes }: { nodes: AccountNode[] }) {
245
+ return (
246
+ <ul className="divide-y">
247
+ {nodes.map((node) => (
248
+ <AccountResultItem key={node.Id} node={node} />
249
+ ))}
250
+ </ul>
251
+ );
252
+ }
253
+
254
+ function AccountResultItem({ node }: { node: AccountNode }) {
255
+ return (
256
+ <li>
257
+ <Link
258
+ to={`/accounts/${node.Id}`}
259
+ className="flex items-center justify-between py-3 px-3 -mx-3 rounded-md transition-colors hover:bg-accent"
260
+ >
261
+ <div>
262
+ <span className="font-medium">{fieldValue(node.Name) ?? "\u2014"}</span>
263
+ <p className="text-sm text-muted-foreground">
264
+ {[fieldValue(node.Industry), fieldValue(node.Type)].filter(Boolean).join(" \u00B7 ") ||
265
+ "\u2014"}
266
+ </p>
267
+ </div>
268
+ <div className="text-right text-sm">
269
+ <p>{fieldValue(node.Phone) ?? ""}</p>
270
+ <p className="text-muted-foreground">{fieldValue(node.Owner?.Name) ?? ""}</p>
271
+ </div>
272
+ </Link>
273
+ </li>
274
+ );
275
+ }
@@ -0,0 +1,34 @@
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
+ }
@@ -0,0 +1,84 @@
1
+ import { createDataSDK } from "@salesforce/sdk-data";
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 response = await data.graphql?.<TQuery, TVariables>(query, {
25
+ first,
26
+ after,
27
+ where,
28
+ orderBy,
29
+ } as TVariables);
30
+
31
+ if (response?.errors?.length) {
32
+ throw new Error(response.errors.map((e) => e.message).join("; "));
33
+ }
34
+
35
+ const result = (response?.data as Record<string, unknown> | undefined)?.uiapi as
36
+ | Record<string, unknown>
37
+ | undefined;
38
+ const queryResult = (result?.query as Record<string, unknown> | undefined)?.[objectName] as
39
+ | TResult
40
+ | undefined;
41
+
42
+ if (!queryResult) {
43
+ throw new Error(`No ${objectName} data returned`);
44
+ }
45
+
46
+ return queryResult;
47
+ }
48
+
49
+ /**
50
+ * Executes a GraphQL aggregate/groupBy query and extracts picklist options
51
+ * from the standard `uiapi.aggregate.<ObjectName>` response shape.
52
+ */
53
+ export async function fetchDistinctValues<TQuery>(
54
+ query: string,
55
+ objectName: string,
56
+ fieldName: string,
57
+ ): Promise<PicklistOption[]> {
58
+ const data = await createDataSDK();
59
+ const response = await data.graphql?.<TQuery>(query);
60
+ const errors = response?.errors;
61
+
62
+ if (errors?.length) {
63
+ throw new Error(errors.map((e) => e.message).join("; "));
64
+ }
65
+
66
+ const result = (response?.data as Record<string, unknown> | undefined)?.uiapi as
67
+ | Record<string, unknown>
68
+ | undefined;
69
+ const aggregate = (result?.aggregate as Record<string, unknown> | undefined)?.[objectName] as
70
+ | { edges?: Array<{ node?: { aggregate?: Record<string, unknown> } }> }
71
+ | undefined;
72
+
73
+ const edges = aggregate?.edges ?? [];
74
+ return edges
75
+ .map((edge) => {
76
+ const field = edge?.node?.aggregate?.[fieldName] as
77
+ | { value?: string | null; displayValue?: string | null; label?: string | null }
78
+ | undefined;
79
+ const value = field?.value;
80
+ if (!value) return null;
81
+ return { value, label: field.label ?? field.displayValue ?? value };
82
+ })
83
+ .filter((opt): opt is PicklistOption => opt !== null);
84
+ }
@@ -0,0 +1,89 @@
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
+ }
@@ -0,0 +1,127 @@
1
+ import { Card, CardContent, CardHeader, CardTitle } from "../../../components/ui/card";
2
+ import { Button } from "../../../components/ui/button";
3
+ import {
4
+ Collapsible,
5
+ CollapsibleContent,
6
+ CollapsibleTrigger,
7
+ } from "../../../components/ui/collapsible";
8
+ import { cn } from "../../../lib/utils";
9
+ import { ChevronDown } from "lucide-react";
10
+ import { useState } from "react";
11
+ import type { FilterFieldConfig, ActiveFilterValue } from "../utils/filterUtils";
12
+ import { TextFilter } from "./filters/TextFilter";
13
+ import { SelectFilter } from "./filters/SelectFilter";
14
+ import { NumericRangeFilter } from "./filters/NumericRangeFilter";
15
+ import { BooleanFilter } from "./filters/BooleanFilter";
16
+ import { DateFilter } from "./filters/DateFilter";
17
+ import { DateRangeFilter } from "./filters/DateRangeFilter";
18
+ import { MultiSelectFilter } from "./filters/MultiSelectFilter";
19
+ import { SearchFilter } from "./filters/SearchFilter";
20
+
21
+ interface FilterPanelProps extends Omit<React.ComponentProps<"div">, "onReset"> {
22
+ configs: FilterFieldConfig[];
23
+ filters: ActiveFilterValue[];
24
+ onFilterChange: (field: string, value: ActiveFilterValue | undefined) => void;
25
+ onReset: () => void;
26
+ headerProps?: React.ComponentProps<typeof CardHeader>;
27
+ titleProps?: React.ComponentProps<typeof CardTitle>;
28
+ contentProps?: React.ComponentProps<typeof CardContent>;
29
+ resetButtonProps?: React.ComponentProps<typeof Button>;
30
+ toggleButtonProps?: React.ComponentProps<typeof Button>;
31
+ }
32
+
33
+ function getFilterValue(
34
+ filters: ActiveFilterValue[],
35
+ field: string,
36
+ ): ActiveFilterValue | undefined {
37
+ return filters.find((f) => f.field === field);
38
+ }
39
+
40
+ function renderFilter(
41
+ config: FilterFieldConfig,
42
+ value: ActiveFilterValue | undefined,
43
+ onChange: (value: ActiveFilterValue | undefined) => void,
44
+ ) {
45
+ switch (config.type) {
46
+ case "text":
47
+ return <TextFilter config={config} value={value} onChange={onChange} />;
48
+ case "picklist":
49
+ return <SelectFilter config={config} value={value} onChange={onChange} />;
50
+ case "numeric":
51
+ return <NumericRangeFilter config={config} value={value} onChange={onChange} />;
52
+ case "boolean":
53
+ return <BooleanFilter config={config} value={value} onChange={onChange} />;
54
+ case "date":
55
+ return <DateFilter config={config} value={value} onChange={onChange} />;
56
+ case "daterange":
57
+ return <DateRangeFilter config={config} value={value} onChange={onChange} />;
58
+ case "multipicklist":
59
+ return <MultiSelectFilter config={config} value={value} onChange={onChange} />;
60
+ case "search":
61
+ return <SearchFilter config={config} value={value} onChange={onChange} />;
62
+ }
63
+ }
64
+
65
+ export function FilterPanel({
66
+ configs,
67
+ filters,
68
+ onFilterChange,
69
+ onReset,
70
+ className,
71
+ headerProps,
72
+ titleProps,
73
+ contentProps,
74
+ resetButtonProps,
75
+ toggleButtonProps,
76
+ ...props
77
+ }: FilterPanelProps) {
78
+ const [open, setOpen] = useState(true);
79
+ const hasActiveFilters = filters.length > 0;
80
+
81
+ return (
82
+ <Card className={cn(className)} {...props}>
83
+ <Collapsible open={open} onOpenChange={setOpen}>
84
+ <CardHeader
85
+ {...headerProps}
86
+ className={cn(
87
+ "flex flex-row items-center justify-between space-y-0 pb-2",
88
+ headerProps?.className,
89
+ )}
90
+ >
91
+ <CardTitle
92
+ {...titleProps}
93
+ className={cn("text-base font-semibold", titleProps?.className)}
94
+ >
95
+ {titleProps?.children ?? <h2>Filters</h2>}
96
+ </CardTitle>
97
+ <div className="flex items-center gap-1">
98
+ {hasActiveFilters && (
99
+ <Button variant="destructive" size="sm" onClick={onReset} {...resetButtonProps}>
100
+ {resetButtonProps?.children ?? "Reset"}
101
+ </Button>
102
+ )}
103
+ <CollapsibleTrigger asChild>
104
+ <Button variant="ghost" size="icon" {...toggleButtonProps}>
105
+ <ChevronDown
106
+ className={`h-4 w-4 transition-transform ${open ? "" : "-rotate-90"}`}
107
+ />
108
+ <span className="sr-only">Toggle filters</span>
109
+ </Button>
110
+ </CollapsibleTrigger>
111
+ </div>
112
+ </CardHeader>
113
+ <CollapsibleContent>
114
+ <CardContent {...contentProps} className={cn("space-y-4 pt-0", contentProps?.className)}>
115
+ {configs.map((config) => (
116
+ <div key={config.field}>
117
+ {renderFilter(config, getFilterValue(filters, config.field), (value) =>
118
+ onFilterChange(config.field, value),
119
+ )}
120
+ </div>
121
+ ))}
122
+ </CardContent>
123
+ </CollapsibleContent>
124
+ </Collapsible>
125
+ </Card>
126
+ );
127
+ }
@@ -0,0 +1,66 @@
1
+ import { Link } from "react-router";
2
+ import {
3
+ Breadcrumb,
4
+ BreadcrumbList,
5
+ BreadcrumbItem,
6
+ BreadcrumbLink,
7
+ BreadcrumbSeparator,
8
+ BreadcrumbPage,
9
+ } from "../../../components/ui/breadcrumb";
10
+ import { Skeleton } from "../../../components/ui/skeleton";
11
+
12
+ interface ObjectBreadcrumbProps {
13
+ listPath: string;
14
+ listLabel: string;
15
+ recordName?: string;
16
+ loading?: boolean;
17
+ includeHome?: boolean; // default is true
18
+ homeLabel?: string; // default is "Home"
19
+ }
20
+
21
+ export function ObjectBreadcrumb({
22
+ listPath,
23
+ listLabel,
24
+ recordName,
25
+ loading,
26
+ includeHome = true,
27
+ homeLabel = "Home",
28
+ }: ObjectBreadcrumbProps) {
29
+ const isDetailView = loading || recordName;
30
+
31
+ return (
32
+ <Breadcrumb className="mb-3">
33
+ <BreadcrumbList>
34
+ {includeHome && (
35
+ <BreadcrumbItem>
36
+ <BreadcrumbLink asChild>
37
+ <Link to="/">{homeLabel}</Link>
38
+ </BreadcrumbLink>
39
+ </BreadcrumbItem>
40
+ )}
41
+ <BreadcrumbSeparator />
42
+ {isDetailView ? (
43
+ <>
44
+ <BreadcrumbItem>
45
+ <BreadcrumbLink asChild>
46
+ <Link to={listPath}>{listLabel}</Link>
47
+ </BreadcrumbLink>
48
+ </BreadcrumbItem>
49
+ <BreadcrumbSeparator />
50
+ <BreadcrumbItem>
51
+ {loading && !recordName ? (
52
+ <Skeleton className="h-4 w-32" />
53
+ ) : (
54
+ <BreadcrumbPage>{recordName}</BreadcrumbPage>
55
+ )}
56
+ </BreadcrumbItem>
57
+ </>
58
+ ) : (
59
+ <BreadcrumbItem>
60
+ <BreadcrumbPage>{listLabel}</BreadcrumbPage>
61
+ </BreadcrumbItem>
62
+ )}
63
+ </BreadcrumbList>
64
+ </Breadcrumb>
65
+ );
66
+ }