@salesforce/webapp-template-app-react-template-b2x-experimental 1.59.1 → 1.60.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 (73) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/package-lock.json +15 -15
  3. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/api/index.ts +19 -0
  4. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/api/objectDetailService.ts +125 -0
  5. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/api/objectInfoGraphQLService.ts +194 -0
  6. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/api/objectInfoService.ts +199 -0
  7. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/api/recordListGraphQLService.ts +365 -0
  8. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/FiltersPanel.tsx +375 -0
  9. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/LoadingFallback.tsx +61 -0
  10. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/SearchResultCard.tsx +131 -0
  11. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/alerts/status-alert.tsx +45 -0
  12. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/DetailFields.tsx +55 -0
  13. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/DetailForm.tsx +146 -0
  14. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/DetailHeader.tsx +34 -0
  15. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
  16. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/Section.tsx +108 -0
  17. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/SectionRow.tsx +20 -0
  18. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
  19. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
  20. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
  21. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
  22. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
  23. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
  24. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
  25. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/detail/formatted/index.ts +6 -0
  26. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/filters/FilterField.tsx +54 -0
  27. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/filters/FilterInput.tsx +55 -0
  28. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/filters/FilterSelect.tsx +72 -0
  29. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/forms/filters-form.tsx +114 -0
  30. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/forms/submit-button.tsx +47 -0
  31. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/layout/card-layout.tsx +19 -0
  32. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/search/ResultCardFields.tsx +71 -0
  33. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/search/SearchHeader.tsx +31 -0
  34. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/search/SearchPagination.tsx +144 -0
  35. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/search/SearchResultsPanel.tsx +197 -0
  36. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
  37. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/constants.ts +39 -0
  38. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/features/global-search/index.ts +33 -0
  39. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/hooks/form.tsx +208 -0
  40. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/hooks/index.ts +22 -0
  41. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/hooks/useObjectInfoBatch.ts +65 -0
  42. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/hooks/useObjectSearchData.ts +380 -0
  43. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/hooks/useRecordDetailLayout.ts +156 -0
  44. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/hooks/useRecordListGraphQL.ts +135 -0
  45. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/pages/DetailPage.tsx +109 -0
  46. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/pages/GlobalSearch.tsx +229 -0
  47. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/pages/Home.tsx +11 -10
  48. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/routes.tsx +23 -1
  49. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/types/filters/filters.ts +120 -0
  50. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/types/filters/picklist.ts +32 -0
  51. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/types/index.ts +4 -0
  52. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/types/objectInfo/objectInfo.ts +166 -0
  53. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/types/recordDetail/recordDetail.ts +61 -0
  54. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/types/search/searchResults.ts +229 -0
  55. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/apiUtils.ts +125 -0
  56. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/cacheUtils.ts +76 -0
  57. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/debounce.ts +89 -0
  58. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/fieldUtils.ts +354 -0
  59. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/fieldValueExtractor.ts +67 -0
  60. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/filterUtils.ts +32 -0
  61. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/formDataTransformUtils.ts +260 -0
  62. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/formUtils.ts +142 -0
  63. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
  64. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
  65. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/graphQLRecordAdapter.ts +90 -0
  66. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/index.ts +59 -0
  67. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/layoutTransformUtils.ts +236 -0
  68. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/linkUtils.ts +14 -0
  69. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/paginationUtils.ts +49 -0
  70. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/recordUtils.ts +159 -0
  71. package/dist/force-app/main/default/webapplications/appreacttemplateb2x/src/utils/sanitizationUtils.ts +49 -0
  72. package/dist/package.json +1 -1
  73. package/package.json +2 -2
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Record GraphQL Service
3
+ *
4
+ * Single service for querying Salesforce object records via GraphQL UI API (uiapi.query).
5
+ * Handles both list (paginated, filter, sort, search) and single-record-by-id using one query shape.
6
+ *
7
+ * @module api/recordListGraphQLService
8
+ */
9
+
10
+ import { getDataSDK } from "@salesforce/sdk-data";
11
+ import type { Column } from "../types/search/searchResults";
12
+ import type { FilterCriteria } from "../types/filters/filters";
13
+
14
+ const DEFAULT_PAGE_SIZE = 50;
15
+
16
+ /** Tree of selection: leaf is "value", branch is nested fields. Keys starting with __on_ are inline fragments (e.g. __on_User). */
17
+ interface SelectionTree {
18
+ [key: string]: "value" | SelectionTree;
19
+ }
20
+
21
+ /**
22
+ * Polymorphic relationship fields and the concrete GraphQL types they can resolve to.
23
+ * Only relationship names listed here use inline fragments; others use direct selection
24
+ * (e.g. Parent -> Parent { Name { value } } because "Parent" is not a schema type).
25
+ * We use a single fragment per field to avoid schema validation errors (e.g. User and Group
26
+ * cannot both be spread in the same selection in some contexts).
27
+ */
28
+ const POLYMORPHIC_RELATIONSHIP_TYPES: Record<string, string[]> = {
29
+ Owner: ["User"],
30
+ CreatedBy: ["User"],
31
+ LastModifiedBy: ["User"],
32
+ };
33
+
34
+ /**
35
+ * Builds a selection tree from columns (fieldApiName). Simple fields (e.g. Name, OwnerId) become
36
+ * top-level leaves. Relationship fields that are in POLYMORPHIC_RELATIONSHIP_TYPES use a single
37
+ * inline fragment on the first concrete type (e.g. ... on User). All other relationship fields
38
+ * (e.g. Parent, Account) use direct selection (e.g. Parent { Name { value } }) because the
39
+ * relationship name is not necessarily a GraphQL type name (e.g. Parent resolves to Account).
40
+ */
41
+ function buildSelectionTree(columns: Column[]): SelectionTree {
42
+ const allFieldNames = new Set(columns.map((c) => (c.fieldApiName ?? "").trim()).filter(Boolean));
43
+ const tree: SelectionTree = { Id: "value" };
44
+ for (const col of columns) {
45
+ const name = (col.fieldApiName ?? "").trim();
46
+ if (!name) continue;
47
+ const parts = name.split(".");
48
+ if (parts.length === 1) {
49
+ const fieldName = parts[0];
50
+ const hasCorrespondingId = allFieldNames.has(`${fieldName}Id`);
51
+ if (hasCorrespondingId) {
52
+ const knownTypes = POLYMORPHIC_RELATIONSHIP_TYPES[fieldName];
53
+ if (knownTypes?.length) {
54
+ // Use a single inline fragment to avoid "User can never be Group" validation errors
55
+ const typeName = knownTypes[0];
56
+ tree[fieldName] = { [`__on_${typeName}`]: { Name: "value" } };
57
+ } else {
58
+ // Relationship name (e.g. Parent) is not a GraphQL type; use direct selection
59
+ tree[fieldName] = { Name: "value" };
60
+ }
61
+ } else {
62
+ tree[fieldName] = "value";
63
+ }
64
+ } else {
65
+ let current = tree;
66
+ for (let i = 0; i < parts.length; i++) {
67
+ const part = parts[i];
68
+ const isLeaf = i === parts.length - 1;
69
+ if (isLeaf) {
70
+ current[part] = "value";
71
+ } else {
72
+ const existing = current[part];
73
+ if (existing === "value") continue;
74
+ if (!existing) {
75
+ current[part] = {};
76
+ }
77
+ current = current[part] as SelectionTree;
78
+ }
79
+ }
80
+ }
81
+ }
82
+ return tree;
83
+ }
84
+
85
+ /**
86
+ * Serializes a selection tree to GraphQL selection set string.
87
+ * Keys starting with __on_ are emitted as inline fragments (... on TypeName { ... }).
88
+ * Id is scalar (no subselection); other leaves use { value }.
89
+ */
90
+ function serializeSelectionTree(tree: SelectionTree, indent: string): string {
91
+ const fragmentKeys = Object.keys(tree).filter((k) => k.startsWith("__on_"));
92
+ const normalKeys = Object.keys(tree).filter((k) => !k.startsWith("__on_"));
93
+ normalKeys.sort((a, b) => {
94
+ if (a === "Id") return -1;
95
+ if (b === "Id") return 1;
96
+ return a.localeCompare(b);
97
+ });
98
+ fragmentKeys.sort();
99
+ const lines: string[] = [];
100
+ const childIndent = `${indent} `;
101
+ for (const key of normalKeys) {
102
+ const val = tree[key];
103
+ if (val === "value") {
104
+ if (key === "Id") {
105
+ lines.push(`${indent}Id`);
106
+ } else {
107
+ lines.push(`${indent}${key} { value }`);
108
+ }
109
+ } else {
110
+ lines.push(`${indent}${key} {`);
111
+ lines.push(serializeSelectionTree(val, childIndent));
112
+ lines.push(`${indent}}`);
113
+ }
114
+ }
115
+ for (const key of fragmentKeys) {
116
+ const typeName = key.slice(5);
117
+ const val = tree[key];
118
+ if (val && typeof val === "object") {
119
+ lines.push(`${indent}... on ${typeName} {`);
120
+ lines.push(serializeSelectionTree(val, childIndent));
121
+ lines.push(`${indent}}`);
122
+ }
123
+ }
124
+ return lines.join("\n");
125
+ }
126
+
127
+ function buildNodeSelection(columns: Column[]): string {
128
+ const tree = buildSelectionTree(columns);
129
+ return serializeSelectionTree(tree, " ");
130
+ }
131
+
132
+ /**
133
+ * Builds GraphQL where clause from filter criteria and optional search text.
134
+ * Search text is applied as Name like %query%. Multiple conditions are combined with and.
135
+ *
136
+ * @param criteria - Field filters (fieldPath, operator, values).
137
+ * @param searchQuery - Optional; adds Name like %searchQuery% when provided.
138
+ */
139
+ export function buildWhereFromCriteria(
140
+ criteria: FilterCriteria[],
141
+ searchQuery?: string,
142
+ ): Record<string, unknown> | null {
143
+ const conditions: Record<string, unknown>[] = [];
144
+
145
+ if (searchQuery && searchQuery.trim()) {
146
+ const term = `%${searchQuery.trim()}%`;
147
+ conditions.push({ Name: { like: term } });
148
+ }
149
+
150
+ for (const c of criteria) {
151
+ if (!c.fieldPath || !c.operator || !c.values?.length) continue;
152
+ const value = c.values[0];
153
+ const op = c.operator;
154
+ const parts = c.fieldPath.split(".");
155
+ const fieldClause = { [op]: value };
156
+ if (parts.length === 1) {
157
+ conditions.push({ [parts[0]]: fieldClause });
158
+ } else {
159
+ let nested: Record<string, unknown> = { [parts[parts.length - 1]]: fieldClause };
160
+ for (let i = parts.length - 2; i >= 0; i--) {
161
+ nested = { [parts[i]]: nested };
162
+ }
163
+ conditions.push(nested);
164
+ }
165
+ }
166
+
167
+ if (conditions.length === 0) return null;
168
+ if (conditions.length === 1) return conditions[0] as Record<string, unknown>;
169
+ return { and: conditions };
170
+ }
171
+
172
+ /**
173
+ * Parses sortBy string (e.g. "Name", "Name ASC", "AnnualRevenue DESC") into GraphQL orderBy shape.
174
+ * Default direction is ASC.
175
+ */
176
+ export function buildOrderByFromSort(
177
+ sortBy: string,
178
+ ): Record<string, { order: "ASC" | "DESC" }> | null {
179
+ const trimmed = (sortBy ?? "").trim();
180
+ if (!trimmed || trimmed.toLowerCase() === "relevance") return null;
181
+ const parts = trimmed.split(/\s+/);
182
+ const field = parts[0];
183
+ const dir = parts[1]?.toUpperCase() === "DESC" ? "DESC" : "ASC";
184
+ return { [field]: { order: dir } };
185
+ }
186
+
187
+ /** Variables for the GetRecords GraphQL operation. */
188
+ export interface RecordListGraphQLVariables {
189
+ first?: number;
190
+ after?: string | null;
191
+ where?: Record<string, unknown> | null;
192
+ orderBy?: Record<string, unknown> | null;
193
+ }
194
+
195
+ export interface RecordListGraphQLOptions {
196
+ objectApiName: string;
197
+ columns: Column[];
198
+ /** When set, fetches a single record by Id (first=1, where Id eq); used for detail view. */
199
+ recordId?: string | null;
200
+ first?: number;
201
+ after?: string | null;
202
+ filters?: FilterCriteria[];
203
+ orderBy?: Record<string, unknown> | null;
204
+ searchQuery?: string;
205
+ }
206
+
207
+ /**
208
+ * Builds the GraphQL query string for uiapi.query.{objectApiName}.
209
+ * Used for both list (pagination, where, orderBy) and single record (where Id eq, first 1).
210
+ *
211
+ * @param objectApiName - API name of the object (e.g. Account, Contact).
212
+ * @param columns - Field selection (becomes node selection via buildNodeSelection).
213
+ * @param options - Optional where and orderBy; when recordId is used, where is set to Id eq.
214
+ */
215
+ export function buildGetRecordsQuery(
216
+ objectApiName: string,
217
+ columns: Column[],
218
+ options?: { where?: Record<string, unknown> | null; orderBy?: Record<string, unknown> | null },
219
+ ): string {
220
+ const nodeSelection = buildNodeSelection(columns);
221
+ const hasWhere = options?.where != null && Object.keys(options.where).length > 0;
222
+ const hasOrderBy = options?.orderBy != null && Object.keys(options.orderBy).length > 0;
223
+
224
+ const filterType = `${objectApiName}_Filter`;
225
+ const orderByType = `${objectApiName}_OrderBy`;
226
+
227
+ const varDecls = [
228
+ "$first: Int",
229
+ "$after: String",
230
+ ...(hasWhere ? [`$where: ${filterType}`] : []),
231
+ ...(hasOrderBy ? [`$orderBy: ${orderByType}`] : []),
232
+ ];
233
+ const opArgs = [
234
+ "first: $first",
235
+ "after: $after",
236
+ ...(hasWhere ? ["where: $where"] : []),
237
+ ...(hasOrderBy ? ["orderBy: $orderBy"] : []),
238
+ ];
239
+
240
+ return `query GetRecords(${varDecls.join(", ")}) {
241
+ uiapi {
242
+ query {
243
+ ${objectApiName}(${opArgs.join(", ")}) {
244
+ edges {
245
+ node {
246
+ ${nodeSelection}
247
+ }
248
+ }
249
+ pageInfo {
250
+ hasNextPage
251
+ hasPreviousPage
252
+ endCursor
253
+ startCursor
254
+ },
255
+ totalCount,
256
+ pageResultCount
257
+ }
258
+ }
259
+ }
260
+ }`;
261
+ }
262
+
263
+ export interface RecordListGraphQLResult {
264
+ uiapi?: {
265
+ query?: {
266
+ [key: string]: {
267
+ edges?: Array<{ node?: Record<string, unknown> }>;
268
+ pageInfo?: {
269
+ hasNextPage?: boolean;
270
+ hasPreviousPage?: boolean;
271
+ endCursor?: string | null;
272
+ startCursor?: string | null;
273
+ };
274
+ };
275
+ };
276
+ };
277
+ }
278
+
279
+ /** GraphQL node shape for a single record (Id + field selections with value/nested). */
280
+ export type GraphQLRecordNode = Record<string, unknown>;
281
+
282
+ /**
283
+ * Fetches records for the given object via GraphQL (single query for both list and single record).
284
+ *
285
+ * - List: pass first, after, filters, orderBy, searchQuery.
286
+ * - Single record: pass recordId; first is set to 1 and where includes Id eq.
287
+ *
288
+ * @param options.objectApiName - API name of the object (e.g. Account, Contact).
289
+ * @param options.columns - Field selection (from filters-derived columns or layout-derived optionalFields).
290
+ * @param options.recordId - If set, fetches one record by Id (first=1, where Id eq).
291
+ * @param options.first - Page size (default 50; ignored when recordId is set).
292
+ * @param options.after - Cursor for next page.
293
+ * @param options.filters - Filter criteria (mapped to where).
294
+ * @param options.orderBy - GraphQL orderBy; use buildOrderByFromSort(sortBy) when needed.
295
+ * @param options.searchQuery - Text search (Name like %query% in where).
296
+ * @returns Connection result (edges, pageInfo); for recordId callers use edges[0].node.
297
+ */
298
+ export async function getRecordsGraphQL(
299
+ options: RecordListGraphQLOptions,
300
+ ): Promise<RecordListGraphQLResult> {
301
+ const {
302
+ objectApiName,
303
+ columns,
304
+ recordId,
305
+ first = DEFAULT_PAGE_SIZE,
306
+ after = null,
307
+ filters = [],
308
+ orderBy = null,
309
+ searchQuery,
310
+ } = options;
311
+
312
+ const listWhere = buildWhereFromCriteria(filters, searchQuery);
313
+ const where =
314
+ recordId != null && recordId !== ""
315
+ ? listWhere != null && Object.keys(listWhere).length > 0
316
+ ? { and: [{ Id: { eq: recordId } }, listWhere] }
317
+ : { Id: { eq: recordId } }
318
+ : listWhere;
319
+ const effectiveFirst = recordId != null && recordId !== "" ? 1 : first;
320
+ const hasWhere = where != null && Object.keys(where).length > 0;
321
+ const hasOrderBy = orderBy != null && Object.keys(orderBy).length > 0;
322
+
323
+ const query = buildGetRecordsQuery(objectApiName, columns, { where, orderBy });
324
+ const variables: Record<string, unknown> = {
325
+ first: effectiveFirst,
326
+ after: after ?? null,
327
+ ...(hasWhere && where ? { where } : {}),
328
+ ...(hasOrderBy && orderBy ? { orderBy } : {}),
329
+ };
330
+
331
+ const data = await getDataSDK();
332
+ const response = await data.graphql?.<RecordListGraphQLResult>(query, variables);
333
+
334
+ if (response?.errors?.length) {
335
+ const errorMessages = response.errors.map((e) => e.message).join("; ");
336
+ throw new Error(`GraphQL Error: ${errorMessages}`);
337
+ }
338
+
339
+ return response?.data ?? ({} as RecordListGraphQLResult);
340
+ }
341
+
342
+ /**
343
+ * Fetches a single record by Id. Uses the same GraphQL query as list (getRecordsGraphQL with recordId + first 1).
344
+ *
345
+ * @param objectApiName - API name of the object.
346
+ * @param recordId - Record Id.
347
+ * @param columns - Field selection (e.g. layout-derived optionalFields as Column[]).
348
+ * @returns The record node or null if not found.
349
+ */
350
+ export async function getRecordByIdGraphQL(
351
+ objectApiName: string,
352
+ recordId: string,
353
+ columns: Column[],
354
+ ): Promise<GraphQLRecordNode | null> {
355
+ const result = await getRecordsGraphQL({
356
+ objectApiName,
357
+ columns,
358
+ recordId,
359
+ first: 1,
360
+ });
361
+ const edges =
362
+ result?.uiapi?.query?.[objectApiName]?.edges ??
363
+ ([] as Array<{ node?: Record<string, unknown> }>);
364
+ return (edges[0]?.node ?? null) as GraphQLRecordNode | null;
365
+ }