@salesforce/webapp-template-app-react-sample-b2x-experimental 1.68.0 → 1.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/data/Lease__c.json +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
- package/dist/package.json +1 -1
- package/package.json +2 -2
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Button } from "../components/ui/button";
|
|
2
1
|
import {
|
|
3
2
|
Card,
|
|
4
3
|
CardHeader,
|
|
@@ -7,92 +6,165 @@ import {
|
|
|
7
6
|
CardFooter,
|
|
8
7
|
} from "../components/ui/card";
|
|
9
8
|
import { Link } from "react-router";
|
|
9
|
+
import { useWeather } from "@/hooks/useWeather";
|
|
10
|
+
import { useMaintenanceRequests } from "@/hooks/useMaintenanceRequests";
|
|
11
|
+
|
|
12
|
+
function maintenanceProgressPercent(status: string | null): number {
|
|
13
|
+
switch (status) {
|
|
14
|
+
case "New":
|
|
15
|
+
return 0;
|
|
16
|
+
case "Assigned":
|
|
17
|
+
return 25;
|
|
18
|
+
case "In Progress":
|
|
19
|
+
return 50;
|
|
20
|
+
case "On Hold":
|
|
21
|
+
return 75;
|
|
22
|
+
case "Resolved":
|
|
23
|
+
return 100;
|
|
24
|
+
default:
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatRequestDate(iso: string | null): string {
|
|
30
|
+
if (!iso) return "—";
|
|
31
|
+
try {
|
|
32
|
+
const d = new Date(iso);
|
|
33
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
|
|
34
|
+
} catch {
|
|
35
|
+
return iso;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
10
38
|
|
|
11
39
|
export default function Dashboard() {
|
|
40
|
+
const { data: weather, loading: weatherLoading, error: weatherError } = useWeather();
|
|
41
|
+
const {
|
|
42
|
+
requests: maintenanceRequests,
|
|
43
|
+
loading: maintenanceLoading,
|
|
44
|
+
error: maintenanceError,
|
|
45
|
+
} = useMaintenanceRequests();
|
|
46
|
+
const recentMaintenance = maintenanceRequests.slice(0, 3);
|
|
47
|
+
|
|
12
48
|
return (
|
|
13
49
|
<div className="mx-auto grid max-w-[1100px] grid-cols-1 gap-6 md:grid-cols-2">
|
|
14
50
|
<div className="space-y-6">
|
|
15
|
-
<Card>
|
|
16
|
-
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
17
|
-
<CardTitle className="text-base text-primary">Community News</CardTitle>
|
|
18
|
-
</CardHeader>
|
|
19
|
-
<CardContent className="space-y-2">
|
|
20
|
-
<h3 className="text-lg font-semibold text-gray-700">
|
|
21
|
-
Sip & Skyline: Rooftop Wine Tasting Mixer
|
|
22
|
-
</h3>
|
|
23
|
-
<p className="text-sm text-muted-foreground">Thursday, January 22nd, at 6:30 PM</p>
|
|
24
|
-
<p className="text-sm leading-relaxed text-foreground">
|
|
25
|
-
We're partnering with local favorite Mission Cellars to bring you a curated selection
|
|
26
|
-
of Bay Area wines paired with artisanal charcuterie.
|
|
27
|
-
</p>
|
|
28
|
-
<div className="mt-4 h-36 rounded-xl bg-muted" aria-hidden />
|
|
29
|
-
</CardContent>
|
|
30
|
-
</Card>
|
|
31
|
-
<Card>
|
|
51
|
+
<Card className="rounded-2xl shadow-md">
|
|
32
52
|
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
|
33
53
|
<CardTitle className="text-base text-primary">Maintenance</CardTitle>
|
|
34
|
-
<
|
|
54
|
+
<Link
|
|
55
|
+
to="/maintenance"
|
|
56
|
+
className="inline-flex h-8 cursor-pointer items-center justify-center rounded-xl bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors duration-200 hover:bg-primary/90"
|
|
57
|
+
>
|
|
58
|
+
+ New Request
|
|
59
|
+
</Link>
|
|
35
60
|
</CardHeader>
|
|
36
61
|
<CardContent className="space-y-4">
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
{maintenanceLoading && <p className="text-sm text-muted-foreground">Loading…</p>}
|
|
63
|
+
{maintenanceError && (
|
|
64
|
+
<p className="text-sm text-destructive" role="alert">
|
|
65
|
+
{maintenanceError}
|
|
66
|
+
</p>
|
|
67
|
+
)}
|
|
68
|
+
{!maintenanceLoading && !maintenanceError && recentMaintenance.length === 0 && (
|
|
69
|
+
<p className="text-sm text-muted-foreground">
|
|
70
|
+
No requests yet.{" "}
|
|
71
|
+
<Link to="/maintenance" className="text-primary hover:underline">
|
|
72
|
+
Submit one
|
|
73
|
+
</Link>
|
|
74
|
+
.
|
|
75
|
+
</p>
|
|
76
|
+
)}
|
|
77
|
+
{!maintenanceLoading &&
|
|
78
|
+
!maintenanceError &&
|
|
79
|
+
recentMaintenance.map((r) => {
|
|
80
|
+
const pct = maintenanceProgressPercent(r.status);
|
|
81
|
+
const isResolved = r.status === "Resolved";
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
key={r.id}
|
|
85
|
+
className="flex items-start gap-4 border-b border-border py-3 last:border-b-0"
|
|
86
|
+
>
|
|
87
|
+
<div className="size-14 shrink-0 rounded-lg bg-muted" aria-hidden />
|
|
88
|
+
<div className="min-w-0 flex-1">
|
|
89
|
+
<p className="font-semibold text-foreground">
|
|
90
|
+
{r.title ?? r.description ?? "—"}
|
|
91
|
+
</p>
|
|
92
|
+
<p className="text-xs text-muted-foreground">
|
|
93
|
+
Submitted {formatRequestDate(r.dateRequested)}.
|
|
94
|
+
</p>
|
|
95
|
+
<div className="my-2 h-1.5 overflow-hidden rounded-full bg-muted">
|
|
96
|
+
<div
|
|
97
|
+
className="h-full rounded-full bg-primary transition-[width]"
|
|
98
|
+
style={{ width: `${pct}%` }}
|
|
99
|
+
aria-hidden
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
<p className={`text-xs ${isResolved ? "text-green-600" : "text-primary"}`}>
|
|
103
|
+
{isResolved ? "100% Completed" : `${pct}% ${r.status ?? "New"}`}
|
|
104
|
+
</p>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
59
109
|
</CardContent>
|
|
60
110
|
<CardFooter className="justify-end pt-2">
|
|
61
|
-
<Link
|
|
111
|
+
<Link
|
|
112
|
+
to="/maintenance"
|
|
113
|
+
className="cursor-pointer text-sm font-medium text-primary transition-colors duration-200 hover:underline"
|
|
114
|
+
>
|
|
62
115
|
See All
|
|
63
116
|
</Link>
|
|
64
117
|
</CardFooter>
|
|
65
118
|
</Card>
|
|
66
119
|
</div>
|
|
67
120
|
<div>
|
|
68
|
-
<Card>
|
|
121
|
+
<Card className="rounded-2xl shadow-md">
|
|
69
122
|
<CardHeader>
|
|
70
123
|
<CardTitle className="text-primary">Weather</CardTitle>
|
|
71
|
-
<p className="text-sm text-muted-foreground">
|
|
124
|
+
<p className="text-sm text-muted-foreground">
|
|
125
|
+
{new Date().toLocaleDateString("en-US", {
|
|
126
|
+
weekday: "short",
|
|
127
|
+
month: "short",
|
|
128
|
+
day: "numeric",
|
|
129
|
+
year: "numeric",
|
|
130
|
+
})}
|
|
131
|
+
</p>
|
|
72
132
|
</CardHeader>
|
|
73
133
|
<CardContent className="space-y-4">
|
|
74
|
-
<p className="text-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
<p className="text-base font-semibold text-foreground">{72 - i}°</p>
|
|
134
|
+
{weatherLoading && <p className="text-sm text-muted-foreground">Loading weather…</p>}
|
|
135
|
+
{weatherError && (
|
|
136
|
+
<p className="text-sm text-destructive" role="alert">
|
|
137
|
+
{weatherError}
|
|
138
|
+
</p>
|
|
139
|
+
)}
|
|
140
|
+
{!weatherLoading && !weatherError && weather && (
|
|
141
|
+
<>
|
|
142
|
+
<p className="text-base text-foreground">{weather.current.description}</p>
|
|
143
|
+
<p className="text-4xl font-bold text-foreground">{weather.current.tempF}°F</p>
|
|
144
|
+
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
|
|
145
|
+
<span>{weather.current.windSpeedMph} mph Wind</span>
|
|
146
|
+
<span>{weather.current.humidity}% Humidity</span>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="flex gap-2 border-b border-border pb-2">
|
|
149
|
+
<span className="border-b-2 border-primary pb-1 text-sm font-semibold text-primary">
|
|
150
|
+
Today
|
|
151
|
+
</span>
|
|
93
152
|
</div>
|
|
94
|
-
|
|
95
|
-
|
|
153
|
+
{weather.hourly.length > 0 && (
|
|
154
|
+
<div className="flex flex-wrap gap-4">
|
|
155
|
+
{weather.hourly.map((h) => (
|
|
156
|
+
<div
|
|
157
|
+
key={h.time}
|
|
158
|
+
className="min-w-[60px] rounded-xl bg-muted/50 p-2 text-center"
|
|
159
|
+
>
|
|
160
|
+
<p className="text-xs text-muted-foreground">{h.time}</p>
|
|
161
|
+
<p className="text-base font-semibold text-foreground">{h.tempF}°</p>
|
|
162
|
+
</div>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
</>
|
|
167
|
+
)}
|
|
96
168
|
</CardContent>
|
|
97
169
|
</Card>
|
|
98
170
|
</div>
|
package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useParams, useNavigate } from "react-router";
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
|
4
|
+
import { Skeleton } from "../components/ui/skeleton";
|
|
5
|
+
import { Alert, AlertDescription, AlertTitle } from "../components/ui/alert";
|
|
6
|
+
import { AlertCircle } from "lucide-react";
|
|
7
|
+
import DetailHeader from "../components/detail/DetailHeader";
|
|
8
|
+
import { UiApiDetailForm } from "../components/detail/UiApiDetailForm";
|
|
9
|
+
import { OBJECT_API_NAMES, DEFAULT_DETAIL_PAGE_TITLE } from "../constants";
|
|
10
|
+
import { toRecordDisplayNameMetadata } from "../utils/fieldUtils";
|
|
11
|
+
import { useRecordDetailLayout } from "../hooks/useRecordDetailLayout";
|
|
12
|
+
import { getGraphQLRecordDisplayName } from "../utils/graphQLNodeFieldUtils";
|
|
13
|
+
|
|
14
|
+
export default function DetailPage() {
|
|
15
|
+
const { objectApiName: objectApiNameParam, recordId } = useParams<{
|
|
16
|
+
objectApiName: string;
|
|
17
|
+
recordId: string;
|
|
18
|
+
}>();
|
|
19
|
+
const navigate = useNavigate();
|
|
20
|
+
const objectApiName = objectApiNameParam ?? OBJECT_API_NAMES[0];
|
|
21
|
+
|
|
22
|
+
const { layout, record, objectMetadata, loading, error } = useRecordDetailLayout({
|
|
23
|
+
objectApiName,
|
|
24
|
+
recordId: recordId ?? null,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const recordTitle = useMemo(
|
|
28
|
+
() =>
|
|
29
|
+
record
|
|
30
|
+
? getGraphQLRecordDisplayName(record, toRecordDisplayNameMetadata(objectMetadata))
|
|
31
|
+
: DEFAULT_DETAIL_PAGE_TITLE,
|
|
32
|
+
[record, objectMetadata],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const handleBack = () => navigate(-1);
|
|
36
|
+
|
|
37
|
+
if (loading) {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12"
|
|
41
|
+
role="status"
|
|
42
|
+
aria-live="polite"
|
|
43
|
+
aria-label="Loading record details"
|
|
44
|
+
>
|
|
45
|
+
<span className="sr-only">Loading record details</span>
|
|
46
|
+
<Skeleton className="h-10 w-32 mb-6" aria-hidden="true" />
|
|
47
|
+
<Card aria-hidden="true">
|
|
48
|
+
<CardHeader>
|
|
49
|
+
<Skeleton className="h-8 w-3/4" />
|
|
50
|
+
</CardHeader>
|
|
51
|
+
<CardContent className="space-y-4">
|
|
52
|
+
{[1, 2, 3, 4].map((i) => (
|
|
53
|
+
<div key={i} className="space-y-2">
|
|
54
|
+
<Skeleton className="h-4 w-24" />
|
|
55
|
+
<Skeleton className="h-4 w-full" />
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
</CardContent>
|
|
59
|
+
</Card>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (error) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
67
|
+
<DetailHeader title="" onBack={handleBack} />
|
|
68
|
+
<Alert variant="destructive" role="alert">
|
|
69
|
+
<AlertCircle className="h-4 w-4" aria-hidden="true" />
|
|
70
|
+
<AlertTitle>Error</AlertTitle>
|
|
71
|
+
<AlertDescription>{error}</AlertDescription>
|
|
72
|
+
</Alert>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!layout || !record) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
80
|
+
<DetailHeader title="" onBack={handleBack} />
|
|
81
|
+
<Alert role="alert">
|
|
82
|
+
<AlertCircle className="h-4 w-4" aria-hidden="true" />
|
|
83
|
+
<AlertTitle>Not Found</AlertTitle>
|
|
84
|
+
<AlertDescription>Record not found</AlertDescription>
|
|
85
|
+
</Alert>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12" aria-label="Record details">
|
|
92
|
+
<DetailHeader title={recordTitle} onBack={handleBack} />
|
|
93
|
+
<Card>
|
|
94
|
+
<CardHeader>
|
|
95
|
+
<CardTitle className="text-2xl">{recordTitle}</CardTitle>
|
|
96
|
+
</CardHeader>
|
|
97
|
+
<CardContent>
|
|
98
|
+
<UiApiDetailForm
|
|
99
|
+
objectApiName={objectApiName}
|
|
100
|
+
recordId={recordId!}
|
|
101
|
+
layout={layout}
|
|
102
|
+
record={record}
|
|
103
|
+
objectMetadata={objectMetadata}
|
|
104
|
+
/>
|
|
105
|
+
</CardContent>
|
|
106
|
+
</Card>
|
|
107
|
+
</main>
|
|
108
|
+
);
|
|
109
|
+
}
|
package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GlobalSearch Page Component
|
|
3
|
+
*
|
|
4
|
+
* Main page component for displaying global search results.
|
|
5
|
+
* Uses GraphQL API (useRecordListGraphQL) for list data; results are adapted to the
|
|
6
|
+
* same record shape as before so SearchResultCard and filters/sort/pagination work unchanged.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* - Supports single object search (no tabs)
|
|
10
|
+
* - Displays filters panel on the left and results on the right
|
|
11
|
+
* - Pagination uses a cursor stack: we only query forward (first + after) and store endCursor per page;
|
|
12
|
+
* Previous re-queries using the stored cursor for the previous page so both Next and Previous work.
|
|
13
|
+
*/
|
|
14
|
+
import { useMemo, useState, useCallback, useEffect, useRef } from "react";
|
|
15
|
+
import { useParams } from "react-router";
|
|
16
|
+
import { OBJECT_API_NAMES, DEFAULT_PAGE_SIZE } from "../constants";
|
|
17
|
+
import { useObjectListMetadata } from "../hooks/useObjectSearchData";
|
|
18
|
+
import { useObjectInfoBatch } from "../hooks/useObjectInfoBatch";
|
|
19
|
+
import { useRecordListGraphQL } from "../hooks/useRecordListGraphQL";
|
|
20
|
+
import FiltersPanel from "../components/FiltersPanel";
|
|
21
|
+
import SearchHeader from "../components/search/SearchHeader";
|
|
22
|
+
import SearchResultsPanel from "../components/search/SearchResultsPanel";
|
|
23
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
|
24
|
+
import { Skeleton } from "../components/ui/skeleton";
|
|
25
|
+
import type { FilterCriteria } from "../types/filters/filters";
|
|
26
|
+
import type { SearchResultRecord } from "../types/search/searchResults";
|
|
27
|
+
import { graphQLNodeToSearchResultRecordData } from "../utils/graphQLRecordAdapter";
|
|
28
|
+
|
|
29
|
+
const EMPTY_HIGHLIGHT = { fields: {}, snippet: null };
|
|
30
|
+
const EMPTY_SEARCH_INFO = { isPromoted: false, isSpellCorrected: false };
|
|
31
|
+
|
|
32
|
+
export default function GlobalSearch() {
|
|
33
|
+
const { query } = useParams<{ query: string }>();
|
|
34
|
+
|
|
35
|
+
const objectApiName = OBJECT_API_NAMES[0];
|
|
36
|
+
|
|
37
|
+
const [searchPageSize, setSearchPageSize] = useState(DEFAULT_PAGE_SIZE);
|
|
38
|
+
const [afterCursor, setAfterCursor] = useState<string | null>(null);
|
|
39
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
40
|
+
/** Cursor stack: cursorStack[i] is the `after` value that returns page i. cursorStack[0] = null (first page). */
|
|
41
|
+
const [cursorStack, setCursorStack] = useState<(string | null)[]>([null]);
|
|
42
|
+
const [appliedFilters, setAppliedFilters] = useState<FilterCriteria[]>([]);
|
|
43
|
+
const [sortBy, setSortBy] = useState("Name");
|
|
44
|
+
|
|
45
|
+
const decodedQuery = useMemo(() => {
|
|
46
|
+
if (!query) return "";
|
|
47
|
+
try {
|
|
48
|
+
return decodeURIComponent(query);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
return query;
|
|
51
|
+
}
|
|
52
|
+
}, [query]);
|
|
53
|
+
|
|
54
|
+
const isBrowseAll = decodedQuery === "browse__all";
|
|
55
|
+
const searchQuery = isBrowseAll ? "" : decodedQuery.trim();
|
|
56
|
+
|
|
57
|
+
// Reset pagination when the URL search query changes so we don't use an old cursor with a new result set
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setAfterCursor(null);
|
|
60
|
+
setPageIndex(0);
|
|
61
|
+
setCursorStack([null]);
|
|
62
|
+
}, [query]);
|
|
63
|
+
|
|
64
|
+
const listMeta = useObjectListMetadata(objectApiName);
|
|
65
|
+
const { objectInfos } = useObjectInfoBatch([...OBJECT_API_NAMES]);
|
|
66
|
+
const labelPlural = (objectInfos[0]?.labelPlural as string | undefined) ?? "records";
|
|
67
|
+
const {
|
|
68
|
+
edges,
|
|
69
|
+
pageInfo,
|
|
70
|
+
loading: resultsLoading,
|
|
71
|
+
error: resultsError,
|
|
72
|
+
} = useRecordListGraphQL({
|
|
73
|
+
objectApiName,
|
|
74
|
+
first: searchPageSize,
|
|
75
|
+
after: afterCursor,
|
|
76
|
+
filters: appliedFilters,
|
|
77
|
+
sortBy: sortBy === "relevance" ? "Name" : sortBy,
|
|
78
|
+
searchQuery: searchQuery || undefined,
|
|
79
|
+
columns: listMeta.columns,
|
|
80
|
+
columnsLoading: listMeta.loading,
|
|
81
|
+
columnsError: listMeta.error,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Store endCursor for the next page so we can re-query when user clicks Next; also enables Previous via stack.
|
|
85
|
+
// Only update when not loading so a stale response cannot write a cursor into the wrong stack index (e.g. after rapid Next clicks).
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (resultsLoading) return;
|
|
88
|
+
const cursor = pageInfo?.endCursor ?? null;
|
|
89
|
+
if (cursor == null) return;
|
|
90
|
+
setCursorStack((prev) => {
|
|
91
|
+
const next = [...prev];
|
|
92
|
+
next[pageIndex + 1] = cursor;
|
|
93
|
+
return next;
|
|
94
|
+
});
|
|
95
|
+
}, [resultsLoading, pageInfo?.endCursor, pageIndex]);
|
|
96
|
+
|
|
97
|
+
const results: SearchResultRecord[] = useMemo(
|
|
98
|
+
() =>
|
|
99
|
+
(edges ?? []).map((edge) => ({
|
|
100
|
+
record: graphQLNodeToSearchResultRecordData(
|
|
101
|
+
edge?.node as Record<string, unknown>,
|
|
102
|
+
objectApiName,
|
|
103
|
+
),
|
|
104
|
+
highlightInfo: EMPTY_HIGHLIGHT,
|
|
105
|
+
searchInfo: EMPTY_SEARCH_INFO,
|
|
106
|
+
})),
|
|
107
|
+
[edges, objectApiName],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const nextPageToken = pageInfo?.endCursor ?? null;
|
|
111
|
+
/** Entry cursor for the previous page; used when user clicks Previous to re-query with after=cursorStack[pageIndex-1]. */
|
|
112
|
+
const previousPageToken = pageIndex > 0 ? (cursorStack[pageIndex - 1] ?? null) : null;
|
|
113
|
+
const hasNextPage = pageInfo?.hasNextPage === true;
|
|
114
|
+
const hasPreviousPage = pageIndex > 0;
|
|
115
|
+
const currentPageToken = pageIndex.toString();
|
|
116
|
+
|
|
117
|
+
const cursorStackRef = useRef(cursorStack);
|
|
118
|
+
const pageIndexRef = useRef(pageIndex);
|
|
119
|
+
cursorStackRef.current = cursorStack;
|
|
120
|
+
pageIndexRef.current = pageIndex;
|
|
121
|
+
|
|
122
|
+
const canRenderFilters =
|
|
123
|
+
!listMeta.loading && listMeta.filters !== undefined && listMeta.picklistValues !== undefined;
|
|
124
|
+
|
|
125
|
+
const handleApplyFilters = useCallback((filterCriteria: FilterCriteria[]) => {
|
|
126
|
+
setAppliedFilters(filterCriteria);
|
|
127
|
+
setAfterCursor(null);
|
|
128
|
+
setPageIndex(0);
|
|
129
|
+
setCursorStack([null]);
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const handlePageChange = useCallback(
|
|
133
|
+
(newPageToken: string, direction?: "next" | "prev" | "first") => {
|
|
134
|
+
if (direction === "first" || newPageToken === "0") {
|
|
135
|
+
setAfterCursor(null);
|
|
136
|
+
setPageIndex(0);
|
|
137
|
+
} else if (direction === "prev") {
|
|
138
|
+
const idx = pageIndexRef.current;
|
|
139
|
+
const stack = cursorStackRef.current;
|
|
140
|
+
const prevCursor = idx > 0 ? (stack[idx - 1] ?? null) : null;
|
|
141
|
+
setAfterCursor(prevCursor);
|
|
142
|
+
setPageIndex((prev) => Math.max(0, prev - 1));
|
|
143
|
+
} else {
|
|
144
|
+
setAfterCursor(newPageToken);
|
|
145
|
+
setPageIndex((prev) => prev + 1);
|
|
146
|
+
}
|
|
147
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
148
|
+
},
|
|
149
|
+
[],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const handlePageSizeChange = useCallback((newPageSize: number) => {
|
|
153
|
+
setSearchPageSize(newPageSize);
|
|
154
|
+
setAfterCursor(null);
|
|
155
|
+
setPageIndex(0);
|
|
156
|
+
setCursorStack([null]);
|
|
157
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
const handleSortByChange = useCallback((newSortBy: string) => {
|
|
161
|
+
setSortBy(newSortBy);
|
|
162
|
+
setAfterCursor(null);
|
|
163
|
+
setPageIndex(0);
|
|
164
|
+
setCursorStack([null]);
|
|
165
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
return (
|
|
169
|
+
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
170
|
+
<SearchHeader query={decodedQuery} isBrowseAll={isBrowseAll} labelPlural={labelPlural} />
|
|
171
|
+
|
|
172
|
+
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
173
|
+
<aside className="lg:col-span-1" aria-label="Filters panel">
|
|
174
|
+
{canRenderFilters ? (
|
|
175
|
+
<FiltersPanel
|
|
176
|
+
filters={listMeta.filters}
|
|
177
|
+
picklistValues={listMeta.picklistValues}
|
|
178
|
+
loading={listMeta.loading}
|
|
179
|
+
objectApiName={objectApiName}
|
|
180
|
+
onApplyFilters={handleApplyFilters}
|
|
181
|
+
/>
|
|
182
|
+
) : (
|
|
183
|
+
<Card className="w-full" role="region" aria-label="Filters panel">
|
|
184
|
+
<CardHeader>
|
|
185
|
+
<CardTitle>Filters</CardTitle>
|
|
186
|
+
</CardHeader>
|
|
187
|
+
<CardContent
|
|
188
|
+
className="space-y-4"
|
|
189
|
+
role="status"
|
|
190
|
+
aria-live="polite"
|
|
191
|
+
aria-label="Loading filters"
|
|
192
|
+
>
|
|
193
|
+
<span className="sr-only">Loading filters</span>
|
|
194
|
+
{[1, 2, 3].map((i) => (
|
|
195
|
+
<div key={i} className="space-y-2" aria-hidden="true">
|
|
196
|
+
<Skeleton className="h-4 w-24" />
|
|
197
|
+
<Skeleton className="h-9 w-full" />
|
|
198
|
+
</div>
|
|
199
|
+
))}
|
|
200
|
+
</CardContent>
|
|
201
|
+
</Card>
|
|
202
|
+
)}
|
|
203
|
+
</aside>
|
|
204
|
+
|
|
205
|
+
<section className="lg:col-span-3" aria-label="Search results">
|
|
206
|
+
<SearchResultsPanel
|
|
207
|
+
objectApiName={objectApiName}
|
|
208
|
+
columns={listMeta.columns}
|
|
209
|
+
results={results}
|
|
210
|
+
columnsLoading={listMeta.loading}
|
|
211
|
+
resultsLoading={resultsLoading}
|
|
212
|
+
columnsError={listMeta.error}
|
|
213
|
+
resultsError={resultsError}
|
|
214
|
+
currentPageToken={currentPageToken}
|
|
215
|
+
nextPageToken={nextPageToken}
|
|
216
|
+
previousPageToken={previousPageToken}
|
|
217
|
+
hasNextPage={hasNextPage}
|
|
218
|
+
hasPreviousPage={hasPreviousPage}
|
|
219
|
+
pageSize={searchPageSize}
|
|
220
|
+
sortBy={sortBy}
|
|
221
|
+
onPageChange={handlePageChange}
|
|
222
|
+
onPageSizeChange={handlePageSizeChange}
|
|
223
|
+
onSortByChange={handleSortByChange}
|
|
224
|
+
/>
|
|
225
|
+
</section>
|
|
226
|
+
</div>
|
|
227
|
+
</main>
|
|
228
|
+
);
|
|
229
|
+
}
|