@salesforce/webapp-template-app-react-sample-b2x-experimental 1.68.0 → 1.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CHANGELOG.md +16 -0
- package/dist/force-app/main/default/data/Lease__c.json +13 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/package.json +13 -8
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/applicationApi.ts +78 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/graphqlClient.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/index.ts +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/leadApi.ts +69 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/maintenanceRequestApi.ts +177 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectDetailService.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoGraphQLService.ts +194 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/objectInfoService.ts +199 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyDetailGraphQL.ts +497 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/propertyListingGraphQL.ts +190 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/api/recordListGraphQLService.ts +365 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/appLayout.tsx +20 -30
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/FiltersPanel.tsx +375 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyListingCard.tsx +164 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/PropertyMap.tsx +113 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/SearchResultCard.tsx +131 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/alerts/status-alert.tsx +45 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/detail/formatted/index.ts +6 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/layout/card-layout.tsx +19 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/components/shared/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/constants.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/features/global-search/index.ts +33 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/form.tsx +204 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/index.ts +22 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useGeocode.ts +35 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useMaintenanceRequests.ts +39 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectInfoBatch.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useObjectSearchData.ts +395 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyAddresses.ts +36 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyDetail.ts +99 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyListingSearch.ts +75 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyMapMarkers.ts +100 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/usePropertyPrimaryImages.ts +51 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordDetailLayout.ts +156 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/hooks/useWeather.ts +173 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Application.tsx +263 -76
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Contact.tsx +158 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Dashboard.tsx +137 -65
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/GlobalSearch.tsx +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Home.tsx +469 -21
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/Maintenance.tsx +244 -95
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyDetails.tsx +211 -39
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertyListings.tsx +26 -10
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearch.tsx +165 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/pages/PropertySearchPlaceholder.tsx +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-01.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-02.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-03.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-04.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-05.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-06.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-07.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-08.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-09.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-10.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-11.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-12.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-13.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-14.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-15.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-16.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-17.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-18.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-19.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-20.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-21.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-22.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-23.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-24.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/public/property-25.jpg +0 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/routes.tsx +32 -6
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/styles/global.css +23 -63
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/filters.ts +120 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/filters/picklist.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/index.ts +4 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/leaflet.d.ts +17 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/objectInfo/objectInfo.ts +166 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/apiUtils.ts +125 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/debounce.ts +89 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/geocode.ts +65 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLObjectInfoAdapter.ts +319 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/index.ts +59 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/webapplications/appreactsampleb2x/src/utils/sanitizationUtils.ts +49 -0
- package/dist/package.json +1 -1
- package/package.json +2 -2
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls +0 -111
- package/dist/force-app/main/default/classes/MaintenanceRequestListAction.cls-meta.xml +0 -6
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls +0 -93
- package/dist/force-app/main/default/classes/MaintenanceRequestUpdatePriorityAction.cls-meta.xml +0 -6
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GlobalSearchInput Component
|
|
3
|
+
*
|
|
4
|
+
* Search input with two actions: Search (navigate to results for query) and
|
|
5
|
+
* Browse All (navigate to same results UI with all records for the object).
|
|
6
|
+
*/
|
|
7
|
+
import { useState, useCallback, useMemo, useId } from "react";
|
|
8
|
+
import type { KeyboardEvent, ChangeEvent } from "react";
|
|
9
|
+
import { useNavigate } from "react-router";
|
|
10
|
+
import { Card, CardContent } from "../ui/card";
|
|
11
|
+
import { Input } from "../ui/input";
|
|
12
|
+
import { Button } from "../ui/button";
|
|
13
|
+
import { Search } from "lucide-react";
|
|
14
|
+
import { OBJECT_API_NAMES } from "../../constants";
|
|
15
|
+
import { useObjectInfoBatch } from "../../hooks/useObjectInfoBatch";
|
|
16
|
+
|
|
17
|
+
const BROWSE_SEGMENT = "browse__all";
|
|
18
|
+
|
|
19
|
+
const FALLBACK_LABEL_PLURAL = "records";
|
|
20
|
+
|
|
21
|
+
export function GlobalSearchInput() {
|
|
22
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
23
|
+
const navigate = useNavigate();
|
|
24
|
+
const inputId = useId();
|
|
25
|
+
const searchButtonId = useId();
|
|
26
|
+
const browseButtonId = useId();
|
|
27
|
+
const inputDescriptionId = `${inputId}-description`;
|
|
28
|
+
const { objectInfos } = useObjectInfoBatch([...OBJECT_API_NAMES]);
|
|
29
|
+
const labelPlural = (objectInfos[0]?.labelPlural as string | undefined) ?? FALLBACK_LABEL_PLURAL;
|
|
30
|
+
|
|
31
|
+
const handleInputChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
|
32
|
+
setSearchQuery(e.target.value);
|
|
33
|
+
}, []);
|
|
34
|
+
|
|
35
|
+
const handleSearch = useCallback(() => {
|
|
36
|
+
const trimmed = searchQuery.trim();
|
|
37
|
+
if (trimmed) {
|
|
38
|
+
navigate(`/global-search/${encodeURIComponent(trimmed)}`);
|
|
39
|
+
}
|
|
40
|
+
}, [searchQuery, navigate]);
|
|
41
|
+
|
|
42
|
+
const handleBrowseAll = useCallback(() => {
|
|
43
|
+
navigate(`/global-search/${BROWSE_SEGMENT}`);
|
|
44
|
+
}, [navigate]);
|
|
45
|
+
|
|
46
|
+
const handleKeyDown = useCallback(
|
|
47
|
+
(e: KeyboardEvent<HTMLInputElement>) => {
|
|
48
|
+
if (e.key === "Enter") {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
handleSearch();
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
[handleSearch],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const isSearchDisabled = useMemo(() => !searchQuery.trim(), [searchQuery]);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="w-full max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
60
|
+
<Card className="w-full">
|
|
61
|
+
<CardContent className="pt-6">
|
|
62
|
+
<div className="flex flex-col sm:flex-row gap-3">
|
|
63
|
+
<div className="flex-1 relative">
|
|
64
|
+
<Search
|
|
65
|
+
className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-600"
|
|
66
|
+
aria-hidden="true"
|
|
67
|
+
/>
|
|
68
|
+
<Input
|
|
69
|
+
id={inputId}
|
|
70
|
+
type="search"
|
|
71
|
+
placeholder={`Search for ${labelPlural}`}
|
|
72
|
+
value={searchQuery}
|
|
73
|
+
onChange={handleInputChange}
|
|
74
|
+
onKeyDown={handleKeyDown}
|
|
75
|
+
className="pl-10"
|
|
76
|
+
aria-label={`Search for ${labelPlural}`}
|
|
77
|
+
aria-describedby={inputDescriptionId}
|
|
78
|
+
/>
|
|
79
|
+
<p id={inputDescriptionId} className="sr-only">
|
|
80
|
+
Enter your search query and press Enter or click Search. Or click Browse All to see
|
|
81
|
+
all {labelPlural}.
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
|
|
86
|
+
<Button
|
|
87
|
+
id={searchButtonId}
|
|
88
|
+
onClick={handleSearch}
|
|
89
|
+
disabled={isSearchDisabled}
|
|
90
|
+
className="w-full sm:w-auto"
|
|
91
|
+
aria-label="Search"
|
|
92
|
+
aria-describedby={inputDescriptionId}
|
|
93
|
+
variant="default"
|
|
94
|
+
>
|
|
95
|
+
<Search className="h-4 w-4 mr-2" aria-hidden="true" />
|
|
96
|
+
Search
|
|
97
|
+
</Button>
|
|
98
|
+
<Button
|
|
99
|
+
id={browseButtonId}
|
|
100
|
+
variant="outline"
|
|
101
|
+
onClick={handleBrowseAll}
|
|
102
|
+
className="w-full sm:w-auto"
|
|
103
|
+
aria-label={`Browse all ${labelPlural}`}
|
|
104
|
+
aria-describedby={inputDescriptionId}
|
|
105
|
+
>
|
|
106
|
+
Browse All {labelPlural}
|
|
107
|
+
</Button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</CardContent>
|
|
111
|
+
</Card>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-wide Constants
|
|
3
|
+
*
|
|
4
|
+
* Defines constants used throughout the global search feature.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Object API names to search across in the global search feature.
|
|
9
|
+
* Currently supports single object search.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* - Array of Salesforce object API names
|
|
13
|
+
* - First element is used as the primary search object
|
|
14
|
+
* - Can be extended to support multiple objects in the future
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const objectApiName = OBJECT_API_NAMES[0]; // 'Account'
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const OBJECT_API_NAMES = ["Account"] as const;
|
|
22
|
+
|
|
23
|
+
/** Fallback title when record display name cannot be resolved (e.g. before load or no name field). */
|
|
24
|
+
export const DEFAULT_DETAIL_PAGE_TITLE = "Untitled";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default page size for search results pagination.
|
|
28
|
+
* This should match one of the values in PAGE_SIZE_OPTIONS from paginationUtils.
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* - Default value is 20 (second option in PAGE_SIZE_OPTIONS)
|
|
32
|
+
* - Can be changed by user via pagination controls
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export const DEFAULT_PAGE_SIZE = 20;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Search Feature - Public API
|
|
3
|
+
*
|
|
4
|
+
* Main entry point for the global search feature.
|
|
5
|
+
* Exports all public APIs, components, and utilities that can be consumed by other packages.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* This is the public API of the feature. Components and utilities exported here
|
|
9
|
+
* can be imported by other packages that depend on this feature.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* // In another package's appLayout.tsx
|
|
14
|
+
* import { GlobalSearchInput } from 'feature-react-global-search';
|
|
15
|
+
*
|
|
16
|
+
* export default function AppLayout() {
|
|
17
|
+
* return (
|
|
18
|
+
* <div>
|
|
19
|
+
* <GlobalSearchInput />
|
|
20
|
+
* <Outlet />
|
|
21
|
+
* </div>
|
|
22
|
+
* );
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export * from "../../api";
|
|
28
|
+
export * from "../../hooks";
|
|
29
|
+
export * from "../../utils";
|
|
30
|
+
export * from "../../constants";
|
|
31
|
+
export * from "../../routes";
|
|
32
|
+
export * from "../../hooks";
|
|
33
|
+
export * from "../../types";
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { useId } from "react";
|
|
2
|
+
import { createFormHookContexts, createFormHook } from "@tanstack/react-form";
|
|
3
|
+
import { Field, FieldDescription, FieldError, FieldLabel } from "../components/ui/field";
|
|
4
|
+
import { Input } from "../components/ui/input";
|
|
5
|
+
import {
|
|
6
|
+
Select,
|
|
7
|
+
SelectContent,
|
|
8
|
+
SelectItem,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from "../components/ui/select";
|
|
12
|
+
import { cn } from "../lib/utils";
|
|
13
|
+
import type { PicklistValue } from "../types/filters/picklist";
|
|
14
|
+
import { getUniqueErrors } from "../utils/formUtils";
|
|
15
|
+
|
|
16
|
+
export type { FormError } from "../utils/formUtils";
|
|
17
|
+
export { validateRangeValues } from "../utils/formUtils";
|
|
18
|
+
|
|
19
|
+
export const { fieldContext, formContext, useFieldContext, useFormContext } =
|
|
20
|
+
createFormHookContexts();
|
|
21
|
+
|
|
22
|
+
interface FilterTextFieldProps extends Omit<
|
|
23
|
+
React.ComponentProps<typeof Input>,
|
|
24
|
+
"name" | "value" | "onBlur" | "onChange" | "aria-invalid"
|
|
25
|
+
> {
|
|
26
|
+
label: string;
|
|
27
|
+
description?: React.ReactNode;
|
|
28
|
+
placeholder?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function FilterTextField({
|
|
32
|
+
label,
|
|
33
|
+
id: providedId,
|
|
34
|
+
description,
|
|
35
|
+
placeholder,
|
|
36
|
+
type = "text",
|
|
37
|
+
...props
|
|
38
|
+
}: FilterTextFieldProps) {
|
|
39
|
+
const field = useFieldContext<string>();
|
|
40
|
+
const generatedId = useId();
|
|
41
|
+
const id = providedId ?? generatedId;
|
|
42
|
+
const descriptionId = `${id}-description`;
|
|
43
|
+
const errorId = `${id}-error`;
|
|
44
|
+
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
45
|
+
|
|
46
|
+
const uniqueErrors = getUniqueErrors(field.state.meta.errors);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Field data-invalid={isInvalid}>
|
|
50
|
+
<FieldLabel htmlFor={id}>{label}</FieldLabel>
|
|
51
|
+
{description && <FieldDescription id={descriptionId}>{description}</FieldDescription>}
|
|
52
|
+
<Input
|
|
53
|
+
id={id}
|
|
54
|
+
name={field.name as string}
|
|
55
|
+
type={type}
|
|
56
|
+
value={field.state.value ?? ""}
|
|
57
|
+
onBlur={field.handleBlur}
|
|
58
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => field.handleChange(e.target.value)}
|
|
59
|
+
placeholder={placeholder}
|
|
60
|
+
aria-invalid={isInvalid}
|
|
61
|
+
aria-describedby={cn(description && descriptionId, isInvalid && errorId)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
{isInvalid && uniqueErrors.length > 0 && <FieldError errors={uniqueErrors} />}
|
|
65
|
+
</Field>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface FilterSelectFieldProps {
|
|
70
|
+
label: string;
|
|
71
|
+
id?: string;
|
|
72
|
+
description?: React.ReactNode;
|
|
73
|
+
placeholder?: string;
|
|
74
|
+
options: PicklistValue[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function FilterSelectField({
|
|
78
|
+
label,
|
|
79
|
+
id: providedId,
|
|
80
|
+
description,
|
|
81
|
+
placeholder = "Select...",
|
|
82
|
+
options,
|
|
83
|
+
}: FilterSelectFieldProps) {
|
|
84
|
+
const field = useFieldContext<string>();
|
|
85
|
+
const generatedId = useId();
|
|
86
|
+
const id = providedId ?? generatedId;
|
|
87
|
+
const descriptionId = `${id}-description`;
|
|
88
|
+
const errorId = `${id}-error`;
|
|
89
|
+
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
90
|
+
|
|
91
|
+
const uniqueErrors = getUniqueErrors(field.state.meta.errors);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<Field data-invalid={isInvalid}>
|
|
95
|
+
<FieldLabel htmlFor={id}>{label}</FieldLabel>
|
|
96
|
+
{description && <FieldDescription id={descriptionId}>{description}</FieldDescription>}
|
|
97
|
+
<Select value={field.state.value ?? ""} onValueChange={(value) => field.handleChange(value)}>
|
|
98
|
+
<SelectTrigger
|
|
99
|
+
id={id}
|
|
100
|
+
aria-invalid={isInvalid}
|
|
101
|
+
aria-describedby={cn(description && descriptionId, isInvalid && errorId)}
|
|
102
|
+
>
|
|
103
|
+
<SelectValue placeholder={placeholder} />
|
|
104
|
+
</SelectTrigger>
|
|
105
|
+
<SelectContent>
|
|
106
|
+
{options.map((option) => {
|
|
107
|
+
if (!option || !option.value) return null;
|
|
108
|
+
return (
|
|
109
|
+
<SelectItem key={option.value} value={option.value}>
|
|
110
|
+
{option.label || option.value}
|
|
111
|
+
</SelectItem>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</SelectContent>
|
|
115
|
+
</Select>
|
|
116
|
+
{isInvalid && uniqueErrors.length > 0 && <FieldError errors={uniqueErrors} />}
|
|
117
|
+
</Field>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
interface FilterRangeFieldProps extends Omit<
|
|
122
|
+
React.ComponentProps<typeof Input>,
|
|
123
|
+
"name" | "value" | "onBlur" | "onChange" | "aria-invalid"
|
|
124
|
+
> {
|
|
125
|
+
label?: string;
|
|
126
|
+
description?: React.ReactNode;
|
|
127
|
+
placeholder?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function FilterRangeFieldBase({
|
|
131
|
+
label,
|
|
132
|
+
id: providedId,
|
|
133
|
+
description,
|
|
134
|
+
placeholder,
|
|
135
|
+
type = "text",
|
|
136
|
+
...props
|
|
137
|
+
}: FilterRangeFieldProps) {
|
|
138
|
+
const field = useFieldContext<string>();
|
|
139
|
+
const generatedId = useId();
|
|
140
|
+
const id = providedId ?? generatedId;
|
|
141
|
+
const descriptionId = `${id}-description`;
|
|
142
|
+
const errorId = `${id}-error`;
|
|
143
|
+
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
144
|
+
|
|
145
|
+
const uniqueErrors = getUniqueErrors(field.state.meta.errors);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div>
|
|
149
|
+
{label && <FieldLabel htmlFor={id}>{label}</FieldLabel>}
|
|
150
|
+
{description && <FieldDescription id={descriptionId}>{description}</FieldDescription>}
|
|
151
|
+
<Input
|
|
152
|
+
id={id}
|
|
153
|
+
name={field.name as string}
|
|
154
|
+
type={type}
|
|
155
|
+
value={field.state.value ?? ""}
|
|
156
|
+
onBlur={field.handleBlur}
|
|
157
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => field.handleChange(e.target.value)}
|
|
158
|
+
placeholder={placeholder}
|
|
159
|
+
aria-invalid={isInvalid}
|
|
160
|
+
aria-describedby={cn(description && descriptionId, isInvalid && errorId)}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
{isInvalid && uniqueErrors.length > 0 && <FieldError errors={uniqueErrors} />}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface FilterRangeMinFieldProps extends Omit<
|
|
169
|
+
React.ComponentProps<typeof Input>,
|
|
170
|
+
"name" | "value" | "onBlur" | "onChange" | "aria-invalid"
|
|
171
|
+
> {
|
|
172
|
+
label?: string;
|
|
173
|
+
description?: React.ReactNode;
|
|
174
|
+
placeholder?: string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface FilterRangeMaxFieldProps extends Omit<
|
|
178
|
+
React.ComponentProps<typeof Input>,
|
|
179
|
+
"name" | "value" | "onBlur" | "onChange" | "aria-invalid"
|
|
180
|
+
> {
|
|
181
|
+
label?: string;
|
|
182
|
+
description?: React.ReactNode;
|
|
183
|
+
placeholder?: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function FilterRangeMinField({ placeholder = "Min", ...props }: FilterRangeMinFieldProps) {
|
|
187
|
+
return <FilterRangeFieldBase placeholder={placeholder} {...props} />;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function FilterRangeMaxField({ placeholder = "Max", ...props }: FilterRangeMaxFieldProps) {
|
|
191
|
+
return <FilterRangeFieldBase placeholder={placeholder} {...props} />;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const { useAppForm } = createFormHook({
|
|
195
|
+
fieldContext,
|
|
196
|
+
formContext,
|
|
197
|
+
fieldComponents: {
|
|
198
|
+
FilterTextField,
|
|
199
|
+
FilterSelectField,
|
|
200
|
+
FilterRangeMinField,
|
|
201
|
+
FilterRangeMaxField,
|
|
202
|
+
},
|
|
203
|
+
formComponents: {},
|
|
204
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Hooks
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
export {
|
|
5
|
+
useObjectColumns,
|
|
6
|
+
useObjectSearchResults,
|
|
7
|
+
useObjectFilters,
|
|
8
|
+
useObjectListMetadata,
|
|
9
|
+
} from "./useObjectSearchData";
|
|
10
|
+
export type { ObjectListMetadata } from "./useObjectSearchData";
|
|
11
|
+
export { useRecordListGraphQL } from "./useRecordListGraphQL";
|
|
12
|
+
export type {
|
|
13
|
+
UseRecordListGraphQLReturn,
|
|
14
|
+
UseRecordListGraphQLOptions,
|
|
15
|
+
} from "./useRecordListGraphQL";
|
|
16
|
+
export { useRecordDetailLayout } from "./useRecordDetailLayout";
|
|
17
|
+
export type {
|
|
18
|
+
UseRecordDetailLayoutReturn,
|
|
19
|
+
UseRecordDetailLayoutParams,
|
|
20
|
+
} from "./useRecordDetailLayout";
|
|
21
|
+
export { useObjectInfoBatch } from "./useObjectInfoBatch";
|
|
22
|
+
export type { UseObjectInfoBatchResult } from "./useObjectInfoBatch";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { geocodeAddress, type GeocodeResult } from "@/utils/geocode";
|
|
3
|
+
|
|
4
|
+
export function useGeocode(address: string | null | undefined): {
|
|
5
|
+
coords: GeocodeResult | null;
|
|
6
|
+
loading: boolean;
|
|
7
|
+
} {
|
|
8
|
+
const [coords, setCoords] = useState<GeocodeResult | null>(null);
|
|
9
|
+
const [loading, setLoading] = useState(false);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!address?.trim()) {
|
|
13
|
+
setCoords(null);
|
|
14
|
+
setLoading(false);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let cancelled = false;
|
|
18
|
+
setLoading(true);
|
|
19
|
+
geocodeAddress(address)
|
|
20
|
+
.then((result) => {
|
|
21
|
+
if (!cancelled) setCoords(result);
|
|
22
|
+
})
|
|
23
|
+
.catch(() => {
|
|
24
|
+
if (!cancelled) setCoords(null);
|
|
25
|
+
})
|
|
26
|
+
.finally(() => {
|
|
27
|
+
if (!cancelled) setLoading(false);
|
|
28
|
+
});
|
|
29
|
+
return () => {
|
|
30
|
+
cancelled = true;
|
|
31
|
+
};
|
|
32
|
+
}, [address?.trim() ?? ""]);
|
|
33
|
+
|
|
34
|
+
return { coords, loading };
|
|
35
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetches Maintenance_Request__c list and exposes refetch for after create.
|
|
3
|
+
*/
|
|
4
|
+
import { useState, useEffect, useCallback } from "react";
|
|
5
|
+
import {
|
|
6
|
+
queryMaintenanceRequests,
|
|
7
|
+
type MaintenanceRequestSummary,
|
|
8
|
+
} from "@/api/maintenanceRequestApi";
|
|
9
|
+
|
|
10
|
+
export function useMaintenanceRequests(): {
|
|
11
|
+
requests: MaintenanceRequestSummary[];
|
|
12
|
+
loading: boolean;
|
|
13
|
+
error: string | null;
|
|
14
|
+
refetch: () => Promise<void>;
|
|
15
|
+
} {
|
|
16
|
+
const [requests, setRequests] = useState<MaintenanceRequestSummary[]>([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
const [error, setError] = useState<string | null>(null);
|
|
19
|
+
|
|
20
|
+
const fetchList = useCallback(async () => {
|
|
21
|
+
setLoading(true);
|
|
22
|
+
setError(null);
|
|
23
|
+
try {
|
|
24
|
+
const list = await queryMaintenanceRequests(50, null);
|
|
25
|
+
setRequests(list);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
setError(e instanceof Error ? e.message : "Failed to load maintenance requests");
|
|
28
|
+
setRequests([]);
|
|
29
|
+
} finally {
|
|
30
|
+
setLoading(false);
|
|
31
|
+
}
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
fetchList();
|
|
36
|
+
}, [fetchList]);
|
|
37
|
+
|
|
38
|
+
return { requests, loading, error, refetch: fetchList };
|
|
39
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useObjectInfoBatch
|
|
3
|
+
*
|
|
4
|
+
* Fetches object metadata (label, labelPlural, fields, etc.) for the given object API names.
|
|
5
|
+
* Uses the shared cache in objectInfoService so List, Home, and Detail views reuse one request.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useRef } from "react";
|
|
9
|
+
import { objectInfoService } from "../api/objectInfoService";
|
|
10
|
+
import type { ObjectInfoResult } from "../types/objectInfo/objectInfo";
|
|
11
|
+
|
|
12
|
+
export interface UseObjectInfoBatchResult {
|
|
13
|
+
/** Object metadata in the same order as the requested objectApiNames. */
|
|
14
|
+
objectInfos: ObjectInfoResult[];
|
|
15
|
+
loading: boolean;
|
|
16
|
+
error: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fetches batch object info for the given object API names. Results are cached;
|
|
21
|
+
* multiple callers (List, Home, Detail) share the same request.
|
|
22
|
+
*
|
|
23
|
+
* @param objectApiNames - Array of object API names (e.g. OBJECT_API_NAMES)
|
|
24
|
+
* @returns objectInfos (same order as input), loading, error
|
|
25
|
+
*/
|
|
26
|
+
export function useObjectInfoBatch(objectApiNames: string[]): UseObjectInfoBatchResult {
|
|
27
|
+
const [state, setState] = useState<UseObjectInfoBatchResult>({
|
|
28
|
+
objectInfos: [],
|
|
29
|
+
loading: objectApiNames.length > 0,
|
|
30
|
+
error: null,
|
|
31
|
+
});
|
|
32
|
+
const isCancelled = useRef(false);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
isCancelled.current = false;
|
|
36
|
+
const names = objectApiNames.filter(Boolean);
|
|
37
|
+
if (names.length === 0) {
|
|
38
|
+
setState({ objectInfos: [], loading: false, error: null });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
setState((s) => ({ ...s, loading: true, error: null }));
|
|
42
|
+
objectInfoService
|
|
43
|
+
.getObjectInfoBatch(names.join(","))
|
|
44
|
+
.then((res) => {
|
|
45
|
+
if (isCancelled.current) return;
|
|
46
|
+
const objectInfos = names
|
|
47
|
+
.map((apiName) => res.results?.find((r) => r.result?.apiName === apiName)?.result)
|
|
48
|
+
.filter((r) => r != null) as ObjectInfoResult[];
|
|
49
|
+
setState({ objectInfos, loading: false, error: null });
|
|
50
|
+
})
|
|
51
|
+
.catch((err) => {
|
|
52
|
+
if (isCancelled.current) return;
|
|
53
|
+
setState({
|
|
54
|
+
objectInfos: [],
|
|
55
|
+
loading: false,
|
|
56
|
+
error: err instanceof Error ? err.message : (err as string),
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
return () => {
|
|
60
|
+
isCancelled.current = true;
|
|
61
|
+
};
|
|
62
|
+
}, [objectApiNames.join(",")]);
|
|
63
|
+
|
|
64
|
+
return state;
|
|
65
|
+
}
|