@salesforce/webapp-template-app-react-sample-b2e-experimental 1.72.0 → 1.73.1
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/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/objects/Maintenance_Request__c/Maintenance_Request__c.object-meta.xml +11 -1
- package/dist/force-app/main/default/objects/Maintenance_Worker__c/Maintenance_Worker__c.object-meta.xml +6 -1
- package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +6 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +7 -5
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenanceWorkers.ts +60 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationsTable.tsx +59 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/FiltersFromApi.tsx +200 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ListPageFilters.tsx +97 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceTable.tsx +2 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ObjectSelect.tsx +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +6 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/dashboard/GlobalSearchBar.tsx +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/FilterErrorAlert.tsx +15 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageErrorState.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageLoadingState.tsx +18 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldRange.tsx +40 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldSelect.tsx +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldText.tsx +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/ListPageFilterRow.tsx +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageContainer.tsx +9 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageHeader.tsx +21 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/list/ListPageWithFilters.tsx +70 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/recordListGraphQLService.ts +364 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultCard.tsx +136 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/form.tsx +209 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/filters.ts +121 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/index.ts +5 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/sanitizationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useAccumulatedListPages.ts +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useListPage.ts +167 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/index.ts +8 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationAdapter.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationColumns.ts +28 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/constants.ts +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/fieldMappers.ts +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/filterUtils.ts +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/globalSearchConstants.ts +40 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listFilters.ts +152 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listPageConfig.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceAdapter.ts +110 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceColumns.ts +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerAdapter.ts +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerColumns.ts +25 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/objectApiNames.ts +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyAdapter.ts +68 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyColumns.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/routeConfig.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +10 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Applications.tsx +47 -62
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +130 -98
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +74 -91
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/MaintenanceWorkers.tsx +138 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +166 -85
- package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +41 -2
- package/dist/package.json +1 -1
- package/package.json +5 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared allowlist for link protocols (e.g. for FormattedUrl, and future mailto/tel if rendered as links).
|
|
3
|
+
* Centralizes protocol checks so new link types can be added in one place.
|
|
4
|
+
*/
|
|
5
|
+
export const ALLOWED_LINK_PROTOCOLS = ["http:", "https:"] as const;
|
|
6
|
+
|
|
7
|
+
export function isAllowedLinkUrl(value: string): boolean {
|
|
8
|
+
try {
|
|
9
|
+
const u = new URL(value);
|
|
10
|
+
return (ALLOWED_LINK_PROTOCOLS as readonly string[]).includes(u.protocol);
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -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) => parseInt(opt.value, 10));
|
|
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,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record utilities: layout-derived fields for GraphQL fetch, safe keys, ID validation.
|
|
3
|
+
*
|
|
4
|
+
* calculateFieldsToFetch: from layout + object metadata → field names and relation map;
|
|
5
|
+
* used by objectDetailService and list to build columns. findIdFieldForRelationship ensures
|
|
6
|
+
* Id + relationship name are both requested for reference display.
|
|
7
|
+
*
|
|
8
|
+
* @module utils/recordUtils
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ObjectInfoResult } from "../types/objectInfo/objectInfo";
|
|
12
|
+
import type {
|
|
13
|
+
LayoutResponse,
|
|
14
|
+
LayoutRow,
|
|
15
|
+
LayoutSection,
|
|
16
|
+
LayoutItem,
|
|
17
|
+
LayoutComponent,
|
|
18
|
+
} from "../types/recordDetail/recordDetail";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Find the Id field (reference foreign key) whose relationshipName matches the given name,
|
|
22
|
+
* so we can request both Id and relationship in the record query for display.
|
|
23
|
+
*/
|
|
24
|
+
function findIdFieldForRelationship(
|
|
25
|
+
metadata: ObjectInfoResult,
|
|
26
|
+
relationshipName: string,
|
|
27
|
+
): string | null {
|
|
28
|
+
if (!metadata.fields || !relationshipName) return null;
|
|
29
|
+
for (const [apiName, field] of Object.entries(metadata.fields)) {
|
|
30
|
+
const isReference = field.dataType != null && field.dataType.toLowerCase() === "reference";
|
|
31
|
+
if (field.relationshipName === relationshipName && (isReference || apiName.endsWith("Id"))) {
|
|
32
|
+
return apiName;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const getFetchableFieldsFromLayoutItem = function (
|
|
39
|
+
metadata: ObjectInfoResult,
|
|
40
|
+
layoutItem: LayoutItem,
|
|
41
|
+
relationFieldMap: Record<string, string>,
|
|
42
|
+
) {
|
|
43
|
+
const fields: Record<string, string> = {};
|
|
44
|
+
layoutItem.layoutComponents.forEach((comp: LayoutComponent) => {
|
|
45
|
+
// check if this is a field to add
|
|
46
|
+
if (!comp.apiName || comp.componentType !== "Field") {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// add field: fieldType
|
|
51
|
+
const fieldMetadata = metadata.fields[comp.apiName];
|
|
52
|
+
fields[comp.apiName] = fieldMetadata ? fieldMetadata.dataType : "";
|
|
53
|
+
|
|
54
|
+
// add relatedField if one exists (Id field -> add relationship name so we request Owner.Name)
|
|
55
|
+
if (comp.apiName in metadata.fields) {
|
|
56
|
+
const relationshipName = fieldMetadata?.relationshipName;
|
|
57
|
+
if (relationshipName) {
|
|
58
|
+
fields[relationshipName] = fieldMetadata.dataType;
|
|
59
|
+
|
|
60
|
+
relationFieldMap[comp.apiName] = relationshipName;
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
// layout component is relationship name (e.g. Owner); ensure we also request the Id
|
|
64
|
+
// so buildSelectionTree sees both OwnerId and Owner and requests Owner { Name { value } }
|
|
65
|
+
const idField = findIdFieldForRelationship(metadata, comp.apiName);
|
|
66
|
+
if (idField) {
|
|
67
|
+
const idMeta = metadata.fields[idField];
|
|
68
|
+
fields[idField] = idMeta ? idMeta.dataType : "";
|
|
69
|
+
relationFieldMap[idField] = comp.apiName;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return fields;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const getFetchableFieldsFromLayoutRow = function (
|
|
77
|
+
metadata: ObjectInfoResult,
|
|
78
|
+
layoutRow: LayoutRow,
|
|
79
|
+
relationFieldMap: Record<string, string>,
|
|
80
|
+
) {
|
|
81
|
+
let fieldsFromRow: Record<string, string> = {};
|
|
82
|
+
layoutRow.layoutItems.forEach((item: LayoutItem) => {
|
|
83
|
+
Object.assign(
|
|
84
|
+
fieldsFromRow,
|
|
85
|
+
getFetchableFieldsFromLayoutItem(metadata, item, relationFieldMap),
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
return fieldsFromRow;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const getFetchableFieldsFromSection = function (
|
|
92
|
+
metadata: ObjectInfoResult,
|
|
93
|
+
section: LayoutSection,
|
|
94
|
+
relationFieldMap: Record<string, string>,
|
|
95
|
+
) {
|
|
96
|
+
let fieldsFromSection: Record<string, string> = {};
|
|
97
|
+
section.layoutRows.forEach((row: LayoutRow) => {
|
|
98
|
+
Object.assign(
|
|
99
|
+
fieldsFromSection,
|
|
100
|
+
getFetchableFieldsFromLayoutRow(metadata, row, relationFieldMap),
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
return fieldsFromSection;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const getFetchableFieldsFromLayout = function (
|
|
107
|
+
metadata: ObjectInfoResult,
|
|
108
|
+
layout: LayoutResponse,
|
|
109
|
+
relationFieldMap: Record<string, string>,
|
|
110
|
+
) {
|
|
111
|
+
let fieldsFromLayout: Record<string, string> = {};
|
|
112
|
+
layout.sections.forEach((section) => {
|
|
113
|
+
Object.assign(
|
|
114
|
+
fieldsFromLayout,
|
|
115
|
+
getFetchableFieldsFromSection(metadata, section, relationFieldMap),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
return fieldsFromLayout;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns field API names to request for records from layout + object metadata.
|
|
123
|
+
* Includes both Id and relationship name for reference fields so GraphQL can fetch display value.
|
|
124
|
+
*
|
|
125
|
+
* @param metadata - Object info (fields with dataType, relationshipName).
|
|
126
|
+
* @param layout - Layout response (sections, layoutItems, layoutComponents).
|
|
127
|
+
* @param shouldPrefixedWithEntityName - If true, prefix names with object (e.g. Account.Name).
|
|
128
|
+
* @returns [fieldNames, fieldTypes, relationFieldMap] for buildSelectionTree / optionalFields.
|
|
129
|
+
*/
|
|
130
|
+
export const calculateFieldsToFetch = function (
|
|
131
|
+
metadata: ObjectInfoResult,
|
|
132
|
+
layout: LayoutResponse,
|
|
133
|
+
shouldPrefixedWithEntityName: boolean,
|
|
134
|
+
): [string[], string[], Record<string, string>] {
|
|
135
|
+
const relationFieldMap: Record<string, string> = {};
|
|
136
|
+
// populating fields to query for layout
|
|
137
|
+
const fields = getFetchableFieldsFromLayout(metadata, layout, relationFieldMap);
|
|
138
|
+
let fieldsToFetch = Object.keys(fields);
|
|
139
|
+
if (shouldPrefixedWithEntityName) {
|
|
140
|
+
fieldsToFetch = fieldsToFetch.map((field) => `${metadata.apiName}.${field}`);
|
|
141
|
+
}
|
|
142
|
+
// populate field types for o11y logging
|
|
143
|
+
const fieldTypes = Object.values(fields).filter((fieldType) => fieldType !== "");
|
|
144
|
+
return [fieldsToFetch, fieldTypes, relationFieldMap];
|
|
145
|
+
};
|
|
146
|
+
/** Type guard: true if id is a non-empty string matching 15- or 18-char Salesforce ID format. */
|
|
147
|
+
export function isValidSalesforceId(id: string | null | undefined): id is string {
|
|
148
|
+
if (!id || typeof id !== "string") return false;
|
|
149
|
+
return /^[a-zA-Z0-9]{15}$|^[a-zA-Z0-9]{18}$/.test(id);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Safe React key from record id or fallback to prefix-index. */
|
|
153
|
+
export function getSafeKey(
|
|
154
|
+
recordId: string | null | undefined,
|
|
155
|
+
index: number,
|
|
156
|
+
prefix: string = "result",
|
|
157
|
+
): string {
|
|
158
|
+
return isValidSalesforceId(recordId) ? recordId : `${prefix}-${index}`;
|
|
159
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitization Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utility functions for sanitizing user input to prevent injection attacks.
|
|
5
|
+
* These utilities provide basic sanitization for filter values.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sanitizes a string value by removing potentially dangerous characters
|
|
10
|
+
* and trimming whitespace.
|
|
11
|
+
*
|
|
12
|
+
* This is a basic sanitization - for production, consider using a library like DOMPurify for more
|
|
13
|
+
* comprehensive sanitization.
|
|
14
|
+
* Also, note this is NOT an end-to-end security control.
|
|
15
|
+
* Client-side sanitization can be bypassed by any attacker using `curl` or Postman.
|
|
16
|
+
* To prevent injection attacks (SOSL Injection, XSS):
|
|
17
|
+
* 1. The BACKEND (Salesforce API) handles SOSL injection if parameters are passed correctly.
|
|
18
|
+
* 2. React handles XSS automatically when rendering variables in JSX (e.g., <div>{value}</div>).
|
|
19
|
+
* Do not rely on this function for end-to-end security enforcement.
|
|
20
|
+
*
|
|
21
|
+
* @param value - The string value to sanitize
|
|
22
|
+
* @returns Sanitized string value
|
|
23
|
+
*
|
|
24
|
+
* @remarks
|
|
25
|
+
* - Removes control characters (except newlines, tabs, carriage returns)
|
|
26
|
+
* - Trims leading/trailing whitespace
|
|
27
|
+
* - Limits length to prevent DoS attacks (default: 1000 characters)
|
|
28
|
+
* - Preserves alphanumeric, spaces, and common punctuation
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* const sanitized = sanitizeFilterValue(userInput);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function sanitizeFilterValue(value: string, maxLength: number = 1000): string {
|
|
36
|
+
if (typeof value !== "string") {
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let sanitized = value.trim();
|
|
41
|
+
|
|
42
|
+
if (sanitized.length > maxLength) {
|
|
43
|
+
sanitized = sanitized.substring(0, maxLength);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
47
|
+
|
|
48
|
+
return sanitized;
|
|
49
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, type Dispatch, type SetStateAction } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Accumulates list pages when the GraphQL hook returns one page at a time.
|
|
5
|
+
* When loading goes from true to false: if afterCursor is null, replaces the list;
|
|
6
|
+
* otherwise appends (load more). Caller must reset accumulated when search/filters/sort change.
|
|
7
|
+
*/
|
|
8
|
+
export function useAccumulatedListPages<T>(
|
|
9
|
+
edges: Array<{ node?: unknown }>,
|
|
10
|
+
loading: boolean,
|
|
11
|
+
afterCursor: string | null,
|
|
12
|
+
mapNode: (node: unknown) => T,
|
|
13
|
+
): [T[], Dispatch<SetStateAction<T[]>>] {
|
|
14
|
+
const [accumulated, setAccumulated] = useState<T[]>([]);
|
|
15
|
+
const prevLoadingRef = useRef(loading);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const isFirstPage = afterCursor === null;
|
|
19
|
+
const justFinishedLoading = prevLoadingRef.current && !loading;
|
|
20
|
+
if (justFinishedLoading) {
|
|
21
|
+
const list = edges.map((e) => mapNode(e.node));
|
|
22
|
+
if (isFirstPage) setAccumulated(list);
|
|
23
|
+
else setAccumulated((prev) => [...prev, ...list]);
|
|
24
|
+
}
|
|
25
|
+
prevLoadingRef.current = loading;
|
|
26
|
+
}, [loading, edges, afterCursor, mapNode]);
|
|
27
|
+
|
|
28
|
+
return [accumulated, setAccumulated];
|
|
29
|
+
}
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useListPage.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { useEffect, useState, useRef, useMemo, useCallback } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router";
|
|
3
|
+
import {
|
|
4
|
+
useObjectListMetadata,
|
|
5
|
+
useRecordListGraphQL,
|
|
6
|
+
type FilterCriteria,
|
|
7
|
+
} from "@salesforce/webapp-template-feature-react-global-search-experimental";
|
|
8
|
+
import { PAGE_SIZE_LIST } from "../lib/constants.js";
|
|
9
|
+
import {
|
|
10
|
+
buildFilterCriteriaFromFormValues,
|
|
11
|
+
getDefaultFilterFormValues,
|
|
12
|
+
getApplicableFilters,
|
|
13
|
+
} from "../lib/filterUtils.js";
|
|
14
|
+
import { useAccumulatedListPages } from "./useAccumulatedListPages.js";
|
|
15
|
+
import type { ListPageConfig } from "../lib/listPageConfig.js";
|
|
16
|
+
|
|
17
|
+
/** Picklist option shape from useObjectListMetadata (label/value). */
|
|
18
|
+
export interface PicklistOption {
|
|
19
|
+
label?: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseListPageResult<T> {
|
|
24
|
+
filters: ReturnType<typeof getApplicableFilters>;
|
|
25
|
+
picklistValues: Record<string, PicklistOption[]>;
|
|
26
|
+
formValues: Record<string, string>;
|
|
27
|
+
onFormValueChange: (key: string, value: string) => void;
|
|
28
|
+
onApplyFilters: () => void;
|
|
29
|
+
onResetFilters: () => void;
|
|
30
|
+
filterError: string | null;
|
|
31
|
+
loading: boolean;
|
|
32
|
+
error: string | null;
|
|
33
|
+
items: T[];
|
|
34
|
+
canLoadMore: boolean;
|
|
35
|
+
onLoadMore: () => void;
|
|
36
|
+
loadMoreLoading: boolean;
|
|
37
|
+
sortBy?: string;
|
|
38
|
+
onSortChange?: (sortBy: string) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Shared hook for list pages with API-driven filters and GraphQL data.
|
|
43
|
+
* Uses useObjectListMetadata and useRecordListGraphQL; search comes from URL ?q=.
|
|
44
|
+
* Pass the returned props to ListPageWithFilters and render your table/list as children.
|
|
45
|
+
*/
|
|
46
|
+
export function useListPage<T>(config: ListPageConfig<T>): UseListPageResult<T> {
|
|
47
|
+
const [searchParams] = useSearchParams();
|
|
48
|
+
const searchQuery = searchParams.get("q") ?? "";
|
|
49
|
+
|
|
50
|
+
const [afterCursor, setAfterCursor] = useState<string | null>(null);
|
|
51
|
+
const [appliedFilters, setAppliedFilters] = useState<FilterCriteria[]>([]);
|
|
52
|
+
const [filterFormValues, setFilterFormValues] = useState<Record<string, string>>({});
|
|
53
|
+
const [filterError, setFilterError] = useState<string | null>(null);
|
|
54
|
+
const hasInitializedFiltersRef = useRef(false);
|
|
55
|
+
const [sortBy, setSortBy] = useState(config.defaultSort);
|
|
56
|
+
|
|
57
|
+
const listMeta = useObjectListMetadata(config.objectApiName);
|
|
58
|
+
const columns = useMemo(() => config.getColumns(listMeta.columns), [listMeta.columns, config]);
|
|
59
|
+
const filters = useMemo(
|
|
60
|
+
() => getApplicableFilters(listMeta.filters ?? [], config.filterExcludedFieldPaths),
|
|
61
|
+
[listMeta.filters, config.filterExcludedFieldPaths],
|
|
62
|
+
);
|
|
63
|
+
const picklistValues = listMeta.picklistValues ?? {};
|
|
64
|
+
|
|
65
|
+
const effectiveSort = config.sortable ? sortBy : config.defaultSort;
|
|
66
|
+
|
|
67
|
+
const {
|
|
68
|
+
edges,
|
|
69
|
+
pageInfo,
|
|
70
|
+
loading: resultsLoading,
|
|
71
|
+
error: resultsError,
|
|
72
|
+
} = useRecordListGraphQL({
|
|
73
|
+
objectApiName: config.objectApiName,
|
|
74
|
+
columns,
|
|
75
|
+
columnsLoading: listMeta.loading,
|
|
76
|
+
columnsError: listMeta.error,
|
|
77
|
+
first: PAGE_SIZE_LIST,
|
|
78
|
+
after: afterCursor,
|
|
79
|
+
searchQuery: searchQuery.trim() || undefined,
|
|
80
|
+
sortBy: effectiveSort,
|
|
81
|
+
filters: appliedFilters,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const mapNode = useCallback((node: unknown) => config.nodeToItem(node), [config]);
|
|
85
|
+
|
|
86
|
+
const [accumulated, setAccumulated] = useAccumulatedListPages(
|
|
87
|
+
edges,
|
|
88
|
+
resultsLoading,
|
|
89
|
+
afterCursor,
|
|
90
|
+
mapNode,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (filters.length === 0) return;
|
|
95
|
+
if (!hasInitializedFiltersRef.current) {
|
|
96
|
+
hasInitializedFiltersRef.current = true;
|
|
97
|
+
setFilterFormValues(getDefaultFilterFormValues(filters));
|
|
98
|
+
}
|
|
99
|
+
}, [filters]);
|
|
100
|
+
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
setAfterCursor(null);
|
|
103
|
+
setAccumulated([]);
|
|
104
|
+
}, [searchQuery, appliedFilters, effectiveSort, setAccumulated]);
|
|
105
|
+
|
|
106
|
+
const loading = listMeta.loading || resultsLoading;
|
|
107
|
+
const error = listMeta.error ?? resultsError ?? null;
|
|
108
|
+
const hasNextPage = Boolean(pageInfo?.hasNextPage);
|
|
109
|
+
const endCursor = pageInfo?.endCursor ?? null;
|
|
110
|
+
|
|
111
|
+
const onLoadMore = useCallback(() => {
|
|
112
|
+
if (endCursor && !searchQuery.trim()) setAfterCursor(endCursor);
|
|
113
|
+
}, [endCursor, searchQuery]);
|
|
114
|
+
|
|
115
|
+
const onApplyFilters = useCallback(() => {
|
|
116
|
+
setFilterError(null);
|
|
117
|
+
const result = buildFilterCriteriaFromFormValues(
|
|
118
|
+
config.objectApiName,
|
|
119
|
+
filters,
|
|
120
|
+
filterFormValues,
|
|
121
|
+
);
|
|
122
|
+
if (result.rangeError) {
|
|
123
|
+
setFilterError(result.rangeError);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
setAppliedFilters(result.criteria);
|
|
127
|
+
setAfterCursor(null);
|
|
128
|
+
}, [config.objectApiName, filters, filterFormValues]);
|
|
129
|
+
|
|
130
|
+
const onResetFilters = useCallback(() => {
|
|
131
|
+
setFilterFormValues(getDefaultFilterFormValues(filters));
|
|
132
|
+
setAppliedFilters([]);
|
|
133
|
+
setAfterCursor(null);
|
|
134
|
+
setFilterError(null);
|
|
135
|
+
}, [filters]);
|
|
136
|
+
|
|
137
|
+
const onFormValueChange = useCallback((key: string, value: string) => {
|
|
138
|
+
setFilterFormValues((prev) => ({ ...prev, [key]: value }));
|
|
139
|
+
setFilterError(null);
|
|
140
|
+
}, []);
|
|
141
|
+
|
|
142
|
+
const onSortChange = useCallback((newSortBy: string) => {
|
|
143
|
+
setSortBy(newSortBy);
|
|
144
|
+
setAfterCursor(null);
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
const result: UseListPageResult<T> = {
|
|
148
|
+
filters,
|
|
149
|
+
picklistValues: picklistValues as Record<string, PicklistOption[]>,
|
|
150
|
+
formValues: filterFormValues,
|
|
151
|
+
onFormValueChange,
|
|
152
|
+
onApplyFilters,
|
|
153
|
+
onResetFilters,
|
|
154
|
+
filterError,
|
|
155
|
+
loading,
|
|
156
|
+
error,
|
|
157
|
+
items: accumulated,
|
|
158
|
+
canLoadMore: hasNextPage && !searchQuery.trim(),
|
|
159
|
+
onLoadMore,
|
|
160
|
+
loadMoreLoading: resultsLoading,
|
|
161
|
+
};
|
|
162
|
+
if (config.sortable) {
|
|
163
|
+
result.sortBy = sortBy;
|
|
164
|
+
result.onSortChange = onSortChange;
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* feature-react-
|
|
2
|
+
* Package entry for @salesforce/webapp-template-feature-react-global-search-experimental.
|
|
3
|
+
* Exports only API, hooks, utils, types, and constants so that consuming apps do not
|
|
4
|
+
* pull in the feature's UI (routes, __inherit__ components) which depend on @radix-ui.
|
|
3
5
|
*/
|
|
4
|
-
|
|
5
|
-
export
|
|
6
|
-
export
|
|
6
|
+
export * from "./features/global-search/api";
|
|
7
|
+
export * from "./features/global-search/hooks";
|
|
8
|
+
export * from "./features/global-search/utils";
|
|
9
|
+
export * from "./features/global-search/types";
|
|
10
|
+
export * from "./constants";
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationAdapter.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: maps Application__c GraphQL node to app Application type.
|
|
3
|
+
*/
|
|
4
|
+
import type { Application } from "./types.js";
|
|
5
|
+
|
|
6
|
+
interface ApplicationNode {
|
|
7
|
+
Id?: string;
|
|
8
|
+
Name?: { value?: string };
|
|
9
|
+
Status__c?: { value?: string };
|
|
10
|
+
User__r?: { Name?: { value?: string } };
|
|
11
|
+
Property__r?: { Address__c?: { value?: string }; Name?: { value?: string } };
|
|
12
|
+
CreatedDate?: { value?: string };
|
|
13
|
+
Employment__c?: { value?: string };
|
|
14
|
+
References__c?: { value?: string };
|
|
15
|
+
Start_Date__c?: { value?: string };
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function nodeToApplication(node: Record<string, unknown> | undefined): Application {
|
|
20
|
+
const n = (node ?? {}) as ApplicationNode;
|
|
21
|
+
const created = n.CreatedDate?.value;
|
|
22
|
+
return {
|
|
23
|
+
id: n.Id ?? "",
|
|
24
|
+
applicantName: n.User__r?.Name?.value ?? "Unknown",
|
|
25
|
+
propertyAddress: n.Property__r?.Address__c?.value ?? "",
|
|
26
|
+
submittedDate: created ? new Date(created).toLocaleDateString() : "",
|
|
27
|
+
status: n.Status__c?.value ?? "",
|
|
28
|
+
employment: n.Employment__c?.value,
|
|
29
|
+
references: n.References__c?.value,
|
|
30
|
+
startDate: n.Start_Date__c?.value,
|
|
31
|
+
propertyName: n.Property__r?.Name?.value,
|
|
32
|
+
};
|
|
33
|
+
}
|
package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationColumns.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Column config for Application__c list.
|
|
3
|
+
*/
|
|
4
|
+
import type { Column } from "@salesforce/webapp-template-feature-react-global-search-experimental";
|
|
5
|
+
|
|
6
|
+
export const APPLICATION_EXTRA_COLUMNS: Column[] = [
|
|
7
|
+
{ fieldApiName: "Name", label: "Name", searchable: true, sortable: true },
|
|
8
|
+
{ fieldApiName: "Status__c", label: "Status", searchable: true, sortable: true },
|
|
9
|
+
{ fieldApiName: "User__r.Name", label: "Applicant", searchable: true, sortable: false },
|
|
10
|
+
{
|
|
11
|
+
fieldApiName: "Property__r.Address__c",
|
|
12
|
+
label: "Property Address",
|
|
13
|
+
searchable: true,
|
|
14
|
+
sortable: false,
|
|
15
|
+
},
|
|
16
|
+
{ fieldApiName: "Property__r.Name", label: "Property Name", searchable: true, sortable: false },
|
|
17
|
+
{ fieldApiName: "CreatedDate", label: "Submitted", searchable: false, sortable: true },
|
|
18
|
+
{ fieldApiName: "Employment__c", label: "Employment", searchable: true, sortable: false },
|
|
19
|
+
{ fieldApiName: "References__c", label: "References", searchable: true, sortable: false },
|
|
20
|
+
{ fieldApiName: "Start_Date__c", label: "Start Date", searchable: false, sortable: true },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export function getApplicationListColumns(columns: Column[]): Column[] {
|
|
24
|
+
if (columns.length === 0) return APPLICATION_EXTRA_COLUMNS;
|
|
25
|
+
const existing = new Set(columns.map((c) => c.fieldApiName));
|
|
26
|
+
const toAdd = APPLICATION_EXTRA_COLUMNS.filter((c) => !existing.has(c.fieldApiName));
|
|
27
|
+
return toAdd.length === 0 ? columns : [...columns, ...toAdd];
|
|
28
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-wide constants.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const PAGE_SIZE_LIST = 12;
|
|
6
|
+
export const DASHBOARD_MAINTENANCE_LIMIT = 5;
|
|
7
|
+
|
|
8
|
+
export const PROPERTY_FILTER_EXCLUDED_FIELD_PATHS = new Set([
|
|
9
|
+
"CreatedDate",
|
|
10
|
+
"Hero_Image__c",
|
|
11
|
+
"Year_Built__c",
|
|
12
|
+
"Available_Date__c",
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export const MAINTENANCE_WORKER_FILTER_EXCLUDED_FIELD_PATHS = new Set<string>([]);
|
|
16
|
+
export const MAINTENANCE_FILTER_EXCLUDED_FIELD_PATHS = new Set(["Scheduled__c"]);
|
|
17
|
+
export const APPLICATION_FILTER_EXCLUDED_FIELD_PATHS = new Set<string>([]);
|
|
18
|
+
|
|
19
|
+
export const MAINTENANCE_WORKER_OBJECT_API_NAME = "Maintenance_Worker__c" as const;
|
|
20
|
+
|
|
21
|
+
export const FALLBACK_LABEL_PROPERTIES_PLURAL = "Properties";
|
|
22
|
+
export const FALLBACK_LABEL_MAINTENANCE_PLURAL = "Maintenance Requests";
|
|
23
|
+
export const FALLBACK_LABEL_MAINTENANCE_WORKERS_PLURAL = "Maintenance Workers";
|
|
24
|
+
export const FALLBACK_LABEL_APPLICATIONS_PLURAL = "Applications";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps Salesforce API field paths (from getObjectListFilters) to our app record properties.
|
|
3
|
+
* Used by applyFilterCriteria when filtering list data with FilterCriteria from the feature API.
|
|
4
|
+
*/
|
|
5
|
+
import type { Property, MaintenanceRequest, Application } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const PROPERTY_FIELD_MAP: Record<string, (p: Property) => string | number | undefined> = {
|
|
8
|
+
Name: (p) => p.name,
|
|
9
|
+
Address__c: (p) => p.address,
|
|
10
|
+
Type__c: (p) => p.type,
|
|
11
|
+
Status__c: (p) => p.status,
|
|
12
|
+
Description__c: (p) => p.description,
|
|
13
|
+
Monthly_Rent__c: (p) => p.monthlyRent,
|
|
14
|
+
Bedrooms__c: (p) => p.bedrooms,
|
|
15
|
+
Bathrooms__c: (p) => p.bathrooms,
|
|
16
|
+
Sq_Ft__c: (p) => p.sqFt,
|
|
17
|
+
Year_Built__c: (p) => p.yearBuilt,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MAINTENANCE_REQUEST_FIELD_MAP: Record<
|
|
21
|
+
string,
|
|
22
|
+
(r: MaintenanceRequest) => string | number | undefined
|
|
23
|
+
> = {
|
|
24
|
+
Status__c: (r) => r.status,
|
|
25
|
+
Type__c: (r) => r.issueType,
|
|
26
|
+
Description__c: (r) => r.description,
|
|
27
|
+
Priority__c: (r) => r.priority,
|
|
28
|
+
propertyAddress: (r) => r.propertyAddress,
|
|
29
|
+
tenantName: (r) => r.tenantName,
|
|
30
|
+
"Property__r.Address__c": (r) => r.propertyAddress,
|
|
31
|
+
"Property__r.Name": (r) => r.tenantUnit ?? r.propertyAddress,
|
|
32
|
+
"User__r.Name": (r) => r.tenantName,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const APPLICATION_FIELD_MAP: Record<string, (a: Application) => string | number | undefined> = {
|
|
36
|
+
Status__c: (a) => a.status,
|
|
37
|
+
Start_Date__c: (a) => a.submittedDate || a.startDate,
|
|
38
|
+
applicantName: (a) => a.applicantName,
|
|
39
|
+
propertyAddress: (a) => a.propertyAddress,
|
|
40
|
+
Employment__c: (a) => a.employment,
|
|
41
|
+
References__c: (a) => a.references,
|
|
42
|
+
"Property__r.Address__c": (a) => a.propertyAddress,
|
|
43
|
+
"Property__r.Name": (a) => a.propertyName ?? a.propertyAddress,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function getPropertyRecordValue(
|
|
47
|
+
record: Property,
|
|
48
|
+
fieldPath: string,
|
|
49
|
+
): string | number | undefined {
|
|
50
|
+
const getter = PROPERTY_FIELD_MAP[fieldPath];
|
|
51
|
+
if (getter) return getter(record);
|
|
52
|
+
return (record as unknown as Record<string, unknown>)[fieldPath] as string | number | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getMaintenanceRequestRecordValue(
|
|
56
|
+
record: MaintenanceRequest,
|
|
57
|
+
fieldPath: string,
|
|
58
|
+
): string | number | undefined {
|
|
59
|
+
const getter = MAINTENANCE_REQUEST_FIELD_MAP[fieldPath];
|
|
60
|
+
if (getter) return getter(record);
|
|
61
|
+
return (record as unknown as Record<string, unknown>)[fieldPath] as string | number | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getApplicationRecordValue(
|
|
65
|
+
record: Application,
|
|
66
|
+
fieldPath: string,
|
|
67
|
+
): string | number | undefined {
|
|
68
|
+
const getter = APPLICATION_FIELD_MAP[fieldPath];
|
|
69
|
+
if (getter) return getter(record);
|
|
70
|
+
return (record as unknown as Record<string, unknown>)[fieldPath] as string | number | undefined;
|
|
71
|
+
}
|