@salesforce/webapp-template-app-react-sample-b2x-experimental 1.68.0 → 1.69.0
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/data/Lease__c.json +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
- package/dist/package.json +1 -1
- package/package.json +2 -2
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object Search Data Hooks
|
|
3
|
+
*
|
|
4
|
+
* - useObjectListMetadata: single source for list-view metadata (filters → columns + picklists). Use in list pages to avoid duplicate state and API calls.
|
|
5
|
+
* - useObjectColumns / useObjectFilters: thin wrappers over useObjectListMetadata for backward compatibility.
|
|
6
|
+
* - getSharedFilters: module-level deduplication for getObjectListFilters across hook instances.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useState, useEffect, useRef, useMemo } from "react";
|
|
10
|
+
import { objectInfoService, type SearchParams } from "../api/objectInfoService";
|
|
11
|
+
import type {
|
|
12
|
+
Column,
|
|
13
|
+
SearchResultRecord,
|
|
14
|
+
SearchResultRecordData,
|
|
15
|
+
} from "../types/search/searchResults";
|
|
16
|
+
import type { Filter, FilterCriteria } from "../types/filters/filters";
|
|
17
|
+
import type { PicklistValue } from "../types/filters/picklist";
|
|
18
|
+
import { createFiltersKey } from "../utils/cacheUtils";
|
|
19
|
+
|
|
20
|
+
// --- Shared filters cache (deduplicates getObjectListFilters across useObjectColumns + useObjectFilters) ---
|
|
21
|
+
const sharedFiltersCache = new Map<string, Filter[]>();
|
|
22
|
+
const sharedFiltersInFlight = new Map<string, Promise<Filter[]>>();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns filters for the object, deduplicating the API call across hook instances.
|
|
26
|
+
* Does not pass abort signal to the API so the shared request is not aborted when
|
|
27
|
+
* one consumer's effect cleans up (e.g. React Strict Mode); callers still guard with isCancelled.
|
|
28
|
+
*/
|
|
29
|
+
function getSharedFilters(objectApiName: string): Promise<Filter[]> {
|
|
30
|
+
const cached = sharedFiltersCache.get(objectApiName);
|
|
31
|
+
if (cached) return Promise.resolve(cached);
|
|
32
|
+
const inFlight = sharedFiltersInFlight.get(objectApiName);
|
|
33
|
+
if (inFlight) return inFlight;
|
|
34
|
+
const promise = objectInfoService
|
|
35
|
+
.getObjectListFilters(objectApiName)
|
|
36
|
+
.then((filters) => {
|
|
37
|
+
sharedFiltersCache.set(objectApiName, filters);
|
|
38
|
+
sharedFiltersInFlight.delete(objectApiName);
|
|
39
|
+
return filters;
|
|
40
|
+
})
|
|
41
|
+
.catch((err) => {
|
|
42
|
+
sharedFiltersInFlight.delete(objectApiName);
|
|
43
|
+
throw err;
|
|
44
|
+
});
|
|
45
|
+
sharedFiltersInFlight.set(objectApiName, promise);
|
|
46
|
+
return promise;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Shared Types ---
|
|
50
|
+
export interface FiltersData {
|
|
51
|
+
filters: Filter[];
|
|
52
|
+
picklistValues: Record<string, PicklistValue[]>;
|
|
53
|
+
loading: boolean;
|
|
54
|
+
error: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Derives column definitions from filter definitions for list/result UI.
|
|
59
|
+
*/
|
|
60
|
+
function filtersToColumns(filters: Filter[]): Column[] {
|
|
61
|
+
return filters.map((f) => ({
|
|
62
|
+
fieldApiName: f.targetFieldPath,
|
|
63
|
+
label: f.label,
|
|
64
|
+
searchable: true,
|
|
65
|
+
sortable: true,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ObjectListMetadata {
|
|
70
|
+
columns: Column[];
|
|
71
|
+
filters: Filter[];
|
|
72
|
+
picklistValues: Record<string, PicklistValue[]>;
|
|
73
|
+
loading: boolean;
|
|
74
|
+
error: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Single hook for list-view metadata: filters (shared API), derived columns, and picklist values.
|
|
79
|
+
* Use this in list/search pages to avoid duplicate useObjectColumns + useObjectFilters and duplicate state.
|
|
80
|
+
*/
|
|
81
|
+
export function useObjectListMetadata(objectApiName: string | null): ObjectListMetadata {
|
|
82
|
+
const [state, setState] = useState<{
|
|
83
|
+
columns: Column[];
|
|
84
|
+
filters: Filter[];
|
|
85
|
+
picklistValues: Record<string, PicklistValue[]>;
|
|
86
|
+
loading: boolean;
|
|
87
|
+
error: string | null;
|
|
88
|
+
}>({
|
|
89
|
+
columns: [],
|
|
90
|
+
filters: [],
|
|
91
|
+
picklistValues: {},
|
|
92
|
+
loading: true,
|
|
93
|
+
error: null,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!objectApiName) {
|
|
98
|
+
setState((s) => ({ ...s, loading: false, error: "Invalid object" }));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let isCancelled = false;
|
|
103
|
+
const ac = new AbortController();
|
|
104
|
+
|
|
105
|
+
const run = async () => {
|
|
106
|
+
setState((s) => ({ ...s, loading: true, error: null }));
|
|
107
|
+
try {
|
|
108
|
+
const filters = await getSharedFilters(objectApiName!);
|
|
109
|
+
if (isCancelled) return;
|
|
110
|
+
|
|
111
|
+
const selectFilters = filters.filter((f) => f.affordance?.toLowerCase() === "select");
|
|
112
|
+
const picklistPromises = selectFilters.map((f) =>
|
|
113
|
+
objectInfoService
|
|
114
|
+
.getPicklistValues(objectApiName!, f.targetFieldPath, undefined, ac.signal)
|
|
115
|
+
.then((values) => ({ fieldPath: f.targetFieldPath, values }))
|
|
116
|
+
.catch((err) => {
|
|
117
|
+
if (err?.name === "AbortError") throw err;
|
|
118
|
+
return { fieldPath: f.targetFieldPath, values: [] as PicklistValue[] };
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
const picklistResults = await Promise.all(picklistPromises);
|
|
122
|
+
if (isCancelled) return;
|
|
123
|
+
|
|
124
|
+
const picklistValues: Record<string, PicklistValue[]> = {};
|
|
125
|
+
picklistResults.forEach(({ fieldPath, values }) => {
|
|
126
|
+
picklistValues[fieldPath] = values;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
setState({
|
|
130
|
+
columns: filtersToColumns(filters),
|
|
131
|
+
filters,
|
|
132
|
+
picklistValues,
|
|
133
|
+
loading: false,
|
|
134
|
+
error: null,
|
|
135
|
+
});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (isCancelled || (err instanceof Error && err.name === "AbortError")) return;
|
|
138
|
+
setState((s) => ({
|
|
139
|
+
...s,
|
|
140
|
+
columns: [],
|
|
141
|
+
filters: [],
|
|
142
|
+
picklistValues: {},
|
|
143
|
+
loading: false,
|
|
144
|
+
error: "Failed to load list metadata",
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
run();
|
|
150
|
+
return () => {
|
|
151
|
+
isCancelled = true;
|
|
152
|
+
ac.abort();
|
|
153
|
+
};
|
|
154
|
+
}, [objectApiName]);
|
|
155
|
+
|
|
156
|
+
return state;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Hook: useObjectColumns
|
|
161
|
+
* Thin wrapper over useObjectListMetadata for backward compatibility.
|
|
162
|
+
*/
|
|
163
|
+
export function useObjectColumns(objectApiName: string | null) {
|
|
164
|
+
const { columns, loading, error } = useObjectListMetadata(objectApiName);
|
|
165
|
+
return {
|
|
166
|
+
columns: objectApiName ? columns : [],
|
|
167
|
+
columnsLoading: loading,
|
|
168
|
+
columnsError: error,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Hook: useObjectSearchResults
|
|
174
|
+
*
|
|
175
|
+
* Fetches search results for a specific object based on the provided query parameters.
|
|
176
|
+
* Maintains the *latest* result set for the object in state to prevent redundant
|
|
177
|
+
* network requests when the component re-renders with the same parameters.
|
|
178
|
+
* Includes debouncing for search queries (but not pagination).
|
|
179
|
+
*
|
|
180
|
+
* @param objectApiName - The API name of the object to search
|
|
181
|
+
* @param searchQuery - The search query string
|
|
182
|
+
* @param searchPageSize - Number of results per page (default: 50)
|
|
183
|
+
* @param searchPageToken - Pagination token (default: '0')
|
|
184
|
+
* @param filters - Array of filter criteria to apply (default: [])
|
|
185
|
+
* @param sortBy - Sort field and direction (default: 'relevance')
|
|
186
|
+
* @returns Object containing results array, pagination tokens, loading state, and error state
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```tsx
|
|
190
|
+
* const { results, nextPageToken, previousPageToken, currentPageToken, resultsLoading, resultsError } = useObjectSearchResults(
|
|
191
|
+
* 'Account',
|
|
192
|
+
* 'test query',
|
|
193
|
+
* 25,
|
|
194
|
+
* '0',
|
|
195
|
+
* [{ objectApiName: 'Account', fieldPath: 'Name', operator: 'contains', values: ['test'] }]
|
|
196
|
+
* );
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
export function useObjectSearchResults(
|
|
200
|
+
objectApiName: string | null,
|
|
201
|
+
searchQuery: string,
|
|
202
|
+
searchPageSize: number = 50,
|
|
203
|
+
searchPageToken: string = "0",
|
|
204
|
+
filters: FilterCriteria[] = [],
|
|
205
|
+
sortBy: string = "relevance",
|
|
206
|
+
) {
|
|
207
|
+
const [resultsCache, setResultsCache] = useState<
|
|
208
|
+
Record<
|
|
209
|
+
string,
|
|
210
|
+
{
|
|
211
|
+
results: SearchResultRecord[];
|
|
212
|
+
query: string;
|
|
213
|
+
pageToken: string;
|
|
214
|
+
pageSize: number;
|
|
215
|
+
filtersKey: string;
|
|
216
|
+
sortBy: string;
|
|
217
|
+
nextPageToken: string | null;
|
|
218
|
+
previousPageToken: string | null;
|
|
219
|
+
currentPageToken: string;
|
|
220
|
+
}
|
|
221
|
+
>
|
|
222
|
+
>({});
|
|
223
|
+
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
|
224
|
+
const [error, setError] = useState<Record<string, string | null>>({});
|
|
225
|
+
|
|
226
|
+
const debounceTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
227
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
228
|
+
const resultsCacheRef = useRef(resultsCache);
|
|
229
|
+
|
|
230
|
+
const filtersKey = useMemo(() => {
|
|
231
|
+
const filtersArray = Array.isArray(filters) ? filters : [];
|
|
232
|
+
return createFiltersKey(filtersArray);
|
|
233
|
+
}, [filters]);
|
|
234
|
+
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
resultsCacheRef.current = resultsCache;
|
|
237
|
+
}, [resultsCache]);
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (!objectApiName || !searchQuery.trim()) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let isCancelled = false;
|
|
245
|
+
const abortController = new AbortController();
|
|
246
|
+
|
|
247
|
+
if (abortControllerRef.current) {
|
|
248
|
+
abortControllerRef.current.abort();
|
|
249
|
+
}
|
|
250
|
+
abortControllerRef.current = abortController;
|
|
251
|
+
|
|
252
|
+
if (debounceTimeout.current) {
|
|
253
|
+
clearTimeout(debounceTimeout.current);
|
|
254
|
+
debounceTimeout.current = null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const cached = resultsCacheRef.current[objectApiName];
|
|
258
|
+
if (
|
|
259
|
+
!abortController.signal.aborted &&
|
|
260
|
+
cached &&
|
|
261
|
+
cached.query === searchQuery &&
|
|
262
|
+
cached.pageToken === searchPageToken &&
|
|
263
|
+
cached.pageSize === searchPageSize &&
|
|
264
|
+
cached.filtersKey === filtersKey &&
|
|
265
|
+
cached.sortBy === sortBy
|
|
266
|
+
) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (abortController.signal.aborted) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const fetchResults = async () => {
|
|
275
|
+
setLoading((prev) => ({ ...prev, [objectApiName]: true }));
|
|
276
|
+
setError((prev) => ({ ...prev, [objectApiName]: null }));
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const searchParams: SearchParams = {
|
|
280
|
+
sortBy: sortBy === "relevance" ? "" : sortBy,
|
|
281
|
+
filters: filters,
|
|
282
|
+
pageSize: searchPageSize,
|
|
283
|
+
pageToken: searchPageToken,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const keywordSearchResult = await objectInfoService.searchResults(
|
|
287
|
+
searchQuery,
|
|
288
|
+
objectApiName,
|
|
289
|
+
searchParams,
|
|
290
|
+
abortController.signal,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (isCancelled || abortController.signal.aborted) return;
|
|
294
|
+
|
|
295
|
+
const normalizedRecords = keywordSearchResult.records.map((r) => ({
|
|
296
|
+
record: r.record as SearchResultRecordData,
|
|
297
|
+
highlightInfo: r.highlightInfo,
|
|
298
|
+
searchInfo: r.searchInfo,
|
|
299
|
+
}));
|
|
300
|
+
|
|
301
|
+
const nextPageToken: string | null = keywordSearchResult.nextPageToken ?? null;
|
|
302
|
+
const previousPageToken: string | null = keywordSearchResult.previousPageToken ?? null;
|
|
303
|
+
|
|
304
|
+
setResultsCache((prev): typeof prev => ({
|
|
305
|
+
...prev,
|
|
306
|
+
[objectApiName]: {
|
|
307
|
+
results: normalizedRecords,
|
|
308
|
+
query: searchQuery,
|
|
309
|
+
pageToken: searchPageToken,
|
|
310
|
+
pageSize: searchPageSize,
|
|
311
|
+
filtersKey: filtersKey,
|
|
312
|
+
sortBy,
|
|
313
|
+
nextPageToken,
|
|
314
|
+
previousPageToken,
|
|
315
|
+
currentPageToken: keywordSearchResult.currentPageToken,
|
|
316
|
+
},
|
|
317
|
+
}));
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
setError((prev) => ({ ...prev, [objectApiName]: "Unable to load search results" }));
|
|
323
|
+
// Cache empty result so we skip refetch on remount (avoid infinite loop on API error)
|
|
324
|
+
setResultsCache((prev) => ({
|
|
325
|
+
...prev,
|
|
326
|
+
[objectApiName]: {
|
|
327
|
+
results: [],
|
|
328
|
+
query: searchQuery,
|
|
329
|
+
pageToken: searchPageToken,
|
|
330
|
+
pageSize: searchPageSize,
|
|
331
|
+
filtersKey: filtersKey,
|
|
332
|
+
sortBy,
|
|
333
|
+
nextPageToken: null,
|
|
334
|
+
previousPageToken: null,
|
|
335
|
+
currentPageToken: searchPageToken,
|
|
336
|
+
},
|
|
337
|
+
}));
|
|
338
|
+
} finally {
|
|
339
|
+
if (!isCancelled) {
|
|
340
|
+
setLoading((prev) => ({ ...prev, [objectApiName]: false }));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
if (searchPageToken === "0") {
|
|
346
|
+
debounceTimeout.current = setTimeout(() => {
|
|
347
|
+
fetchResults();
|
|
348
|
+
}, 300);
|
|
349
|
+
} else {
|
|
350
|
+
fetchResults();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return () => {
|
|
354
|
+
isCancelled = true;
|
|
355
|
+
abortController.abort();
|
|
356
|
+
if (debounceTimeout.current) {
|
|
357
|
+
clearTimeout(debounceTimeout.current);
|
|
358
|
+
debounceTimeout.current = null;
|
|
359
|
+
}
|
|
360
|
+
if (abortControllerRef.current === abortController) {
|
|
361
|
+
abortControllerRef.current = null;
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}, [objectApiName, searchQuery, searchPageSize, searchPageToken, filtersKey, sortBy]);
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
results: objectApiName ? resultsCache[objectApiName]?.results || [] : [],
|
|
368
|
+
nextPageToken: objectApiName ? resultsCache[objectApiName]?.nextPageToken || null : null,
|
|
369
|
+
previousPageToken: objectApiName
|
|
370
|
+
? resultsCache[objectApiName]?.previousPageToken || null
|
|
371
|
+
: null,
|
|
372
|
+
currentPageToken: objectApiName ? resultsCache[objectApiName]?.currentPageToken || "0" : "0",
|
|
373
|
+
resultsLoading: objectApiName ? loading[objectApiName] || false : false,
|
|
374
|
+
resultsError: objectApiName ? error[objectApiName] || null : null,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Hook: useObjectFilters
|
|
380
|
+
* Thin wrapper over useObjectListMetadata for backward compatibility.
|
|
381
|
+
*/
|
|
382
|
+
export function useObjectFilters(objectApiName: string | null) {
|
|
383
|
+
const { filters, picklistValues, loading, error } = useObjectListMetadata(objectApiName);
|
|
384
|
+
const filtersData: Record<string, FiltersData> = objectApiName
|
|
385
|
+
? {
|
|
386
|
+
[objectApiName]: {
|
|
387
|
+
filters,
|
|
388
|
+
picklistValues,
|
|
389
|
+
loading,
|
|
390
|
+
error,
|
|
391
|
+
},
|
|
392
|
+
}
|
|
393
|
+
: {};
|
|
394
|
+
return { filtersData };
|
|
395
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches Address__c for each property id from search results.
|
|
3
|
+
* Returns propertyId -> address for display on listing cards.
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useEffect } from "react";
|
|
6
|
+
import { fetchPropertyAddresses } from "@/api/propertyDetailGraphQL";
|
|
7
|
+
import { getPropertyIdFromRecord } from "./usePropertyPrimaryImages";
|
|
8
|
+
import type { SearchResultRecord } from "@/types/search/searchResults";
|
|
9
|
+
|
|
10
|
+
export function usePropertyAddresses(results: SearchResultRecord[]): Record<string, string> {
|
|
11
|
+
const [map, setMap] = useState<Record<string, string>>({});
|
|
12
|
+
|
|
13
|
+
const propertyIds = results
|
|
14
|
+
.map((r) => r?.record && getPropertyIdFromRecord(r.record))
|
|
15
|
+
.filter((id): id is string => Boolean(id));
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (propertyIds.length === 0) {
|
|
19
|
+
setMap({});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
fetchPropertyAddresses([...new Set(propertyIds)])
|
|
24
|
+
.then((next) => {
|
|
25
|
+
if (!cancelled) setMap(next);
|
|
26
|
+
})
|
|
27
|
+
.catch(() => {
|
|
28
|
+
if (!cancelled) setMap({});
|
|
29
|
+
});
|
|
30
|
+
return () => {
|
|
31
|
+
cancelled = true;
|
|
32
|
+
};
|
|
33
|
+
}, [propertyIds.join(",")]);
|
|
34
|
+
|
|
35
|
+
return map;
|
|
36
|
+
}
|
package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches Property_Listing__c by id, then related Property__c, images, costs, and features.
|
|
3
|
+
*/
|
|
4
|
+
import { useState, useEffect, useCallback } from "react";
|
|
5
|
+
import {
|
|
6
|
+
fetchListingById,
|
|
7
|
+
fetchPropertyById,
|
|
8
|
+
fetchImagesByPropertyId,
|
|
9
|
+
fetchCostsByPropertyId,
|
|
10
|
+
fetchFeaturesByPropertyId,
|
|
11
|
+
type ListingDetail,
|
|
12
|
+
type PropertyDetail,
|
|
13
|
+
type PropertyImageRecord,
|
|
14
|
+
type PropertyCostRecord,
|
|
15
|
+
type PropertyFeatureRecord,
|
|
16
|
+
} from "@/api/propertyDetailGraphQL";
|
|
17
|
+
|
|
18
|
+
export interface PropertyDetailState {
|
|
19
|
+
listing: ListingDetail | null;
|
|
20
|
+
property: PropertyDetail | null;
|
|
21
|
+
images: PropertyImageRecord[];
|
|
22
|
+
costs: PropertyCostRecord[];
|
|
23
|
+
features: PropertyFeatureRecord[];
|
|
24
|
+
loading: boolean;
|
|
25
|
+
error: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function usePropertyDetail(
|
|
29
|
+
listingId: string | undefined,
|
|
30
|
+
): PropertyDetailState & { refetch: () => void } {
|
|
31
|
+
const [listing, setListing] = useState<ListingDetail | null>(null);
|
|
32
|
+
const [property, setProperty] = useState<PropertyDetail | null>(null);
|
|
33
|
+
const [images, setImages] = useState<PropertyImageRecord[]>([]);
|
|
34
|
+
const [costs, setCosts] = useState<PropertyCostRecord[]>([]);
|
|
35
|
+
const [features, setFeatures] = useState<PropertyFeatureRecord[]>([]);
|
|
36
|
+
const [loading, setLoading] = useState(true);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
|
|
39
|
+
const load = useCallback(async () => {
|
|
40
|
+
if (!listingId?.trim()) {
|
|
41
|
+
setListing(null);
|
|
42
|
+
setProperty(null);
|
|
43
|
+
setImages([]);
|
|
44
|
+
setCosts([]);
|
|
45
|
+
setFeatures([]);
|
|
46
|
+
setLoading(false);
|
|
47
|
+
setError(null);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
setLoading(true);
|
|
51
|
+
setError(null);
|
|
52
|
+
try {
|
|
53
|
+
const listingData = await fetchListingById(listingId);
|
|
54
|
+
setListing(listingData ?? null);
|
|
55
|
+
if (!listingData?.propertyId) {
|
|
56
|
+
setProperty(null);
|
|
57
|
+
setImages([]);
|
|
58
|
+
setCosts([]);
|
|
59
|
+
setFeatures([]);
|
|
60
|
+
setLoading(false);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const [propertyData, imagesData, costsData, featuresData] = await Promise.all([
|
|
64
|
+
fetchPropertyById(listingData.propertyId),
|
|
65
|
+
fetchImagesByPropertyId(listingData.propertyId),
|
|
66
|
+
fetchCostsByPropertyId(listingData.propertyId),
|
|
67
|
+
fetchFeaturesByPropertyId(listingData.propertyId),
|
|
68
|
+
]);
|
|
69
|
+
setProperty(propertyData ?? null);
|
|
70
|
+
setImages(imagesData ?? []);
|
|
71
|
+
setCosts(costsData ?? []);
|
|
72
|
+
setFeatures(featuresData ?? []);
|
|
73
|
+
} catch (e) {
|
|
74
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
75
|
+
setListing(null);
|
|
76
|
+
setProperty(null);
|
|
77
|
+
setImages([]);
|
|
78
|
+
setCosts([]);
|
|
79
|
+
setFeatures([]);
|
|
80
|
+
} finally {
|
|
81
|
+
setLoading(false);
|
|
82
|
+
}
|
|
83
|
+
}, [listingId]);
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
load();
|
|
87
|
+
}, [load]);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
listing,
|
|
91
|
+
property,
|
|
92
|
+
images,
|
|
93
|
+
costs,
|
|
94
|
+
features,
|
|
95
|
+
loading,
|
|
96
|
+
error,
|
|
97
|
+
refetch: load,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property Listing search via GraphQL. Shows all when no search term.
|
|
3
|
+
*/
|
|
4
|
+
import { useState, useEffect, useRef, useCallback } from "react";
|
|
5
|
+
import { queryPropertyListingsGraphQL } from "@/api/propertyListingGraphQL";
|
|
6
|
+
import type { SearchResultRecord } from "@/types/search/searchResults";
|
|
7
|
+
|
|
8
|
+
export function usePropertyListingSearch(searchQuery: string, pageSize: number, pageToken: string) {
|
|
9
|
+
const [results, setResults] = useState<SearchResultRecord[]>([]);
|
|
10
|
+
const [nextPageToken, setNextPageToken] = useState<string | null>(null);
|
|
11
|
+
const [previousPageToken, setPreviousPageToken] = useState<string | null>(null);
|
|
12
|
+
const [loading, setLoading] = useState(false);
|
|
13
|
+
const [error, setError] = useState<string | null>(null);
|
|
14
|
+
|
|
15
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
16
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
17
|
+
|
|
18
|
+
const fetchResults = useCallback(async () => {
|
|
19
|
+
const ac = new AbortController();
|
|
20
|
+
abortControllerRef.current = ac;
|
|
21
|
+
setLoading(true);
|
|
22
|
+
setError(null);
|
|
23
|
+
try {
|
|
24
|
+
const afterCursor = pageToken === "0" || pageToken === "" ? null : pageToken;
|
|
25
|
+
const result = await queryPropertyListingsGraphQL(
|
|
26
|
+
searchQuery,
|
|
27
|
+
pageSize,
|
|
28
|
+
afterCursor,
|
|
29
|
+
ac.signal,
|
|
30
|
+
);
|
|
31
|
+
if (ac.signal.aborted) return;
|
|
32
|
+
setResults(result.records);
|
|
33
|
+
setNextPageToken(result.nextPageToken);
|
|
34
|
+
setPreviousPageToken(result.previousPageToken);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (ac.signal.aborted || (err instanceof Error && err.name === "AbortError")) return;
|
|
37
|
+
setError(err instanceof Error ? err.message : "Unable to load property listings");
|
|
38
|
+
setResults([]);
|
|
39
|
+
setNextPageToken(null);
|
|
40
|
+
setPreviousPageToken(null);
|
|
41
|
+
} finally {
|
|
42
|
+
if (!ac.signal.aborted) setLoading(false);
|
|
43
|
+
}
|
|
44
|
+
}, [searchQuery, pageSize, pageToken]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (debounceRef.current) {
|
|
48
|
+
clearTimeout(debounceRef.current);
|
|
49
|
+
debounceRef.current = null;
|
|
50
|
+
}
|
|
51
|
+
if (abortControllerRef.current) {
|
|
52
|
+
abortControllerRef.current.abort();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// When on first page, debounce to avoid firing on every keystroke
|
|
56
|
+
if (pageToken === "0" || pageToken === "") {
|
|
57
|
+
debounceRef.current = setTimeout(() => fetchResults(), 300);
|
|
58
|
+
} else {
|
|
59
|
+
fetchResults();
|
|
60
|
+
}
|
|
61
|
+
return () => {
|
|
62
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
63
|
+
abortControllerRef.current?.abort();
|
|
64
|
+
};
|
|
65
|
+
}, [searchQuery, pageSize, pageToken, fetchResults]);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
results,
|
|
69
|
+
nextPageToken,
|
|
70
|
+
previousPageToken,
|
|
71
|
+
currentPageToken: pageToken === "" ? "0" : pageToken,
|
|
72
|
+
resultsLoading: loading,
|
|
73
|
+
resultsError: error,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches property addresses for the current page of results only, geocodes them in parallel,
|
|
3
|
+
* and returns map markers (one pin per property in the current window).
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useEffect } from "react";
|
|
6
|
+
import { fetchPropertyAddresses } from "@/api/propertyDetailGraphQL";
|
|
7
|
+
import { geocodeAddress } from "@/utils/geocode";
|
|
8
|
+
import { getPropertyIdFromRecord } from "./usePropertyPrimaryImages";
|
|
9
|
+
import type { SearchResultRecord } from "@/types/search/searchResults";
|
|
10
|
+
import type { MapMarker } from "@/components/PropertyMap";
|
|
11
|
+
|
|
12
|
+
function getListingName(record: {
|
|
13
|
+
fields?: Record<string, { value?: unknown; displayValue?: string | null }>;
|
|
14
|
+
}): string {
|
|
15
|
+
const f = record.fields?.Name;
|
|
16
|
+
if (!f || typeof f !== "object") return "Property";
|
|
17
|
+
if (f.displayValue != null && f.displayValue !== "") return String(f.displayValue);
|
|
18
|
+
if (f.value != null && typeof f.value === "string") return f.value;
|
|
19
|
+
return "Property";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function usePropertyMapMarkers(results: SearchResultRecord[]): {
|
|
23
|
+
markers: MapMarker[];
|
|
24
|
+
loading: boolean;
|
|
25
|
+
} {
|
|
26
|
+
const [markers, setMarkers] = useState<MapMarker[]>([]);
|
|
27
|
+
const [loading, setLoading] = useState(false);
|
|
28
|
+
|
|
29
|
+
// Only the current page / current window of results
|
|
30
|
+
const propertyIds = results
|
|
31
|
+
.map((r) => r?.record && getPropertyIdFromRecord(r.record))
|
|
32
|
+
.filter((id): id is string => Boolean(id));
|
|
33
|
+
const propertyIdToLabel = new Map<string, string>();
|
|
34
|
+
for (const r of results) {
|
|
35
|
+
if (!r?.record) continue;
|
|
36
|
+
const id = getPropertyIdFromRecord(r.record);
|
|
37
|
+
if (id && !propertyIdToLabel.has(id)) {
|
|
38
|
+
propertyIdToLabel.set(id, getListingName(r.record));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (propertyIds.length === 0) {
|
|
44
|
+
setMarkers([]);
|
|
45
|
+
setLoading(false);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let cancelled = false;
|
|
49
|
+
setLoading(true);
|
|
50
|
+
const uniqIds = [...new Set(propertyIds)];
|
|
51
|
+
fetchPropertyAddresses(uniqIds)
|
|
52
|
+
.then((idToAddress) => {
|
|
53
|
+
if (cancelled) return;
|
|
54
|
+
const toGeocode = Object.entries(idToAddress).filter(
|
|
55
|
+
([, addr]) => addr != null && addr.trim() !== "",
|
|
56
|
+
);
|
|
57
|
+
if (toGeocode.length === 0) {
|
|
58
|
+
setMarkers([]);
|
|
59
|
+
setLoading(false);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Geocode all addresses in parallel (only current window of results)
|
|
63
|
+
Promise.all(
|
|
64
|
+
toGeocode.map(([id, address]) =>
|
|
65
|
+
geocodeAddress(address.replace(/\n/g, ", ")).then((coords) =>
|
|
66
|
+
coords ? { id, coords } : null,
|
|
67
|
+
),
|
|
68
|
+
),
|
|
69
|
+
)
|
|
70
|
+
.then((resolved) => {
|
|
71
|
+
if (cancelled) return;
|
|
72
|
+
const nextMarkers: MapMarker[] = resolved
|
|
73
|
+
.filter((r): r is { id: string; coords: { lat: number; lng: number } } => r != null)
|
|
74
|
+
.map(({ id, coords }) => ({
|
|
75
|
+
lat: coords.lat,
|
|
76
|
+
lng: coords.lng,
|
|
77
|
+
label: propertyIdToLabel.get(id) ?? "Property",
|
|
78
|
+
}));
|
|
79
|
+
setMarkers(nextMarkers);
|
|
80
|
+
})
|
|
81
|
+
.catch(() => {
|
|
82
|
+
if (!cancelled) setMarkers([]);
|
|
83
|
+
})
|
|
84
|
+
.finally(() => {
|
|
85
|
+
if (!cancelled) setLoading(false);
|
|
86
|
+
});
|
|
87
|
+
})
|
|
88
|
+
.catch(() => {
|
|
89
|
+
if (!cancelled) {
|
|
90
|
+
setMarkers([]);
|
|
91
|
+
setLoading(false);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return () => {
|
|
95
|
+
cancelled = true;
|
|
96
|
+
};
|
|
97
|
+
}, [propertyIds.join(",")]);
|
|
98
|
+
|
|
99
|
+
return { markers, loading };
|
|
100
|
+
}
|