@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
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface UseAsyncDataResult<T> {
|
|
4
|
+
data: T | null;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
error: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface CacheOptions {
|
|
10
|
+
/** Unique cache key. Used for lookups and invalidation via `clearCacheEntry`. */
|
|
11
|
+
key: string;
|
|
12
|
+
/** Time-to-live in ms. Default: 30_000 (30s) */
|
|
13
|
+
ttl?: number;
|
|
14
|
+
/** Max entries in the cache. Default: 50. Evicts oldest entry when exceeded. */
|
|
15
|
+
maxSize?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CacheEntry {
|
|
19
|
+
data: unknown;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Module-level cache shared across all useCachedAsyncData consumers.
|
|
25
|
+
* Cleared automatically on page reload since it only lives in memory.
|
|
26
|
+
* Keys are caller-provided so different hook call-sites get independent entries.
|
|
27
|
+
*/
|
|
28
|
+
const cache = new Map<string, CacheEntry>();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns a cached entry if it exists and hasn't exceeded its TTL.
|
|
32
|
+
* Expired entries are deleted lazily here rather than on a timer,
|
|
33
|
+
* so there's no background cleanup overhead.
|
|
34
|
+
*/
|
|
35
|
+
function getValidEntry(key: string, ttl: number): CacheEntry | undefined {
|
|
36
|
+
const entry = cache.get(key);
|
|
37
|
+
if (!entry) return undefined;
|
|
38
|
+
if (Date.now() - entry.timestamp > ttl) {
|
|
39
|
+
cache.delete(key);
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Stores a result in the cache. If the cache is at capacity and the key
|
|
47
|
+
* is new, the oldest entry (first in Map insertion order — FIFO) is evicted.
|
|
48
|
+
*/
|
|
49
|
+
function setEntry(key: string, data: unknown, maxSize: number): void {
|
|
50
|
+
if (!cache.has(key) && cache.size >= maxSize) {
|
|
51
|
+
const firstKey = cache.keys().next().value;
|
|
52
|
+
if (firstKey !== undefined) cache.delete(firstKey);
|
|
53
|
+
}
|
|
54
|
+
cache.set(key, { data, timestamp: Date.now() });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Removes a single cache entry by key. The key must match the
|
|
59
|
+
* `options.key` passed to `useCachedAsyncData`.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* // Hook:
|
|
63
|
+
* useCachedAsyncData(() => fetchAccountDetail(id), [id], { key: `account:${id}` });
|
|
64
|
+
*
|
|
65
|
+
* // Invalidate after a mutation:
|
|
66
|
+
* await updateAccount(id, fields);
|
|
67
|
+
* clearCacheEntry(`account:${id}`);
|
|
68
|
+
*/
|
|
69
|
+
export function clearCacheEntry(key: string): void {
|
|
70
|
+
cache.delete(key);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Removes all cache entries. Useful for global invalidation scenarios
|
|
75
|
+
* like user logout or after a bulk operation.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* async function handleLogout() {
|
|
79
|
+
* await logout();
|
|
80
|
+
* clearCache();
|
|
81
|
+
* navigate("/login");
|
|
82
|
+
* }
|
|
83
|
+
*/
|
|
84
|
+
export function clearCache(): void {
|
|
85
|
+
cache.clear();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Async data hook with in-memory caching. Works like `useAsyncData` but
|
|
90
|
+
* avoids redundant network calls when the same data is requested again
|
|
91
|
+
* (e.g. navigating away from a page and back).
|
|
92
|
+
*
|
|
93
|
+
* Cache behaviour:
|
|
94
|
+
* - **Key**: Provided via `options.key`. Must be unique per logical data source.
|
|
95
|
+
* Use the same key with `clearCacheEntry` to invalidate.
|
|
96
|
+
* - **Hit**: Data is returned synchronously on the initial render —
|
|
97
|
+
* `loading` starts as `false`, so there's no flash of a loading state.
|
|
98
|
+
* - **Miss**: The fetcher runs, and the result is stored for future hits.
|
|
99
|
+
* - **TTL**: Entries expire after `options.ttl` ms (default 30 s).
|
|
100
|
+
* Expiry is checked lazily on read, not on a timer.
|
|
101
|
+
* - **Max size**: Oldest entries are evicted FIFO when the cache exceeds
|
|
102
|
+
* `options.maxSize` (default 50).
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // Cache picklist options for 5 minutes (data rarely changes)
|
|
106
|
+
* const { data: types } = useCachedAsyncData(fetchDistinctTypes, [], {
|
|
107
|
+
* key: "distinctTypes",
|
|
108
|
+
* ttl: 300_000,
|
|
109
|
+
* });
|
|
110
|
+
*
|
|
111
|
+
* // Cache search results with default 30 s TTL (back-nav returns instantly)
|
|
112
|
+
* const { data } = useCachedAsyncData(
|
|
113
|
+
* () => searchAccounts({ where, orderBy, first, after }),
|
|
114
|
+
* [where, orderBy, first, after],
|
|
115
|
+
* { key: `accounts:${JSON.stringify({ where, orderBy, first, after })}` },
|
|
116
|
+
* );
|
|
117
|
+
*
|
|
118
|
+
* // Invalidate a specific entry after a mutation
|
|
119
|
+
* await updateAccount(id, fields);
|
|
120
|
+
* clearCacheEntry(`account:${id}`);
|
|
121
|
+
*
|
|
122
|
+
* // Or clear everything (e.g. on logout)
|
|
123
|
+
* clearCache();
|
|
124
|
+
*/
|
|
125
|
+
export function useCachedAsyncData<T>(
|
|
126
|
+
fetcher: () => Promise<T>,
|
|
127
|
+
deps: React.DependencyList,
|
|
128
|
+
options: CacheOptions,
|
|
129
|
+
): UseAsyncDataResult<T> {
|
|
130
|
+
const ttl = options.ttl ?? 30_000;
|
|
131
|
+
const maxSize = options.maxSize ?? 50;
|
|
132
|
+
const cacheKey = options.key;
|
|
133
|
+
|
|
134
|
+
// Synchronous cache check during state initialization so a cache hit
|
|
135
|
+
// never triggers a loading → loaded transition (avoids UI flicker).
|
|
136
|
+
const cached = getValidEntry(cacheKey, ttl);
|
|
137
|
+
|
|
138
|
+
const [data, setData] = useState<T | null>((cached?.data as T) ?? null);
|
|
139
|
+
const [loading, setLoading] = useState(!cached);
|
|
140
|
+
const [error, setError] = useState<string | null>(null);
|
|
141
|
+
|
|
142
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps --- deps are explicitly managed by the caller
|
|
143
|
+
const memoizedFetcher = useCallback(fetcher, deps);
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
// Re-check the cache inside the effect because deps may have changed
|
|
147
|
+
// since the initial render (e.g. StrictMode double-invoke).
|
|
148
|
+
const entry = getValidEntry(cacheKey, ttl);
|
|
149
|
+
if (entry) {
|
|
150
|
+
setData(entry.data as T);
|
|
151
|
+
setLoading(false);
|
|
152
|
+
setError(null);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// No cache hit — fetch from the network.
|
|
157
|
+
let cancelled = false;
|
|
158
|
+
setLoading(true);
|
|
159
|
+
setError(null);
|
|
160
|
+
|
|
161
|
+
memoizedFetcher()
|
|
162
|
+
.then((result) => {
|
|
163
|
+
if (!cancelled) {
|
|
164
|
+
setEntry(cacheKey, result, maxSize);
|
|
165
|
+
setData(result);
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
.catch((err) => {
|
|
169
|
+
console.error(err);
|
|
170
|
+
if (!cancelled) setError(err instanceof Error ? err.message : "An error occurred");
|
|
171
|
+
})
|
|
172
|
+
.finally(() => {
|
|
173
|
+
if (!cancelled) setLoading(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Cleanup: if deps change or the component unmounts before the fetch
|
|
177
|
+
// completes, the cancelled flag prevents stale state updates.
|
|
178
|
+
return () => {
|
|
179
|
+
cancelled = true;
|
|
180
|
+
};
|
|
181
|
+
}, [memoizedFetcher, cacheKey, ttl, maxSize]);
|
|
182
|
+
|
|
183
|
+
return { data, loading, error };
|
|
184
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useSearchParams } from "react-router";
|
|
3
|
+
import type { FilterFieldConfig, ActiveFilterValue } from "../utils/filterUtils";
|
|
4
|
+
import type { SortFieldConfig, SortState } from "../utils/sortUtils";
|
|
5
|
+
import { filtersToSearchParams, searchParamsToFilters, buildFilter } from "../utils/filterUtils";
|
|
6
|
+
import { buildOrderBy } from "../utils/sortUtils";
|
|
7
|
+
import { debounce } from "../utils/debounce";
|
|
8
|
+
|
|
9
|
+
/** How long to wait before flushing local state changes to the URL. */
|
|
10
|
+
const URL_SYNC_DEBOUNCE_MS = 300;
|
|
11
|
+
|
|
12
|
+
export interface PaginationConfig {
|
|
13
|
+
defaultPageSize: number;
|
|
14
|
+
validPageSizes: number[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseObjectSearchParamsReturn<TFilter, TOrderBy> {
|
|
18
|
+
filters: {
|
|
19
|
+
active: ActiveFilterValue[];
|
|
20
|
+
set: (field: string, value: ActiveFilterValue | undefined) => void;
|
|
21
|
+
remove: (field: string) => void;
|
|
22
|
+
};
|
|
23
|
+
sort: {
|
|
24
|
+
current: SortState | null;
|
|
25
|
+
set: (sort: SortState | null) => void;
|
|
26
|
+
};
|
|
27
|
+
query: { where: TFilter; orderBy: TOrderBy };
|
|
28
|
+
pagination: {
|
|
29
|
+
pageSize: number;
|
|
30
|
+
pageIndex: number;
|
|
31
|
+
afterCursor: string | undefined;
|
|
32
|
+
setPageSize: (size: number) => void;
|
|
33
|
+
goToNextPage: (cursor: string) => void;
|
|
34
|
+
goToPreviousPage: () => void;
|
|
35
|
+
};
|
|
36
|
+
resetAll: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Manages filter, sort, and cursor-based pagination state for an object search page.
|
|
41
|
+
*
|
|
42
|
+
* ## State model
|
|
43
|
+
* Local React state is the primary driver for instant UI updates.
|
|
44
|
+
* URL search params act as the durable source of truth so that a page
|
|
45
|
+
* refresh or shared link restores the same view. Changes are synced to
|
|
46
|
+
* the URL via a debounced write (300 ms) to avoid excessive history entries.
|
|
47
|
+
*
|
|
48
|
+
* ## Return shape
|
|
49
|
+
* Returns memoized groups so each group's reference is stable unless its
|
|
50
|
+
* contents change — safe to pass directly as props to `React.memo` children.
|
|
51
|
+
*
|
|
52
|
+
* - `filters` — active filter values + set/remove callbacks
|
|
53
|
+
* - `sort` — current sort state + set callback
|
|
54
|
+
* - `query` — derived `where` / `orderBy` objects ready for the API
|
|
55
|
+
* - `pagination` — page size, page index, cursor, and navigation callbacks
|
|
56
|
+
* - `resetAll` — clears all filters, sort, and pagination in one call
|
|
57
|
+
*/
|
|
58
|
+
export function useObjectSearchParams<TFilter, TOrderBy>(
|
|
59
|
+
filterConfigs: FilterFieldConfig[],
|
|
60
|
+
_sortConfigs?: SortFieldConfig[],
|
|
61
|
+
paginationConfig?: PaginationConfig,
|
|
62
|
+
) {
|
|
63
|
+
const defaultPageSize = paginationConfig?.defaultPageSize ?? 10;
|
|
64
|
+
const validPageSizes = paginationConfig?.validPageSizes ?? [defaultPageSize];
|
|
65
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
66
|
+
|
|
67
|
+
// Seed local state from URL on initial load
|
|
68
|
+
const initial = useMemo(
|
|
69
|
+
() => searchParamsToFilters(searchParams, filterConfigs),
|
|
70
|
+
// Only run on mount — local state takes over after that, no deps needed
|
|
71
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
72
|
+
[],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const [filters, setFilters] = useState<ActiveFilterValue[]>(initial.filters);
|
|
76
|
+
const [sort, setLocalSort] = useState<SortState | null>(initial.sort);
|
|
77
|
+
|
|
78
|
+
// Pagination — cursor-based with a stack to support "previous page" navigation.
|
|
79
|
+
const getValidPageSize = (size: number) =>
|
|
80
|
+
validPageSizes.includes(size) ? size : defaultPageSize;
|
|
81
|
+
|
|
82
|
+
const [pageSize, setPageSizeState] = useState<number>(
|
|
83
|
+
getValidPageSize(initial.pageSize ?? defaultPageSize),
|
|
84
|
+
);
|
|
85
|
+
const [pageIndex, setPageIndex] = useState(initial.pageIndex);
|
|
86
|
+
const [afterCursor, setAfterCursor] = useState<string | undefined>(undefined);
|
|
87
|
+
const cursorStackRef = useRef<string[]>([]);
|
|
88
|
+
|
|
89
|
+
// Debounced URL sync — keeps URL in sync without blocking the UI
|
|
90
|
+
const syncToUrl = useCallback(
|
|
91
|
+
(
|
|
92
|
+
nextFilters: ActiveFilterValue[],
|
|
93
|
+
nextSort: SortState | null,
|
|
94
|
+
nextPageSize?: number,
|
|
95
|
+
nextPageIndex?: number,
|
|
96
|
+
) => {
|
|
97
|
+
const params = filtersToSearchParams(nextFilters, nextSort, nextPageSize, nextPageIndex);
|
|
98
|
+
setSearchParams(params, { replace: true });
|
|
99
|
+
},
|
|
100
|
+
[setSearchParams],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const debouncedSyncRef = useRef(debounce(syncToUrl, URL_SYNC_DEBOUNCE_MS));
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
debouncedSyncRef.current = debounce(syncToUrl, URL_SYNC_DEBOUNCE_MS);
|
|
106
|
+
}, [syncToUrl]);
|
|
107
|
+
|
|
108
|
+
// Snapshot ref — lets callbacks read the latest state without being
|
|
109
|
+
// recreated on every render (avoids infinite useCallback chains).
|
|
110
|
+
const stateRef = useRef({ filters, sort, pageSize, pageIndex });
|
|
111
|
+
stateRef.current = { filters, sort, pageSize, pageIndex };
|
|
112
|
+
|
|
113
|
+
// Any filter/sort change resets pagination to the first page.
|
|
114
|
+
const resetPagination = useCallback(() => {
|
|
115
|
+
setPageIndex(0);
|
|
116
|
+
setAfterCursor(undefined);
|
|
117
|
+
cursorStackRef.current = [];
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
// -- Filter callbacks -------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
const setFilter = useCallback(
|
|
123
|
+
(field: string, value: ActiveFilterValue | undefined) => {
|
|
124
|
+
const { sort: s, pageSize: ps } = stateRef.current;
|
|
125
|
+
setFilters((prev) => {
|
|
126
|
+
const next = prev.filter((f) => f.field !== field);
|
|
127
|
+
if (value) next.push(value);
|
|
128
|
+
debouncedSyncRef.current(next, s, ps);
|
|
129
|
+
return next;
|
|
130
|
+
});
|
|
131
|
+
resetPagination();
|
|
132
|
+
},
|
|
133
|
+
[resetPagination],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const removeFilter = useCallback(
|
|
137
|
+
(field: string) => {
|
|
138
|
+
const { sort: s, pageSize: ps } = stateRef.current;
|
|
139
|
+
setFilters((prev) => {
|
|
140
|
+
const next = prev.filter((f) => f.field !== field);
|
|
141
|
+
debouncedSyncRef.current(next, s, ps);
|
|
142
|
+
return next;
|
|
143
|
+
});
|
|
144
|
+
resetPagination();
|
|
145
|
+
},
|
|
146
|
+
[resetPagination],
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// -- Sort callback ----------------------------------------------------------
|
|
150
|
+
|
|
151
|
+
const setSort = useCallback(
|
|
152
|
+
(nextSort: SortState | null) => {
|
|
153
|
+
const { filters: f, pageSize: ps } = stateRef.current;
|
|
154
|
+
setLocalSort(nextSort);
|
|
155
|
+
debouncedSyncRef.current(f, nextSort, ps);
|
|
156
|
+
resetPagination();
|
|
157
|
+
},
|
|
158
|
+
[resetPagination],
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// -- Reset ------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
const resetAll = useCallback(() => {
|
|
164
|
+
setFilters([]);
|
|
165
|
+
setLocalSort(null);
|
|
166
|
+
resetPagination();
|
|
167
|
+
syncToUrl([], null, defaultPageSize, 0);
|
|
168
|
+
setPageSizeState(defaultPageSize);
|
|
169
|
+
}, [syncToUrl, resetPagination]);
|
|
170
|
+
|
|
171
|
+
// -- Pagination callbacks ---------------------------------------------------
|
|
172
|
+
// Uses a cursor stack to track visited pages. "Next" pushes the current
|
|
173
|
+
// endCursor onto the stack; "Previous" pops it to restore the prior cursor.
|
|
174
|
+
|
|
175
|
+
const goToNextPage = useCallback((endCursor: string) => {
|
|
176
|
+
cursorStackRef.current = [...cursorStackRef.current, endCursor];
|
|
177
|
+
setAfterCursor(endCursor);
|
|
178
|
+
setPageIndex((prev) => {
|
|
179
|
+
const nextIndex = prev + 1;
|
|
180
|
+
const { filters: f, sort: s, pageSize: ps } = stateRef.current;
|
|
181
|
+
debouncedSyncRef.current(f, s, ps, nextIndex);
|
|
182
|
+
return nextIndex;
|
|
183
|
+
});
|
|
184
|
+
}, []);
|
|
185
|
+
|
|
186
|
+
const goToPreviousPage = useCallback(() => {
|
|
187
|
+
const stack = cursorStackRef.current;
|
|
188
|
+
const next = stack.slice(0, -1);
|
|
189
|
+
cursorStackRef.current = next;
|
|
190
|
+
setAfterCursor(next.length > 0 ? next[next.length - 1] : undefined);
|
|
191
|
+
setPageIndex((prev) => {
|
|
192
|
+
const nextIndex = Math.max(0, prev - 1);
|
|
193
|
+
const { filters: f, sort: s, pageSize: ps } = stateRef.current;
|
|
194
|
+
debouncedSyncRef.current(f, s, ps, nextIndex);
|
|
195
|
+
return nextIndex;
|
|
196
|
+
});
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
const setPageSize = useCallback(
|
|
200
|
+
(newSize: number) => {
|
|
201
|
+
const validated = getValidPageSize(newSize);
|
|
202
|
+
const { filters: f, sort: s } = stateRef.current;
|
|
203
|
+
setPageSizeState(validated);
|
|
204
|
+
resetPagination();
|
|
205
|
+
debouncedSyncRef.current(f, s, validated);
|
|
206
|
+
},
|
|
207
|
+
[resetPagination],
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// -- Derived query objects ---------------------------------------------------
|
|
211
|
+
// Translate local filter/sort state into API-ready `where` and `orderBy`.
|
|
212
|
+
|
|
213
|
+
const where = useMemo(
|
|
214
|
+
() => buildFilter<TFilter>(filters, filterConfigs),
|
|
215
|
+
[filters, filterConfigs],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const orderBy = useMemo(() => buildOrderBy<TOrderBy>(sort), [sort]);
|
|
219
|
+
|
|
220
|
+
// -- Memoized return groups -------------------------------------------------
|
|
221
|
+
// Each group is individually memoized so its object reference stays stable
|
|
222
|
+
// unless the contained values change. This makes it safe to pass a group
|
|
223
|
+
// (e.g. `pagination`) directly as props to a React.memo child without
|
|
224
|
+
// causing unnecessary re-renders.
|
|
225
|
+
|
|
226
|
+
const filtersGroup = useMemo(
|
|
227
|
+
() => ({ active: filters, set: setFilter, remove: removeFilter }),
|
|
228
|
+
[filters, setFilter, removeFilter],
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const sortGroup = useMemo(() => ({ current: sort, set: setSort }), [sort, setSort]);
|
|
232
|
+
|
|
233
|
+
const query = useMemo(() => ({ where, orderBy }), [where, orderBy]);
|
|
234
|
+
|
|
235
|
+
const pagination = useMemo(
|
|
236
|
+
() => ({ pageSize, pageIndex, afterCursor, setPageSize, goToNextPage, goToPreviousPage }),
|
|
237
|
+
[pageSize, pageIndex, afterCursor, setPageSize, goToNextPage, goToPreviousPage],
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
filters: filtersGroup,
|
|
242
|
+
sort: sortGroup,
|
|
243
|
+
query,
|
|
244
|
+
pagination,
|
|
245
|
+
resetAll,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a debounced version of the provided function.
|
|
3
|
+
*
|
|
4
|
+
* Each call to the returned function resets the internal timer. The wrapped
|
|
5
|
+
* function is only invoked once the timer expires without being reset. This
|
|
6
|
+
* makes it ideal for rate-limiting high-frequency events like input changes.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam T - The function signature to debounce.
|
|
9
|
+
* @param fn - The function to debounce.
|
|
10
|
+
* @param ms - The debounce delay in milliseconds.
|
|
11
|
+
* @returns A new function with the same signature that delays execution.
|
|
12
|
+
*/
|
|
13
|
+
export function debounce<T extends (...args: never[]) => void>(
|
|
14
|
+
fn: T,
|
|
15
|
+
ms: number,
|
|
16
|
+
): (...args: Parameters<T>) => void {
|
|
17
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
18
|
+
return (...args: Parameters<T>) => {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
timer = setTimeout(() => fn(...args), ms);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function fieldValue(
|
|
2
|
+
field: { displayValue?: string | null; value?: unknown } | null | undefined,
|
|
3
|
+
): string | null {
|
|
4
|
+
if (field?.displayValue != null) return field.displayValue;
|
|
5
|
+
if (field?.value != null) return String(field.value);
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getAddressFieldLines(address: {
|
|
10
|
+
street?: string | null;
|
|
11
|
+
city?: string | null;
|
|
12
|
+
state?: string | null;
|
|
13
|
+
postalCode?: string | null;
|
|
14
|
+
country?: string | null;
|
|
15
|
+
}) {
|
|
16
|
+
const cityStateZip = [address.city, address.state].filter(Boolean).join(", ");
|
|
17
|
+
const cityStateZipLine = [cityStateZip, address.postalCode].filter(Boolean).join(" ");
|
|
18
|
+
const lines = [address.street, cityStateZipLine, address.country].filter(Boolean);
|
|
19
|
+
if (lines.length === 0) return null;
|
|
20
|
+
return lines;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatDateTimeField(
|
|
24
|
+
value?: string | null,
|
|
25
|
+
...args: Parameters<Date["toLocaleString"]>
|
|
26
|
+
) {
|
|
27
|
+
if (!value) return null;
|
|
28
|
+
return new Date(value).toLocaleString(...args);
|
|
29
|
+
}
|