@salesforce/webapp-template-app-react-sample-b2e-experimental 1.73.0 → 1.74.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 (117) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/objects/Maintenance_Request__c/Maintenance_Request__c.object-meta.xml +11 -1
  3. package/dist/force-app/main/default/objects/Maintenance_Worker__c/Maintenance_Worker__c.object-meta.xml +6 -1
  4. package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +6 -1
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +7 -5
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenanceWorkers.ts +60 -0
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationsTable.tsx +59 -62
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/FiltersFromApi.tsx +200 -0
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ListPageFilters.tsx +97 -0
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceTable.tsx +2 -1
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ObjectSelect.tsx +39 -0
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +6 -4
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/dashboard/GlobalSearchBar.tsx +125 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/FilterErrorAlert.tsx +15 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageErrorState.tsx +19 -0
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageLoadingState.tsx +18 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldRange.tsx +40 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldSelect.tsx +190 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldText.tsx +32 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/ListPageFilterRow.tsx +100 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageContainer.tsx +9 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageHeader.tsx +21 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/list/ListPageWithFilters.tsx +70 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/constants.ts +39 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/index.ts +19 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectDetailService.ts +125 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoGraphQLService.ts +194 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoService.ts +199 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/recordListGraphQLService.ts +364 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailFields.tsx +55 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailForm.tsx +146 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/Section.tsx +108 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/SectionRow.tsx +20 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/index.ts +6 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterField.tsx +54 -0
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterInput.tsx +55 -0
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/filters-form.tsx +114 -0
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/submit-button.tsx +47 -0
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchHeader.tsx +31 -0
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchPagination.tsx +144 -0
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultCard.tsx +136 -0
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
  57. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterInput.tsx +55 -0
  58. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterSelect.tsx +72 -0
  59. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/form.tsx +209 -0
  60. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/index.ts +22 -0
  61. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +65 -0
  62. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectSearchData.ts +395 -0
  63. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +156 -0
  64. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
  65. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/DetailPage.tsx +109 -0
  66. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/GlobalSearch.tsx +229 -0
  67. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/filters.ts +121 -0
  68. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/picklist.ts +32 -0
  69. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/index.ts +5 -0
  70. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/objectInfo/objectInfo.ts +166 -0
  71. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
  72. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/search/searchResults.ts +229 -0
  73. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/apiUtils.ts +125 -0
  74. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/cacheUtils.ts +76 -0
  75. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/debounce.ts +89 -0
  76. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldUtils.ts +354 -0
  77. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
  78. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/filterUtils.ts +32 -0
  79. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
  80. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formUtils.ts +142 -0
  81. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
  82. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +319 -0
  83. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
  84. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/index.ts +59 -0
  85. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
  86. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/linkUtils.ts +14 -0
  87. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/paginationUtils.ts +49 -0
  88. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/recordUtils.ts +159 -0
  89. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/sanitizationUtils.ts +49 -0
  90. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useAccumulatedListPages.ts +29 -0
  91. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useListPage.ts +167 -0
  92. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/index.ts +8 -4
  93. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationAdapter.ts +33 -0
  94. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationColumns.ts +28 -0
  95. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/constants.ts +24 -0
  96. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/fieldMappers.ts +71 -0
  97. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/filterUtils.ts +165 -0
  98. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/globalSearchConstants.ts +40 -0
  99. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listFilters.ts +152 -0
  100. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listPageConfig.ts +65 -0
  101. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceAdapter.ts +110 -0
  102. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceColumns.ts +24 -0
  103. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerAdapter.ts +29 -0
  104. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerColumns.ts +25 -0
  105. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/objectApiNames.ts +13 -0
  106. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyAdapter.ts +68 -0
  107. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyColumns.ts +17 -0
  108. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/routeConfig.ts +35 -0
  109. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +10 -0
  110. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Applications.tsx +47 -62
  111. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +130 -98
  112. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +74 -91
  113. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/MaintenanceWorkers.tsx +138 -0
  114. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +166 -85
  115. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +41 -2
  116. package/dist/package.json +1 -1
  117. package/package.json +5 -1
@@ -0,0 +1,319 @@
1
+ /**
2
+ * GraphQL objectInfos → REST-compatible object info and picklist.
3
+ *
4
+ * graphQLObjectInfoToObjectInfoResult / graphQLObjectInfosToBatchResponse: used by objectInfoService
5
+ * after getObjectInfosGraphQL. extractPicklistValuesFromGraphQLObjectInfo: used for getPicklistValues.
6
+ * Normalizes casing and adds synthetic compound fields (e.g. BillingAddress) for layout/dataType.
7
+ *
8
+ * @module utils/graphQLObjectInfoAdapter
9
+ */
10
+
11
+ import type {
12
+ ObjectInfoBatchResponse,
13
+ ObjectInfoResult,
14
+ Field,
15
+ RecordTypeInfo,
16
+ ThemeInfo,
17
+ } from "../types/objectInfo/objectInfo";
18
+ import type { PicklistValue } from "../types/filters/picklist";
19
+
20
+ type GraphQLNode = Record<string, unknown>;
21
+
22
+ function getStr(node: GraphQLNode, key: string): string {
23
+ const v = node[key] ?? node[key.charAt(0).toLowerCase() + key.slice(1)];
24
+ return typeof v === "string" ? v : "";
25
+ }
26
+
27
+ function getNum(node: GraphQLNode, key: string): number {
28
+ const v = node[key] ?? node[key.charAt(0).toLowerCase() + key.slice(1)];
29
+ return typeof v === "number" ? v : 0;
30
+ }
31
+
32
+ function getBool(node: GraphQLNode, key: string): boolean {
33
+ const v = node[key] ?? node[key.charAt(0).toLowerCase() + key.slice(1)];
34
+ return typeof v === "boolean" ? v : false;
35
+ }
36
+
37
+ function getArr<T>(node: GraphQLNode, key: string, map: (item: unknown) => T): T[] {
38
+ const v = node[key] ?? node[key.charAt(0).toLowerCase() + key.slice(1)];
39
+ if (!Array.isArray(v)) return [];
40
+ return v.map((item) => map(item));
41
+ }
42
+
43
+ function getObj(node: GraphQLNode, key: string): Record<string, unknown> {
44
+ const v = node[key] ?? node[key.charAt(0).toLowerCase() + key.slice(1)];
45
+ return v != null && typeof v === "object" && !Array.isArray(v)
46
+ ? (v as Record<string, unknown>)
47
+ : {};
48
+ }
49
+
50
+ /** GraphQL returns dependentFields as [DependentField]; REST expects Record. Normalize to object. */
51
+ function toDependentFieldsRecord(v: unknown): Record<string, unknown> {
52
+ if (v == null) return {};
53
+ if (Array.isArray(v)) return {};
54
+ return typeof v === "object" ? (v as Record<string, unknown>) : {};
55
+ }
56
+
57
+ /** Canonical dataType casing for formatters (Phone, FormattedAddress) and reference resolution (Reference). */
58
+ const DATA_TYPE_CANONICAL: Record<string, string> = {
59
+ phone: "Phone",
60
+ email: "Email",
61
+ url: "Url",
62
+ address: "Address",
63
+ reference: "Reference",
64
+ };
65
+
66
+ function normalizeDataType(raw: string): string {
67
+ if (!raw) return raw;
68
+ const lower = raw.toLowerCase();
69
+ return DATA_TYPE_CANONICAL[lower] ?? raw;
70
+ }
71
+
72
+ /** Normalize GraphQL Field to our Field type (REST-compatible). */
73
+ function mapField(fieldNode: unknown): [string, Field] {
74
+ const n = (fieldNode != null && typeof fieldNode === "object" ? fieldNode : {}) as GraphQLNode;
75
+ const apiName = getStr(n, "ApiName") || getStr(n, "apiName");
76
+ const label = getStr(n, "label");
77
+ const dataType = normalizeDataType(getStr(n, "dataType"));
78
+ const relationshipName = getStr(n, "relationshipName") || null;
79
+ const reference = getBool(n, "reference");
80
+ const compound = getBool(n, "compound");
81
+ const compoundFieldName = getStr(n, "compoundFieldName") || null;
82
+ const compoundComponentName = getStr(n, "compoundComponentName") || null;
83
+ const controllingFields = getArr(n, "controllingFields", String);
84
+ const controllerName = getStr(n, "controllerName") || null;
85
+ const referenceToInfos = getArr(n, "referenceToInfos", (item) => {
86
+ const r = (item != null && typeof item === "object" ? item : {}) as GraphQLNode;
87
+ return {
88
+ apiName: getStr(r, "apiName") || getStr(r, "ApiName"),
89
+ nameFields: getArr(r, "nameFields", String),
90
+ };
91
+ });
92
+ return [
93
+ apiName,
94
+ {
95
+ apiName,
96
+ label,
97
+ dataType,
98
+ relationshipName,
99
+ reference,
100
+ compound,
101
+ compoundFieldName,
102
+ compoundComponentName,
103
+ controllingFields,
104
+ controllerName,
105
+ referenceToInfos,
106
+ calculated: getBool(n, "calculated"),
107
+ createable: getBool(n, "createable"),
108
+ custom: getBool(n, "custom"),
109
+ defaultValue: n.defaultValue ?? null,
110
+ defaultedOnCreate: getBool(n, "defaultedOnCreate"),
111
+ digits: getNum(n, "digits"),
112
+ externalId: getBool(n, "externalId"),
113
+ extraTypeInfo: getStr(n, "extraTypeInfo") || null,
114
+ filterable: getBool(n, "filterable"),
115
+ filteredLookupInfo: null,
116
+ highScaleNumber: getBool(n, "highScaleNumber"),
117
+ htmlFormatted: getBool(n, "htmlFormatted"),
118
+ inlineHelpText: getStr(n, "inlineHelpText") || null,
119
+ length: getNum(n, "length"),
120
+ maskType: getStr(n, "maskType") || null,
121
+ nameField: getBool(n, "nameField"),
122
+ polymorphicForeignKey: getBool(n, "polymorphicForeignKey"),
123
+ precision: getNum(n, "precision"),
124
+ required: getBool(n, "required"),
125
+ scale: getNum(n, "scale"),
126
+ searchPrefilterable: getBool(n, "searchPrefilterable"),
127
+ sortable: getBool(n, "sortable"),
128
+ unique: getBool(n, "unique"),
129
+ updateable: getBool(n, "updateable"),
130
+ referenceTargetField: getStr(n, "referenceTargetField") || null,
131
+ } as Field,
132
+ ];
133
+ }
134
+
135
+ /** Normalize GraphQL recordTypeInfo to RecordTypeInfo. */
136
+ function mapRecordTypeInfo(rtNode: unknown): [string, RecordTypeInfo] {
137
+ const n = (rtNode != null && typeof rtNode === "object" ? rtNode : {}) as GraphQLNode;
138
+ const recordTypeId = getStr(n, "recordTypeId");
139
+ return [
140
+ recordTypeId,
141
+ {
142
+ recordTypeId,
143
+ name: getStr(n, "name"),
144
+ master: getBool(n, "master"),
145
+ available: getBool(n, "available"),
146
+ defaultRecordTypeMapping: getBool(n, "defaultRecordTypeMapping"),
147
+ } as RecordTypeInfo,
148
+ ];
149
+ }
150
+
151
+ function toFieldsMap(fieldsNode: unknown): Record<string, Field> {
152
+ if (fieldsNode == null) return {};
153
+ if (Array.isArray(fieldsNode)) {
154
+ return Object.fromEntries((fieldsNode as unknown[]).map(mapField).filter(([k]) => k));
155
+ }
156
+ if (typeof fieldsNode === "object" && !Array.isArray(fieldsNode)) {
157
+ const entries = Object.entries(fieldsNode as Record<string, unknown>).map(([, v]) =>
158
+ mapField(v),
159
+ );
160
+ return Object.fromEntries(entries.filter(([k]) => k));
161
+ }
162
+ return {};
163
+ }
164
+
165
+ /**
166
+ * Ensures compound parent fields (e.g. BillingAddress, ShippingAddress) exist in the fields map
167
+ * with dataType "Address" so layout transform can set item.dataType for FormattedAddress.
168
+ * GraphQL may only return component fields (BillingStreet, etc.) with compoundFieldName.
169
+ */
170
+ function ensureCompoundParentFields(fields: Record<string, Field>): Record<string, Field> {
171
+ const result = { ...fields };
172
+ for (const field of Object.values(fields)) {
173
+ const compoundName = field.compoundFieldName;
174
+ if (!compoundName || result[compoundName] != null) continue;
175
+ const dataType =
176
+ compoundName.endsWith("Address") || compoundName.endsWith("address") ? "Address" : "String";
177
+ result[compoundName] = {
178
+ ...field,
179
+ apiName: compoundName,
180
+ label: compoundName,
181
+ dataType,
182
+ compound: true,
183
+ compoundFieldName: null,
184
+ compoundComponentName: null,
185
+ } as Field;
186
+ }
187
+ return result;
188
+ }
189
+
190
+ function toRecordTypeInfosMap(rtNode: unknown): Record<string, RecordTypeInfo> {
191
+ if (rtNode == null) return {};
192
+ if (Array.isArray(rtNode)) {
193
+ return Object.fromEntries((rtNode as unknown[]).map(mapRecordTypeInfo).filter(([k]) => k));
194
+ }
195
+ if (typeof rtNode === "object" && !Array.isArray(rtNode)) {
196
+ const entries = Object.entries(rtNode as Record<string, unknown>).map(([, v]) =>
197
+ mapRecordTypeInfo(v),
198
+ );
199
+ return Object.fromEntries(entries.filter(([k]) => k));
200
+ }
201
+ return {};
202
+ }
203
+
204
+ /** Map a single GraphQL ObjectInfo node to ObjectInfoResult (REST-compatible). */
205
+ export function graphQLObjectInfoToObjectInfoResult(node: GraphQLNode): ObjectInfoResult {
206
+ const themeInfoNode = getObj(node, "themeInfo");
207
+ const themeInfo: ThemeInfo = {
208
+ color: getStr(themeInfoNode, "color"),
209
+ iconUrl: getStr(themeInfoNode, "iconUrl"),
210
+ };
211
+
212
+ const childRelationships = getArr(node, "childRelationships", (item) => {
213
+ const c = (item != null && typeof item === "object" ? item : {}) as GraphQLNode;
214
+ return {
215
+ childObjectApiName: getStr(c, "childObjectApiName"),
216
+ fieldName: getStr(c, "fieldName"),
217
+ junctionIdListNames: getArr(c, "junctionIdListNames", String),
218
+ junctionReferenceTo: getArr(c, "junctionReferenceTo", String),
219
+ relationshipName: getStr(c, "relationshipName"),
220
+ };
221
+ });
222
+
223
+ const apiName = getStr(node, "ApiName") || getStr(node, "apiName");
224
+ const fieldsNode = node.fields ?? node.Fields;
225
+ const recordTypeInfosNode = node.recordTypeInfos ?? node.RecordTypeInfos;
226
+
227
+ return {
228
+ apiName,
229
+ label: getStr(node, "label"),
230
+ labelPlural: getStr(node, "labelPlural"),
231
+ nameFields: getArr(node, "nameFields", String),
232
+ defaultRecordTypeId: getStr(node, "defaultRecordTypeId") || "012000000000000AAA",
233
+ keyPrefix: getStr(node, "keyPrefix"),
234
+ layoutable: getBool(node, "layoutable"),
235
+ queryable: getBool(node, "queryable"),
236
+ searchable: getBool(node, "searchable"),
237
+ updateable: getBool(node, "updateable"),
238
+ deletable: getBool(node, "deletable"),
239
+ createable: getBool(node, "createable"),
240
+ custom: getBool(node, "custom"),
241
+ mruEnabled: getBool(node, "mruEnabled"),
242
+ feedEnabled: getBool(node, "feedEnabled"),
243
+ fields: ensureCompoundParentFields(toFieldsMap(fieldsNode)),
244
+ recordTypeInfos: toRecordTypeInfosMap(recordTypeInfosNode),
245
+ themeInfo,
246
+ childRelationships,
247
+ associateEntityType: getStr(node, "associateEntityType") || null,
248
+ associateParentEntity: getStr(node, "associateParentEntity") || null,
249
+ compactLayoutable: getBool(node, "compactLayoutable"),
250
+ searchLayoutable: getBool(node, "searchLayoutable"),
251
+ dependentFields: toDependentFieldsRecord(node.dependentFields ?? node.DependentFields),
252
+ eTag: getStr(node, "eTag"),
253
+ } as ObjectInfoResult;
254
+ }
255
+
256
+ /** Convert GraphQL objectInfos array to ObjectInfoBatchResponse (REST shape). */
257
+ export function graphQLObjectInfosToBatchResponse(
258
+ nodes: GraphQLNode[],
259
+ _requestedApiNames: string[],
260
+ ): ObjectInfoBatchResponse {
261
+ const results = nodes.map((node) => ({
262
+ result: graphQLObjectInfoToObjectInfoResult(node),
263
+ statusCode: 200,
264
+ }));
265
+ return { results } as ObjectInfoBatchResponse;
266
+ }
267
+
268
+ /**
269
+ * Extract picklist values for a field from a GraphQL ObjectInfo node (raw response).
270
+ * Uses picklistValuesByRecordTypeIDs; prefers the given recordTypeId or first available.
271
+ */
272
+ export function extractPicklistValuesFromGraphQLObjectInfo(
273
+ node: GraphQLNode,
274
+ fieldName: string,
275
+ recordTypeId?: string,
276
+ ): PicklistValue[] {
277
+ const fieldsNode = node.fields ?? node.Fields;
278
+ if (fieldsNode == null || typeof fieldsNode !== "object") return [];
279
+ const fieldMap = Array.isArray(fieldsNode)
280
+ ? Object.fromEntries(
281
+ (fieldsNode as unknown[]).map((f: unknown) => {
282
+ const n = (f != null && typeof f === "object" ? f : {}) as GraphQLNode;
283
+ const k = getStr(n, "ApiName") || getStr(n, "apiName");
284
+ return [k, n];
285
+ }),
286
+ )
287
+ : (fieldsNode as Record<string, GraphQLNode>);
288
+ const field = fieldMap[fieldName];
289
+ if (!field || typeof field !== "object") return [];
290
+ const picklistData = field.picklistValuesByRecordTypeIDs ?? field.PicklistValuesByRecordTypeIDs;
291
+ if (!picklistData || !Array.isArray(picklistData)) return [];
292
+ const rtId = recordTypeId ?? "012000000000000AAA";
293
+ const arr = picklistData as Array<Record<string, unknown>>;
294
+ const byRecordType = arr.find((p) => (p.recordTypeID ?? p.recordTypeId) === rtId);
295
+ if (!byRecordType) {
296
+ const first = arr[0];
297
+ if (!first) return [];
298
+ return mapPicklistValues(first);
299
+ }
300
+ return mapPicklistValues(byRecordType);
301
+ }
302
+
303
+ function mapPicklistValues(byRecordType: Record<string, unknown>): PicklistValue[] {
304
+ const raw = byRecordType.picklistValues;
305
+ if (!Array.isArray(raw)) return [];
306
+ return raw.map((item: unknown) => {
307
+ const n = (item != null && typeof item === "object" ? item : {}) as Record<string, unknown>;
308
+ return {
309
+ label: typeof n.label === "string" ? n.label : "",
310
+ value: typeof n.value === "string" ? n.value : "",
311
+ validFor: Array.isArray(n.validFor) ? n.validFor : undefined,
312
+ attributes: (n.attributes != null && typeof n.attributes === "object"
313
+ ? n.attributes
314
+ : undefined) as Record<string, unknown> | undefined,
315
+ defaultValue: typeof n.defaultValue === "boolean" ? n.defaultValue : undefined,
316
+ active: typeof n.active === "boolean" ? n.active : undefined,
317
+ } as PicklistValue;
318
+ });
319
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Adapts GraphQL UI API node shape to SearchResultRecordData so existing list UI
3
+ * (SearchResultCard, ResultCardFields, getNestedFieldValue) works unchanged.
4
+ *
5
+ * GraphQL node: { Id, Name: { value }, Owner: { Alias: { value } }, ... }
6
+ * SearchResultRecordData: { id, fields: Record<string, FieldValue>, apiName, ... }
7
+ * FieldValue: { displayValue, value }
8
+ */
9
+
10
+ import type { FieldValue, SearchResultRecordData } from "../types/search/searchResults";
11
+
12
+ function isValueLeaf(obj: unknown): obj is { value: string | number | boolean | null } {
13
+ return (
14
+ typeof obj === "object" &&
15
+ obj !== null &&
16
+ "value" in obj &&
17
+ (Object.keys(obj).length === 1 || (Object.keys(obj).length === 2 && "displayValue" in obj))
18
+ );
19
+ }
20
+
21
+ function graphQLValueToFieldValue(val: unknown): FieldValue {
22
+ if (val === null || val === undefined) {
23
+ return { displayValue: null, value: null };
24
+ }
25
+ if (isValueLeaf(val)) {
26
+ const v = val.value;
27
+ const display =
28
+ typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? v : null;
29
+ return { displayValue: display as string | null, value: v };
30
+ }
31
+ if (typeof val === "object" && val !== null && !Array.isArray(val)) {
32
+ const nested = graphQLNodeToFields(val as Record<string, unknown>);
33
+ const firstFv = nested && Object.values(nested)[0];
34
+ const display =
35
+ firstFv && typeof (firstFv as FieldValue).value !== "object"
36
+ ? ((firstFv as FieldValue).value as string | null)
37
+ : null;
38
+ return { displayValue: display, value: { fields: nested ?? {} } };
39
+ }
40
+ return { displayValue: null, value: val };
41
+ }
42
+
43
+ function graphQLNodeToFields(node: Record<string, unknown>): Record<string, FieldValue> {
44
+ const fields: Record<string, FieldValue> = {};
45
+ for (const [key, val] of Object.entries(node)) {
46
+ if (key === "Id" || val === undefined) continue;
47
+ fields[key] = graphQLValueToFieldValue(val);
48
+ }
49
+ return fields;
50
+ }
51
+
52
+ /**
53
+ * Converts a GraphQL connection node (from getRecordsGraphQL) to SearchResultRecordData
54
+ * so it can be passed to SearchResultCard and other components that expect the keyword-search record shape.
55
+ */
56
+ export function graphQLNodeToSearchResultRecordData(
57
+ node: Record<string, unknown> | undefined,
58
+ objectApiName: string,
59
+ ): SearchResultRecordData {
60
+ if (!node || typeof node !== "object") {
61
+ return {
62
+ id: "",
63
+ apiName: objectApiName,
64
+ childRelationships: {},
65
+ eTag: "",
66
+ fields: {},
67
+ lastModifiedById: null,
68
+ lastModifiedDate: null,
69
+ recordTypeId: null,
70
+ recordTypeInfo: null,
71
+ systemModstamp: null,
72
+ weakEtag: 0,
73
+ };
74
+ }
75
+ const id = (node.Id as string) ?? "";
76
+ const fields = graphQLNodeToFields(node);
77
+ return {
78
+ id,
79
+ apiName: objectApiName,
80
+ childRelationships: {},
81
+ eTag: "",
82
+ fields,
83
+ lastModifiedById: null,
84
+ lastModifiedDate: null,
85
+ recordTypeId: null,
86
+ recordTypeInfo: null,
87
+ systemModstamp: null,
88
+ weakEtag: 0,
89
+ };
90
+ }
@@ -0,0 +1,59 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Utilities
3
+ // ---------------------------------------------------------------------------
4
+ export { fetchAndValidate, safeEncodePath } from "./apiUtils";
5
+ export type { FetchAndValidateOptions } from "./apiUtils";
6
+ export { createFiltersKey } from "./cacheUtils";
7
+ export { calculateFieldsToFetch, getSafeKey, isValidSalesforceId } from "./recordUtils";
8
+ export {
9
+ getNestedFieldValue,
10
+ getRecordDisplayName,
11
+ toRecordDisplayNameMetadata,
12
+ getDisplayValueForLayoutItem,
13
+ getDisplayValueForDetailField,
14
+ formatAddressFromConstituents,
15
+ formatAddressDisplay,
16
+ formatDateTimeForDisplay,
17
+ isAddressConstituents,
18
+ getAddressPartsFromConstituents,
19
+ extractFieldValue,
20
+ } from "./fieldUtils";
21
+ export type {
22
+ AddressParts,
23
+ LayoutItemDisplayResult,
24
+ RecordDisplayNameMetadata,
25
+ } from "./fieldUtils";
26
+ export { calculateFormData } from "./formDataTransformUtils";
27
+ export type { ObjectInfoMetadata } from "./formDataTransformUtils";
28
+ export { getTransformedSections } from "./layoutTransformUtils";
29
+ export type {
30
+ ObjectInfo,
31
+ ObjectInfoField,
32
+ PicklistOption,
33
+ LayoutTransformContext,
34
+ TransformedLayoutItem,
35
+ TransformedLayoutRow,
36
+ TransformedSection,
37
+ } from "./layoutTransformUtils";
38
+ export { parseFilterValue } from "./filterUtils";
39
+ export { sanitizeFilterValue } from "./sanitizationUtils";
40
+ export { getFormValueByPath, getUniqueErrors, validateRangeValues, isFormError } from "./formUtils";
41
+ export type { FormError } from "./formUtils";
42
+ export {
43
+ PAGE_SIZE_OPTIONS,
44
+ VALID_PAGE_SIZES,
45
+ getValidPageSize,
46
+ isValidPageSize,
47
+ } from "./paginationUtils";
48
+ export { isAllowedLinkUrl, ALLOWED_LINK_PROTOCOLS } from "./linkUtils";
49
+ export { graphQLNodeToSearchResultRecordData } from "./graphQLRecordAdapter";
50
+ export {
51
+ getGraphQLNodeValue,
52
+ getDisplayValueForLayoutItemFromNode,
53
+ getDisplayValueForDetailFieldFromNode,
54
+ getGraphQLRecordDisplayName,
55
+ } from "./graphQLNodeFieldUtils";
56
+ export type {
57
+ GraphQLRecordDisplayNameMetadata,
58
+ ObjectMetadataForDisplay,
59
+ } from "./graphQLNodeFieldUtils";
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Transforms Layout API sections into a structure for the detail form: section → rows → items.
3
+ * Uses layout response (sections, layoutRows, layoutItems, layoutComponents) and optional
4
+ * object info for compound field names and dataType. Section merge when useHeading === false.
5
+ */
6
+
7
+ import type { LayoutSection, LayoutRow, LayoutItem } from "../types/recordDetail/recordDetail";
8
+
9
+ const HIDE_EMPTY_SECTIONS = true;
10
+ const EMPTY_OPTIONS: PicklistOption[] = [];
11
+
12
+ export interface ObjectInfoField {
13
+ compoundFieldName?: string;
14
+ dataType?: string;
15
+ }
16
+
17
+ export interface ObjectInfo {
18
+ apiName?: string;
19
+ fields?: Record<string, ObjectInfoField>;
20
+ }
21
+
22
+ /** Picklist/lookup options for a field (e.g. [{ label, value }]). */
23
+ export type PicklistOption = {
24
+ label: string | null;
25
+ value: string | number | boolean;
26
+ validFor?: unknown[];
27
+ };
28
+
29
+ export interface LayoutTransformContext {
30
+ recordId: string;
31
+ objectInfo?: ObjectInfo | null;
32
+ lookupRecords?: Record<string, PicklistOption[] | null> | null;
33
+ getSectionCollapsedState: (sectionId: string) => boolean;
34
+ calculatePicklistValues?: (itemApiName: string, item: LayoutItem) => PicklistOption[] | null;
35
+ formOverrides?: { fieldVariant?: string } | null;
36
+ }
37
+
38
+ export interface TransformedLayoutItem {
39
+ key: string;
40
+ isField: boolean;
41
+ label?: string;
42
+ required?: boolean;
43
+ readOnly?: boolean;
44
+ apiName?: string;
45
+ contextName?: string;
46
+ options?: PicklistOption[];
47
+ variant?: string;
48
+ dataType?: string;
49
+ layoutComponentApiNames?: string[];
50
+ }
51
+
52
+ /** Single row in a section. */
53
+ export interface TransformedLayoutRow {
54
+ key: string;
55
+ layoutItems: TransformedLayoutItem[];
56
+ }
57
+
58
+ /** Section ready for Section/SectionRow rendering. */
59
+ export interface TransformedSection {
60
+ id: string;
61
+ key: string;
62
+ heading: string;
63
+ useHeading: boolean;
64
+ collapsible: boolean;
65
+ collapsed: boolean;
66
+ layoutRows: TransformedLayoutRow[];
67
+ }
68
+
69
+ export function createSectionKey(index: number): string {
70
+ return "section-" + index;
71
+ }
72
+
73
+ export function getTransformedSections(
74
+ sections: LayoutSection[],
75
+ transformContext: LayoutTransformContext,
76
+ ): TransformedSection[] {
77
+ const calculatedSections: TransformedSection[] = [];
78
+ let previousSection: TransformedSection | null = null;
79
+
80
+ sections.forEach((section, index) => {
81
+ if (previousSection !== null && section.useHeading === false) {
82
+ const sectionKey = createSectionKey(index);
83
+ const appendedRows = section.layoutRows
84
+ .map((row, i) => rowTransform(row, i, sectionKey, transformContext))
85
+ .filter((r): r is TransformedLayoutRow => r !== null);
86
+ previousSection.layoutRows.push(...appendedRows);
87
+ return;
88
+ }
89
+
90
+ const newSection = sectionTransform(section, index, transformContext);
91
+ if (newSection) {
92
+ calculatedSections.push(newSection);
93
+ previousSection = newSection;
94
+ }
95
+ });
96
+
97
+ return calculatedSections;
98
+ }
99
+
100
+ export function sectionTransform(
101
+ section: LayoutSection,
102
+ index: number,
103
+ transformContext: LayoutTransformContext,
104
+ ): TransformedSection | null {
105
+ const { getSectionCollapsedState } = transformContext;
106
+ const sectionKey = createSectionKey(index);
107
+ const layoutRows = section.layoutRows
108
+ .map((row, i) => rowTransform(row, i, sectionKey, transformContext))
109
+ .filter((r): r is TransformedLayoutRow => r !== null);
110
+
111
+ if (layoutRows.length === 0 && HIDE_EMPTY_SECTIONS) {
112
+ return null;
113
+ }
114
+
115
+ return {
116
+ key: sectionKey,
117
+ collapsible: section.collapsible,
118
+ collapsed: getSectionCollapsedState(section.id),
119
+ useHeading: section.useHeading,
120
+ heading: section.heading,
121
+ id: section.id,
122
+ layoutRows,
123
+ };
124
+ }
125
+
126
+ export function rowTransform(
127
+ row: LayoutRow,
128
+ index: number,
129
+ sectionKey: string,
130
+ transformContext: LayoutTransformContext,
131
+ ): TransformedLayoutRow | null {
132
+ const layoutItems = row.layoutItems.map((item, i) => transformItem(item, i, transformContext));
133
+
134
+ const allItemsHaveNoComponents = layoutItems.every((item) => !item.apiName || !item.isField);
135
+ if (allItemsHaveNoComponents) {
136
+ return null;
137
+ }
138
+ return {
139
+ key: sectionKey + "-" + index,
140
+ layoutItems,
141
+ };
142
+ }
143
+
144
+ export function transformItem(
145
+ item: LayoutItem,
146
+ index: number,
147
+ transformContext: LayoutTransformContext,
148
+ ): TransformedLayoutItem {
149
+ const { recordId, objectInfo, lookupRecords, calculatePicklistValues, formOverrides } =
150
+ transformContext;
151
+
152
+ let itemApiName: string | undefined;
153
+ let itemComponentType: string | undefined;
154
+
155
+ if (item.layoutComponents.length >= 1) {
156
+ const itemComponent = item.layoutComponents[0];
157
+ itemComponentType = itemComponent.componentType;
158
+ const componentApiName = itemComponent.apiName;
159
+ const topLevelCompoundName =
160
+ item.layoutComponents.length > 1 &&
161
+ componentApiName &&
162
+ objectInfo?.fields?.[componentApiName]?.compoundFieldName;
163
+ if (topLevelCompoundName) {
164
+ itemApiName = topLevelCompoundName;
165
+ } else {
166
+ itemApiName = componentApiName ?? undefined;
167
+ }
168
+ }
169
+
170
+ const lookupOptions =
171
+ itemApiName != null && lookupRecords?.[itemApiName] != null ? lookupRecords[itemApiName] : null;
172
+
173
+ const isFieldType = itemComponentType === "Field";
174
+
175
+ const options: PicklistOption[] =
176
+ lookupOptions ??
177
+ (itemApiName ? (calculatePicklistValues?.(itemApiName, item) ?? null) : null) ??
178
+ EMPTY_OPTIONS;
179
+
180
+ const fieldMeta = itemApiName ? objectInfo?.fields?.[itemApiName] : undefined;
181
+ const layoutComponentApiNames = item.layoutComponents
182
+ .filter((c) => c.componentType === "Field" && c.apiName != null)
183
+ .map((c) => c.apiName as string);
184
+
185
+ let newItem: TransformedLayoutItem = {
186
+ key: "item-" + index,
187
+ apiName: itemApiName,
188
+ contextName: recordId,
189
+ label: item.label,
190
+ required: item.required,
191
+ variant: formOverrides?.fieldVariant ?? "label-stacked",
192
+ readOnly: !item.editableForUpdate,
193
+ isField: isFieldType,
194
+ options,
195
+ dataType: fieldMeta?.dataType,
196
+ layoutComponentApiNames:
197
+ layoutComponentApiNames.length > 0 ? layoutComponentApiNames : undefined,
198
+ };
199
+
200
+ if (objectInfo?.apiName?.endsWith("__kav")) {
201
+ newItem = { ...newItem, readOnly: true };
202
+ }
203
+
204
+ if (newItem.required === true && newItem.readOnly === true) {
205
+ newItem = { ...newItem, required: false };
206
+ }
207
+
208
+ return newItem;
209
+ }
210
+
211
+ export function layoutReducer<T>(
212
+ sections: TransformedSection[],
213
+ reducer: (
214
+ acc: T,
215
+ ctx: {
216
+ section: TransformedSection;
217
+ layoutRow: TransformedLayoutRow;
218
+ layoutItem: TransformedLayoutItem;
219
+ },
220
+ ) => T,
221
+ initialValue: T,
222
+ ): T {
223
+ let accumulator = initialValue;
224
+ sections.forEach((section) =>
225
+ section.layoutRows.forEach((layoutRow) =>
226
+ layoutRow.layoutItems.forEach((layoutItem) => {
227
+ accumulator = reducer(accumulator, {
228
+ section,
229
+ layoutRow,
230
+ layoutItem,
231
+ });
232
+ }),
233
+ ),
234
+ );
235
+ return accumulator;
236
+ }