@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.
Files changed (138) hide show
  1. package/dist/CHANGELOG.md +19 -0
  2. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +6 -5
  3. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphql-operations-types.ts +12058 -214
  4. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +18 -15
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{propertyListingGraphQL.ts → properties/propertyListingGraphQL.ts} +1 -1
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +4 -2
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{TopBar.tsx → layout/TopBar.tsx} +2 -2
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestList.tsx → maintenanceRequests/MaintenanceRequestList.tsx} +4 -4
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestListItem.tsx → maintenanceRequests/MaintenanceRequestListItem.tsx} +3 -3
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/maintenanceRequests/MaintenanceSummaryDetailsModal.tsx +87 -0
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertyListingCard.tsx → properties/PropertyListingCard.tsx} +1 -1
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/{features/global-search/components/search/SearchPagination.tsx → components/properties/PropertyListingSearchPagination.tsx} +20 -28
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants/propertyListing.ts +4 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/accountSearchService.ts +46 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +19 -0
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +19 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +121 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/api/query/searchAccounts.graphql +51 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +357 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/AccountSearch.tsx +303 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/__examples__/pages/Home.tsx +34 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/api/objectSearchService.ts +84 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/ActiveFilters.tsx +89 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/FilterContext.tsx +73 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/ObjectBreadcrumb.tsx +66 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/PaginationControls.tsx +109 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/SearchBar.tsx +41 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/SortControl.tsx +143 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/BooleanFilter.tsx +74 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/DateFilter.tsx +121 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/DateRangeFilter.tsx +69 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/MultiSelectFilter.tsx +98 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/NumericRangeFilter.tsx +85 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/SearchFilter.tsx +37 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/SelectFilter.tsx +93 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/components/filters/TextFilter.tsx +74 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useAsyncData.ts +54 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useCachedAsyncData.ts +184 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/hooks/useObjectSearchParams.ts +247 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/debounce.ts +22 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/fieldUtils.ts +29 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/filterUtils.ts +372 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/object-search/utils/sortUtils.ts +38 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +1 -1
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +2 -2
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +1 -1
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingAmenities.ts +2 -2
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +2 -2
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +3 -3
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +2 -2
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +3 -3
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +2 -2
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +4 -2
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +2 -2
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +1 -1
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +12 -10
  57. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +6 -18
  58. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/propertyListingPaginationUtils.ts +18 -0
  59. package/dist/package-lock.json +2 -2
  60. package/dist/package.json +1 -1
  61. package/dist/scripts/graphql-search.sh +69 -17
  62. package/package.json +1 -1
  63. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/MaintenanceDetailsModal.tsx +0 -128
  64. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectDetailService.ts +0 -102
  65. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectInfoGraphQLService.ts +0 -137
  66. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/objectInfoService.ts +0 -95
  67. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/api/recordListGraphQLService.ts +0 -364
  68. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailFields.tsx +0 -55
  69. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailForm.tsx +0 -146
  70. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailHeader.tsx +0 -34
  71. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/DetailLayoutSections.tsx +0 -80
  72. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/Section.tsx +0 -108
  73. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/SectionRow.tsx +0 -20
  74. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/UiApiDetailForm.tsx +0 -140
  75. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +0 -73
  76. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +0 -29
  77. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +0 -17
  78. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +0 -24
  79. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedText.tsx +0 -11
  80. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +0 -29
  81. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterField.tsx +0 -54
  82. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterInput.tsx +0 -55
  83. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FilterSelect.tsx +0 -72
  84. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/filters/FiltersPanel.tsx +0 -380
  85. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/forms/filters-form.tsx +0 -114
  86. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/forms/submit-button.tsx +0 -47
  87. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/GlobalSearchInput.tsx +0 -114
  88. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/ResultCardFields.tsx +0 -71
  89. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchHeader.tsx +0 -31
  90. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchResultCard.tsx +0 -138
  91. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/search/SearchResultsPanel.tsx +0 -197
  92. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/components/shared/LoadingFallback.tsx +0 -61
  93. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/constants.ts +0 -39
  94. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/filters/FilterInput.tsx +0 -55
  95. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/filters/FilterSelect.tsx +0 -72
  96. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/form.tsx +0 -209
  97. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useObjectInfoBatch.ts +0 -72
  98. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useObjectSearchData.ts +0 -174
  99. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useRecordDetailLayout.ts +0 -137
  100. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/hooks/useRecordListGraphQL.ts +0 -135
  101. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/pages/DetailPage.tsx +0 -109
  102. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/pages/GlobalSearch.tsx +0 -235
  103. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/filters/filters.ts +0 -121
  104. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/filters/picklist.ts +0 -6
  105. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/objectInfo/objectInfo.ts +0 -49
  106. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/recordDetail/recordDetail.ts +0 -61
  107. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/types/schema.d.ts +0 -200
  108. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/apiUtils.ts +0 -59
  109. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/cacheUtils.ts +0 -76
  110. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/debounce.ts +0 -90
  111. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/fieldUtils.ts +0 -354
  112. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/fieldValueExtractor.ts +0 -67
  113. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/filterUtils.ts +0 -32
  114. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/formDataTransformUtils.ts +0 -260
  115. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/formUtils.ts +0 -142
  116. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLNodeFieldUtils.ts +0 -186
  117. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +0 -77
  118. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/graphQLRecordAdapter.ts +0 -90
  119. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/layoutTransformUtils.ts +0 -236
  120. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/linkUtils.ts +0 -14
  121. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/paginationUtils.ts +0 -49
  122. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/recordUtils.ts +0 -159
  123. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/utils/sanitizationUtils.ts +0 -50
  124. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingPriceRange.ts +0 -64
  125. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/index.ts +0 -120
  126. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/About.tsx +0 -8
  127. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/HelpCenter.tsx +0 -29
  128. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +0 -100
  129. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{applicationApi.ts → applications/applicationApi.ts} +0 -0
  130. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{maintenanceRequestApi.ts → maintenanceRequests/maintenanceRequestApi.ts} +0 -0
  131. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/{propertyDetailGraphQL.ts → properties/propertyDetailGraphQL.ts} +0 -0
  132. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{WeatherWidget.tsx → dashboard/WeatherWidget.tsx} +0 -0
  133. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{NavMenu.tsx → layout/VerticalNav.tsx} +0 -0
  134. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{MaintenanceRequestIcon.tsx → maintenanceRequests/MaintenanceRequestIcon.tsx} +0 -0
  135. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{StatusBadge.tsx → maintenanceRequests/StatusBadge.tsx} +0 -0
  136. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertyMap.tsx → properties/PropertyMap.tsx} +0 -0
  137. /package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/{PropertySearchFilters.tsx → properties/PropertySearchFilters.tsx} +0 -0
  138. /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
+ }