@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.
Files changed (131) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/data/Lease__c.json +13 -0
  3. package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
  4. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
  57. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
  58. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
  59. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
  60. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
  61. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
  62. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
  63. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
  64. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
  65. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
  66. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
  67. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
  68. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
  69. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
  70. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
  71. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
  72. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
  73. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
  74. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
  75. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
  76. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
  77. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
  78. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
  79. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
  80. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
  81. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
  82. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
  83. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
  84. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
  85. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
  86. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
  87. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
  88. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
  89. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
  90. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
  91. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
  92. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
  93. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
  94. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
  95. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
  96. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
  97. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
  98. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
  99. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
  100. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
  101. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
  102. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
  103. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
  104. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
  105. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
  106. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
  107. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
  108. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
  109. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
  110. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
  111. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
  112. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
  113. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
  114. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
  115. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
  116. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
  117. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
  118. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
  119. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
  120. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
  121. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
  122. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
  123. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
  124. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
  125. package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
  126. package/dist/package.json +1 -1
  127. package/package.json +2 -2
  128. package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
  129. package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
  130. package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
  131. package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Fetches primary image URL for each property id from search results.
3
+ * Returns a map of propertyId -> imageUrl for use in listing cards.
4
+ */
5
+ import { useState, useEffect } from "react";
6
+ import { fetchPrimaryImagesByPropertyIds } from "@/api/propertyDetailGraphQL";
7
+ import type { SearchResultRecord } from "@/types/search/searchResults";
8
+
9
+ export function getPropertyIdFromRecord(record: {
10
+ fields?: Record<string, { value?: unknown }>;
11
+ }): string | null {
12
+ const f = record.fields?.Property__c;
13
+ if (!f || typeof f !== "object") return null;
14
+ const v = (f as { value?: unknown }).value;
15
+ return typeof v === "string" ? v : null;
16
+ }
17
+
18
+ export function usePropertyPrimaryImages(
19
+ results: SearchResultRecord[],
20
+ ): Record<string, string> & { loading: boolean } {
21
+ const [map, setMap] = useState<Record<string, string>>({});
22
+ const [loading, setLoading] = useState(false);
23
+
24
+ const propertyIds = results
25
+ .map((r) => r?.record && getPropertyIdFromRecord(r.record))
26
+ .filter((id): id is string => Boolean(id));
27
+
28
+ useEffect(() => {
29
+ if (propertyIds.length === 0) {
30
+ setMap({});
31
+ return;
32
+ }
33
+ let cancelled = false;
34
+ setLoading(true);
35
+ fetchPrimaryImagesByPropertyIds(propertyIds)
36
+ .then((next) => {
37
+ if (!cancelled) setMap(next);
38
+ })
39
+ .catch(() => {
40
+ if (!cancelled) setMap({});
41
+ })
42
+ .finally(() => {
43
+ if (!cancelled) setLoading(false);
44
+ });
45
+ return () => {
46
+ cancelled = true;
47
+ };
48
+ }, [propertyIds.join(",")]);
49
+
50
+ return Object.assign(map, { loading });
51
+ }
@@ -0,0 +1,156 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { objectDetailService } from "../api/objectDetailService";
3
+ import type { LayoutResponse } from "../types/recordDetail/recordDetail";
4
+ import type { ObjectInfoResult } from "../types/objectInfo/objectInfo";
5
+ import type { GraphQLRecordNode } from "../api/recordListGraphQLService";
6
+
7
+ export interface UseRecordDetailLayoutReturn {
8
+ layout: LayoutResponse | null;
9
+ record: GraphQLRecordNode | null;
10
+ objectMetadata: ObjectInfoResult | null;
11
+ loading: boolean;
12
+ error: string | null;
13
+ }
14
+
15
+ export interface UseRecordDetailLayoutParams {
16
+ objectApiName: string | null;
17
+ recordId: string | null;
18
+ recordTypeId?: string | null;
19
+ /** When provided, skips the fetch and uses this data (avoids duplicate API calls when parent already fetched). Callers should memoize this (e.g. useMemo) to avoid unnecessary effect runs. */
20
+ initialData?: {
21
+ layout: LayoutResponse;
22
+ record: GraphQLRecordNode;
23
+ objectMetadata: ObjectInfoResult;
24
+ } | null;
25
+ }
26
+
27
+ const MAX_CACHE_SIZE = 50;
28
+ /** Cache entries older than this are treated as stale and refetched. */
29
+ const CACHE_TTL_MS = 5 * 60 * 1000;
30
+
31
+ type CacheEntry = {
32
+ layout: LayoutResponse;
33
+ record: GraphQLRecordNode;
34
+ objectMetadata: ObjectInfoResult;
35
+ cachedAt: number;
36
+ };
37
+
38
+ /**
39
+ * Detail page data: layout (REST), object metadata (GraphQL), single record (GraphQL).
40
+ *
41
+ * Calls objectDetailService.getRecordDetail once per objectApiName/recordId/recordTypeId.
42
+ * Caches result in memory (TTL 5min, max 50 entries). Used by DetailPage and UiApiDetailForm.
43
+ *
44
+ * @param objectApiName - Object API name.
45
+ * @param recordId - Record Id.
46
+ * @param recordTypeId - Optional record type (default master).
47
+ * @returns { layout, record, objectMetadata, loading, error }.
48
+ */
49
+ export function useRecordDetailLayout({
50
+ objectApiName,
51
+ recordId,
52
+ recordTypeId = null,
53
+ initialData = null,
54
+ }: UseRecordDetailLayoutParams): UseRecordDetailLayoutReturn {
55
+ const [layout, setLayout] = useState<LayoutResponse | null>(initialData?.layout ?? null);
56
+ const [record, setRecord] = useState<GraphQLRecordNode | null>(initialData?.record ?? null);
57
+ const [objectMetadata, setObjectMetadata] = useState<ObjectInfoResult | null>(
58
+ initialData?.objectMetadata ?? null,
59
+ );
60
+ const [loading, setLoading] = useState(!initialData);
61
+ const [error, setError] = useState<string | null>(null);
62
+
63
+ const cacheKey =
64
+ objectApiName && recordId ? `${objectApiName}:${recordId}:${recordTypeId ?? "default"}` : null;
65
+ const cacheRef = useRef<Map<string, CacheEntry>>(new Map());
66
+
67
+ useEffect(() => {
68
+ if (!objectApiName || !recordId) {
69
+ setError("Invalid object or record ID");
70
+ setLoading(false);
71
+ return;
72
+ }
73
+
74
+ // Skip fetch when parent already provided data (avoids duplicate API calls)
75
+ if (
76
+ initialData?.layout != null &&
77
+ initialData?.record != null &&
78
+ initialData?.objectMetadata != null
79
+ ) {
80
+ return;
81
+ }
82
+
83
+ const cached = cacheRef.current.get(cacheKey!);
84
+ const now = Date.now();
85
+ if (cached && now - cached.cachedAt < CACHE_TTL_MS) {
86
+ setLayout(cached.layout);
87
+ setRecord(cached.record);
88
+ setObjectMetadata(cached.objectMetadata);
89
+ setLoading(false);
90
+ setError(null);
91
+ return;
92
+ }
93
+
94
+ let isCancelled = false;
95
+ const abortController = new AbortController();
96
+
97
+ const fetchDetail = async () => {
98
+ setLoading(true);
99
+ setError(null);
100
+
101
+ try {
102
+ const {
103
+ layout: layoutData,
104
+ record: recordData,
105
+ objectMetadata: objectMetadataData,
106
+ } = await objectDetailService.getRecordDetail(
107
+ objectApiName,
108
+ recordId,
109
+ recordTypeId ?? undefined,
110
+ abortController.signal,
111
+ );
112
+
113
+ if (isCancelled) return;
114
+
115
+ const cache = cacheRef.current;
116
+ if (cache.size >= MAX_CACHE_SIZE) {
117
+ const firstKey = cache.keys().next().value;
118
+ if (firstKey != null) cache.delete(firstKey);
119
+ }
120
+ cache.set(cacheKey!, {
121
+ layout: layoutData,
122
+ record: recordData,
123
+ objectMetadata: objectMetadataData,
124
+ cachedAt: Date.now(),
125
+ });
126
+ setLayout(layoutData);
127
+ setRecord(recordData);
128
+ setObjectMetadata(objectMetadataData);
129
+ } catch (err) {
130
+ if (isCancelled || (err instanceof Error && err.name === "AbortError")) {
131
+ return;
132
+ }
133
+ setError("Failed to load record details");
134
+ } finally {
135
+ if (!isCancelled) {
136
+ setLoading(false);
137
+ }
138
+ }
139
+ };
140
+
141
+ fetchDetail();
142
+
143
+ return () => {
144
+ isCancelled = true;
145
+ abortController.abort();
146
+ };
147
+ }, [objectApiName, recordId, recordTypeId, cacheKey, initialData]);
148
+
149
+ return {
150
+ layout,
151
+ record,
152
+ objectMetadata,
153
+ loading,
154
+ error,
155
+ };
156
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Record list hook: GraphQL records with filter, sort, pagination, search.
3
+ * Use for list/search views; detail view uses useRecordDetailLayout instead.
4
+ *
5
+ * @module hooks/useRecordListGraphQL
6
+ */
7
+
8
+ import { useState, useEffect, useCallback } from "react";
9
+ import { useObjectColumns } from "./useObjectSearchData";
10
+ import {
11
+ getRecordsGraphQL,
12
+ buildOrderByFromSort,
13
+ type RecordListGraphQLResult,
14
+ } from "../api/recordListGraphQLService";
15
+ import type { Column } from "../types/search/searchResults";
16
+ import type { FilterCriteria } from "../types/filters/filters";
17
+
18
+ const EMPTY_FILTERS: FilterCriteria[] = [];
19
+
20
+ export interface UseRecordListGraphQLOptions {
21
+ objectApiName: string;
22
+ first?: number;
23
+ after?: string | null;
24
+ filters?: FilterCriteria[];
25
+ sortBy?: string;
26
+ searchQuery?: string;
27
+ /** When provided, skips useObjectColumns (use from parent e.g. useObjectListMetadata). */
28
+ columns?: Column[];
29
+ columnsLoading?: boolean;
30
+ columnsError?: string | null;
31
+ }
32
+
33
+ export interface UseRecordListGraphQLReturn {
34
+ data: RecordListGraphQLResult | null;
35
+ edges: Array<{ node?: Record<string, unknown> }>;
36
+ pageInfo: {
37
+ hasNextPage?: boolean;
38
+ hasPreviousPage?: boolean;
39
+ endCursor?: string | null;
40
+ startCursor?: string | null;
41
+ } | null;
42
+ loading: boolean;
43
+ error: string | null;
44
+ columnsLoading: boolean;
45
+ columnsError: string | null;
46
+ refetch: () => void;
47
+ }
48
+
49
+ /**
50
+ * Fetches records via GraphQL for the given object with filter, sort, pagination, and search.
51
+ */
52
+ export function useRecordListGraphQL(
53
+ options: UseRecordListGraphQLOptions,
54
+ ): UseRecordListGraphQLReturn {
55
+ const {
56
+ objectApiName,
57
+ first = 50,
58
+ after = null,
59
+ filters = EMPTY_FILTERS,
60
+ sortBy = "",
61
+ searchQuery = "",
62
+ columns: columnsProp,
63
+ columnsLoading: columnsLoadingProp,
64
+ columnsError: columnsErrorProp,
65
+ } = options;
66
+
67
+ const fromParent = columnsProp !== undefined;
68
+ const fromHook = useObjectColumns(fromParent ? null : objectApiName);
69
+
70
+ const columns = fromParent ? columnsProp : fromHook.columns;
71
+ const columnsLoading = fromParent ? (columnsLoadingProp ?? false) : fromHook.columnsLoading;
72
+ const columnsError = fromParent ? (columnsErrorProp ?? null) : fromHook.columnsError;
73
+
74
+ const [data, setData] = useState<RecordListGraphQLResult | null>(null);
75
+ const [loading, setLoading] = useState(false);
76
+ const [error, setError] = useState<string | null>(null);
77
+
78
+ const fetchRecords = useCallback(() => {
79
+ if (columnsLoading || columnsError || columns.length === 0) return;
80
+
81
+ setLoading(true);
82
+ setError(null);
83
+ const orderBy = buildOrderByFromSort(sortBy);
84
+
85
+ getRecordsGraphQL({
86
+ objectApiName,
87
+ columns,
88
+ first,
89
+ after,
90
+ filters,
91
+ orderBy,
92
+ searchQuery: searchQuery.trim() || undefined,
93
+ })
94
+ .then((result) => {
95
+ setData(result);
96
+ })
97
+ .catch((err) => {
98
+ setError(err instanceof Error ? err.message : "Failed to load records");
99
+ })
100
+ .finally(() => {
101
+ setLoading(false);
102
+ });
103
+ }, [
104
+ objectApiName,
105
+ columns,
106
+ columnsLoading,
107
+ columnsError,
108
+ first,
109
+ after,
110
+ filters,
111
+ sortBy,
112
+ searchQuery,
113
+ ]);
114
+
115
+ useEffect(() => {
116
+ if (!objectApiName || columnsLoading || columnsError) return;
117
+ if (columns.length === 0 && !columnsLoading) return;
118
+ fetchRecords();
119
+ }, [objectApiName, columns, columnsLoading, columnsError, fetchRecords]);
120
+
121
+ const objectData = data?.uiapi?.query?.[objectApiName];
122
+ const edges = objectData?.edges ?? [];
123
+ const pageInfo = objectData?.pageInfo ?? null;
124
+
125
+ return {
126
+ data,
127
+ edges,
128
+ pageInfo,
129
+ loading: columnsLoading || loading,
130
+ error: columnsError || error,
131
+ columnsLoading,
132
+ columnsError,
133
+ refetch: fetchRecords,
134
+ };
135
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Real weather data via Open-Meteo (free, no API key).
3
+ * Uses San Francisco as default location; optional lat/lng override.
4
+ */
5
+ import { useState, useEffect } from "react";
6
+
7
+ const DEFAULT_LAT = 37.7749;
8
+ const DEFAULT_LNG = -122.4194;
9
+
10
+ // WMO weather codes → short label (Open-Meteo)
11
+ const WEATHER_LABELS: Record<number, string> = {
12
+ 0: "Clear",
13
+ 1: "Mainly clear",
14
+ 2: "Partly cloudy",
15
+ 3: "Overcast",
16
+ 45: "Foggy",
17
+ 48: "Depositing rime fog",
18
+ 51: "Light drizzle",
19
+ 53: "Drizzle",
20
+ 55: "Dense drizzle",
21
+ 61: "Slight rain",
22
+ 63: "Rain",
23
+ 65: "Heavy rain",
24
+ 71: "Slight snow",
25
+ 73: "Snow",
26
+ 75: "Heavy snow",
27
+ 77: "Snow grains",
28
+ 80: "Slight rain showers",
29
+ 81: "Rain showers",
30
+ 82: "Heavy rain showers",
31
+ 85: "Slight snow showers",
32
+ 86: "Heavy snow showers",
33
+ 95: "Thunderstorm",
34
+ 96: "Thunderstorm + hail",
35
+ 99: "Thunderstorm + heavy hail",
36
+ };
37
+
38
+ function weatherLabel(code: number): string {
39
+ return WEATHER_LABELS[code] ?? "Unknown";
40
+ }
41
+
42
+ function celsiusToFahrenheit(c: number): number {
43
+ return Math.round((c * 9) / 5 + 32);
44
+ }
45
+
46
+ export interface WeatherCurrent {
47
+ description: string;
48
+ tempF: number;
49
+ humidity: number;
50
+ windSpeedKmh: number;
51
+ windSpeedMph: number;
52
+ }
53
+
54
+ export interface WeatherHour {
55
+ time: string; // ISO or "h:mm a"
56
+ tempF: number;
57
+ }
58
+
59
+ export interface WeatherData {
60
+ current: WeatherCurrent;
61
+ hourly: WeatherHour[];
62
+ timezone: string;
63
+ }
64
+
65
+ async function fetchWeather(lat: number, lng: number): Promise<WeatherData> {
66
+ const url = new URL("https://api.open-meteo.com/v1/forecast");
67
+ url.searchParams.set("latitude", String(lat));
68
+ url.searchParams.set("longitude", String(lng));
69
+ url.searchParams.set(
70
+ "current",
71
+ "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m",
72
+ );
73
+ url.searchParams.set("hourly", "temperature_2m");
74
+ url.searchParams.set("timezone", "America/Los_Angeles");
75
+ url.searchParams.set("forecast_days", "1");
76
+
77
+ const res = await fetch(url.toString());
78
+ if (!res.ok) throw new Error(`Weather failed: ${res.status}`);
79
+ const data = (await res.json()) as {
80
+ current?: {
81
+ temperature_2m?: number;
82
+ relative_humidity_2m?: number;
83
+ weather_code?: number;
84
+ wind_speed_10m?: number;
85
+ };
86
+ hourly?: { time?: string[]; temperature_2m?: (number | null)[] };
87
+ timezone?: string;
88
+ };
89
+
90
+ const cur = data.current ?? {};
91
+ const tempC = cur.temperature_2m ?? 0;
92
+ const humidity = cur.relative_humidity_2m ?? 0;
93
+ const windKmh = cur.wind_speed_10m ?? 0;
94
+ const windMph = Math.round(windKmh * 0.621371 * 10) / 10;
95
+ const code = cur.weather_code ?? 0;
96
+
97
+ const hourly: WeatherHour[] = [];
98
+ const times = data.hourly?.time ?? [];
99
+ const temps = data.hourly?.temperature_2m ?? [];
100
+ const now = new Date();
101
+ for (let i = 0; i < Math.min(12, times.length); i++) {
102
+ const t = times[i];
103
+ const temp = temps[i];
104
+ if (t && temp != null) {
105
+ const d = new Date(t);
106
+ if (d >= now) {
107
+ hourly.push({
108
+ time: d.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", hour12: true }),
109
+ tempF: celsiusToFahrenheit(temp),
110
+ });
111
+ }
112
+ }
113
+ }
114
+ // If no future hours, use first 4 from response
115
+ if (hourly.length === 0) {
116
+ for (let i = 0; i < Math.min(4, times.length); i++) {
117
+ const t = times[i];
118
+ const temp = temps[i];
119
+ if (t && temp != null) {
120
+ hourly.push({
121
+ time: new Date(t).toLocaleTimeString("en-US", {
122
+ hour: "numeric",
123
+ minute: "2-digit",
124
+ hour12: true,
125
+ }),
126
+ tempF: celsiusToFahrenheit(temp),
127
+ });
128
+ }
129
+ }
130
+ }
131
+
132
+ return {
133
+ current: {
134
+ description: weatherLabel(code),
135
+ tempF: celsiusToFahrenheit(tempC),
136
+ humidity,
137
+ windSpeedKmh: windKmh,
138
+ windSpeedMph: windMph,
139
+ },
140
+ hourly: hourly.slice(0, 6),
141
+ timezone: data.timezone ?? "America/Los_Angeles",
142
+ };
143
+ }
144
+
145
+ export function useWeather(lat?: number | null, lng?: number | null) {
146
+ const latitude = lat ?? DEFAULT_LAT;
147
+ const longitude = lng ?? DEFAULT_LNG;
148
+
149
+ const [data, setData] = useState<WeatherData | null>(null);
150
+ const [loading, setLoading] = useState(true);
151
+ const [error, setError] = useState<string | null>(null);
152
+
153
+ useEffect(() => {
154
+ let cancelled = false;
155
+ setLoading(true);
156
+ setError(null);
157
+ fetchWeather(latitude, longitude)
158
+ .then((d) => {
159
+ if (!cancelled) setData(d);
160
+ })
161
+ .catch((e) => {
162
+ if (!cancelled) setError(e instanceof Error ? e.message : "Failed to load weather");
163
+ })
164
+ .finally(() => {
165
+ if (!cancelled) setLoading(false);
166
+ });
167
+ return () => {
168
+ cancelled = true;
169
+ };
170
+ }, [latitude, longitude]);
171
+
172
+ return { data, loading, error };
173
+ }