@salesforce/webapp-template-app-react-sample-b2x-experimental 1.112.7 → 1.112.9
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 +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +6 -5
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphql-operations-types.ts +12058 -214
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +18 -15
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{propertyListingGraphQL.ts → properties/propertyListingGraphQL.ts} +1 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +4 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{TopBar.tsx → layout/TopBar.tsx} +2 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestList.tsx → maintenanceRequests/MaintenanceRequestList.tsx} +4 -4
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestListItem.tsx → maintenanceRequests/MaintenanceRequestListItem.tsx} +3 -3
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/maintenanceRequests/MaintenanceSummaryDetailsModal.tsx +87 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertyListingCard.tsx → properties/PropertyListingCard.tsx} +1 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/{features/global-search/components/search/SearchPagination.tsx → components/properties/PropertyListingSearchPagination.tsx} +20 -28
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants/propertyListing.ts +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/accountSearchService.ts +46 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/AccountSearch.tsx +303 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/Home.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/api/objectSearchService.ts +84 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/ActiveFilters.tsx +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/FilterContext.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/PaginationControls.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/SearchBar.tsx +41 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/SortControl.tsx +143 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/BooleanFilter.tsx +74 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/DateFilter.tsx +121 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/DateRangeFilter.tsx +69 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/MultiSelectFilter.tsx +98 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/NumericRangeFilter.tsx +85 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/SearchFilter.tsx +37 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/SelectFilter.tsx +93 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/TextFilter.tsx +74 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useAsyncData.ts +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useCachedAsyncData.ts +184 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useObjectSearchParams.ts +247 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/debounce.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/fieldUtils.ts +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/filterUtils.ts +372 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/sortUtils.ts +38 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +1 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +2 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +1 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingAmenities.ts +2 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +2 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +3 -3
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +2 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +3 -3
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +2 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +4 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +2 -2
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +1 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +12 -10
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +6 -18
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/propertyListingPaginationUtils.ts +18 -0
- package/dist/package-lock.json +2 -2
- package/dist/package.json +1 -1
- package/dist/scripts/graphql-search.sh +69 -17
- package/package.json +1 -1
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/MaintenanceDetailsModal.tsx +0 -128
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectDetailService.ts +0 -102
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectInfoGraphQLService.ts +0 -137
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectInfoService.ts +0 -95
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/recordListGraphQLService.ts +0 -364
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailFields.tsx +0 -55
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailForm.tsx +0 -146
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailHeader.tsx +0 -34
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailLayoutSections.tsx +0 -80
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/Section.tsx +0 -108
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/SectionRow.tsx +0 -20
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/UiApiDetailForm.tsx +0 -140
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +0 -73
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +0 -29
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +0 -17
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +0 -24
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedText.tsx +0 -11
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +0 -29
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterField.tsx +0 -54
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterInput.tsx +0 -55
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterSelect.tsx +0 -72
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FiltersPanel.tsx +0 -380
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/forms/filters-form.tsx +0 -114
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/forms/submit-button.tsx +0 -47
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/GlobalSearchInput.tsx +0 -114
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/ResultCardFields.tsx +0 -71
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchHeader.tsx +0 -31
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchResultCard.tsx +0 -138
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchResultsPanel.tsx +0 -197
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/shared/LoadingFallback.tsx +0 -61
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/constants.ts +0 -39
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/filters/FilterInput.tsx +0 -55
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/filters/FilterSelect.tsx +0 -72
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/form.tsx +0 -209
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useObjectInfoBatch.ts +0 -72
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useObjectSearchData.ts +0 -174
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useRecordDetailLayout.ts +0 -137
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useRecordListGraphQL.ts +0 -135
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/pages/DetailPage.tsx +0 -109
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/pages/GlobalSearch.tsx +0 -235
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/filters/filters.ts +0 -121
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/filters/picklist.ts +0 -6
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/objectInfo/objectInfo.ts +0 -49
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/recordDetail/recordDetail.ts +0 -61
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/schema.d.ts +0 -200
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/apiUtils.ts +0 -59
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/cacheUtils.ts +0 -76
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/debounce.ts +0 -90
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/fieldUtils.ts +0 -354
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/fieldValueExtractor.ts +0 -67
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/filterUtils.ts +0 -32
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/formDataTransformUtils.ts +0 -260
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/formUtils.ts +0 -142
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLNodeFieldUtils.ts +0 -186
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +0 -77
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLRecordAdapter.ts +0 -90
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/layoutTransformUtils.ts +0 -236
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/linkUtils.ts +0 -14
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/paginationUtils.ts +0 -49
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/recordUtils.ts +0 -159
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/sanitizationUtils.ts +0 -50
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingPriceRange.ts +0 -64
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/index.ts +0 -120
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/About.tsx +0 -8
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/HelpCenter.tsx +0 -29
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +0 -100
- /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{applicationApi.ts → applications/applicationApi.ts} +0 -0
- /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{maintenanceRequestApi.ts → maintenanceRequests/maintenanceRequestApi.ts} +0 -0
- /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{propertyDetailGraphQL.ts → properties/propertyDetailGraphQL.ts} +0 -0
- /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{WeatherWidget.tsx → dashboard/WeatherWidget.tsx} +0 -0
- /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{NavMenu.tsx → layout/VerticalNav.tsx} +0 -0
- /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestIcon.tsx → maintenanceRequests/MaintenanceRequestIcon.tsx} +0 -0
- /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{StatusBadge.tsx → maintenanceRequests/StatusBadge.tsx} +0 -0
- /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertyMap.tsx → properties/PropertyMap.tsx} +0 -0
- /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertySearchFilters.tsx → properties/PropertySearchFilters.tsx} +0 -0
- /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/{features/global-search/types/search → types}/searchResults.ts +0 -0
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Adapts GraphQL UI API node shape to SearchResultRecordData so existing list UI
|
|
3
|
-
* (SearchResultCard, ResultCardFields, getNestedFieldValue) works unchanged.
|
|
4
|
-
*
|
|
5
|
-
* GraphQL node: { Id, Name: { value }, Owner: { Alias: { value } }, ... }
|
|
6
|
-
* SearchResultRecordData: { id, fields: Record<string, FieldValue>, apiName, ... }
|
|
7
|
-
* FieldValue: { displayValue, value }
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { FieldValue, SearchResultRecordData } from "../types/search/searchResults";
|
|
11
|
-
|
|
12
|
-
function isValueLeaf(obj: unknown): obj is { value: string | number | boolean | null } {
|
|
13
|
-
return (
|
|
14
|
-
typeof obj === "object" &&
|
|
15
|
-
obj !== null &&
|
|
16
|
-
"value" in obj &&
|
|
17
|
-
(Object.keys(obj).length === 1 || (Object.keys(obj).length === 2 && "displayValue" in obj))
|
|
18
|
-
);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function graphQLValueToFieldValue(val: unknown): FieldValue {
|
|
22
|
-
if (val === null || val === undefined) {
|
|
23
|
-
return { displayValue: null, value: null };
|
|
24
|
-
}
|
|
25
|
-
if (isValueLeaf(val)) {
|
|
26
|
-
const v = val.value;
|
|
27
|
-
const display =
|
|
28
|
-
typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? v : null;
|
|
29
|
-
return { displayValue: display as string | null, value: v };
|
|
30
|
-
}
|
|
31
|
-
if (typeof val === "object" && val !== null && !Array.isArray(val)) {
|
|
32
|
-
const nested = graphQLNodeToFields(val as Record<string, unknown>);
|
|
33
|
-
const firstFv = nested && Object.values(nested)[0];
|
|
34
|
-
const display =
|
|
35
|
-
firstFv && typeof (firstFv as FieldValue).value !== "object"
|
|
36
|
-
? ((firstFv as FieldValue).value as string | null)
|
|
37
|
-
: null;
|
|
38
|
-
return { displayValue: display, value: { fields: nested ?? {} } };
|
|
39
|
-
}
|
|
40
|
-
return { displayValue: null, value: val };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function graphQLNodeToFields(node: Record<string, unknown>): Record<string, FieldValue> {
|
|
44
|
-
const fields: Record<string, FieldValue> = {};
|
|
45
|
-
for (const [key, val] of Object.entries(node)) {
|
|
46
|
-
if (key === "Id" || val === undefined) continue;
|
|
47
|
-
fields[key] = graphQLValueToFieldValue(val);
|
|
48
|
-
}
|
|
49
|
-
return fields;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Converts a GraphQL connection node (from getRecordsGraphQL) to SearchResultRecordData
|
|
54
|
-
* so it can be passed to SearchResultCard and other components that expect the keyword-search record shape.
|
|
55
|
-
*/
|
|
56
|
-
export function graphQLNodeToSearchResultRecordData(
|
|
57
|
-
node: Record<string, unknown> | undefined,
|
|
58
|
-
objectApiName: string,
|
|
59
|
-
): SearchResultRecordData {
|
|
60
|
-
if (!node || typeof node !== "object") {
|
|
61
|
-
return {
|
|
62
|
-
id: "",
|
|
63
|
-
apiName: objectApiName,
|
|
64
|
-
childRelationships: {},
|
|
65
|
-
eTag: "",
|
|
66
|
-
fields: {},
|
|
67
|
-
lastModifiedById: null,
|
|
68
|
-
lastModifiedDate: null,
|
|
69
|
-
recordTypeId: null,
|
|
70
|
-
recordTypeInfo: null,
|
|
71
|
-
systemModstamp: null,
|
|
72
|
-
weakEtag: 0,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
const id = (node.Id as string) ?? "";
|
|
76
|
-
const fields = graphQLNodeToFields(node);
|
|
77
|
-
return {
|
|
78
|
-
id,
|
|
79
|
-
apiName: objectApiName,
|
|
80
|
-
childRelationships: {},
|
|
81
|
-
eTag: "",
|
|
82
|
-
fields,
|
|
83
|
-
lastModifiedById: null,
|
|
84
|
-
lastModifiedDate: null,
|
|
85
|
-
recordTypeId: null,
|
|
86
|
-
recordTypeInfo: null,
|
|
87
|
-
systemModstamp: null,
|
|
88
|
-
weakEtag: 0,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transforms Layout API sections into a structure for the detail form: section → rows → items.
|
|
3
|
-
* Uses layout response (sections, layoutRows, layoutItems, layoutComponents) and optional
|
|
4
|
-
* object info for compound field names and dataType. Section merge when useHeading === false.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { LayoutSection, LayoutRow, LayoutItem } from "../types/recordDetail/recordDetail";
|
|
8
|
-
|
|
9
|
-
const HIDE_EMPTY_SECTIONS = true;
|
|
10
|
-
const EMPTY_OPTIONS: PicklistOption[] = [];
|
|
11
|
-
|
|
12
|
-
export interface ObjectInfoField {
|
|
13
|
-
compoundFieldName?: string;
|
|
14
|
-
dataType?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ObjectInfo {
|
|
18
|
-
apiName?: string;
|
|
19
|
-
fields?: Record<string, ObjectInfoField>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Picklist/lookup options for a field (e.g. [{ label, value }]). */
|
|
23
|
-
export type PicklistOption = {
|
|
24
|
-
label: string | null;
|
|
25
|
-
value: string | number | boolean;
|
|
26
|
-
validFor?: unknown[];
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export interface LayoutTransformContext {
|
|
30
|
-
recordId: string;
|
|
31
|
-
objectInfo?: ObjectInfo | null;
|
|
32
|
-
lookupRecords?: Record<string, PicklistOption[] | null> | null;
|
|
33
|
-
getSectionCollapsedState: (sectionId: string) => boolean;
|
|
34
|
-
calculatePicklistValues?: (itemApiName: string, item: LayoutItem) => PicklistOption[] | null;
|
|
35
|
-
formOverrides?: { fieldVariant?: string } | null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface TransformedLayoutItem {
|
|
39
|
-
key: string;
|
|
40
|
-
isField: boolean;
|
|
41
|
-
label?: string;
|
|
42
|
-
required?: boolean;
|
|
43
|
-
readOnly?: boolean;
|
|
44
|
-
apiName?: string;
|
|
45
|
-
contextName?: string;
|
|
46
|
-
options?: PicklistOption[];
|
|
47
|
-
variant?: string;
|
|
48
|
-
dataType?: string;
|
|
49
|
-
layoutComponentApiNames?: string[];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** Single row in a section. */
|
|
53
|
-
export interface TransformedLayoutRow {
|
|
54
|
-
key: string;
|
|
55
|
-
layoutItems: TransformedLayoutItem[];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Section ready for Section/SectionRow rendering. */
|
|
59
|
-
export interface TransformedSection {
|
|
60
|
-
id: string;
|
|
61
|
-
key: string;
|
|
62
|
-
heading: string;
|
|
63
|
-
useHeading: boolean;
|
|
64
|
-
collapsible: boolean;
|
|
65
|
-
collapsed: boolean;
|
|
66
|
-
layoutRows: TransformedLayoutRow[];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function createSectionKey(index: number): string {
|
|
70
|
-
return "section-" + index;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function getTransformedSections(
|
|
74
|
-
sections: LayoutSection[],
|
|
75
|
-
transformContext: LayoutTransformContext,
|
|
76
|
-
): TransformedSection[] {
|
|
77
|
-
const calculatedSections: TransformedSection[] = [];
|
|
78
|
-
let previousSection: TransformedSection | null = null;
|
|
79
|
-
|
|
80
|
-
sections.forEach((section, index) => {
|
|
81
|
-
if (previousSection !== null && section.useHeading === false) {
|
|
82
|
-
const sectionKey = createSectionKey(index);
|
|
83
|
-
const appendedRows = section.layoutRows
|
|
84
|
-
.map((row, i) => rowTransform(row, i, sectionKey, transformContext))
|
|
85
|
-
.filter((r): r is TransformedLayoutRow => r !== null);
|
|
86
|
-
previousSection.layoutRows.push(...appendedRows);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const newSection = sectionTransform(section, index, transformContext);
|
|
91
|
-
if (newSection) {
|
|
92
|
-
calculatedSections.push(newSection);
|
|
93
|
-
previousSection = newSection;
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
return calculatedSections;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function sectionTransform(
|
|
101
|
-
section: LayoutSection,
|
|
102
|
-
index: number,
|
|
103
|
-
transformContext: LayoutTransformContext,
|
|
104
|
-
): TransformedSection | null {
|
|
105
|
-
const { getSectionCollapsedState } = transformContext;
|
|
106
|
-
const sectionKey = createSectionKey(index);
|
|
107
|
-
const layoutRows = section.layoutRows
|
|
108
|
-
.map((row, i) => rowTransform(row, i, sectionKey, transformContext))
|
|
109
|
-
.filter((r): r is TransformedLayoutRow => r !== null);
|
|
110
|
-
|
|
111
|
-
if (layoutRows.length === 0 && HIDE_EMPTY_SECTIONS) {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
key: sectionKey,
|
|
117
|
-
collapsible: section.collapsible,
|
|
118
|
-
collapsed: getSectionCollapsedState(section.id),
|
|
119
|
-
useHeading: section.useHeading,
|
|
120
|
-
heading: section.heading,
|
|
121
|
-
id: section.id,
|
|
122
|
-
layoutRows,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export function rowTransform(
|
|
127
|
-
row: LayoutRow,
|
|
128
|
-
index: number,
|
|
129
|
-
sectionKey: string,
|
|
130
|
-
transformContext: LayoutTransformContext,
|
|
131
|
-
): TransformedLayoutRow | null {
|
|
132
|
-
const layoutItems = row.layoutItems.map((item, i) => transformItem(item, i, transformContext));
|
|
133
|
-
|
|
134
|
-
const allItemsHaveNoComponents = layoutItems.every((item) => !item.apiName || !item.isField);
|
|
135
|
-
if (allItemsHaveNoComponents) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
key: sectionKey + "-" + index,
|
|
140
|
-
layoutItems,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function transformItem(
|
|
145
|
-
item: LayoutItem,
|
|
146
|
-
index: number,
|
|
147
|
-
transformContext: LayoutTransformContext,
|
|
148
|
-
): TransformedLayoutItem {
|
|
149
|
-
const { recordId, objectInfo, lookupRecords, calculatePicklistValues, formOverrides } =
|
|
150
|
-
transformContext;
|
|
151
|
-
|
|
152
|
-
let itemApiName: string | undefined;
|
|
153
|
-
let itemComponentType: string | undefined;
|
|
154
|
-
|
|
155
|
-
if (item.layoutComponents.length >= 1) {
|
|
156
|
-
const itemComponent = item.layoutComponents[0];
|
|
157
|
-
itemComponentType = itemComponent.componentType;
|
|
158
|
-
const componentApiName = itemComponent.apiName;
|
|
159
|
-
const topLevelCompoundName =
|
|
160
|
-
item.layoutComponents.length > 1 &&
|
|
161
|
-
componentApiName &&
|
|
162
|
-
objectInfo?.fields?.[componentApiName]?.compoundFieldName;
|
|
163
|
-
if (topLevelCompoundName) {
|
|
164
|
-
itemApiName = topLevelCompoundName;
|
|
165
|
-
} else {
|
|
166
|
-
itemApiName = componentApiName ?? undefined;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const lookupOptions =
|
|
171
|
-
itemApiName != null && lookupRecords?.[itemApiName] != null ? lookupRecords[itemApiName] : null;
|
|
172
|
-
|
|
173
|
-
const isFieldType = itemComponentType === "Field";
|
|
174
|
-
|
|
175
|
-
const options: PicklistOption[] =
|
|
176
|
-
lookupOptions ??
|
|
177
|
-
(itemApiName ? (calculatePicklistValues?.(itemApiName, item) ?? null) : null) ??
|
|
178
|
-
EMPTY_OPTIONS;
|
|
179
|
-
|
|
180
|
-
const fieldMeta = itemApiName ? objectInfo?.fields?.[itemApiName] : undefined;
|
|
181
|
-
const layoutComponentApiNames = item.layoutComponents
|
|
182
|
-
.filter((c) => c.componentType === "Field" && c.apiName != null)
|
|
183
|
-
.map((c) => c.apiName as string);
|
|
184
|
-
|
|
185
|
-
let newItem: TransformedLayoutItem = {
|
|
186
|
-
key: "item-" + index,
|
|
187
|
-
apiName: itemApiName,
|
|
188
|
-
contextName: recordId,
|
|
189
|
-
label: item.label,
|
|
190
|
-
required: item.required,
|
|
191
|
-
variant: formOverrides?.fieldVariant ?? "label-stacked",
|
|
192
|
-
readOnly: !item.editableForUpdate,
|
|
193
|
-
isField: isFieldType,
|
|
194
|
-
options,
|
|
195
|
-
dataType: fieldMeta?.dataType,
|
|
196
|
-
layoutComponentApiNames:
|
|
197
|
-
layoutComponentApiNames.length > 0 ? layoutComponentApiNames : undefined,
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
if (objectInfo?.apiName?.endsWith("__kav")) {
|
|
201
|
-
newItem = { ...newItem, readOnly: true };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (newItem.required === true && newItem.readOnly === true) {
|
|
205
|
-
newItem = { ...newItem, required: false };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return newItem;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function layoutReducer<T>(
|
|
212
|
-
sections: TransformedSection[],
|
|
213
|
-
reducer: (
|
|
214
|
-
acc: T,
|
|
215
|
-
ctx: {
|
|
216
|
-
section: TransformedSection;
|
|
217
|
-
layoutRow: TransformedLayoutRow;
|
|
218
|
-
layoutItem: TransformedLayoutItem;
|
|
219
|
-
},
|
|
220
|
-
) => T,
|
|
221
|
-
initialValue: T,
|
|
222
|
-
): T {
|
|
223
|
-
let accumulator = initialValue;
|
|
224
|
-
sections.forEach((section) =>
|
|
225
|
-
section.layoutRows.forEach((layoutRow) =>
|
|
226
|
-
layoutRow.layoutItems.forEach((layoutItem) => {
|
|
227
|
-
accumulator = reducer(accumulator, {
|
|
228
|
-
section,
|
|
229
|
-
layoutRow,
|
|
230
|
-
layoutItem,
|
|
231
|
-
});
|
|
232
|
-
}),
|
|
233
|
-
),
|
|
234
|
-
);
|
|
235
|
-
return accumulator;
|
|
236
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
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?.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?.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
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
// eslint-disable-next-line no-control-regex -- intentionally matching control chars for sanitization
|
|
47
|
-
sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
48
|
-
|
|
49
|
-
return sanitized;
|
|
50
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fetches the available min/max listing price for the current search (no price/bedroom filters).
|
|
3
|
-
* Use to render the filter bar only after knowing the available price range.
|
|
4
|
-
*/
|
|
5
|
-
import { useState, useEffect, useRef } from "react";
|
|
6
|
-
import { queryPropertyListingPriceRange } from "@/api/propertyListingGraphQL";
|
|
7
|
-
|
|
8
|
-
/** Cap for slider max when dataset has outliers; UI never sees a higher max. */
|
|
9
|
-
const SLIDER_PRICE_CAP = 50_000;
|
|
10
|
-
|
|
11
|
-
export interface PropertyListingPriceRange {
|
|
12
|
-
priceMin: number;
|
|
13
|
-
priceMax: number;
|
|
14
|
-
/** True when raw max was > cap and we capped for the slider (show "50,000+"). */
|
|
15
|
-
maxCapped?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Fallback when the price-range API call fails. */
|
|
19
|
-
const DEFAULT_PRICE_RANGE: PropertyListingPriceRange = { priceMin: 0, priceMax: 100_000 };
|
|
20
|
-
|
|
21
|
-
function capPriceRange(range: { priceMin: number; priceMax: number }): PropertyListingPriceRange {
|
|
22
|
-
if (range.priceMax <= SLIDER_PRICE_CAP)
|
|
23
|
-
return { priceMin: range.priceMin, priceMax: range.priceMax };
|
|
24
|
-
return {
|
|
25
|
-
priceMin: range.priceMin,
|
|
26
|
-
priceMax: SLIDER_PRICE_CAP,
|
|
27
|
-
maxCapped: true,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function usePropertyListingPriceRange(searchQuery: string): {
|
|
32
|
-
priceRange: PropertyListingPriceRange | null;
|
|
33
|
-
loading: boolean;
|
|
34
|
-
error: string | null;
|
|
35
|
-
} {
|
|
36
|
-
const [priceRange, setPriceRange] = useState<PropertyListingPriceRange | null>(null);
|
|
37
|
-
const [loading, setLoading] = useState(true);
|
|
38
|
-
const [error, setError] = useState<string | null>(null);
|
|
39
|
-
const cancelledRef = useRef(false);
|
|
40
|
-
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
cancelledRef.current = false;
|
|
43
|
-
setLoading(true);
|
|
44
|
-
setError(null);
|
|
45
|
-
queryPropertyListingPriceRange(searchQuery)
|
|
46
|
-
.then((range) => {
|
|
47
|
-
if (!cancelledRef.current) setPriceRange(range ? capPriceRange(range) : null);
|
|
48
|
-
})
|
|
49
|
-
.catch((err) => {
|
|
50
|
-
if (!cancelledRef.current) {
|
|
51
|
-
setError(err instanceof Error ? err.message : "Failed to load price range");
|
|
52
|
-
setPriceRange(capPriceRange(DEFAULT_PRICE_RANGE));
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
.finally(() => {
|
|
56
|
-
if (!cancelledRef.current) setLoading(false);
|
|
57
|
-
});
|
|
58
|
-
return () => {
|
|
59
|
-
cancelledRef.current = true;
|
|
60
|
-
};
|
|
61
|
-
}, [searchQuery]);
|
|
62
|
-
|
|
63
|
-
return { priceRange, loading, error };
|
|
64
|
-
}
|