@schandlergarcia/sf-web-components 2.2.0 → 2.3.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 (61) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/brands/engine/app/api/graphql-operations-types.ts +11260 -0
  3. package/brands/engine/app/api/graphqlClient.ts +25 -0
  4. package/brands/engine/app/api/partnerQueries.ts +212 -0
  5. package/brands/engine/app/appLayout.tsx +13 -0
  6. package/brands/engine/app/components/AgentforceConversationClient.tsx +201 -0
  7. package/brands/engine/app/components/__inherit_AgentforceConversationClient.tsx +3 -0
  8. package/brands/engine/app/components/alerts/status-alert.tsx +49 -0
  9. package/brands/engine/app/components/layouts/card-layout.tsx +29 -0
  10. package/brands/engine/app/components/workspace/CommandCenter.tsx +16 -0
  11. package/brands/engine/app/config/agentApi.ts +36 -0
  12. package/brands/engine/app/features/object-search/__examples__/api/accountSearchService.ts +46 -0
  13. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
  14. package/brands/engine/app/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
  15. package/brands/engine/app/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
  16. package/brands/engine/app/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
  17. package/brands/engine/app/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
  18. package/brands/engine/app/features/object-search/__examples__/pages/AccountSearch.tsx +312 -0
  19. package/brands/engine/app/features/object-search/__examples__/pages/Home.tsx +34 -0
  20. package/brands/engine/app/features/object-search/api/objectSearchService.ts +84 -0
  21. package/brands/engine/app/features/object-search/components/ActiveFilters.tsx +89 -0
  22. package/brands/engine/app/features/object-search/components/FilterContext.tsx +83 -0
  23. package/brands/engine/app/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
  24. package/brands/engine/app/features/object-search/components/PaginationControls.tsx +109 -0
  25. package/brands/engine/app/features/object-search/components/SearchBar.tsx +41 -0
  26. package/brands/engine/app/features/object-search/components/SortControl.tsx +143 -0
  27. package/brands/engine/app/features/object-search/components/filters/BooleanFilter.tsx +78 -0
  28. package/brands/engine/app/features/object-search/components/filters/DateFilter.tsx +128 -0
  29. package/brands/engine/app/features/object-search/components/filters/DateRangeFilter.tsx +70 -0
  30. package/brands/engine/app/features/object-search/components/filters/FilterFieldWrapper.tsx +33 -0
  31. package/brands/engine/app/features/object-search/components/filters/MultiSelectFilter.tsx +97 -0
  32. package/brands/engine/app/features/object-search/components/filters/NumericRangeFilter.tsx +163 -0
  33. package/brands/engine/app/features/object-search/components/filters/SearchFilter.tsx +50 -0
  34. package/brands/engine/app/features/object-search/components/filters/SelectFilter.tsx +97 -0
  35. package/brands/engine/app/features/object-search/components/filters/TextFilter.tsx +91 -0
  36. package/brands/engine/app/features/object-search/hooks/useAsyncData.ts +54 -0
  37. package/brands/engine/app/features/object-search/hooks/useCachedAsyncData.ts +184 -0
  38. package/brands/engine/app/features/object-search/hooks/useDebouncedCallback.ts +34 -0
  39. package/brands/engine/app/features/object-search/hooks/useObjectSearchParams.ts +252 -0
  40. package/brands/engine/app/features/object-search/utils/debounce.ts +25 -0
  41. package/brands/engine/app/features/object-search/utils/fieldUtils.ts +29 -0
  42. package/brands/engine/app/features/object-search/utils/filterUtils.ts +404 -0
  43. package/brands/engine/app/features/object-search/utils/sortUtils.ts +38 -0
  44. package/brands/engine/app/hooks/useEngineLiveData.ts +49 -0
  45. package/brands/engine/app/hooks/useEvaAgent.ts +288 -0
  46. package/brands/engine/app/hooks/usePartnerDashboardData.ts +141 -0
  47. package/brands/engine/app/navigationMenu.tsx +80 -0
  48. package/brands/engine/app/pages/AccountObjectDetailPage.tsx +361 -0
  49. package/brands/engine/app/pages/AccountSearch.tsx +305 -0
  50. package/brands/engine/app/pages/BlankDashboard.tsx +15 -0
  51. package/brands/engine/app/pages/DataTest.tsx +78 -0
  52. package/brands/engine/app/pages/Home.tsx +5 -0
  53. package/brands/engine/app/pages/NotFound.tsx +19 -0
  54. package/brands/engine/app/pages/PartnerHubDashboard.tsx +2010 -0
  55. package/brands/engine/app/pages/Search.tsx +13 -0
  56. package/brands/engine/app/router-utils.tsx +35 -0
  57. package/brands/engine/app/routes.tsx +39 -0
  58. package/brands/engine/app/styles/global.css +270 -0
  59. package/package.json +1 -1
  60. package/scripts/apply-brand.mjs +159 -76
  61. package/scripts/postinstall.mjs +24 -5
@@ -0,0 +1,252 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useSearchParams } from "react-router";
3
+ import type { FilterFieldConfig, ActiveFilterValue } from "../utils/filterUtils";
4
+ import type { SortFieldConfig, SortState } from "../utils/sortUtils";
5
+ import { filtersToSearchParams, searchParamsToFilters, buildFilter } from "../utils/filterUtils";
6
+ import { buildOrderBy } from "../utils/sortUtils";
7
+ import { debounce } from "../utils/debounce";
8
+
9
+ /** How long to wait before flushing local state changes to the URL. */
10
+ const URL_SYNC_DEBOUNCE_MS = 300;
11
+
12
+ export interface PaginationConfig {
13
+ defaultPageSize: number;
14
+ validPageSizes: number[];
15
+ }
16
+
17
+ export interface UseObjectSearchParamsReturn<TFilter, TOrderBy> {
18
+ filters: {
19
+ active: ActiveFilterValue[];
20
+ set: (field: string, value: ActiveFilterValue | undefined) => void;
21
+ remove: (field: string) => void;
22
+ };
23
+ sort: {
24
+ current: SortState | null;
25
+ set: (sort: SortState | null) => void;
26
+ };
27
+ query: { where: TFilter; orderBy: TOrderBy };
28
+ pagination: {
29
+ pageSize: number;
30
+ pageIndex: number;
31
+ afterCursor: string | undefined;
32
+ setPageSize: (size: number) => void;
33
+ goToNextPage: (cursor: string) => void;
34
+ goToPreviousPage: () => void;
35
+ };
36
+ resetAll: () => void;
37
+ }
38
+
39
+ /**
40
+ * Manages filter, sort, and cursor-based pagination state for an object search page.
41
+ *
42
+ * ## State model
43
+ * Local React state is the primary driver for instant UI updates.
44
+ * URL search params act as the durable source of truth so that a page
45
+ * refresh or shared link restores the same view. Changes are synced to
46
+ * the URL via a debounced write (300 ms) to avoid excessive history entries.
47
+ *
48
+ * ## Return shape
49
+ * Returns memoized groups so each group's reference is stable unless its
50
+ * contents change — safe to pass directly as props to `React.memo` children.
51
+ *
52
+ * - `filters` — active filter values + set/remove callbacks
53
+ * - `sort` — current sort state + set callback
54
+ * - `query` — derived `where` / `orderBy` objects ready for the API
55
+ * - `pagination` — page size, page index, cursor, and navigation callbacks
56
+ * - `resetAll` — clears all filters, sort, and pagination in one call
57
+ */
58
+ export function useObjectSearchParams<TFilter, TOrderBy>(
59
+ filterConfigs: FilterFieldConfig[],
60
+ _sortConfigs?: SortFieldConfig[],
61
+ paginationConfig?: PaginationConfig,
62
+ ) {
63
+ const defaultPageSize = paginationConfig?.defaultPageSize ?? 10;
64
+ const validPageSizes = useMemo(
65
+ () => paginationConfig?.validPageSizes ?? [defaultPageSize],
66
+ [paginationConfig?.validPageSizes, defaultPageSize],
67
+ );
68
+ const [searchParams, setSearchParams] = useSearchParams();
69
+
70
+ // Seed local state from URL on initial load
71
+ const initial = useMemo(
72
+ () => searchParamsToFilters(searchParams, filterConfigs),
73
+ // Only run on mount — local state takes over after that, no deps needed
74
+ // eslint-disable-next-line react-hooks/exhaustive-deps
75
+ [],
76
+ );
77
+
78
+ const [filters, setFilters] = useState<ActiveFilterValue[]>(initial.filters);
79
+ const [sort, setLocalSort] = useState<SortState | null>(initial.sort);
80
+
81
+ // Pagination — cursor-based with a stack to support "previous page" navigation.
82
+ const getValidPageSize = useCallback(
83
+ (size: number) => (validPageSizes.includes(size) ? size : defaultPageSize),
84
+ [validPageSizes, defaultPageSize],
85
+ );
86
+
87
+ const [pageSize, setPageSizeState] = useState<number>(
88
+ getValidPageSize(initial.pageSize ?? defaultPageSize),
89
+ );
90
+ const [pageIndex, setPageIndex] = useState(initial.pageIndex);
91
+ const [afterCursor, setAfterCursor] = useState<string | undefined>(undefined);
92
+ const cursorStackRef = useRef<string[]>([]);
93
+
94
+ // Debounced URL sync — keeps URL in sync without blocking the UI
95
+ const syncToUrl = useCallback(
96
+ (
97
+ nextFilters: ActiveFilterValue[],
98
+ nextSort: SortState | null,
99
+ nextPageSize?: number,
100
+ nextPageIndex?: number,
101
+ ) => {
102
+ const params = filtersToSearchParams(nextFilters, nextSort, nextPageSize, nextPageIndex);
103
+ setSearchParams(params, { replace: true });
104
+ },
105
+ [setSearchParams],
106
+ );
107
+
108
+ const debouncedSyncRef = useRef(debounce(syncToUrl, URL_SYNC_DEBOUNCE_MS));
109
+ useEffect(() => {
110
+ debouncedSyncRef.current = debounce(syncToUrl, URL_SYNC_DEBOUNCE_MS);
111
+ }, [syncToUrl]);
112
+
113
+ // Snapshot ref — lets callbacks read the latest state without being
114
+ // recreated on every render (avoids infinite useCallback chains).
115
+ const stateRef = useRef({ filters, sort, pageSize, pageIndex });
116
+ stateRef.current = { filters, sort, pageSize, pageIndex };
117
+
118
+ // Any filter/sort change resets pagination to the first page.
119
+ const resetPagination = useCallback(() => {
120
+ setPageIndex(0);
121
+ setAfterCursor(undefined);
122
+ cursorStackRef.current = [];
123
+ }, []);
124
+
125
+ // -- Filter callbacks -------------------------------------------------------
126
+
127
+ const setFilter = useCallback(
128
+ (field: string, value: ActiveFilterValue | undefined) => {
129
+ const { sort: s, pageSize: ps } = stateRef.current;
130
+ setFilters((prev) => {
131
+ const next = prev.filter((f) => f.field !== field);
132
+ if (value) next.push(value);
133
+ debouncedSyncRef.current(next, s, ps);
134
+ return next;
135
+ });
136
+ resetPagination();
137
+ },
138
+ [resetPagination],
139
+ );
140
+
141
+ const removeFilter = useCallback(
142
+ (field: string) => {
143
+ const { sort: s, pageSize: ps } = stateRef.current;
144
+ setFilters((prev) => {
145
+ const next = prev.filter((f) => f.field !== field);
146
+ debouncedSyncRef.current(next, s, ps);
147
+ return next;
148
+ });
149
+ resetPagination();
150
+ },
151
+ [resetPagination],
152
+ );
153
+
154
+ // -- Sort callback ----------------------------------------------------------
155
+
156
+ const setSort = useCallback(
157
+ (nextSort: SortState | null) => {
158
+ const { filters: f, pageSize: ps } = stateRef.current;
159
+ setLocalSort(nextSort);
160
+ debouncedSyncRef.current(f, nextSort, ps);
161
+ resetPagination();
162
+ },
163
+ [resetPagination],
164
+ );
165
+
166
+ // -- Reset ------------------------------------------------------------------
167
+
168
+ const resetAll = useCallback(() => {
169
+ setFilters([]);
170
+ setLocalSort(null);
171
+ resetPagination();
172
+ syncToUrl([], null, defaultPageSize, 0);
173
+ setPageSizeState(defaultPageSize);
174
+ }, [syncToUrl, resetPagination, defaultPageSize]);
175
+
176
+ // -- Pagination callbacks ---------------------------------------------------
177
+ // Uses a cursor stack to track visited pages. "Next" pushes the current
178
+ // endCursor onto the stack; "Previous" pops it to restore the prior cursor.
179
+
180
+ const goToNextPage = useCallback((endCursor: string) => {
181
+ cursorStackRef.current = [...cursorStackRef.current, endCursor];
182
+ setAfterCursor(endCursor);
183
+ setPageIndex((prev) => {
184
+ const nextIndex = prev + 1;
185
+ const { filters: f, sort: s, pageSize: ps } = stateRef.current;
186
+ debouncedSyncRef.current(f, s, ps, nextIndex);
187
+ return nextIndex;
188
+ });
189
+ }, []);
190
+
191
+ const goToPreviousPage = useCallback(() => {
192
+ const stack = cursorStackRef.current;
193
+ const next = stack.slice(0, -1);
194
+ cursorStackRef.current = next;
195
+ setAfterCursor(next.length > 0 ? next[next.length - 1] : undefined);
196
+ setPageIndex((prev) => {
197
+ const nextIndex = Math.max(0, prev - 1);
198
+ const { filters: f, sort: s, pageSize: ps } = stateRef.current;
199
+ debouncedSyncRef.current(f, s, ps, nextIndex);
200
+ return nextIndex;
201
+ });
202
+ }, []);
203
+
204
+ const setPageSize = useCallback(
205
+ (newSize: number) => {
206
+ const validated = getValidPageSize(newSize);
207
+ const { filters: f, sort: s } = stateRef.current;
208
+ setPageSizeState(validated);
209
+ resetPagination();
210
+ debouncedSyncRef.current(f, s, validated);
211
+ },
212
+ [resetPagination, getValidPageSize],
213
+ );
214
+
215
+ // -- Derived query objects ---------------------------------------------------
216
+ // Translate local filter/sort state into API-ready `where` and `orderBy`.
217
+
218
+ const where = useMemo(
219
+ () => buildFilter<TFilter>(filters, filterConfigs),
220
+ [filters, filterConfigs],
221
+ );
222
+
223
+ const orderBy = useMemo(() => buildOrderBy<TOrderBy>(sort), [sort]);
224
+
225
+ // -- Memoized return groups -------------------------------------------------
226
+ // Each group is individually memoized so its object reference stays stable
227
+ // unless the contained values change. This makes it safe to pass a group
228
+ // (e.g. `pagination`) directly as props to a React.memo child without
229
+ // causing unnecessary re-renders.
230
+
231
+ const filtersGroup = useMemo(
232
+ () => ({ active: filters, set: setFilter, remove: removeFilter }),
233
+ [filters, setFilter, removeFilter],
234
+ );
235
+
236
+ const sortGroup = useMemo(() => ({ current: sort, set: setSort }), [sort, setSort]);
237
+
238
+ const query = useMemo(() => ({ where, orderBy }), [where, orderBy]);
239
+
240
+ const pagination = useMemo(
241
+ () => ({ pageSize, pageIndex, afterCursor, setPageSize, goToNextPage, goToPreviousPage }),
242
+ [pageSize, pageIndex, afterCursor, setPageSize, goToNextPage, goToPreviousPage],
243
+ );
244
+
245
+ return {
246
+ filters: filtersGroup,
247
+ sort: sortGroup,
248
+ query,
249
+ pagination,
250
+ resetAll,
251
+ };
252
+ }
@@ -0,0 +1,25 @@
1
+ /** Default debounce delay for keystroke-driven filter inputs (search, text, numeric). */
2
+ export const FILTER_DEBOUNCE_MS = 300;
3
+
4
+ /**
5
+ * Creates a debounced version of the provided function.
6
+ *
7
+ * Each call to the returned function resets the internal timer. The wrapped
8
+ * function is only invoked once the timer expires without being reset. This
9
+ * makes it ideal for rate-limiting high-frequency events like input changes.
10
+ *
11
+ * @typeParam T - The function signature to debounce.
12
+ * @param fn - The function to debounce.
13
+ * @param ms - The debounce delay in milliseconds.
14
+ * @returns A new function with the same signature that delays execution.
15
+ */
16
+ export function debounce<T extends (...args: any[]) => void>(
17
+ fn: T,
18
+ ms: number,
19
+ ): (...args: Parameters<T>) => void {
20
+ let timer: ReturnType<typeof setTimeout> | undefined;
21
+ return (...args: Parameters<T>) => {
22
+ clearTimeout(timer);
23
+ timer = setTimeout(() => fn(...args), ms);
24
+ };
25
+ }
@@ -0,0 +1,29 @@
1
+ export function fieldValue(
2
+ field: { displayValue?: string | null; value?: unknown } | null | undefined,
3
+ ): string | null {
4
+ if (field?.displayValue != null) return field.displayValue;
5
+ if (field?.value != null) return String(field.value);
6
+ return null;
7
+ }
8
+
9
+ export function getAddressFieldLines(address: {
10
+ street?: string | null;
11
+ city?: string | null;
12
+ state?: string | null;
13
+ postalCode?: string | null;
14
+ country?: string | null;
15
+ }) {
16
+ const cityStateZip = [address.city, address.state].filter(Boolean).join(", ");
17
+ const cityStateZipLine = [cityStateZip, address.postalCode].filter(Boolean).join(" ");
18
+ const lines = [address.street, cityStateZipLine, address.country].filter(Boolean);
19
+ if (lines.length === 0) return null;
20
+ return lines;
21
+ }
22
+
23
+ export function formatDateTimeField(
24
+ value?: string | null,
25
+ ...args: Parameters<Date["toLocaleString"]>
26
+ ) {
27
+ if (!value) return null;
28
+ return new Date(value).toLocaleString(...args);
29
+ }