@salesforce/webapp-template-app-react-template-b2x-experimental 1.76.0 → 1.76.1

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 CHANGED
@@ -3,6 +3,14 @@
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.76.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.76.0...v1.76.1) (2026-03-06)
7
+
8
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
9
+
10
+
11
+
12
+
13
+
6
14
  # [1.76.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.75.1...v1.76.0) (2026-03-06)
7
15
 
8
16
 
@@ -15,8 +15,8 @@
15
15
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@salesforce/sdk-data": "^1.76.0",
19
- "@salesforce/webapp-experimental": "^1.76.0",
18
+ "@salesforce/sdk-data": "^1.76.1",
19
+ "@salesforce/webapp-experimental": "^1.76.1",
20
20
  "@tailwindcss/vite": "^4.1.17",
21
21
  "@tanstack/react-form": "^1.28.4",
22
22
  "class-variance-authority": "^0.7.1",
@@ -40,7 +40,7 @@
40
40
  "@graphql-eslint/eslint-plugin": "^4.1.0",
41
41
  "@graphql-tools/utils": "^11.0.0",
42
42
  "@playwright/test": "^1.49.0",
43
- "@salesforce/vite-plugin-webapp-experimental": "^1.76.0",
43
+ "@salesforce/vite-plugin-webapp-experimental": "^1.76.1",
44
44
  "@testing-library/jest-dom": "^6.6.3",
45
45
  "@testing-library/react": "^16.1.0",
46
46
  "@testing-library/user-event": "^14.5.2",
@@ -36,17 +36,9 @@ export function extractFieldsFromLayout(
36
36
  return optionalFields;
37
37
  }
38
38
 
39
- /**
40
- * Fetches the Full/View layout for an object (REST). Used by detail view to render sections/rows/items.
41
- *
42
- * @param objectApiName - Object API name.
43
- * @param recordTypeId - Record type Id (default master).
44
- * @param signal - Optional abort signal.
45
- */
46
39
  export async function getLayout(
47
40
  objectApiName: string,
48
41
  recordTypeId: string = DEFAULT_RECORD_TYPE_ID,
49
- signal?: AbortSignal,
50
42
  ): Promise<LayoutResponse> {
51
43
  const params = new URLSearchParams({
52
44
  layoutType: "Full",
@@ -54,14 +46,10 @@ export async function getLayout(
54
46
  recordTypeId,
55
47
  });
56
48
  return fetchAndValidate(
57
- (abortSignal) =>
58
- uiApiClient.get(`/layout/${safeEncodePath(objectApiName)}?${params.toString()}`, {
59
- signal: abortSignal,
60
- }),
49
+ () => uiApiClient.get(`/layout/${safeEncodePath(objectApiName)}?${params.toString()}`),
61
50
  {
62
51
  schema: LayoutResponseSchema,
63
52
  errorContext: `layout for ${objectApiName}`,
64
- signal,
65
53
  },
66
54
  );
67
55
  }
@@ -86,24 +74,13 @@ function optionalFieldsToColumns(optionalFields: string[]): Column[] {
86
74
  }));
87
75
  }
88
76
 
89
- /**
90
- * Fetches everything needed for the detail page: layout (REST), object metadata (GraphQL), single record (GraphQL).
91
- * Layout drives which fields are requested; getRecordByIdGraphQL fetches that field set by Id.
92
- *
93
- * @param objectApiName - Object API name.
94
- * @param recordId - Record Id.
95
- * @param recordTypeId - Record type (default master).
96
- * @param signal - Optional abort signal.
97
- * @returns { layout, record, objectMetadata } for DetailForm / UiApiDetailForm.
98
- */
99
77
  export async function getRecordDetail(
100
78
  objectApiName: string,
101
79
  recordId: string,
102
80
  recordTypeId: string = DEFAULT_RECORD_TYPE_ID,
103
- signal?: AbortSignal,
104
81
  ): Promise<RecordDetailResult> {
105
- const layout = await getLayout(objectApiName, recordTypeId, signal);
106
- const objectMetadata = await objectInfoService.getObjectInfoBatch(objectApiName, signal);
82
+ const layout = await getLayout(objectApiName, recordTypeId);
83
+ const objectMetadata = await objectInfoService.getObjectInfoBatch(objectApiName);
107
84
  const firstResult = objectMetadata?.results?.[0]?.result;
108
85
  if (!firstResult) {
109
86
  throw new Error(`Object metadata not found for ${objectApiName}`);
@@ -161,19 +161,10 @@ function buildObjectInfosWithPicklistsQuery(): string {
161
161
  }`;
162
162
  }
163
163
 
164
- /**
165
- * Fetches object metadata for the given objects via GraphQL.
166
- *
167
- * @param apiNames - Object API names (e.g. ["Account", "Contact"]).
168
- * @param options.objectInfoInputs - When set, picklist values for specified fields are included (API v65.0+).
169
- * @param options.signal - Optional abort signal.
170
- * @returns Raw uiapi.objectInfos response (adapt to REST shape via graphQLObjectInfosToBatchResponse).
171
- */
172
164
  export async function getObjectInfosGraphQL(
173
165
  apiNames: string[],
174
166
  options?: {
175
167
  objectInfoInputs?: ObjectInfoInput[] | null;
176
- signal?: AbortSignal;
177
168
  },
178
169
  ): Promise<ObjectInfosGraphQLResponse> {
179
170
  const names = apiNames.length ? apiNames : [];
@@ -1,8 +1,4 @@
1
1
  import { uiApiClient } from "@salesforce/webapp-experimental/api";
2
- import { z } from "zod";
3
- import type { SearchResultsResponse, KeywordSearchResult } from "../types/search/searchResults";
4
- import { SearchResultsResponseSchema } from "../types/search/searchResults";
5
- import { FilterCriteriaArraySchema } from "../types/filters/filters";
6
2
  import type { Filter } from "../types/filters/filters";
7
3
  import { FilterArraySchema } from "../types/filters/filters";
8
4
  import type { PicklistValue } from "../types/filters/picklist";
@@ -18,7 +14,7 @@ import {
18
14
  * Object info and search service.
19
15
  *
20
16
  * - getObjectInfoBatch / getPicklistValues: GraphQL (objectInfoGraphQLService).
21
- * - getObjectListFilters, searchResults: REST (search-info, search/results).
17
+ * - getObjectListFilters: REST (search-info).
22
18
  * Hooks use this service; components do not call it directly.
23
19
  *
24
20
  * @module api/objectInfoService
@@ -36,18 +32,7 @@ function getObjectInfoBatchCacheKey(objectApiNames: string): string {
36
32
  const objectInfoBatchCache = new Map<string, ObjectInfoBatchResponse>();
37
33
  const objectInfoBatchInFlight = new Map<string, Promise<ObjectInfoBatchResponse>>();
38
34
 
39
- /**
40
- * Fetches batch object information for the specified objects via GraphQL (uiapi.objectInfos).
41
- * Results are cached by object set so List, Home, and Detail views share one request.
42
- *
43
- * @param objectApiNames - Comma-separated list of object API names (e.g., "Account,AccountBrand")
44
- * @param signal - Optional AbortSignal to cancel the request
45
- * @returns Promise resolving to the object info batch response (REST-compatible shape)
46
- */
47
- export async function getObjectInfoBatch(
48
- objectApiNames: string,
49
- signal?: AbortSignal,
50
- ): Promise<ObjectInfoBatchResponse> {
35
+ export async function getObjectInfoBatch(objectApiNames: string): Promise<ObjectInfoBatchResponse> {
51
36
  const names = objectApiNames
52
37
  .split(",")
53
38
  .map((s) => s.trim())
@@ -62,7 +47,7 @@ export async function getObjectInfoBatch(
62
47
  if (inFlight) return inFlight;
63
48
  const promise = (async () => {
64
49
  try {
65
- const response = await getObjectInfosGraphQL(names, { signal });
50
+ const response = await getObjectInfosGraphQL(names);
66
51
  const nodes = response?.uiapi?.objectInfos ?? [];
67
52
  const result = graphQLObjectInfosToBatchResponse(nodes, names);
68
53
  objectInfoBatchCache.set(key, result);
@@ -75,24 +60,9 @@ export async function getObjectInfoBatch(
75
60
  return promise;
76
61
  }
77
62
 
78
- /**
79
- * Fetches list filters for a specific object.
80
- * Salesforce Search supports "Search Filters" (refinements) which are configured per object.
81
- * This API returns the available filters (e.g., "Close Date", "Stage") that the user
82
- * can use to narrow down the search results.
83
- * @param objectApiName - The API name of the object (e.g., "Account")
84
- * @param signal - Optional AbortSignal to cancel the request
85
- * @returns Promise resolving to the search filters array
86
- */
87
- export async function getObjectListFilters(
88
- objectApiName: string,
89
- signal?: AbortSignal,
90
- ): Promise<Filter[]> {
63
+ export async function getObjectListFilters(objectApiName: string): Promise<Filter[]> {
91
64
  return fetchAndValidate(
92
- (abortSignal) =>
93
- uiApiClient.get(`/search-info/${safeEncodePath(objectApiName)}/filters`, {
94
- signal: abortSignal,
95
- }),
65
+ () => uiApiClient.get(`/search-info/${safeEncodePath(objectApiName)}/filters`),
96
66
  {
97
67
  schema: FilterArraySchema,
98
68
  errorContext: `filters for ${objectApiName}`,
@@ -100,25 +70,14 @@ export async function getObjectListFilters(
100
70
  if (!data) return [];
101
71
  return Array.isArray(data) ? data : (data as { filters?: unknown }).filters || [];
102
72
  },
103
- signal,
104
73
  },
105
74
  );
106
75
  }
107
76
 
108
- /**
109
- * Fetches picklist values for a specific field via GraphQL (uiapi.objectInfos with objectInfoInputs).
110
- *
111
- * @param objectApiName - The API name of the object (e.g., "Account")
112
- * @param fieldName - The API name of the field (e.g., "Type")
113
- * @param recordTypeId - Optional record type ID (defaults to "012000000000000AAA" which is the default/master record type)
114
- * @param signal - Optional AbortSignal to cancel the request
115
- * @returns Promise resolving to an array of picklist values
116
- */
117
77
  export async function getPicklistValues(
118
78
  objectApiName: string,
119
79
  fieldName: string,
120
80
  recordTypeId: string = "012000000000000AAA",
121
- signal?: AbortSignal,
122
81
  ): Promise<PicklistValue[]> {
123
82
  const response = await getObjectInfosGraphQL([objectApiName], {
124
83
  objectInfoInputs: [
@@ -127,7 +86,6 @@ export async function getPicklistValues(
127
86
  fieldNames: [fieldName],
128
87
  },
129
88
  ],
130
- signal,
131
89
  });
132
90
  const nodes = response?.uiapi?.objectInfos ?? [];
133
91
  const node = nodes[0];
@@ -135,65 +93,8 @@ export async function getPicklistValues(
135
93
  return extractPicklistValuesFromGraphQLObjectInfo(node, fieldName, recordTypeId);
136
94
  }
137
95
 
138
- // Zod Schema for Search Parameters
139
- const SearchParamsSchema = z.object({
140
- filters: FilterCriteriaArraySchema.optional(),
141
- pageSize: z.number().optional(),
142
- pageToken: z.string().optional(),
143
- sortBy: z.string().optional(),
144
- });
145
-
146
- /**
147
- * Search parameters for keyword search
148
- */
149
- export type SearchParams = z.infer<typeof SearchParamsSchema>;
150
-
151
- /**
152
- * Performs a keyword search on a specific object.
153
- * Returns records that match the text query along with pagination information.
154
- *
155
- * @param query - The search query string
156
- * @param objectApiName - The API name of the object to search (e.g., "Account")
157
- * @param params - Optional search parameters (pageSize, pageToken, filters, sortBy)
158
- * @param signal - Optional AbortSignal to cancel the request
159
- * @returns Promise resolving to the keyword search result with records and pagination tokens
160
- */
161
- export async function searchResults(
162
- query: string,
163
- objectApiName: string,
164
- params?: SearchParams,
165
- signal?: AbortSignal,
166
- ): Promise<KeywordSearchResult> {
167
- const searchParams = new URLSearchParams({
168
- q: query,
169
- objectApiName: objectApiName,
170
- });
171
-
172
- const body = {
173
- filters: params?.filters ?? [],
174
- pageSize: params?.pageSize ?? 50,
175
- pageToken: params?.pageToken ?? "0",
176
- sortBy: params?.sortBy ?? "",
177
- };
178
-
179
- const response = await fetchAndValidate<SearchResultsResponse>(
180
- (abortSignal) =>
181
- uiApiClient.post(`/search/results/keyword?${searchParams.toString()}`, body, {
182
- signal: abortSignal,
183
- }),
184
- {
185
- schema: SearchResultsResponseSchema,
186
- errorContext: `search results for ${objectApiName} with query "${query}"`,
187
- signal,
188
- },
189
- );
190
-
191
- return response.keywordSearchResult;
192
- }
193
-
194
96
  export const objectInfoService = {
195
97
  getObjectInfoBatch,
196
98
  getObjectListFilters,
197
99
  getPicklistValues,
198
- searchResults,
199
100
  };
@@ -6,26 +6,16 @@
6
6
  * - getSharedFilters: module-level deduplication for getObjectListFilters across hook instances.
7
7
  */
8
8
 
9
- import { useState, useEffect, useRef, useMemo } from "react";
10
- import { objectInfoService, type SearchParams } from "../api/objectInfoService";
11
- import type {
12
- Column,
13
- SearchResultRecord,
14
- SearchResultRecordData,
15
- } from "../types/search/searchResults";
16
- import type { Filter, FilterCriteria } from "../types/filters/filters";
9
+ import { useState, useEffect } from "react";
10
+ import { objectInfoService } from "../api/objectInfoService";
11
+ import type { Column } from "../types/search/searchResults";
12
+ import type { Filter } from "../types/filters/filters";
17
13
  import type { PicklistValue } from "../types/filters/picklist";
18
- import { createFiltersKey } from "../utils/cacheUtils";
19
14
 
20
15
  // --- Shared filters cache (deduplicates getObjectListFilters across useObjectColumns + useObjectFilters) ---
21
16
  const sharedFiltersCache = new Map<string, Filter[]>();
22
17
  const sharedFiltersInFlight = new Map<string, Promise<Filter[]>>();
23
18
 
24
- /**
25
- * Returns filters for the object, deduplicating the API call across hook instances.
26
- * Does not pass abort signal to the API so the shared request is not aborted when
27
- * one consumer's effect cleans up (e.g. React Strict Mode); callers still guard with isCancelled.
28
- */
29
19
  function getSharedFilters(objectApiName: string): Promise<Filter[]> {
30
20
  const cached = sharedFiltersCache.get(objectApiName);
31
21
  if (cached) return Promise.resolve(cached);
@@ -100,7 +90,6 @@ export function useObjectListMetadata(objectApiName: string | null): ObjectListM
100
90
  }
101
91
 
102
92
  let isCancelled = false;
103
- const ac = new AbortController();
104
93
 
105
94
  const run = async () => {
106
95
  setState((s) => ({ ...s, loading: true, error: null }));
@@ -111,12 +100,9 @@ export function useObjectListMetadata(objectApiName: string | null): ObjectListM
111
100
  const selectFilters = filters.filter((f) => f.affordance?.toLowerCase() === "select");
112
101
  const picklistPromises = selectFilters.map((f) =>
113
102
  objectInfoService
114
- .getPicklistValues(objectApiName!, f.targetFieldPath, undefined, ac.signal)
103
+ .getPicklistValues(objectApiName!, f.targetFieldPath)
115
104
  .then((values) => ({ fieldPath: f.targetFieldPath, values }))
116
- .catch((err) => {
117
- if (err?.name === "AbortError") throw err;
118
- return { fieldPath: f.targetFieldPath, values: [] as PicklistValue[] };
119
- }),
105
+ .catch(() => ({ fieldPath: f.targetFieldPath, values: [] as PicklistValue[] })),
120
106
  );
121
107
  const picklistResults = await Promise.all(picklistPromises);
122
108
  if (isCancelled) return;
@@ -134,7 +120,7 @@ export function useObjectListMetadata(objectApiName: string | null): ObjectListM
134
120
  error: null,
135
121
  });
136
122
  } catch (err) {
137
- if (isCancelled || (err instanceof Error && err.name === "AbortError")) return;
123
+ if (isCancelled) return;
138
124
  setState((s) => ({
139
125
  ...s,
140
126
  columns: [],
@@ -149,7 +135,6 @@ export function useObjectListMetadata(objectApiName: string | null): ObjectListM
149
135
  run();
150
136
  return () => {
151
137
  isCancelled = true;
152
- ac.abort();
153
138
  };
154
139
  }, [objectApiName]);
155
140
 
@@ -169,212 +154,6 @@ export function useObjectColumns(objectApiName: string | null) {
169
154
  };
170
155
  }
171
156
 
172
- /**
173
- * Hook: useObjectSearchResults
174
- *
175
- * Fetches search results for a specific object based on the provided query parameters.
176
- * Maintains the *latest* result set for the object in state to prevent redundant
177
- * network requests when the component re-renders with the same parameters.
178
- * Includes debouncing for search queries (but not pagination).
179
- *
180
- * @param objectApiName - The API name of the object to search
181
- * @param searchQuery - The search query string
182
- * @param searchPageSize - Number of results per page (default: 50)
183
- * @param searchPageToken - Pagination token (default: '0')
184
- * @param filters - Array of filter criteria to apply (default: [])
185
- * @param sortBy - Sort field and direction (default: 'relevance')
186
- * @returns Object containing results array, pagination tokens, loading state, and error state
187
- *
188
- * @example
189
- * ```tsx
190
- * const { results, nextPageToken, previousPageToken, currentPageToken, resultsLoading, resultsError } = useObjectSearchResults(
191
- * 'Account',
192
- * 'test query',
193
- * 25,
194
- * '0',
195
- * [{ objectApiName: 'Account', fieldPath: 'Name', operator: 'contains', values: ['test'] }]
196
- * );
197
- * ```
198
- */
199
- export function useObjectSearchResults(
200
- objectApiName: string | null,
201
- searchQuery: string,
202
- searchPageSize: number = 50,
203
- searchPageToken: string = "0",
204
- filters: FilterCriteria[] = [],
205
- sortBy: string = "relevance",
206
- ) {
207
- const [resultsCache, setResultsCache] = useState<
208
- Record<
209
- string,
210
- {
211
- results: SearchResultRecord[];
212
- query: string;
213
- pageToken: string;
214
- pageSize: number;
215
- filtersKey: string;
216
- sortBy: string;
217
- nextPageToken: string | null;
218
- previousPageToken: string | null;
219
- currentPageToken: string;
220
- }
221
- >
222
- >({});
223
- const [loading, setLoading] = useState<Record<string, boolean>>({});
224
- const [error, setError] = useState<Record<string, string | null>>({});
225
-
226
- const debounceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
227
- const abortControllerRef = useRef<AbortController | null>(null);
228
- const resultsCacheRef = useRef(resultsCache);
229
-
230
- const filtersKey = useMemo(() => {
231
- const filtersArray = Array.isArray(filters) ? filters : [];
232
- return createFiltersKey(filtersArray);
233
- }, [filters]);
234
-
235
- useEffect(() => {
236
- resultsCacheRef.current = resultsCache;
237
- }, [resultsCache]);
238
-
239
- useEffect(() => {
240
- if (!objectApiName || !searchQuery.trim()) {
241
- return;
242
- }
243
-
244
- let isCancelled = false;
245
- const abortController = new AbortController();
246
-
247
- if (abortControllerRef.current) {
248
- abortControllerRef.current.abort();
249
- }
250
- abortControllerRef.current = abortController;
251
-
252
- if (debounceTimeout.current) {
253
- clearTimeout(debounceTimeout.current);
254
- debounceTimeout.current = null;
255
- }
256
-
257
- const cached = resultsCacheRef.current[objectApiName];
258
- if (
259
- !abortController.signal.aborted &&
260
- cached &&
261
- cached.query === searchQuery &&
262
- cached.pageToken === searchPageToken &&
263
- cached.pageSize === searchPageSize &&
264
- cached.filtersKey === filtersKey &&
265
- cached.sortBy === sortBy
266
- ) {
267
- return;
268
- }
269
-
270
- if (abortController.signal.aborted) {
271
- return;
272
- }
273
-
274
- const fetchResults = async () => {
275
- setLoading((prev) => ({ ...prev, [objectApiName]: true }));
276
- setError((prev) => ({ ...prev, [objectApiName]: null }));
277
-
278
- try {
279
- const searchParams: SearchParams = {
280
- sortBy: sortBy === "relevance" ? "" : sortBy,
281
- filters: filters,
282
- pageSize: searchPageSize,
283
- pageToken: searchPageToken,
284
- };
285
-
286
- const keywordSearchResult = await objectInfoService.searchResults(
287
- searchQuery,
288
- objectApiName,
289
- searchParams,
290
- abortController.signal,
291
- );
292
-
293
- if (isCancelled || abortController.signal.aborted) return;
294
-
295
- const normalizedRecords = keywordSearchResult.records.map((r) => ({
296
- record: r.record as SearchResultRecordData,
297
- highlightInfo: r.highlightInfo,
298
- searchInfo: r.searchInfo,
299
- }));
300
-
301
- const nextPageToken: string | null = keywordSearchResult.nextPageToken ?? null;
302
- const previousPageToken: string | null = keywordSearchResult.previousPageToken ?? null;
303
-
304
- setResultsCache((prev): typeof prev => ({
305
- ...prev,
306
- [objectApiName]: {
307
- results: normalizedRecords,
308
- query: searchQuery,
309
- pageToken: searchPageToken,
310
- pageSize: searchPageSize,
311
- filtersKey: filtersKey,
312
- sortBy,
313
- nextPageToken,
314
- previousPageToken,
315
- currentPageToken: keywordSearchResult.currentPageToken,
316
- },
317
- }));
318
- } catch (err) {
319
- if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
320
- return;
321
- }
322
- setError((prev) => ({ ...prev, [objectApiName]: "Unable to load search results" }));
323
- // Cache empty result so we skip refetch on remount (avoid infinite loop on API error)
324
- setResultsCache((prev) => ({
325
- ...prev,
326
- [objectApiName]: {
327
- results: [],
328
- query: searchQuery,
329
- pageToken: searchPageToken,
330
- pageSize: searchPageSize,
331
- filtersKey: filtersKey,
332
- sortBy,
333
- nextPageToken: null,
334
- previousPageToken: null,
335
- currentPageToken: searchPageToken,
336
- },
337
- }));
338
- } finally {
339
- if (!isCancelled) {
340
- setLoading((prev) => ({ ...prev, [objectApiName]: false }));
341
- }
342
- }
343
- };
344
-
345
- if (searchPageToken === "0") {
346
- debounceTimeout.current = setTimeout(() => {
347
- fetchResults();
348
- }, 300);
349
- } else {
350
- fetchResults();
351
- }
352
-
353
- return () => {
354
- isCancelled = true;
355
- abortController.abort();
356
- if (debounceTimeout.current) {
357
- clearTimeout(debounceTimeout.current);
358
- debounceTimeout.current = null;
359
- }
360
- if (abortControllerRef.current === abortController) {
361
- abortControllerRef.current = null;
362
- }
363
- };
364
- }, [objectApiName, searchQuery, searchPageSize, searchPageToken, filtersKey, sortBy]);
365
-
366
- return {
367
- results: objectApiName ? resultsCache[objectApiName]?.results || [] : [],
368
- nextPageToken: objectApiName ? resultsCache[objectApiName]?.nextPageToken || null : null,
369
- previousPageToken: objectApiName
370
- ? resultsCache[objectApiName]?.previousPageToken || null
371
- : null,
372
- currentPageToken: objectApiName ? resultsCache[objectApiName]?.currentPageToken || "0" : "0",
373
- resultsLoading: objectApiName ? loading[objectApiName] || false : false,
374
- resultsError: objectApiName ? error[objectApiName] || null : null,
375
- };
376
- }
377
-
378
157
  /**
379
158
  * Hook: useObjectFilters
380
159
  * Thin wrapper over useObjectListMetadata for backward compatibility.
@@ -16,7 +16,6 @@ export interface UseRecordDetailLayoutParams {
16
16
  objectApiName: string | null;
17
17
  recordId: string | null;
18
18
  recordTypeId?: string | null;
19
- /** When provided, skips the fetch and uses this data (avoids duplicate API calls when parent already fetched). Callers should memoize this (e.g. useMemo) to avoid unnecessary effect runs. */
20
19
  initialData?: {
21
20
  layout: LayoutResponse;
22
21
  record: GraphQLRecordNode;
@@ -25,7 +24,6 @@ export interface UseRecordDetailLayoutParams {
25
24
  }
26
25
 
27
26
  const MAX_CACHE_SIZE = 50;
28
- /** Cache entries older than this are treated as stale and refetched. */
29
27
  const CACHE_TTL_MS = 5 * 60 * 1000;
30
28
 
31
29
  type CacheEntry = {
@@ -35,17 +33,6 @@ type CacheEntry = {
35
33
  cachedAt: number;
36
34
  };
37
35
 
38
- /**
39
- * Detail page data: layout (REST), object metadata (GraphQL), single record (GraphQL).
40
- *
41
- * Calls objectDetailService.getRecordDetail once per objectApiName/recordId/recordTypeId.
42
- * Caches result in memory (TTL 5min, max 50 entries). Used by DetailPage and UiApiDetailForm.
43
- *
44
- * @param objectApiName - Object API name.
45
- * @param recordId - Record Id.
46
- * @param recordTypeId - Optional record type (default master).
47
- * @returns { layout, record, objectMetadata, loading, error }.
48
- */
49
36
  export function useRecordDetailLayout({
50
37
  objectApiName,
51
38
  recordId,
@@ -71,7 +58,6 @@ export function useRecordDetailLayout({
71
58
  return;
72
59
  }
73
60
 
74
- // Skip fetch when parent already provided data (avoids duplicate API calls)
75
61
  if (
76
62
  initialData?.layout != null &&
77
63
  initialData?.record != null &&
@@ -92,7 +78,6 @@ export function useRecordDetailLayout({
92
78
  }
93
79
 
94
80
  let isCancelled = false;
95
- const abortController = new AbortController();
96
81
 
97
82
  const fetchDetail = async () => {
98
83
  setLoading(true);
@@ -107,7 +92,6 @@ export function useRecordDetailLayout({
107
92
  objectApiName,
108
93
  recordId,
109
94
  recordTypeId ?? undefined,
110
- abortController.signal,
111
95
  );
112
96
 
113
97
  if (isCancelled) return;
@@ -127,9 +111,7 @@ export function useRecordDetailLayout({
127
111
  setRecord(recordData);
128
112
  setObjectMetadata(objectMetadataData);
129
113
  } catch (err) {
130
- if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
131
- return;
132
- }
114
+ if (isCancelled) return;
133
115
  setError("Failed to load record details");
134
116
  } finally {
135
117
  if (!isCancelled) {
@@ -142,7 +124,6 @@ export function useRecordDetailLayout({
142
124
 
143
125
  return () => {
144
126
  isCancelled = true;
145
- abortController.abort();
146
127
  };
147
128
  }, [objectApiName, recordId, recordTypeId, cacheKey, initialData]);
148
129
 
@@ -2,77 +2,31 @@
2
2
  * API Utilities
3
3
  *
4
4
  * Generic utility functions for API requests, validation, and URL handling.
5
- * These utilities are framework-agnostic and can be reused across different API services.
6
5
  */
7
6
 
8
7
  import type { ZodSchema } from "zod";
9
8
 
10
- /**
11
- * Options for fetchAndValidate utility function
12
- */
13
9
  export interface FetchAndValidateOptions<T> {
14
- /** Zod schema for validation */
15
10
  schema: ZodSchema<T>;
16
- /** Error context for better error messages (e.g., "object info batch", "list info") */
17
11
  errorContext: string;
18
- /** Optional function to extract/transform data from response before validation */
19
12
  extractData?: (data: unknown) => unknown;
20
- /** Optional AbortSignal to cancel the request */
21
- signal?: AbortSignal;
22
13
  }
23
14
 
24
- /**
25
- * Generic utility function to fetch, parse, and validate API responses.
26
- * Handles common patterns: fetch -> check status -> parse JSON -> validate Zod -> handle errors
27
- *
28
- * @param fetchFn - Function that returns a Promise<Response> (e.g., uiApiClient.get or uiApiClient.post)
29
- * @param options - Configuration options including schema, error context, optional data extraction, and AbortSignal
30
- * @returns Promise resolving to validated data of type T
31
- *
32
- * @remarks
33
- * - Handles abort signals properly to prevent race conditions
34
- * - Provides detailed error messages with context
35
- * - Validates responses using Zod schemas for type safety
36
- * - Supports data extraction/transformation before validation
37
- *
38
- * @example
39
- * ```tsx
40
- * const data = await fetchAndValidate(
41
- * (signal) => apiClient.get('/endpoint', { signal }),
42
- * {
43
- * schema: MySchema,
44
- * errorContext: 'user data',
45
- * extractData: (data) => data.items,
46
- * signal: abortController.signal
47
- * }
48
- * );
49
- * ```
50
- */
51
15
  export async function fetchAndValidate<T>(
52
- fetchFn: (signal?: AbortSignal) => Promise<Response>,
16
+ fetchFn: () => Promise<Response>,
53
17
  options: FetchAndValidateOptions<T>,
54
18
  ): Promise<T> {
55
- const { schema, errorContext, extractData, signal } = options;
19
+ const { schema, errorContext, extractData } = options;
56
20
 
57
21
  try {
58
- const response = await fetchFn(signal);
59
-
60
- if (signal?.aborted) {
61
- throw new DOMException("The operation was aborted.", "AbortError");
62
- }
22
+ const response = await fetchFn();
63
23
 
64
24
  if (!response.ok) {
65
25
  throw new Error(`Failed to fetch ${errorContext}: ${response.status} ${response.statusText}`);
66
26
  }
67
27
 
68
28
  const data = await response.json();
69
-
70
- if (signal?.aborted) {
71
- throw new DOMException("The operation was aborted.", "AbortError");
72
- }
73
-
74
29
  const dataToValidate = extractData ? extractData(data) : data;
75
-
76
30
  const validationResult = schema.safeParse(dataToValidate);
77
31
 
78
32
  if (!validationResult.success) {
@@ -81,14 +35,6 @@ export async function fetchAndValidate<T>(
81
35
 
82
36
  return validationResult.data;
83
37
  } catch (error) {
84
- if (error instanceof DOMException && error.name === "AbortError") {
85
- throw error;
86
- }
87
-
88
- if (error instanceof Error && error.name === "AbortError") {
89
- throw error;
90
- }
91
-
92
38
  if (error instanceof Error && error.name === "ZodError") {
93
39
  throw new Error(`Invalid ${errorContext} response format: ${error.message}`);
94
40
  }
@@ -108,18 +54,6 @@ export async function fetchAndValidate<T>(
108
54
  }
109
55
  }
110
56
 
111
- /**
112
- * Helper to safely encode path components for URLs
113
- * Wraps encodeURIComponent for better semantic meaning
114
- *
115
- * @param segment - The path segment to encode
116
- * @returns URL-encoded path segment
117
- *
118
- * @example
119
- * ```tsx
120
- * const safePath = safeEncodePath('Account Name'); // 'Account%20Name'
121
- * ```
122
- */
123
57
  export function safeEncodePath(segment: string): string {
124
58
  return encodeURIComponent(segment);
125
59
  }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Public API for the Global Search feature package.
3
+ *
4
+ * Design goals:
5
+ * - Export **API services, hooks, types, schemas, and utilities** that customers can import from node_modules.
6
+ * - Do **not** export UI components or feature constants (customers build their own UI).
7
+ *
8
+ * Source implementation lives under `src/features/global-search/**`.
9
+ */
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // API layer
13
+ // ---------------------------------------------------------------------------
14
+
15
+ export { objectInfoService } from "./features/global-search/api/objectInfoService";
16
+ export {
17
+ objectDetailService,
18
+ extractFieldsFromLayout,
19
+ } from "./features/global-search/api/objectDetailService";
20
+ export type { RecordDetailResult } from "./features/global-search/api/objectDetailService";
21
+
22
+ export {
23
+ getRecordsGraphQL,
24
+ getRecordByIdGraphQL,
25
+ buildGetRecordsQuery,
26
+ buildWhereFromCriteria,
27
+ buildOrderByFromSort,
28
+ } from "./features/global-search/api/recordListGraphQLService";
29
+ export type {
30
+ RecordListGraphQLResult,
31
+ RecordListGraphQLVariables,
32
+ RecordListGraphQLOptions,
33
+ GraphQLRecordNode,
34
+ } from "./features/global-search/api/recordListGraphQLService";
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Hooks
38
+ // ---------------------------------------------------------------------------
39
+
40
+ export { useObjectInfoBatch } from "./features/global-search/hooks/useObjectInfoBatch";
41
+ export {
42
+ useObjectListMetadata,
43
+ useObjectColumns,
44
+ useObjectFilters,
45
+ } from "./features/global-search/hooks/useObjectSearchData";
46
+ export { useRecordListGraphQL } from "./features/global-search/hooks/useRecordListGraphQL";
47
+ export { useRecordDetailLayout } from "./features/global-search/hooks/useRecordDetailLayout";
48
+
49
+ export type { ObjectListMetadata } from "./features/global-search/hooks/useObjectSearchData";
50
+
51
+ export type {
52
+ UseRecordListGraphQLOptions,
53
+ UseRecordListGraphQLReturn,
54
+ } from "./features/global-search/hooks/useRecordListGraphQL";
55
+
56
+ export type {
57
+ UseRecordDetailLayoutParams,
58
+ UseRecordDetailLayoutReturn,
59
+ } from "./features/global-search/hooks/useRecordDetailLayout";
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Types + Zod schemas (runtime validation)
63
+ // ---------------------------------------------------------------------------
64
+
65
+ export {
66
+ ColumnArraySchema,
67
+ SearchResultRecordArraySchema,
68
+ KeywordSearchResultSchema,
69
+ SearchResultsResponseSchema,
70
+ } from "./features/global-search/types/search/searchResults";
71
+ export type {
72
+ Column,
73
+ SearchResultRecord,
74
+ KeywordSearchResult,
75
+ SearchResultsResponse,
76
+ } from "./features/global-search/types/search/searchResults";
77
+
78
+ export {
79
+ FilterArraySchema,
80
+ FilterCriteriaArraySchema,
81
+ FILTER_OPERATORS,
82
+ } from "./features/global-search/types/filters/filters";
83
+ export type {
84
+ Filter,
85
+ FilterCriteria,
86
+ FilterOperator,
87
+ FiltersResponse,
88
+ } from "./features/global-search/types/filters/filters";
89
+
90
+ export { PicklistValueArraySchema } from "./features/global-search/types/filters/picklist";
91
+ export type { PicklistValue } from "./features/global-search/types/filters/picklist";
92
+
93
+ export {
94
+ ObjectInfoBatchResponseSchema,
95
+ ObjectInfoResultSchema,
96
+ } from "./features/global-search/types/objectInfo/objectInfo";
97
+ export type {
98
+ ObjectInfoBatchResponse,
99
+ ObjectInfoResult,
100
+ } from "./features/global-search/types/objectInfo/objectInfo";
101
+
102
+ export { LayoutResponseSchema } from "./features/global-search/types/recordDetail/recordDetail";
103
+ export type { LayoutResponse } from "./features/global-search/types/recordDetail/recordDetail";
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Utilities
107
+ // ---------------------------------------------------------------------------
108
+
109
+ export { fetchAndValidate, safeEncodePath } from "./features/global-search/utils/apiUtils";
110
+ export { debounce } from "./features/global-search/utils/debounce";
111
+ export { createFiltersKey } from "./features/global-search/utils/cacheUtils";
112
+ export {
113
+ calculateFieldsToFetch,
114
+ getSafeKey,
115
+ isValidSalesforceId,
116
+ } from "./features/global-search/utils/recordUtils";
117
+ export { parseFilterValue } from "./features/global-search/utils/filterUtils";
118
+ export { sanitizeFilterValue } from "./features/global-search/utils/sanitizationUtils";
119
+ export {
120
+ getGraphQLNodeValue,
121
+ getDisplayValueForDetailFieldFromNode,
122
+ getDisplayValueForLayoutItemFromNode,
123
+ getGraphQLRecordDisplayName,
124
+ } from "./features/global-search/utils/graphQLNodeFieldUtils";
125
+ export { graphQLNodeToSearchResultRecordData } from "./features/global-search/utils/graphQLRecordAdapter";
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-base-sfdx-project-experimental",
3
- "version": "1.76.0",
3
+ "version": "1.76.1",
4
4
  "description": "Base SFDX project template",
5
5
  "private": true,
6
6
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/webapp-template-app-react-template-b2x-experimental",
3
- "version": "1.76.0",
3
+ "version": "1.76.1",
4
4
  "description": "Base reference app template",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",