@salesforce/webapp-template-app-react-sample-b2x-experimental 1.68.0 → 1.69.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 (131) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/data/Lease__c.json +13 -0
  3. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
  4. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
  57. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
  58. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
  59. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
  60. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
  61. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
  62. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
  63. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
  64. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
  65. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
  66. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
  67. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
  68. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
  69. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
  70. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
  71. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
  72. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
  73. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
  74. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
  75. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
  76. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
  77. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
  78. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
  79. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
  80. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
  81. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
  82. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
  83. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
  84. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
  85. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
  86. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
  87. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
  88. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
  89. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
  90. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
  91. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
  92. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
  93. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
  94. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
  95. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
  96. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
  97. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
  98. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
  99. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
  100. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
  101. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
  102. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
  103. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
  104. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
  105. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
  106. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
  107. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
  108. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
  109. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
  110. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
  111. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
  112. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
  113. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
  114. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
  115. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
  116. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
  117. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
  118. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
  119. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
  120. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
  121. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
  122. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
  123. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
  124. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
  125. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
  126. package/dist/package.json +1 -1
  127. package/package.json +2 -2
  128. package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
  129. package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
  130. package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
  131. package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Cache Utilities
3
+ *
4
+ * Utility functions for creating deterministic cache keys and managing cache operations.
5
+ */
6
+
7
+ import type { FilterCriteria } from "../types/filters/filters";
8
+
9
+ /**
10
+ * Creates a deterministic cache key from filter criteria array.
11
+ * Sorts filters and their values to ensure consistent keys regardless of input order.
12
+ *
13
+ * @param filters - Array of filter criteria (FilterCriteria[])
14
+ * @returns Deterministic string key for caching
15
+ *
16
+ * @remarks
17
+ * - Sorts filters by objectApiName, then fieldPath, then operator
18
+ * - Sorts values within each filter to ensure consistency
19
+ * - Handles null/undefined values safely
20
+ * - Prevents cache key collisions from different object ordering
21
+ *
22
+ * Why is sorting required?
23
+ * If a user filters by "Name" then "Date", the array is [Name, Date].
24
+ * If they filter by "Date" then "Name", the array is [Date, Name].
25
+ * - Without sorting, these would generate different cache keys ("Name-Date" vs "Date-Name"),
26
+ * causing the app to re-fetch data it actually already has. Sorting ensures that
27
+ * the order of user clicks doesn't invalidate the cache.
28
+ *
29
+
30
+ * @example
31
+ * ```tsx
32
+ * const cacheKey = createFiltersKey(filters);
33
+ * ```
34
+ */
35
+ export function createFiltersKey(filters: FilterCriteria[]): string {
36
+ if (!Array.isArray(filters) || filters.length === 0) {
37
+ return "[]";
38
+ }
39
+
40
+ const normalized = filters
41
+ .map((filter) => {
42
+ if (!filter || typeof filter !== "object") {
43
+ return null;
44
+ }
45
+
46
+ const f = filter as FilterCriteria;
47
+
48
+ const sortedValues =
49
+ Array.isArray(f.values) && f.values.length > 0
50
+ ? [...f.values].sort((a, b) => {
51
+ const aStr = a.toString();
52
+ const bStr = b.toString();
53
+ return aStr.localeCompare(bStr);
54
+ })
55
+ : [];
56
+
57
+ return {
58
+ objectApiName: f.objectApiName ?? "",
59
+ fieldPath: f.fieldPath ?? "",
60
+ operator: f.operator ?? "",
61
+ values: sortedValues,
62
+ };
63
+ })
64
+ .filter((f): f is NonNullable<typeof f> => f !== null)
65
+ .sort((a, b) => {
66
+ const objectCompare = a.objectApiName.localeCompare(b.objectApiName);
67
+ if (objectCompare !== 0) return objectCompare;
68
+
69
+ const fieldCompare = a.fieldPath.localeCompare(b.fieldPath);
70
+ if (fieldCompare !== 0) return fieldCompare;
71
+
72
+ return a.operator.localeCompare(b.operator);
73
+ });
74
+
75
+ return JSON.stringify(normalized);
76
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Debounce Utility
3
+ *
4
+ * Provides debouncing functionality for functions with React-safe cleanup methods.
5
+ */
6
+
7
+ /**
8
+ * Interface for the debounced function, exposing utility methods.
9
+ */
10
+ export interface DebouncedFunc<T extends (...args: any[]) => any> {
11
+ /**
12
+ * Call the original function, but delayed.
13
+ */
14
+ (...args: Parameters<T>): void;
15
+ /**
16
+ * Cancel any pending execution.
17
+ */
18
+ cancel: () => void;
19
+ /**
20
+ * Immediately execute the pending function (if any) and clear the timer.
21
+ * Useful for saving data before unmounting.
22
+ */
23
+ flush: () => void;
24
+ }
25
+
26
+ /**
27
+ * Creates a debounced function that delays invoking func until after wait milliseconds
28
+ * have elapsed since the last time the debounced function was invoked.
29
+ *
30
+ * @param func - The function to debounce
31
+ * @param wait - The number of milliseconds to delay
32
+ * @returns A debounced function with .cancel() and .flush() methods
33
+ *
34
+ * @remarks
35
+ * - Includes .cancel() method for cleanup in React useEffects
36
+ * - Includes .flush() method to immediately execute pending calls
37
+ * - Preserves function context (this binding)
38
+ * - Type-safe with TypeScript generics
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * const debouncedSearch = debounce((query: string) => {
43
+ * performSearch(query);
44
+ * }, 300);
45
+ *
46
+ * // In useEffect cleanup
47
+ * return () => debouncedSearch.cancel();
48
+ * ```
49
+ */
50
+ export function debounce<T extends (...args: any[]) => any>(
51
+ func: T,
52
+ wait: number,
53
+ ): DebouncedFunc<T> {
54
+ // 1. Type Safety: Use a generic return type compatible with Browser and Node
55
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
56
+ let lastContext: ThisParameterType<T> | null = null;
57
+ let lastArgs: Parameters<T> | null = null;
58
+ function debounced(this: ThisParameterType<T>, ...args: Parameters<T>) {
59
+ // 2. Context Safety: Capture 'this' to support class methods
60
+ lastContext = this;
61
+ lastArgs = args;
62
+ if (timeoutId) {
63
+ clearTimeout(timeoutId);
64
+ }
65
+ timeoutId = setTimeout(() => {
66
+ func.apply(lastContext, lastArgs as Parameters<T>);
67
+ timeoutId = null;
68
+ lastArgs = null;
69
+ lastContext = null;
70
+ }, wait);
71
+ }
72
+ // 3. React Safety: Add a cancel method to clear pending timers
73
+ debounced.cancel = () => {
74
+ if (timeoutId) {
75
+ clearTimeout(timeoutId);
76
+ timeoutId = null;
77
+ }
78
+ lastArgs = null;
79
+ lastContext = null;
80
+ };
81
+
82
+ debounced.flush = () => {
83
+ if (timeoutId && lastArgs) {
84
+ func.apply(lastContext, lastArgs);
85
+ debounced.cancel();
86
+ }
87
+ };
88
+ return debounced;
89
+ }
@@ -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
+ }