@salesforce/webapp-template-app-react-sample-b2e-experimental 1.76.0 → 1.77.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.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/objects/Maintenance_Request__c/Maintenance_Request__c.object-meta.xml +0 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +4 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectDetailService.ts +3 -26
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoGraphQLService.ts +108 -165
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoService.ts +9 -113
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/UiApiDetailForm.tsx +2 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +1 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectSearchData.ts +7 -228
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +1 -20
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/picklist.ts +5 -31
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/objectInfo/objectInfo.ts +46 -163
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/schema.d.ts +200 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/apiUtils.ts +3 -69
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +37 -279
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/recordUtils.ts +4 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/index.ts +117 -3
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceAdapter.ts +2 -23
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +21 -4
- package/dist/package.json +1 -1
- package/package.json +3 -3
|
@@ -6,26 +6,16 @@
|
|
|
6
6
|
* - getSharedFilters: module-level deduplication for getObjectListFilters across hook instances.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useState, useEffect
|
|
10
|
-
import { objectInfoService
|
|
11
|
-
import type {
|
|
12
|
-
|
|
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
|
|
103
|
+
.getPicklistValues(objectApiName!, f.targetFieldPath)
|
|
115
104
|
.then((values) => ({ fieldPath: f.targetFieldPath, values }))
|
|
116
|
-
.catch((
|
|
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
|
|
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
|
|
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
|
|
|
@@ -1,32 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { PicklistValue as GraphQLPicklistValue } from "../objectInfo/objectInfo";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// Zod Schema for Picklist Value
|
|
8
|
-
const PicklistValueSchema = z
|
|
9
|
-
.object({
|
|
10
|
-
// 1. Cleanup: Attributes are usually an object, not just 'unknown'
|
|
11
|
-
attributes: z.record(z.string(), z.unknown()).nullish(),
|
|
12
|
-
label: z.string(),
|
|
13
|
-
|
|
14
|
-
// 2. Precise Typing: 'validFor' is an array of indices (numbers)
|
|
15
|
-
// pointing to the controlling field's values.
|
|
16
|
-
validFor: z.array(z.number()).nullish(),
|
|
17
|
-
value: z.string(),
|
|
18
|
-
|
|
19
|
-
// 3. Usability: Added common API fields that are useful for UI logic
|
|
20
|
-
// (marked optional in case the specific API version omits them)
|
|
21
|
-
defaultValue: z.boolean().nullish(),
|
|
22
|
-
active: z.boolean().nullish(),
|
|
23
|
-
})
|
|
24
|
-
.passthrough();
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Single picklist value from the API
|
|
28
|
-
*/
|
|
29
|
-
export type PicklistValue = z.infer<typeof PicklistValueSchema>;
|
|
30
|
-
|
|
31
|
-
// Export schema for validation
|
|
32
|
-
export const PicklistValueArraySchema = z.array(PicklistValueSchema);
|
|
3
|
+
export type PicklistValue = GraphQLPicklistValue & {
|
|
4
|
+
value: NonNullable<GraphQLPicklistValue["value"]>;
|
|
5
|
+
label: NonNullable<GraphQLPicklistValue["label"]>;
|
|
6
|
+
};
|
|
@@ -1,166 +1,49 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
nameFields: z.array(z.string()),
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
// Zod Schema for Filtered Lookup Info (can be null or object)
|
|
23
|
-
const FilteredLookupInfoSchema = z.record(z.string(), z.unknown()).nullable();
|
|
24
|
-
|
|
25
|
-
// Zod Schema for Field Definition
|
|
26
|
-
// Using passthrough to allow extra fields that might be present in the API response
|
|
27
|
-
const FieldSchema = z
|
|
28
|
-
.object({
|
|
29
|
-
apiName: z.string(),
|
|
30
|
-
calculated: z.boolean(),
|
|
31
|
-
compound: z.boolean(),
|
|
32
|
-
compoundComponentName: z.string().nullable(),
|
|
33
|
-
compoundFieldName: z.string().nullable(),
|
|
34
|
-
controllerName: z.string().nullable(),
|
|
35
|
-
controllingFields: z.array(z.string()),
|
|
36
|
-
createable: z.boolean(),
|
|
37
|
-
custom: z.boolean(),
|
|
38
|
-
dataType: z.string(),
|
|
39
|
-
defaultValue: z.unknown().nullable(),
|
|
40
|
-
defaultedOnCreate: z.boolean(),
|
|
41
|
-
digits: z.number(),
|
|
42
|
-
externalId: z.boolean(),
|
|
43
|
-
extraTypeInfo: z.string().nullable(),
|
|
44
|
-
filterable: z.boolean(),
|
|
45
|
-
filteredLookupInfo: FilteredLookupInfoSchema,
|
|
46
|
-
highScaleNumber: z.boolean(),
|
|
47
|
-
htmlFormatted: z.boolean(),
|
|
48
|
-
inlineHelpText: z.string().nullable(),
|
|
49
|
-
label: z.string(),
|
|
50
|
-
length: z.number(),
|
|
51
|
-
maskType: z.string().nullable(),
|
|
52
|
-
nameField: z.boolean(),
|
|
53
|
-
polymorphicForeignKey: z.boolean(),
|
|
54
|
-
precision: z.number(),
|
|
55
|
-
reference: z.boolean(),
|
|
56
|
-
referenceTargetField: z.string().nullable(),
|
|
57
|
-
referenceToInfos: z.array(ReferenceToInfoSchema),
|
|
58
|
-
relationshipName: z.string().nullable(),
|
|
59
|
-
required: z.boolean(),
|
|
60
|
-
scale: z.number(),
|
|
61
|
-
searchPrefilterable: z.boolean(),
|
|
62
|
-
sortable: z.boolean(),
|
|
63
|
-
unique: z.boolean(),
|
|
64
|
-
updateable: z.boolean(),
|
|
65
|
-
})
|
|
66
|
-
.passthrough();
|
|
67
|
-
|
|
68
|
-
// Zod Schema for Record Type Info
|
|
69
|
-
// Using passthrough to allow extra fields that might be present in the API response
|
|
70
|
-
const RecordTypeInfoSchema = z
|
|
71
|
-
.object({
|
|
72
|
-
available: z.boolean(),
|
|
73
|
-
defaultRecordTypeMapping: z.boolean(),
|
|
74
|
-
master: z.boolean(),
|
|
75
|
-
name: z.string(),
|
|
76
|
-
recordTypeId: z.string(),
|
|
77
|
-
})
|
|
78
|
-
.passthrough();
|
|
79
|
-
|
|
80
|
-
// Zod Schema for Theme Info
|
|
81
|
-
const ThemeInfoSchema = z.object({
|
|
82
|
-
color: z.string(),
|
|
83
|
-
iconUrl: z.string(),
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Zod Schema for Object Info Result
|
|
87
|
-
// Using passthrough to allow extra fields and using FieldSchema/RecordTypeInfoSchema with passthrough
|
|
88
|
-
const ObjectInfoResultSchema = z
|
|
89
|
-
.object({
|
|
90
|
-
apiName: z.string(),
|
|
91
|
-
associateEntityType: z.string().nullable(),
|
|
92
|
-
associateParentEntity: z.string().nullable(),
|
|
93
|
-
childRelationships: z.array(ChildRelationshipSchema),
|
|
94
|
-
compactLayoutable: z.boolean(),
|
|
95
|
-
createable: z.boolean(),
|
|
96
|
-
custom: z.boolean(),
|
|
97
|
-
defaultRecordTypeId: z.string(),
|
|
98
|
-
deletable: z.boolean(),
|
|
99
|
-
dependentFields: z.record(z.string(), z.unknown()),
|
|
100
|
-
eTag: z.string(),
|
|
101
|
-
feedEnabled: z.boolean(),
|
|
102
|
-
// Avoid using FieldSchema because of performance concerns with validating the high number of fields returned from the Salesforce API and causing the UI to freeze.
|
|
103
|
-
fields: z.record(z.string(), z.any()),
|
|
104
|
-
keyPrefix: z.string(),
|
|
105
|
-
label: z.string(),
|
|
106
|
-
labelPlural: z.string(),
|
|
107
|
-
layoutable: z.boolean(),
|
|
108
|
-
mruEnabled: z.boolean(),
|
|
109
|
-
nameFields: z.array(z.string()),
|
|
110
|
-
queryable: z.boolean(),
|
|
111
|
-
// Avoid using RecordTypeInfoSchema because of performance concerns with validating the high number of fields returned from the Salesforce API and causing the UI to freeze.
|
|
112
|
-
recordTypeInfos: z.record(z.string(), z.any()),
|
|
113
|
-
searchLayoutable: z.boolean(),
|
|
114
|
-
searchable: z.boolean(),
|
|
115
|
-
themeInfo: ThemeInfoSchema,
|
|
116
|
-
updateable: z.boolean(),
|
|
117
|
-
})
|
|
118
|
-
.passthrough();
|
|
119
|
-
|
|
120
|
-
// Zod Schema for Object Info Batch Result Item
|
|
121
|
-
const ObjectInfoBatchResultItemSchema = z.object({
|
|
122
|
-
result: ObjectInfoResultSchema,
|
|
123
|
-
statusCode: z.number(),
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Zod Schema for Object Info Batch Response (array of items)
|
|
127
|
-
export const ObjectInfoBatchResponseSchema = z.object({
|
|
128
|
-
results: z.array(ObjectInfoBatchResultItemSchema),
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// TypeScript Types (inferred from Zod schemas)
|
|
132
|
-
export type ChildRelationship = z.infer<typeof ChildRelationshipSchema>;
|
|
133
|
-
export type ReferenceToInfo = z.infer<typeof ReferenceToInfoSchema>;
|
|
134
|
-
export type FilteredLookupInfo = z.infer<typeof FilteredLookupInfoSchema>;
|
|
135
|
-
export type Field = z.infer<typeof FieldSchema>;
|
|
136
|
-
export type RecordTypeInfo = z.infer<typeof RecordTypeInfoSchema>;
|
|
137
|
-
export type ThemeInfo = z.infer<typeof ThemeInfoSchema>;
|
|
138
|
-
export type ObjectInfoBatchResponse = z.infer<typeof ObjectInfoBatchResponseSchema>;
|
|
139
|
-
// Type Patching: Overwriting the "any" from the performance-optimized schema
|
|
140
|
-
// with the strict types defined above. This ensures Developers get strict typing
|
|
141
|
-
// even though the Runtime Validator skips the deep check.
|
|
142
|
-
export type ObjectInfoResult = Omit<
|
|
143
|
-
z.infer<typeof ObjectInfoResultSchema>,
|
|
144
|
-
"fields" | "recordTypeInfos"
|
|
145
|
-
> & {
|
|
146
|
-
fields: Record<string, Field>;
|
|
147
|
-
recordTypeInfos: Record<string, RecordTypeInfo>;
|
|
1
|
+
import type { GetObjectInfosQuery, GetPicklistValuesQuery } from "../schema";
|
|
2
|
+
|
|
3
|
+
// Generic utility types for extracting array item types
|
|
4
|
+
type ArrayItem<T> = T extends (infer Item)[] ? Item : never;
|
|
5
|
+
type NonNullableArrayItem<T> = NonNullable<ArrayItem<NonNullable<T>>>;
|
|
6
|
+
|
|
7
|
+
// ObjectInfos extraction
|
|
8
|
+
export type GetObjectInfosQueryObjectInfos = NonNullable<
|
|
9
|
+
GetObjectInfosQuery["uiapi"]["objectInfos"]
|
|
10
|
+
>;
|
|
11
|
+
export type GetObjectInfosQueryObjectInfo = NonNullableArrayItem<GetObjectInfosQueryObjectInfos>;
|
|
12
|
+
export type GetObjectInfosQueryField = NonNullableArrayItem<
|
|
13
|
+
GetObjectInfosQueryObjectInfo["fields"]
|
|
14
|
+
>;
|
|
15
|
+
|
|
16
|
+
// ObjectInfoResult types
|
|
17
|
+
export type ObjectInfoResult = Omit<GetObjectInfosQueryObjectInfo, "fields"> & {
|
|
18
|
+
fields: Record<string, GetObjectInfosQueryField>;
|
|
148
19
|
};
|
|
149
|
-
export type
|
|
150
|
-
|
|
151
|
-
"result"
|
|
152
|
-
> & {
|
|
153
|
-
result: ObjectInfoResult;
|
|
20
|
+
export type ObjectInfoBatchResponse = {
|
|
21
|
+
results: { result: ObjectInfoResult; statusCode: number }[];
|
|
154
22
|
};
|
|
155
23
|
|
|
156
|
-
//
|
|
157
|
-
export
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
24
|
+
// Picklist values extraction
|
|
25
|
+
export type GetPicklistValuesQueryObjectInfos = GetPicklistValuesQuery["uiapi"]["objectInfos"];
|
|
26
|
+
export type GetPicklistValuesQueryObjectInfo =
|
|
27
|
+
NonNullableArrayItem<GetPicklistValuesQueryObjectInfos>;
|
|
28
|
+
export type GetPicklistValuesQueryField = NonNullableArrayItem<
|
|
29
|
+
GetPicklistValuesQueryObjectInfo["fields"]
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
// Extract picklist-specific field type (the one with picklistValuesByRecordTypeIDs)
|
|
33
|
+
type GetPicklistValuesQueryPicklistField = Extract<
|
|
34
|
+
GetPicklistValuesQueryField,
|
|
35
|
+
{ picklistValuesByRecordTypeIDs?: unknown }
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
// Extract types from picklistValuesByRecordTypeIDs
|
|
39
|
+
type PicklistValuesByRecordTypeIDs =
|
|
40
|
+
GetPicklistValuesQueryPicklistField["picklistValuesByRecordTypeIDs"];
|
|
41
|
+
type PicklistValuesByRecordTypeID = NonNullable<
|
|
42
|
+
NonNullable<PicklistValuesByRecordTypeIDs> extends (infer Item)[] ? Item : null
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
// Extract individual picklist value type
|
|
46
|
+
type PicklistValues = NonNullable<PicklistValuesByRecordTypeID>["picklistValues"];
|
|
47
|
+
export type PicklistValue = NonNullable<
|
|
48
|
+
NonNullable<PicklistValues> extends (infer Item)[] ? Item : null
|
|
49
|
+
>;
|