@salesforce/webapp-template-feature-react-global-search-experimental 1.3.3

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 (111) hide show
  1. package/LICENSE.txt +82 -0
  2. package/README.md +415 -0
  3. package/dist/.a4drules/build-validation.md +81 -0
  4. package/dist/.a4drules/code-quality.md +150 -0
  5. package/dist/.a4drules/graphql/tools/knowledge/lds-explore-graphql-schema.md +227 -0
  6. package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-mutationquery.md +211 -0
  7. package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-readquery.md +185 -0
  8. package/dist/.a4drules/graphql/tools/knowledge/lds-guide-graphql.md +205 -0
  9. package/dist/.a4drules/graphql/tools/schemas/shared.graphqls +1150 -0
  10. package/dist/.a4drules/graphql.md +98 -0
  11. package/dist/.a4drules/images.md +13 -0
  12. package/dist/.a4drules/react.md +361 -0
  13. package/dist/.a4drules/react_image_processing.md +45 -0
  14. package/dist/.a4drules/typescript.md +224 -0
  15. package/dist/.forceignore +15 -0
  16. package/dist/.husky/pre-commit +4 -0
  17. package/dist/.prettierignore +11 -0
  18. package/dist/.prettierrc +17 -0
  19. package/dist/CHANGELOG.md +11 -0
  20. package/dist/README.md +18 -0
  21. package/dist/config/project-scratch-def.json +13 -0
  22. package/dist/force-app/main/default/webapplications/feature-react-global-search/.prettierignore +9 -0
  23. package/dist/force-app/main/default/webapplications/feature-react-global-search/.prettierrc +11 -0
  24. package/dist/force-app/main/default/webapplications/feature-react-global-search/eslint.config.js +113 -0
  25. package/dist/force-app/main/default/webapplications/feature-react-global-search/feature-react-global-search.webapplication-meta.xml +7 -0
  26. package/dist/force-app/main/default/webapplications/feature-react-global-search/index.html +13 -0
  27. package/dist/force-app/main/default/webapplications/feature-react-global-search/package.json +42 -0
  28. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/api/graphql-operations-types.ts +127 -0
  29. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/api/objectInfoService.ts +229 -0
  30. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/api/utils/query/highRevenueAccountsQuery.graphql +29 -0
  31. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/app.tsx +13 -0
  32. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/appLayout.tsx +9 -0
  33. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/book.svg +3 -0
  34. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/copy.svg +4 -0
  35. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/rocket.svg +3 -0
  36. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/star.svg +3 -0
  37. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/codey-1.png +0 -0
  38. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/codey-2.png +0 -0
  39. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/codey-3.png +0 -0
  40. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/vibe-codey.svg +194 -0
  41. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/FiltersPanel.tsx +373 -0
  42. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/LoadingFallback.tsx +61 -0
  43. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/SearchResultCard.tsx +127 -0
  44. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/alerts/status-alert.tsx +45 -0
  45. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/detail/DetailFields.tsx +57 -0
  46. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/detail/DetailHeader.tsx +42 -0
  47. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/filters/FilterField.tsx +54 -0
  48. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/filters/FilterInput.tsx +55 -0
  49. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/filters/FilterSelect.tsx +72 -0
  50. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/forms/filters-form.tsx +114 -0
  51. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/forms/submit-button.tsx +47 -0
  52. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/layout/card-layout.tsx +19 -0
  53. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/ResultCardFields.tsx +71 -0
  54. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/SearchHeader.tsx +23 -0
  55. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/SearchPagination.tsx +162 -0
  56. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/SearchResultsPanel.tsx +184 -0
  57. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/shared/GlobalSearchInput.tsx +110 -0
  58. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/alert.tsx +65 -0
  59. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/button.tsx +56 -0
  60. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/card.tsx +77 -0
  61. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/field.tsx +111 -0
  62. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/index.ts +71 -0
  63. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/input.tsx +19 -0
  64. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/label.tsx +19 -0
  65. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/pagination.tsx +99 -0
  66. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/select.tsx +151 -0
  67. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/skeleton.tsx +7 -0
  68. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/spinner.tsx +21 -0
  69. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/table.tsx +114 -0
  70. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/tabs.tsx +115 -0
  71. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/constants.ts +36 -0
  72. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/index.ts +65 -0
  73. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/hooks/form.tsx +208 -0
  74. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/hooks/useObjectSearchData.ts +419 -0
  75. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/hooks/useRecordDetail.ts +127 -0
  76. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/lib/utils.ts +6 -0
  77. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/About.tsx +12 -0
  78. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/DetailPage.tsx +128 -0
  79. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/GlobalSearch.tsx +173 -0
  80. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/Home.tsx +13 -0
  81. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/NotFound.tsx +18 -0
  82. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/routes.tsx +50 -0
  83. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/styles/global.css +108 -0
  84. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/filters/filters.ts +122 -0
  85. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/filters/picklist.ts +32 -0
  86. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/objectInfo/objectInfo.ts +166 -0
  87. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/search/searchResults.ts +228 -0
  88. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/apiUtils.ts +125 -0
  89. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/cacheUtils.ts +76 -0
  90. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/debounce.ts +89 -0
  91. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/fieldUtils.ts +186 -0
  92. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/fieldValueExtractor.ts +67 -0
  93. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/filterUtils.ts +32 -0
  94. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/formUtils.ts +130 -0
  95. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/paginationUtils.ts +49 -0
  96. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/recordUtils.ts +75 -0
  97. package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/sanitizationUtils.ts +49 -0
  98. package/dist/force-app/main/default/webapplications/feature-react-global-search/tsconfig.json +36 -0
  99. package/dist/force-app/main/default/webapplications/feature-react-global-search/tsconfig.node.json +13 -0
  100. package/dist/force-app/main/default/webapplications/feature-react-global-search/vite-env.d.ts +1 -0
  101. package/dist/force-app/main/default/webapplications/feature-react-global-search/vite.config.ts +82 -0
  102. package/dist/force-app/main/default/webapplications/feature-react-global-search/vitest-env.d.ts +2 -0
  103. package/dist/force-app/main/default/webapplications/feature-react-global-search/vitest.config.ts +11 -0
  104. package/dist/force-app/main/default/webapplications/feature-react-global-search/vitest.setup.ts +1 -0
  105. package/dist/force-app/main/default/webapplications/feature-react-global-search/webapplication.json +7 -0
  106. package/dist/jest.config.js +6 -0
  107. package/dist/package.json +37 -0
  108. package/dist/scripts/apex/hello.apex +10 -0
  109. package/dist/scripts/soql/account.soql +6 -0
  110. package/dist/sfdx-project.json +12 -0
  111. package/package.json +32 -0
@@ -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,186 @@
1
+ /**
2
+ * Field Utilities
3
+ *
4
+ * Utility functions for extracting and working with field values from Salesforce record structures.
5
+ * Handles both simple fields and complex nested field paths (e.g., "Owner.Alias").
6
+ *
7
+ * @remarks
8
+ * These utilities handle the complex nested structure of Salesforce API responses,
9
+ * where fields can contain simple values, complex objects with nested fields, or display values.
10
+ */
11
+
12
+ import type { FieldValue, ComplexFieldValue } from "../types/search/searchResults";
13
+
14
+ /**
15
+ * List of field names to check when extracting display values from complex objects.
16
+ * Used when a field value is a complex object (like Owner) and we need to find a displayable field.
17
+ */
18
+ const DISPLAY_FIELD_CANDIDATES = [
19
+ "Name",
20
+ "CaseNumber",
21
+ "Subject",
22
+ "Title",
23
+ "DeveloperName",
24
+ "ContractNumber",
25
+ ] as const;
26
+
27
+ function isDefined(val: unknown): val is string | number | boolean {
28
+ return val !== null && val !== undefined;
29
+ }
30
+
31
+ function isComplexValue(val: unknown): val is ComplexFieldValue {
32
+ return typeof val === "object" && val !== null && "fields" in val;
33
+ }
34
+
35
+ function extractComplexValue(complex: ComplexFieldValue): string | null {
36
+ const fields = complex.fields;
37
+ if (!fields) return null;
38
+
39
+ for (const fieldName of DISPLAY_FIELD_CANDIDATES) {
40
+ const field = fields[fieldName];
41
+ if (field) {
42
+ if (isDefined(field.displayValue)) {
43
+ return field.displayValue;
44
+ }
45
+ if (isDefined(field.value)) {
46
+ return field.value.toString();
47
+ }
48
+ }
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ function extractFieldPrimitive(field: FieldValue): string | number | boolean | null {
55
+ if (isDefined(field.displayValue)) {
56
+ return field.displayValue;
57
+ }
58
+
59
+ if (isComplexValue(field.value)) {
60
+ const extracted = extractComplexValue(field.value);
61
+ return extracted !== null ? extracted : null;
62
+ }
63
+
64
+ if (isDefined(field.value)) {
65
+ return field.value;
66
+ }
67
+
68
+ return null;
69
+ }
70
+
71
+ /**
72
+ * Gets a field value from a nested object structure using a field path.
73
+ * Handles both simple fields (e.g., "Name") and nested fields (e.g., "Owner.Alias").
74
+ *
75
+ * @param fields - The fields object from the record
76
+ * @param fieldPath - The field path (e.g., "Name" or "Owner.Alias")
77
+ * @returns The displayValue or value, or null if not found
78
+ *
79
+ * @remarks
80
+ * - For simple fields: Returns displayValue if available, otherwise value
81
+ * - For nested fields: Navigates through the path and extracts the final field value
82
+ * - For complex objects: Uses DISPLAY_FIELD_CANDIDATES to find a displayable field
83
+ * - Returns null for missing fields or invalid paths
84
+ *
85
+ * @example
86
+ * ```tsx
87
+ * // Simple field
88
+ * const name = getNestedFieldValue(record.fields, 'Name');
89
+ *
90
+ * // Nested field
91
+ * const ownerAlias = getNestedFieldValue(record.fields, 'Owner.Alias');
92
+ *
93
+ * // Complex object (automatically extracts Name field)
94
+ * const ownerName = getNestedFieldValue(record.fields, 'Owner');
95
+ * ```
96
+ */
97
+ export function getNestedFieldValue(
98
+ fields: Record<string, FieldValue> | undefined,
99
+ fieldPath: string,
100
+ ): string | number | boolean | null {
101
+ if (!fields || !fieldPath) {
102
+ return null;
103
+ }
104
+
105
+ // Split the path by "." to handle nested fields
106
+ const pathParts = fieldPath.split(".");
107
+
108
+ // If it's a simple field (no dots), handle it directly
109
+ if (pathParts.length === 1) {
110
+ const field = fields[fieldPath];
111
+ if (!field) return null;
112
+
113
+ return extractFieldPrimitive(field);
114
+ }
115
+
116
+ // Handle nested fields (e.g., "Owner.Alias")
117
+ let currentFields: Record<string, FieldValue> | undefined = fields;
118
+
119
+ // Navigate through the path parts (all except the last one)
120
+ for (let i = 0; i < pathParts.length - 1; i++) {
121
+ const part = pathParts[i];
122
+ if (!currentFields || !currentFields[part]) {
123
+ return null;
124
+ }
125
+
126
+ const field: FieldValue = currentFields[part];
127
+ // Check if the value is a complex object with fields
128
+ if (isComplexValue(field.value)) {
129
+ currentFields = field.value.fields;
130
+ } else {
131
+ return null;
132
+ }
133
+ }
134
+
135
+ // Get the final field value
136
+ const finalFieldName = pathParts[pathParts.length - 1];
137
+ if (!currentFields || !currentFields[finalFieldName]) {
138
+ return null;
139
+ }
140
+
141
+ const finalField = currentFields[finalFieldName];
142
+ return extractFieldPrimitive(finalField);
143
+ }
144
+
145
+ /**
146
+ * Extracts the display value from a single field value.
147
+ * Handles both simple fields and complex (nested) field values.
148
+ *
149
+ * @param fieldValue - The field value to extract from
150
+ * @param useDisplayValue - Whether to prefer displayValue over value (default: false)
151
+ * @returns Extracted string value or '—' if not found
152
+ *
153
+ * @remarks
154
+ * - Prioritizes displayValue when useDisplayValue is true
155
+ * - Handles complex nested objects by checking common display field candidates
156
+ * - Returns '—' for null/undefined values (useful for UI display)
157
+ *
158
+ * @example
159
+ * ```tsx
160
+ * // Extract with displayValue priority
161
+ * const displayValue = extractFieldValue(field, true);
162
+ *
163
+ * // Extract with value priority
164
+ * const value = extractFieldValue(field, false);
165
+ * ```
166
+ */
167
+ export function extractFieldValue(
168
+ fieldValue: FieldValue | undefined,
169
+ useDisplayValue: boolean = false,
170
+ ): string {
171
+ if (!fieldValue) {
172
+ return "—";
173
+ }
174
+
175
+ if (useDisplayValue && isDefined(fieldValue.displayValue)) {
176
+ return fieldValue.displayValue;
177
+ }
178
+
179
+ const extracted = extractFieldPrimitive(fieldValue);
180
+
181
+ if (extracted !== null) {
182
+ return extracted.toString();
183
+ }
184
+
185
+ return "—";
186
+ }
@@ -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 String(field.value);
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
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Form Utilities
3
+ *
4
+ * Utility functions for form validation and error handling.
5
+ * These utilities are framework-agnostic and can be used with any form library.
6
+ */
7
+
8
+ /**
9
+ * Form error structure from TanStack Form
10
+ * Errors can be objects with a message property or strings
11
+ */
12
+ export interface FormError {
13
+ message: string;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ /**
18
+ * Type guard to check if an error has a message property
19
+ * @param error - The error to check
20
+ * @returns true if the error has a message property
21
+ */
22
+ export function isFormError(error: unknown): error is FormError {
23
+ return (
24
+ typeof error === "object" &&
25
+ error !== null &&
26
+ "message" in error &&
27
+ typeof (error as FormError).message === "string"
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Extracts unique errors by message, filtering out duplicates
33
+ * Handles both Error objects and string errors
34
+ * Converts FormError objects to Error instances for compatibility with UI components
35
+ *
36
+ * @param errors - Array of error objects or strings from form libraries
37
+ * @returns Array of unique Error objects (compatible with FieldError component)
38
+ *
39
+ * @example
40
+ * ```tsx
41
+ * const uniqueErrors = getUniqueErrors(formErrors);
42
+ * ```
43
+ */
44
+ export function getUniqueErrors(errors: unknown[]): Error[] {
45
+ const errorMap = new Map<string, Error>();
46
+
47
+ for (const error of errors) {
48
+ if (isFormError(error)) {
49
+ // Use message as key to deduplicate
50
+ if (!errorMap.has(error.message)) {
51
+ // Convert FormError to Error for compatibility
52
+ const errorObj = new Error(error.message);
53
+ // Preserve additional properties if needed
54
+ Object.assign(errorObj, error);
55
+ errorMap.set(error.message, errorObj);
56
+ }
57
+ } else if (typeof error === "string") {
58
+ // Handle string errors
59
+ if (!errorMap.has(error)) {
60
+ errorMap.set(error, new Error(error));
61
+ }
62
+ } else if (error instanceof Error) {
63
+ // Handle Error objects directly
64
+ if (!errorMap.has(error.message)) {
65
+ errorMap.set(error.message, error);
66
+ }
67
+ }
68
+ }
69
+
70
+ return Array.from(errorMap.values());
71
+ }
72
+
73
+ /**
74
+ * Validates that min value is less than or equal to max value for range filters
75
+ * This utility can be used in form validation logic
76
+ *
77
+ * @param minValue - Minimum value (string or number)
78
+ * @param maxValue - Maximum value (string or number)
79
+ * @returns Error message if validation fails, null if valid
80
+ *
81
+ * @remarks
82
+ * - If both values are empty, validation passes
83
+ * - Only validates if both values can be parsed as numbers
84
+ * - Returns null if values cannot be parsed (lets other validators handle it)
85
+ *
86
+ * @example
87
+ * ```tsx
88
+ * // In form validation
89
+ * const minError = validateRangeValues(minFieldValue, maxFieldValue);
90
+ * if (minError) {
91
+ * setFieldError('minField', minError);
92
+ * }
93
+ * ```
94
+ */
95
+ export function validateRangeValues(
96
+ minValue: string | number | null | undefined,
97
+ maxValue: string | number | null | undefined,
98
+ ): string | null {
99
+ // If both are empty, validation passes
100
+ if (!minValue && !maxValue) {
101
+ return null;
102
+ }
103
+
104
+ // Parse values to numbers if possible
105
+ const parseValue = (val: string | number | null | undefined): number | null => {
106
+ if (val === null || val === undefined || val === "") {
107
+ return null;
108
+ }
109
+ if (typeof val === "number") {
110
+ return val;
111
+ }
112
+ const parsed = Number(val);
113
+ return isNaN(parsed) ? null : parsed;
114
+ };
115
+
116
+ const minNum = parseValue(minValue);
117
+ const maxNum = parseValue(maxValue);
118
+
119
+ // If one is not a number, skip numeric validation (let other validators handle it)
120
+ if (minNum === null || maxNum === null) {
121
+ return null;
122
+ }
123
+
124
+ // Validate min <= max
125
+ if (minNum > maxNum) {
126
+ return "Minimum value must be less than or equal to maximum value";
127
+ }
128
+
129
+ return null;
130
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Pagination Utilities
3
+ *
4
+ * Utility functions for pagination-related operations including page size validation.
5
+ */
6
+
7
+ /**
8
+ * Default page size options for pagination
9
+ */
10
+ export const PAGE_SIZE_OPTIONS = [
11
+ { value: "10", label: "10" },
12
+ { value: "20", label: "20" },
13
+ { value: "50", label: "50" },
14
+ ] as const;
15
+
16
+ /**
17
+ * Valid page size values extracted from PAGE_SIZE_OPTIONS
18
+ */
19
+ export const VALID_PAGE_SIZES = PAGE_SIZE_OPTIONS.map((opt) => Number(opt.value));
20
+
21
+ /**
22
+ * Validates that a page size is one of the allowed options
23
+ * @param size - The page size to validate
24
+ * @returns true if valid, false otherwise
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * if (isValidPageSize(userInput)) {
29
+ * setPageSize(userInput);
30
+ * }
31
+ * ```
32
+ */
33
+ export function isValidPageSize(size: number): boolean {
34
+ return VALID_PAGE_SIZES.includes(size);
35
+ }
36
+
37
+ /**
38
+ * Gets a valid page size, defaulting to the first option if invalid
39
+ * @param size - The page size to validate
40
+ * @returns A valid page size
41
+ *
42
+ * @example
43
+ * ```tsx
44
+ * const safePageSize = getValidPageSize(userInput); // Returns valid size or default
45
+ * ```
46
+ */
47
+ export function getValidPageSize(size: number): number {
48
+ return isValidPageSize(size) ? size : VALID_PAGE_SIZES[0];
49
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Record Utilities
3
+ *
4
+ * Utility functions for working with Salesforce record IDs and generating safe React keys.
5
+ * These utilities ensure type safety and prevent XSS vulnerabilities when using record IDs.
6
+ *
7
+ * @remarks
8
+ * Salesforce record IDs follow a specific format and should be validated before use
9
+ * as React keys or in other contexts where security is important.
10
+ */
11
+
12
+ /**
13
+ * Type guard to validate that a record ID is safe to use as a React key
14
+ * Salesforce IDs follow a specific format (15 or 18 character alphanumeric)
15
+ *
16
+ * @param id - The record ID to validate
17
+ * @returns true if the ID appears to be a valid Salesforce ID format
18
+ *
19
+ * @remarks
20
+ * - Salesforce IDs are either 15 or 18 characters long
21
+ * - 18-character IDs include a 3-character checksum suffix
22
+ * - IDs are case-sensitive alphanumeric strings
23
+ * - Validates format to prevent XSS and ensure type safety
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * if (isValidSalesforceId(record.id)) {
28
+ * // TypeScript knows record.id is string here
29
+ * useRecordId(record.id);
30
+ * }
31
+ * ```
32
+ */
33
+ export function isValidSalesforceId(id: string | null | undefined): id is string {
34
+ if (!id || typeof id !== "string") {
35
+ return false;
36
+ }
37
+ // Salesforce IDs are 15 or 18 characters, alphanumeric (case-sensitive)
38
+ // Pattern: 15 chars or 18 chars (15 + 3 checksum chars)
39
+ return /^[a-zA-Z0-9]{15}$|^[a-zA-Z0-9]{18}$/.test(id);
40
+ }
41
+
42
+ /**
43
+ * Generates a safe key for React list items
44
+ * Uses record ID if valid, otherwise falls back to index-based key
45
+ *
46
+ * @param recordId - The record ID (can be null or undefined)
47
+ * @param index - Fallback index to use if ID is invalid
48
+ * @param prefix - Optional prefix for fallback keys (default: "result")
49
+ * @returns A safe key string that can be used as a React key prop
50
+ *
51
+ * @remarks
52
+ * - Validates ID format before using it as a key
53
+ * - Prevents XSS by ensuring only valid Salesforce IDs are used
54
+ * - Provides fallback for missing or invalid IDs
55
+ * - Ensures React always has a unique, stable key
56
+ *
57
+ * @example
58
+ * ```tsx
59
+ * {records.map((record, index) => (
60
+ * <RecordCard key={getSafeKey(record.id, index)} record={record} />
61
+ * ))}
62
+ * ```
63
+ */
64
+ export function getSafeKey(
65
+ recordId: string | null | undefined,
66
+ index: number,
67
+ prefix: string = "result",
68
+ ): string {
69
+ if (isValidSalesforceId(recordId)) {
70
+ // TypeScript narrows recordId to string here due to type guard
71
+ return recordId;
72
+ }
73
+ // Fallback to index-based key if ID is invalid or missing
74
+ return `${prefix}-${index}`;
75
+ }