@salesforce/webapp-template-app-react-template-b2e-experimental 1.109.4 → 1.109.6

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 (122) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/package.json +5 -6
  3. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/api/graphql-operations-types.ts +11260 -0
  4. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/components/ui/sonner.tsx +20 -0
  5. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/api/accountSearchService.ts +46 -0
  6. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
  7. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
  8. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
  9. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
  10. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
  11. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/pages/AccountSearch.tsx +275 -0
  12. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/__examples__/pages/Home.tsx +34 -0
  13. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/api/objectSearchService.ts +84 -0
  14. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/ActiveFilters.tsx +89 -0
  15. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/FilterPanel.tsx +127 -0
  16. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
  17. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/PaginationControls.tsx +151 -0
  18. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/SearchBar.tsx +41 -0
  19. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/SortControl.tsx +143 -0
  20. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/BooleanFilter.tsx +94 -0
  21. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/DateFilter.tsx +138 -0
  22. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/DateRangeFilter.tsx +78 -0
  23. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/MultiSelectFilter.tsx +106 -0
  24. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/NumericRangeFilter.tsx +102 -0
  25. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/SearchFilter.tsx +40 -0
  26. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/SelectFilter.tsx +97 -0
  27. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/components/filters/TextFilter.tsx +77 -0
  28. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useAsyncData.ts +53 -0
  29. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useCachedAsyncData.ts +183 -0
  30. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/hooks/useObjectSearchParams.ts +225 -0
  31. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/utils/debounce.ts +22 -0
  32. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/utils/fieldUtils.ts +29 -0
  33. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/utils/filterUtils.ts +372 -0
  34. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/object-search/utils/sortUtils.ts +38 -0
  35. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/index.ts +3 -117
  36. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/pages/Home.tsx +10 -11
  37. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/routes.tsx +8 -20
  38. package/dist/package-lock.json +2 -2
  39. package/dist/package.json +1 -1
  40. package/package.json +1 -1
  41. package/dist/.a4drules/skills/designing-webapp-ui-ux/SKILL.md +0 -271
  42. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/charts.csv +0 -26
  43. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/colors.csv +0 -97
  44. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/icons.csv +0 -101
  45. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/landing.csv +0 -31
  46. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/products.csv +0 -97
  47. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/react-performance.csv +0 -45
  48. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/stacks/html-tailwind.csv +0 -56
  49. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/stacks/react.csv +0 -54
  50. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/stacks/shadcn.csv +0 -61
  51. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/styles.csv +0 -68
  52. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/typography.csv +0 -58
  53. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/ui-reasoning.csv +0 -101
  54. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/ux-guidelines.csv +0 -100
  55. package/dist/.a4drules/skills/designing-webapp-ui-ux/data/web-interface.csv +0 -31
  56. package/dist/.a4drules/skills/designing-webapp-ui-ux/scripts/core.js +0 -255
  57. package/dist/.a4drules/skills/designing-webapp-ui-ux/scripts/design_system.js +0 -861
  58. package/dist/.a4drules/skills/designing-webapp-ui-ux/scripts/search.js +0 -98
  59. package/dist/.a4drules/skills/integrating-unsplash-images/SKILL.md +0 -71
  60. package/dist/.a4drules/skills/integrating-unsplash-images/implementation/usage.md +0 -159
  61. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/api/objectDetailService.ts +0 -102
  62. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/api/objectInfoGraphQLService.ts +0 -137
  63. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/api/objectInfoService.ts +0 -95
  64. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/api/recordListGraphQLService.ts +0 -364
  65. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/DetailFields.tsx +0 -55
  66. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/DetailForm.tsx +0 -146
  67. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/DetailHeader.tsx +0 -34
  68. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/DetailLayoutSections.tsx +0 -80
  69. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/Section.tsx +0 -108
  70. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/SectionRow.tsx +0 -20
  71. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/UiApiDetailForm.tsx +0 -140
  72. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +0 -73
  73. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +0 -29
  74. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +0 -17
  75. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +0 -24
  76. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FormattedText.tsx +0 -11
  77. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +0 -29
  78. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/filters/FilterField.tsx +0 -54
  79. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/filters/FilterInput.tsx +0 -55
  80. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/filters/FilterSelect.tsx +0 -72
  81. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/filters/FiltersPanel.tsx +0 -380
  82. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/forms/filters-form.tsx +0 -114
  83. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/forms/submit-button.tsx +0 -47
  84. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/GlobalSearchInput.tsx +0 -114
  85. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/ResultCardFields.tsx +0 -71
  86. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/SearchHeader.tsx +0 -31
  87. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/SearchPagination.tsx +0 -144
  88. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/SearchResultCard.tsx +0 -138
  89. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/search/SearchResultsPanel.tsx +0 -197
  90. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/components/shared/LoadingFallback.tsx +0 -61
  91. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/constants.ts +0 -39
  92. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/filters/FilterInput.tsx +0 -55
  93. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/filters/FilterSelect.tsx +0 -72
  94. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/form.tsx +0 -209
  95. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +0 -72
  96. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useObjectSearchData.ts +0 -174
  97. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +0 -137
  98. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +0 -135
  99. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/pages/DetailPage.tsx +0 -109
  100. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/pages/GlobalSearch.tsx +0 -235
  101. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/filters/filters.ts +0 -121
  102. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/filters/picklist.ts +0 -6
  103. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/objectInfo/objectInfo.ts +0 -49
  104. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/recordDetail/recordDetail.ts +0 -61
  105. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/schema.d.ts +0 -200
  106. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/types/search/searchResults.ts +0 -229
  107. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/apiUtils.ts +0 -59
  108. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/cacheUtils.ts +0 -76
  109. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/debounce.ts +0 -90
  110. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/fieldUtils.ts +0 -354
  111. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/fieldValueExtractor.ts +0 -67
  112. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/filterUtils.ts +0 -32
  113. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/formDataTransformUtils.ts +0 -260
  114. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/formUtils.ts +0 -142
  115. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/graphQLNodeFieldUtils.ts +0 -186
  116. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +0 -77
  117. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/graphQLRecordAdapter.ts +0 -90
  118. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/layoutTransformUtils.ts +0 -236
  119. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/linkUtils.ts +0 -14
  120. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/paginationUtils.ts +0 -49
  121. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/recordUtils.ts +0 -159
  122. package/dist/force-app/main/default/webapplications/appreacttemplateb2e/src/features/global-search/utils/sanitizationUtils.ts +0 -50
@@ -1,59 +0,0 @@
1
- /**
2
- * API Utilities
3
- *
4
- * Generic utility functions for API requests, validation, and URL handling.
5
- */
6
-
7
- import type { ZodSchema } from "zod";
8
-
9
- export interface FetchAndValidateOptions<T> {
10
- schema: ZodSchema<T>;
11
- errorContext: string;
12
- extractData?: (data: unknown) => unknown;
13
- }
14
-
15
- export async function fetchAndValidate<T>(
16
- fetchFn: () => Promise<Response>,
17
- options: FetchAndValidateOptions<T>,
18
- ): Promise<T> {
19
- const { schema, errorContext, extractData } = options;
20
-
21
- try {
22
- const response = await fetchFn();
23
-
24
- if (!response.ok) {
25
- throw new Error(`Failed to fetch ${errorContext}: ${response.status} ${response.statusText}`);
26
- }
27
-
28
- const data = await response.json();
29
- const dataToValidate = extractData ? extractData(data) : data;
30
- const validationResult = schema.safeParse(dataToValidate);
31
-
32
- if (!validationResult.success) {
33
- throw new Error(`Invalid ${errorContext} response format: ${validationResult.error.message}`);
34
- }
35
-
36
- return validationResult.data;
37
- } catch (error) {
38
- if (error instanceof Error && error.name === "ZodError") {
39
- throw new Error(`Invalid ${errorContext} response format: ${error.message}`);
40
- }
41
-
42
- if (
43
- error instanceof Error &&
44
- (error.message.includes("Failed to fetch") || error.message.includes("Invalid"))
45
- ) {
46
- throw error;
47
- }
48
-
49
- throw new Error(
50
- `Error fetching ${errorContext}: ${
51
- error instanceof Error ? error.message : (error?.toString() ?? "Unknown error")
52
- }`,
53
- );
54
- }
55
- }
56
-
57
- export function safeEncodePath(segment: string): string {
58
- return encodeURIComponent(segment);
59
- }
@@ -1,76 +0,0 @@
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
- }
@@ -1,90 +0,0 @@
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
- // eslint-disable-next-line @typescript-eslint/no-this-alias -- required for debouncing class methods
61
- lastContext = this;
62
- lastArgs = args;
63
- if (timeoutId) {
64
- clearTimeout(timeoutId);
65
- }
66
- timeoutId = setTimeout(() => {
67
- func.apply(lastContext, lastArgs as Parameters<T>);
68
- timeoutId = null;
69
- lastArgs = null;
70
- lastContext = null;
71
- }, wait);
72
- }
73
- // 3. React Safety: Add a cancel method to clear pending timers
74
- debounced.cancel = () => {
75
- if (timeoutId) {
76
- clearTimeout(timeoutId);
77
- timeoutId = null;
78
- }
79
- lastArgs = null;
80
- lastContext = null;
81
- };
82
-
83
- debounced.flush = () => {
84
- if (timeoutId && lastArgs) {
85
- func.apply(lastContext, lastArgs);
86
- debounced.cancel();
87
- }
88
- };
89
- return debounced;
90
- }
@@ -1,354 +0,0 @@
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
- }
@@ -1,67 +0,0 @@
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
- }
@@ -1,32 +0,0 @@
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
- }