@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.
- package/LICENSE.txt +82 -0
- package/README.md +415 -0
- package/dist/.a4drules/build-validation.md +81 -0
- package/dist/.a4drules/code-quality.md +150 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-explore-graphql-schema.md +227 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-mutationquery.md +211 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-generate-graphql-readquery.md +185 -0
- package/dist/.a4drules/graphql/tools/knowledge/lds-guide-graphql.md +205 -0
- package/dist/.a4drules/graphql/tools/schemas/shared.graphqls +1150 -0
- package/dist/.a4drules/graphql.md +98 -0
- package/dist/.a4drules/images.md +13 -0
- package/dist/.a4drules/react.md +361 -0
- package/dist/.a4drules/react_image_processing.md +45 -0
- package/dist/.a4drules/typescript.md +224 -0
- package/dist/.forceignore +15 -0
- package/dist/.husky/pre-commit +4 -0
- package/dist/.prettierignore +11 -0
- package/dist/.prettierrc +17 -0
- package/dist/CHANGELOG.md +11 -0
- package/dist/README.md +18 -0
- package/dist/config/project-scratch-def.json +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/.prettierignore +9 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/.prettierrc +11 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/eslint.config.js +113 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/feature-react-global-search.webapplication-meta.xml +7 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/index.html +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/package.json +42 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/api/graphql-operations-types.ts +127 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/api/objectInfoService.ts +229 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/api/utils/query/highRevenueAccountsQuery.graphql +29 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/app.tsx +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/appLayout.tsx +9 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/book.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/copy.svg +4 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/rocket.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/icons/star.svg +3 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/codey-1.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/codey-2.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/codey-3.png +0 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/assets/images/vibe-codey.svg +194 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/FiltersPanel.tsx +373 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/SearchResultCard.tsx +127 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/alerts/status-alert.tsx +45 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/detail/DetailFields.tsx +57 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/detail/DetailHeader.tsx +42 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/layout/card-layout.tsx +19 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/SearchHeader.tsx +23 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/SearchPagination.tsx +162 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/search/SearchResultsPanel.tsx +184 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/shared/GlobalSearchInput.tsx +110 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/alert.tsx +65 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/button.tsx +56 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/card.tsx +77 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/field.tsx +111 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/index.ts +71 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/input.tsx +19 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/label.tsx +19 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/pagination.tsx +99 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/select.tsx +151 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/skeleton.tsx +7 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/spinner.tsx +21 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/table.tsx +114 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/components/ui/tabs.tsx +115 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/constants.ts +36 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/features/global-search/index.ts +65 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/hooks/form.tsx +208 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/hooks/useObjectSearchData.ts +419 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/hooks/useRecordDetail.ts +127 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/lib/utils.ts +6 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/About.tsx +12 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/DetailPage.tsx +128 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/GlobalSearch.tsx +173 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/Home.tsx +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/pages/NotFound.tsx +18 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/routes.tsx +50 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/styles/global.css +108 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/filters/filters.ts +122 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/types/search/searchResults.ts +228 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/fieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/formUtils.ts +130 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/recordUtils.ts +75 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/src/utils/sanitizationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/tsconfig.json +36 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/tsconfig.node.json +13 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/vite-env.d.ts +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/vite.config.ts +82 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/vitest-env.d.ts +2 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/vitest.config.ts +11 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/vitest.setup.ts +1 -0
- package/dist/force-app/main/default/webapplications/feature-react-global-search/webapplication.json +7 -0
- package/dist/jest.config.js +6 -0
- package/dist/package.json +37 -0
- package/dist/scripts/apex/hello.apex +10 -0
- package/dist/scripts/soql/account.soql +6 -0
- package/dist/sfdx-project.json +12 -0
- 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
|
+
}
|