@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,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
+ }