@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,354 @@
1
+ /**
2
+ * Field value extraction and formatting for Salesforce UI API record shapes.
3
+ * Handles primitives, nested paths (e.g. Owner.Alias), reference/relationship display,
4
+ * address and modstamp compound formatting, and layout-item value clubbing.
5
+ */
6
+
7
+ import type { FieldValue, ComplexFieldValue } from "../types/search/searchResults";
8
+
9
+ /** Fallback field names for reference/relationship display when object info nameFields are not available. */
10
+ const DISPLAY_FIELD_CANDIDATES = [
11
+ "Name",
12
+ "CaseNumber",
13
+ "Subject",
14
+ "Title",
15
+ "DeveloperName",
16
+ "ContractNumber",
17
+ ] as const;
18
+
19
+ function isDefined(val: unknown): val is string | number | boolean {
20
+ return val !== null && val !== undefined;
21
+ }
22
+
23
+ function isComplexValue(val: unknown): val is ComplexFieldValue {
24
+ return typeof val === "object" && val !== null && "fields" in val;
25
+ }
26
+
27
+ function extractComplexValue(complex: ComplexFieldValue): string | null {
28
+ const fields = complex.fields;
29
+ if (!fields) return null;
30
+
31
+ for (const fieldName of DISPLAY_FIELD_CANDIDATES) {
32
+ const field = fields[fieldName];
33
+ if (field) {
34
+ if (isDefined(field.displayValue)) {
35
+ return field.displayValue;
36
+ }
37
+ if (isDefined(field.value)) {
38
+ return field.value.toString();
39
+ }
40
+ }
41
+ }
42
+
43
+ return null;
44
+ }
45
+
46
+ function extractFieldPrimitive(field: FieldValue): string | number | boolean | null {
47
+ if (isDefined(field.displayValue)) {
48
+ return field.displayValue;
49
+ }
50
+
51
+ if (isComplexValue(field.value)) {
52
+ const extracted = extractComplexValue(field.value);
53
+ return extracted !== null ? extracted : null;
54
+ }
55
+
56
+ if (isDefined(field.value)) {
57
+ return field.value;
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ export type FieldValueWithRelationship = FieldValue & {
64
+ relationshipField?: FieldValue | null;
65
+ constituents?: Record<string, FieldValue>;
66
+ };
67
+
68
+ /** Id + Date pairs for Created By / Last Modified By (UI API modstamp convention). */
69
+ const MODSTAMP_FIELDS = [
70
+ { idFieldName: "CreatedById", dateFieldName: "CreatedDate" },
71
+ { idFieldName: "LastModifiedById", dateFieldName: "LastModifiedDate" },
72
+ ] as const;
73
+
74
+ function isModstampConstituents(constituents: Record<string, FieldValue>): boolean {
75
+ return MODSTAMP_FIELDS.some(
76
+ ({ idFieldName, dateFieldName }) =>
77
+ idFieldName in constituents && dateFieldName in constituents,
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Formats an ISO 8601 date-time string to the user's locale and timezone.
83
+ * Uses the browser's default locale and local timezone.
84
+ */
85
+ export function formatDateTimeForDisplay(isoOrDateString: string): string {
86
+ if (!isoOrDateString || typeof isoOrDateString !== "string") return isoOrDateString;
87
+ const trimmed = isoOrDateString.trim();
88
+ if (!trimmed) return isoOrDateString;
89
+ const date = new Date(trimmed);
90
+ if (Number.isNaN(date.getTime())) return isoOrDateString;
91
+ try {
92
+ return new Intl.DateTimeFormat(undefined, {
93
+ dateStyle: "medium",
94
+ timeStyle: "short",
95
+ }).format(date);
96
+ } catch {
97
+ return isoOrDateString;
98
+ }
99
+ }
100
+
101
+ function formatModstampDisplay(constituents: Record<string, FieldValue>): string {
102
+ for (const { idFieldName, dateFieldName } of MODSTAMP_FIELDS) {
103
+ const idField = constituents[idFieldName];
104
+ const dateField = constituents[dateFieldName];
105
+ if (!idField || !dateField) continue;
106
+ const idWithRel = idField as FieldValueWithRelationship;
107
+ const name =
108
+ idWithRel.relationshipField != null
109
+ ? getPrimitiveString(idWithRel.relationshipField)
110
+ : getPrimitiveString(idField);
111
+ const dateRaw = getPrimitiveString(dateField);
112
+ const date = dateRaw ? formatDateTimeForDisplay(dateRaw) : "";
113
+ const parts = [name, date].filter(Boolean);
114
+ if (parts.length) return parts.join(" ");
115
+ }
116
+ return "";
117
+ }
118
+
119
+ const ADDRESS_STREET_SUFFIXES = ["Street"];
120
+ const ADDRESS_CITY_SUFFIXES = ["City"];
121
+ const ADDRESS_STATE_SUFFIXES = ["State", "StateCode"];
122
+ const ADDRESS_POSTAL_SUFFIXES = ["PostalCode"];
123
+ const ADDRESS_COUNTRY_SUFFIXES = ["Country", "CountryCode"];
124
+
125
+ function getPrimitiveString(fv: FieldValue | undefined): string {
126
+ if (!fv) return "";
127
+ const p = extractFieldPrimitive(fv);
128
+ return ((p as string) || "").trim();
129
+ }
130
+
131
+ function fieldNameEndsWithOneOf(name: string, suffixes: string[]): boolean {
132
+ return suffixes.some(
133
+ (s) =>
134
+ name === s || name.endsWith(s) || (name.endsWith("__c") && name.slice(0, -3).endsWith(s)),
135
+ );
136
+ }
137
+
138
+ function findAddressPartKey(keys: string[], suffixes: string[]): string | undefined {
139
+ return keys.find((k) => fieldNameEndsWithOneOf(k, suffixes));
140
+ }
141
+
142
+ export interface AddressParts {
143
+ street: string;
144
+ city: string;
145
+ state: string;
146
+ postalCode: string;
147
+ country: string;
148
+ }
149
+
150
+ export function getAddressPartsFromConstituents(
151
+ constituents: Record<string, FieldValue>,
152
+ ): AddressParts {
153
+ const keys = Object.keys(constituents);
154
+ const streetKey = findAddressPartKey(keys, ADDRESS_STREET_SUFFIXES);
155
+ const cityKey = findAddressPartKey(keys, ADDRESS_CITY_SUFFIXES);
156
+ const stateKey = findAddressPartKey(keys, ADDRESS_STATE_SUFFIXES);
157
+ const postalKey = findAddressPartKey(keys, ADDRESS_POSTAL_SUFFIXES);
158
+ const countryKey = findAddressPartKey(keys, ADDRESS_COUNTRY_SUFFIXES);
159
+ return {
160
+ street: streetKey ? getPrimitiveString(constituents[streetKey]) : "",
161
+ city: cityKey ? getPrimitiveString(constituents[cityKey]) : "",
162
+ state: stateKey ? getPrimitiveString(constituents[stateKey]) : "",
163
+ postalCode: postalKey ? getPrimitiveString(constituents[postalKey]) : "",
164
+ country: countryKey ? getPrimitiveString(constituents[countryKey]) : "",
165
+ };
166
+ }
167
+
168
+ export function isAddressConstituents(constituents: Record<string, FieldValue>): boolean {
169
+ const keys = Object.keys(constituents);
170
+ const hasStreet = !!findAddressPartKey(keys, ADDRESS_STREET_SUFFIXES);
171
+ const hasCity = !!findAddressPartKey(keys, ADDRESS_CITY_SUFFIXES);
172
+ const hasState = !!findAddressPartKey(keys, ADDRESS_STATE_SUFFIXES);
173
+ const hasCountry = !!findAddressPartKey(keys, ADDRESS_COUNTRY_SUFFIXES);
174
+ return hasStreet && (hasCity || hasState || hasCountry);
175
+ }
176
+
177
+ export function formatAddressDisplay(parts: AddressParts): string {
178
+ const { street, city, state, postalCode, country } = parts;
179
+ const statePostal = [state, postalCode].filter(Boolean).join(" ");
180
+ const line2 = city ? (statePostal ? `${city}, ${statePostal}` : city) : statePostal;
181
+ const lines = [street, line2, country].filter(Boolean);
182
+ return lines.join("\n").trim();
183
+ }
184
+
185
+ export function formatAddressFromConstituents(constituents: Record<string, FieldValue>): string {
186
+ const parts = getAddressPartsFromConstituents(constituents);
187
+ return formatAddressDisplay(parts);
188
+ }
189
+
190
+ function isModstampApiNames(apiNames: string[]): boolean {
191
+ return MODSTAMP_FIELDS.some(
192
+ ({ idFieldName, dateFieldName }) =>
193
+ apiNames.includes(idFieldName) && apiNames.includes(dateFieldName),
194
+ );
195
+ }
196
+
197
+ function isAddressApiNames(apiNames: string[]): boolean {
198
+ const hasStreet = apiNames.some((n) => fieldNameEndsWithOneOf(n, ADDRESS_STREET_SUFFIXES));
199
+ const hasCity = apiNames.some((n) => fieldNameEndsWithOneOf(n, ADDRESS_CITY_SUFFIXES));
200
+ const hasState = apiNames.some((n) => fieldNameEndsWithOneOf(n, ADDRESS_STATE_SUFFIXES));
201
+ const hasCountry = apiNames.some((n) => fieldNameEndsWithOneOf(n, ADDRESS_COUNTRY_SUFFIXES));
202
+ return hasStreet && (hasCity || hasState || hasCountry);
203
+ }
204
+
205
+ export interface LayoutItemDisplayResult {
206
+ value: string | number | boolean | null;
207
+ dataType?: string;
208
+ }
209
+
210
+ export function getDisplayValueForLayoutItem(
211
+ fields: Record<string, FieldValue> | undefined,
212
+ componentApiNames: string[],
213
+ ): LayoutItemDisplayResult {
214
+ if (!fields || componentApiNames.length === 0) {
215
+ return { value: null };
216
+ }
217
+ if (componentApiNames.length === 1) {
218
+ const value = getDisplayValueForDetailField(
219
+ fields[componentApiNames[0]] as FieldValueWithRelationship | undefined,
220
+ );
221
+ return { value };
222
+ }
223
+ const constituents: Record<string, FieldValue> = {};
224
+ for (const apiName of componentApiNames) {
225
+ if (fields[apiName] != null) constituents[apiName] = fields[apiName];
226
+ }
227
+ if (isModstampApiNames(componentApiNames)) {
228
+ const value = formatModstampDisplay(constituents);
229
+ return { value: value || null };
230
+ }
231
+ if (isAddressApiNames(componentApiNames)) {
232
+ const parts = getAddressPartsFromConstituents(constituents);
233
+ const value = formatAddressDisplay(parts);
234
+ return { value: value || null, dataType: "Address" };
235
+ }
236
+ const values = componentApiNames
237
+ .map((apiName) =>
238
+ getDisplayValueForDetailField(fields[apiName] as FieldValueWithRelationship | undefined),
239
+ )
240
+ .filter((v) => v !== null && v !== undefined && v !== "");
241
+ return { value: values.length > 0 ? values.join(", ") : null };
242
+ }
243
+
244
+ export function getDisplayValueForDetailField(
245
+ field: FieldValueWithRelationship | undefined,
246
+ ): string | number | boolean | null {
247
+ if (!field) return null;
248
+ const withExt = field as FieldValueWithRelationship;
249
+ if (withExt.relationshipField != null) {
250
+ const fromRel = extractFieldPrimitive(withExt.relationshipField);
251
+ if (fromRel !== null && fromRel !== undefined && fromRel !== "") {
252
+ return fromRel;
253
+ }
254
+ }
255
+ if (withExt.constituents != null) {
256
+ if (isModstampConstituents(withExt.constituents)) {
257
+ const formatted = formatModstampDisplay(withExt.constituents);
258
+ if (formatted) return formatted;
259
+ } else if (isAddressConstituents(withExt.constituents)) {
260
+ const formatted = formatAddressFromConstituents(withExt.constituents);
261
+ if (formatted) return formatted;
262
+ }
263
+ }
264
+ return extractFieldPrimitive(field);
265
+ }
266
+
267
+ export function getNestedFieldValue(
268
+ fields: Record<string, FieldValue> | undefined,
269
+ fieldPath: string,
270
+ ): string | number | boolean | null {
271
+ if (!fields || !fieldPath) {
272
+ return null;
273
+ }
274
+
275
+ const pathParts = fieldPath.split(".");
276
+ if (pathParts.length === 1) {
277
+ const field = fields[fieldPath];
278
+ if (!field) return null;
279
+
280
+ return extractFieldPrimitive(field);
281
+ }
282
+
283
+ let currentFields: Record<string, FieldValue> | undefined = fields;
284
+ for (let i = 0; i < pathParts.length - 1; i++) {
285
+ const part = pathParts[i];
286
+ if (!currentFields || !currentFields[part]) {
287
+ return null;
288
+ }
289
+
290
+ const field: FieldValue = currentFields[part];
291
+ if (isComplexValue(field.value)) {
292
+ currentFields = field.value.fields;
293
+ } else {
294
+ return null;
295
+ }
296
+ }
297
+
298
+ const finalFieldName = pathParts[pathParts.length - 1];
299
+ if (!currentFields || !currentFields[finalFieldName]) {
300
+ return null;
301
+ }
302
+
303
+ const finalField = currentFields[finalFieldName];
304
+ return extractFieldPrimitive(finalField);
305
+ }
306
+
307
+ /** Minimal metadata for record display name (nameFields from object info API). */
308
+ export type RecordDisplayNameMetadata = { nameFields?: string[] } | null;
309
+
310
+ /**
311
+ * Resolves a display name for a record: tries metadata.nameFields first, then
312
+ * DISPLAY_FIELD_CANDIDATES (Name, Subject, etc.), then record.id.
313
+ */
314
+ export function getRecordDisplayName(
315
+ record: { id: string; fields: Record<string, FieldValue> },
316
+ metadata?: RecordDisplayNameMetadata,
317
+ ): string {
318
+ const candidates = [...(metadata?.nameFields ?? []), ...DISPLAY_FIELD_CANDIDATES];
319
+ for (const fieldPath of candidates) {
320
+ const v = getNestedFieldValue(record.fields, fieldPath);
321
+ return v as string;
322
+ }
323
+ return record.id;
324
+ }
325
+
326
+ /** Adapts object info (e.g. ObjectInfoResult) to the shape expected by getRecordDisplayName. */
327
+ export function toRecordDisplayNameMetadata(obj: unknown): RecordDisplayNameMetadata {
328
+ if (obj != null && typeof obj === "object" && "nameFields" in obj) {
329
+ const nameFields = (obj as { nameFields?: string[] }).nameFields;
330
+ if (Array.isArray(nameFields)) return { nameFields };
331
+ }
332
+ return null;
333
+ }
334
+
335
+ export function extractFieldValue(
336
+ fieldValue: FieldValue | undefined,
337
+ useDisplayValue: boolean = false,
338
+ ): string {
339
+ if (!fieldValue) {
340
+ return "—";
341
+ }
342
+
343
+ if (useDisplayValue && isDefined(fieldValue.displayValue)) {
344
+ return fieldValue.displayValue;
345
+ }
346
+
347
+ const extracted = extractFieldPrimitive(fieldValue);
348
+
349
+ if (extracted !== null) {
350
+ return extracted as string;
351
+ }
352
+
353
+ return "—";
354
+ }
@@ -0,0 +1,67 @@
1
+ import type { FieldValue, ComplexFieldValue } from "../types/search/searchResults";
2
+
3
+ const DISPLAY_FIELD_CANDIDATES = [
4
+ "Name",
5
+ "CaseNumber",
6
+ "Subject",
7
+ "Title",
8
+ "DeveloperName",
9
+ "ContractNumber",
10
+ ];
11
+ /**
12
+ * Extracts the display value from a field value, handling nested structures
13
+ * For complex fields like Owner, extracts nested values from fields.Name.value
14
+ */
15
+ export function extractFieldValue(
16
+ fieldValue: FieldValue | undefined,
17
+ useDisplayValue: boolean = false,
18
+ ): string {
19
+ if (!fieldValue) {
20
+ return "—";
21
+ }
22
+
23
+ // If displayValue exists and is not null, use it (highest priority)
24
+ if (useDisplayValue && isDefined(fieldValue.displayValue)) {
25
+ return fieldValue.displayValue;
26
+ }
27
+
28
+ // If value is a complex object (like Owner), extract nested value
29
+ if (isComplexValue(fieldValue.value)) {
30
+ return extractComplexValue(fieldValue.value as ComplexFieldValue) ?? "—";
31
+ }
32
+
33
+ // Otherwise use the value directly (for simple fields)
34
+ if (isDefined(fieldValue.value)) {
35
+ return fieldValue.value as string;
36
+ }
37
+
38
+ return "—";
39
+ }
40
+
41
+ /**
42
+ * Helper to safely extract name from related object
43
+ */
44
+ function extractComplexValue(complex: ComplexFieldValue): string | null {
45
+ const fields = complex.fields;
46
+ if (!fields) return null;
47
+ // Scale: Check the candidate list until we find a field that exists and has a value
48
+ for (const fieldName of DISPLAY_FIELD_CANDIDATES) {
49
+ const field = fields[fieldName];
50
+ if (field) {
51
+ // Priority: DisplayValue -> Value
52
+ if (isDefined(field.displayValue)) return field.displayValue;
53
+ if (isDefined(field.value)) return field.value as string;
54
+ }
55
+ }
56
+
57
+ return null;
58
+ }
59
+ /**
60
+ * Type Guard checks
61
+ */
62
+ function isDefined(val: unknown): val is string | number | boolean {
63
+ return val !== null && val !== undefined;
64
+ }
65
+ function isComplexValue(val: unknown): val is ComplexFieldValue {
66
+ return typeof val === "object" && val !== null;
67
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Filter Utilities
3
+ *
4
+ * Utility functions for filter value parsing and transformation.
5
+ * These utilities handle the conversion of form values to filter criteria.
6
+ */
7
+
8
+ /**
9
+ * Parses a string value to either a number or string
10
+ * Attempts to parse as integer, falls back to trimmed string if not a valid number
11
+ *
12
+ * @param val - The value to parse (string)
13
+ * @returns Parsed number if valid, otherwise trimmed string, or empty string if input is empty
14
+ *
15
+ * @remarks
16
+ * - Returns empty string for empty/whitespace input
17
+ * - Attempts integer parsing first
18
+ * - Falls back to trimmed string if parsing fails
19
+ * - Used for filter values that can be either numeric or text
20
+ *
21
+ * @example
22
+ * ```tsx
23
+ * const parsed = parseFilterValue("123"); // 123 (number)
24
+ * const parsed = parseFilterValue("abc"); // "abc" (string)
25
+ * const parsed = parseFilterValue(" "); // "" (empty string)
26
+ * ```
27
+ */
28
+ export function parseFilterValue(val: string): string | number {
29
+ if (!val.trim()) return "";
30
+ const numVal = parseInt(val.trim(), 10);
31
+ return isNaN(numVal) ? val.trim() : numVal;
32
+ }