@salesforce/webapp-template-app-react-sample-b2e-experimental 1.73.0 → 1.74.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 (117) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/objects/Maintenance_Request__c/Maintenance_Request__c.object-meta.xml +11 -1
  3. package/dist/force-app/main/default/objects/Maintenance_Worker__c/Maintenance_Worker__c.object-meta.xml +6 -1
  4. package/dist/force-app/main/default/objects/Property__c/Property__c.object-meta.xml +6 -1
  5. package/dist/force-app/main/default/webapplications/appreactsampleb2e/package.json +7 -5
  6. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/api/maintenanceWorkers.ts +60 -0
  7. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ApplicationsTable.tsx +59 -62
  8. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/FiltersFromApi.tsx +200 -0
  9. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ListPageFilters.tsx +97 -0
  10. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/MaintenanceTable.tsx +2 -1
  11. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/ObjectSelect.tsx +39 -0
  12. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/VerticalNav.tsx +6 -4
  13. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/dashboard/GlobalSearchBar.tsx +125 -0
  14. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/FilterErrorAlert.tsx +15 -0
  15. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageErrorState.tsx +19 -0
  16. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/feedback/PageLoadingState.tsx +18 -0
  17. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldRange.tsx +40 -0
  18. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldSelect.tsx +190 -0
  19. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/FilterFieldText.tsx +32 -0
  20. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/filters/ListPageFilterRow.tsx +100 -0
  21. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageContainer.tsx +9 -0
  22. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/layout/PageHeader.tsx +21 -0
  23. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/components/list/ListPageWithFilters.tsx +70 -0
  24. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/constants.ts +39 -0
  25. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/index.ts +19 -0
  26. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectDetailService.ts +125 -0
  27. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoGraphQLService.ts +194 -0
  28. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/objectInfoService.ts +199 -0
  29. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/api/recordListGraphQLService.ts +364 -0
  30. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailFields.tsx +55 -0
  31. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailForm.tsx +146 -0
  32. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
  33. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
  34. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/Section.tsx +108 -0
  35. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/SectionRow.tsx +20 -0
  36. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
  37. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
  38. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
  39. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
  40. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
  41. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
  42. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
  43. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/detail/formatted/index.ts +6 -0
  44. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterField.tsx +54 -0
  45. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterInput.tsx +55 -0
  46. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
  47. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
  48. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/filters-form.tsx +114 -0
  49. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/forms/submit-button.tsx +47 -0
  50. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
  51. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
  52. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchHeader.tsx +31 -0
  53. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchPagination.tsx +144 -0
  54. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultCard.tsx +136 -0
  55. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
  56. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
  57. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterInput.tsx +55 -0
  58. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/filters/FilterSelect.tsx +72 -0
  59. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/form.tsx +209 -0
  60. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/index.ts +22 -0
  61. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectInfoBatch.ts +65 -0
  62. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useObjectSearchData.ts +395 -0
  63. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordDetailLayout.ts +156 -0
  64. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
  65. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/DetailPage.tsx +109 -0
  66. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/pages/GlobalSearch.tsx +229 -0
  67. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/filters.ts +121 -0
  68. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/filters/picklist.ts +32 -0
  69. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/index.ts +5 -0
  70. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/objectInfo/objectInfo.ts +166 -0
  71. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
  72. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/types/search/searchResults.ts +229 -0
  73. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/apiUtils.ts +125 -0
  74. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/cacheUtils.ts +76 -0
  75. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/debounce.ts +89 -0
  76. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldUtils.ts +354 -0
  77. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
  78. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/filterUtils.ts +32 -0
  79. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
  80. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/formUtils.ts +142 -0
  81. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
  82. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +319 -0
  83. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
  84. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/index.ts +59 -0
  85. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
  86. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/linkUtils.ts +14 -0
  87. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/paginationUtils.ts +49 -0
  88. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/recordUtils.ts +159 -0
  89. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/features/global-search/utils/sanitizationUtils.ts +49 -0
  90. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useAccumulatedListPages.ts +29 -0
  91. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/hooks/useListPage.ts +167 -0
  92. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/index.ts +8 -4
  93. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationAdapter.ts +33 -0
  94. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/applicationColumns.ts +28 -0
  95. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/constants.ts +24 -0
  96. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/fieldMappers.ts +71 -0
  97. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/filterUtils.ts +165 -0
  98. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/globalSearchConstants.ts +40 -0
  99. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listFilters.ts +152 -0
  100. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/listPageConfig.ts +65 -0
  101. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceAdapter.ts +110 -0
  102. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceColumns.ts +24 -0
  103. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerAdapter.ts +29 -0
  104. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/maintenanceWorkerColumns.ts +25 -0
  105. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/objectApiNames.ts +13 -0
  106. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyAdapter.ts +68 -0
  107. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/propertyColumns.ts +17 -0
  108. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/routeConfig.ts +35 -0
  109. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/lib/types.ts +10 -0
  110. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Applications.tsx +47 -62
  111. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Home.tsx +130 -98
  112. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Maintenance.tsx +74 -91
  113. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/MaintenanceWorkers.tsx +138 -0
  114. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/pages/Properties.tsx +166 -85
  115. package/dist/force-app/main/default/webapplications/appreactsampleb2e/src/routes.tsx +41 -2
  116. package/dist/package.json +1 -1
  117. package/package.json +5 -1
@@ -1,46 +1,30 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { ChevronDown } from "lucide-react";
3
- import { getAllMaintenanceRequests, updateMaintenanceStatus } from "../api/maintenance.js";
4
- import type { MaintenanceRequest } from "../lib/types.js";
3
+ import { useListPage } from "../hooks/useListPage.js";
4
+ import { maintenanceRequestsListConfig } from "../lib/listPageConfig.js";
5
+ import { ListPageWithFilters } from "../components/list/ListPageWithFilters.js";
6
+ import { PageHeader } from "../components/layout/PageHeader.js";
5
7
  import { UserAvatar } from "../components/UserAvatar.js";
6
8
  import { StatusBadge } from "../components/StatusBadge.js";
7
9
  import { MaintenanceDetailsModal } from "../components/MaintenanceDetailsModal.js";
10
+ import { updateMaintenanceStatus } from "../api/maintenance.js";
11
+ import type { MaintenanceRequest } from "../lib/types.js";
8
12
 
9
13
  export default function Maintenance() {
10
- const [requests, setRequests] = useState<MaintenanceRequest[]>([]);
11
- const [loading, setLoading] = useState(true);
14
+ const list = useListPage(maintenanceRequestsListConfig);
12
15
  const [selectedRequest, setSelectedRequest] = useState<MaintenanceRequest | null>(null);
13
16
  const [notification, setNotification] = useState<{
14
17
  message: string;
15
18
  type: "success" | "error";
16
19
  } | null>(null);
17
20
 
18
- useEffect(() => {
19
- loadMaintenanceRequests();
20
- }, []);
21
-
22
- // Clear notification after 5 seconds
23
21
  useEffect(() => {
24
22
  if (notification) {
25
- const timer = setTimeout(() => {
26
- setNotification(null);
27
- }, 5000);
23
+ const timer = setTimeout(() => setNotification(null), 5000);
28
24
  return () => clearTimeout(timer);
29
25
  }
30
26
  }, [notification]);
31
27
 
32
- const loadMaintenanceRequests = async () => {
33
- try {
34
- setLoading(true);
35
- const data = await getAllMaintenanceRequests();
36
- setRequests(data);
37
- } catch (error) {
38
- console.error("Error loading maintenance requests:", error);
39
- } finally {
40
- setLoading(false);
41
- }
42
- };
43
-
44
28
  const handleRowClick = (request: MaintenanceRequest) => {
45
29
  setSelectedRequest(request);
46
30
  };
@@ -52,66 +36,63 @@ export default function Maintenance() {
52
36
  const handleSaveStatus = async (requestId: string, status: string) => {
53
37
  try {
54
38
  const success = await updateMaintenanceStatus(requestId, status);
55
-
56
39
  if (success) {
57
- // Update the local state
58
- setRequests((prev) => prev.map((req) => (req.id === requestId ? { ...req, status } : req)));
59
-
60
- // Update selected request if it's the one being edited
61
40
  if (selectedRequest?.id === requestId) {
62
41
  setSelectedRequest({ ...selectedRequest, status });
63
42
  }
64
-
65
- showNotification("Maintenance request status updated successfully!", "success");
43
+ setNotification({
44
+ message: "Maintenance request status updated successfully!",
45
+ type: "success",
46
+ });
66
47
  } else {
67
- showNotification("Failed to update maintenance request status", "error");
48
+ setNotification({ message: "Failed to update maintenance request status", type: "error" });
68
49
  }
69
50
  } catch (error) {
70
51
  console.error("Error updating maintenance request:", error);
71
- showNotification("An error occurred while updating the status", "error");
52
+ setNotification({ message: "An error occurred while updating the status", type: "error" });
72
53
  }
73
54
  };
74
55
 
75
- const showNotification = (message: string, type: "success" | "error") => {
76
- setNotification({ message, type });
77
- };
78
-
79
- if (loading) {
80
- return (
81
- <div className="flex items-center justify-center h-screen bg-gray-50">
82
- <div className="text-lg text-gray-600">Loading maintenance requests...</div>
83
- </div>
84
- );
85
- }
86
-
87
56
  return (
88
- <div className="min-h-screen bg-gray-50 p-8">
89
- <div className="max-w-7xl mx-auto">
90
- {/* Notification */}
91
- {notification && (
92
- <div className="fixed top-4 right-4 z-50 animate-slide-in">
93
- <div
94
- className={`px-6 py-4 rounded-lg shadow-lg ${
95
- notification.type === "success"
96
- ? "bg-green-500 text-white"
97
- : "bg-red-500 text-white"
98
- }`}
99
- >
100
- <div className="flex items-center gap-3">
101
- <span className="text-lg">{notification.type === "success" ? "✓" : "✕"}</span>
102
- <p className="font-medium">{notification.message}</p>
103
- </div>
57
+ <>
58
+ {notification && (
59
+ <div className="fixed top-4 right-4 z-50 animate-slide-in">
60
+ <div
61
+ className={`px-6 py-4 rounded-lg shadow-lg ${
62
+ notification.type === "success" ? "bg-green-500 text-white" : "bg-red-500 text-white"
63
+ }`}
64
+ >
65
+ <div className="flex items-center gap-3">
66
+ <span className="text-lg">{notification.type === "success" ? "✓" : "✕"}</span>
67
+ <p className="font-medium">{notification.message}</p>
104
68
  </div>
105
69
  </div>
106
- )}
107
-
108
- {/* Header */}
109
- <div className="mb-6">
110
- <h1 className="text-2xl font-bold text-gray-900">Maintenance</h1>
111
- <p className="text-gray-600 mt-1">Track and manage maintenance requests</p>
112
70
  </div>
113
-
114
- {/* Table Header */}
71
+ )}
72
+
73
+ <PageHeader
74
+ title="Maintenance Requests"
75
+ description="Track and manage maintenance requests"
76
+ />
77
+
78
+ <ListPageWithFilters
79
+ filterProps={{
80
+ filters: list.filters,
81
+ picklistValues: list.picklistValues,
82
+ formValues: list.formValues,
83
+ onFormValueChange: list.onFormValueChange,
84
+ onApply: list.onApplyFilters,
85
+ onReset: list.onResetFilters,
86
+ ariaLabel: maintenanceRequestsListConfig.filtersAriaLabel,
87
+ }}
88
+ filterError={list.filterError}
89
+ loading={list.loading}
90
+ error={list.error}
91
+ loadingMessage={maintenanceRequestsListConfig.loadingMessage}
92
+ isEmpty={list.items.length === 0}
93
+ searchPlaceholder="Search by description, tenant, property, status..."
94
+ searchAriaLabel="Search maintenance requests"
95
+ >
115
96
  <div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
116
97
  <div className="grid grid-cols-12 gap-4 px-6 py-4 bg-gray-50 border-b border-gray-200">
117
98
  <div className="col-span-6 flex items-center gap-2">
@@ -131,21 +112,17 @@ export default function Maintenance() {
131
112
  </span>
132
113
  </div>
133
114
  </div>
134
-
135
- {/* Table Rows */}
136
115
  <div className="divide-y divide-gray-200">
137
- {requests.length === 0 ? (
116
+ {list.items.length === 0 ? (
138
117
  <div className="text-center py-12 text-gray-500">No maintenance requests found</div>
139
118
  ) : (
140
- requests.map((request) => (
119
+ list.items.map((request) => (
141
120
  <div
142
121
  key={request.id}
143
122
  onClick={() => handleRowClick(request)}
144
123
  className="grid grid-cols-12 gap-4 px-6 py-5 hover:bg-gray-50 transition-colors cursor-pointer"
145
124
  >
146
- {/* Maintenance Task */}
147
125
  <div className="col-span-6 flex items-center gap-4">
148
- {/* Task Image */}
149
126
  <div className="w-16 h-16 rounded-lg bg-gray-200 flex-shrink-0 overflow-hidden">
150
127
  {request.imageUrl ? (
151
128
  <img
@@ -159,8 +136,6 @@ export default function Maintenance() {
159
136
  </div>
160
137
  )}
161
138
  </div>
162
-
163
- {/* Task Details */}
164
139
  <div className="flex-1 min-w-0">
165
140
  <h3 className="font-semibold text-gray-900 truncate mb-1">
166
141
  {request.description}
@@ -168,8 +143,6 @@ export default function Maintenance() {
168
143
  <p className="text-sm text-gray-500">By Tenant</p>
169
144
  </div>
170
145
  </div>
171
-
172
- {/* Tenant Unit */}
173
146
  <div className="col-span-4 flex items-center">
174
147
  <div className="flex items-center gap-3">
175
148
  <UserAvatar name={request.tenantName || "Unknown"} size="md" />
@@ -183,8 +156,6 @@ export default function Maintenance() {
183
156
  </div>
184
157
  </div>
185
158
  </div>
186
-
187
- {/* Status */}
188
159
  <div className="col-span-2 flex items-center">
189
160
  <StatusBadge status={request.status} />
190
161
  </div>
@@ -194,16 +165,28 @@ export default function Maintenance() {
194
165
  </div>
195
166
  </div>
196
167
 
197
- {/* Maintenance Details Modal */}
198
- {selectedRequest && (
199
- <MaintenanceDetailsModal
200
- request={selectedRequest}
201
- isOpen={!!selectedRequest}
202
- onClose={handleCloseModal}
203
- onSave={handleSaveStatus}
204
- />
168
+ {list.canLoadMore && (
169
+ <div className="flex justify-center mt-6">
170
+ <button
171
+ type="button"
172
+ onClick={list.onLoadMore}
173
+ disabled={list.loadMoreLoading}
174
+ className="px-6 py-2 bg-purple-700 hover:bg-purple-800 text-white rounded-lg text-sm font-medium disabled:opacity-50"
175
+ >
176
+ {list.loadMoreLoading ? "Loading..." : "Load More"}
177
+ </button>
178
+ </div>
205
179
  )}
206
- </div>
207
- </div>
180
+ </ListPageWithFilters>
181
+
182
+ {selectedRequest && (
183
+ <MaintenanceDetailsModal
184
+ request={selectedRequest}
185
+ isOpen={!!selectedRequest}
186
+ onClose={handleCloseModal}
187
+ onSave={handleSaveStatus}
188
+ />
189
+ )}
190
+ </>
208
191
  );
209
192
  }
@@ -0,0 +1,138 @@
1
+ import { useState } from "react";
2
+ import { useListPage } from "../hooks/useListPage.js";
3
+ import { maintenanceWorkersListConfig } from "../lib/listPageConfig.js";
4
+ import { ListPageWithFilters } from "../components/list/ListPageWithFilters.js";
5
+ import { PageHeader } from "../components/layout/PageHeader.js";
6
+ import type { MaintenanceWorker } from "../lib/types.js";
7
+
8
+ export default function MaintenanceWorkers() {
9
+ const list = useListPage(maintenanceWorkersListConfig);
10
+ const [selectedWorker, setSelectedWorker] = useState<MaintenanceWorker | null>(null);
11
+
12
+ return (
13
+ <>
14
+ <PageHeader title="Maintenance Workers" description="View and filter maintenance workers" />
15
+
16
+ <ListPageWithFilters
17
+ filterProps={{
18
+ filters: list.filters,
19
+ picklistValues: list.picklistValues,
20
+ formValues: list.formValues,
21
+ onFormValueChange: list.onFormValueChange,
22
+ onApply: list.onApplyFilters,
23
+ onReset: list.onResetFilters,
24
+ ariaLabel: maintenanceWorkersListConfig.filtersAriaLabel,
25
+ }}
26
+ filterError={list.filterError}
27
+ loading={list.loading}
28
+ error={list.error}
29
+ loadingMessage={maintenanceWorkersListConfig.loadingMessage}
30
+ isEmpty={list.items.length === 0}
31
+ searchPlaceholder="Search by name, organization, status..."
32
+ searchAriaLabel="Search workers"
33
+ >
34
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
35
+ <div className="grid grid-cols-12 gap-4 px-6 py-4 bg-gray-50 border-b border-gray-200">
36
+ <div className="col-span-5">
37
+ <span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
38
+ Name
39
+ </span>
40
+ </div>
41
+ <div className="col-span-4">
42
+ <span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
43
+ Organization
44
+ </span>
45
+ </div>
46
+ <div className="col-span-2">
47
+ <span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
48
+ Active Requests
49
+ </span>
50
+ </div>
51
+ <div className="col-span-1">
52
+ <span className="text-sm font-semibold text-purple-700 uppercase tracking-wide">
53
+ Status
54
+ </span>
55
+ </div>
56
+ </div>
57
+ <div className="divide-y divide-gray-200">
58
+ {list.items.length === 0 ? (
59
+ <div className="text-center py-12 text-gray-500">No maintenance workers found</div>
60
+ ) : (
61
+ list.items.map((worker) => (
62
+ <div
63
+ key={worker.id}
64
+ onClick={() => setSelectedWorker(worker)}
65
+ className="grid grid-cols-12 gap-4 px-6 py-4 hover:bg-gray-50 transition-colors cursor-pointer"
66
+ >
67
+ <div className="col-span-5 font-medium text-gray-900">{worker.name}</div>
68
+ <div className="col-span-4 text-gray-600">{worker.organization ?? "—"}</div>
69
+ <div className="col-span-2 text-gray-600">
70
+ {worker.activeRequestsCount ?? "—"}
71
+ </div>
72
+ <div className="col-span-1 text-gray-600">{worker.status ?? "—"}</div>
73
+ </div>
74
+ ))
75
+ )}
76
+ </div>
77
+ </div>
78
+ {list.canLoadMore && (
79
+ <div className="flex justify-center mt-6">
80
+ <button
81
+ type="button"
82
+ onClick={list.onLoadMore}
83
+ disabled={list.loadMoreLoading}
84
+ className="px-6 py-2 bg-purple-700 hover:bg-purple-800 text-white rounded-lg text-sm font-medium disabled:opacity-50"
85
+ >
86
+ {list.loadMoreLoading ? "Loading..." : "Load More"}
87
+ </button>
88
+ </div>
89
+ )}
90
+ </ListPageWithFilters>
91
+ {selectedWorker && (
92
+ <div
93
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
94
+ role="dialog"
95
+ aria-modal="true"
96
+ aria-labelledby="worker-dialog-title"
97
+ onClick={() => setSelectedWorker(null)}
98
+ >
99
+ <div
100
+ className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 p-6"
101
+ onClick={(e) => e.stopPropagation()}
102
+ >
103
+ <h2 id="worker-dialog-title" className="text-lg font-semibold text-gray-900 mb-4">
104
+ Worker Details
105
+ </h2>
106
+ <dl className="space-y-2 text-sm">
107
+ <div>
108
+ <dt className="text-gray-500">Name</dt>
109
+ <dd className="font-medium text-gray-900">{selectedWorker.name}</dd>
110
+ </div>
111
+ <div>
112
+ <dt className="text-gray-500">Organization</dt>
113
+ <dd className="text-gray-900">{selectedWorker.organization ?? "—"}</dd>
114
+ </div>
115
+ <div>
116
+ <dt className="text-gray-500">Phone</dt>
117
+ <dd className="text-gray-900">{selectedWorker.phone ?? "—"}</dd>
118
+ </div>
119
+ <div>
120
+ <dt className="text-gray-500">Status</dt>
121
+ <dd className="text-gray-900">{selectedWorker.status ?? "—"}</dd>
122
+ </div>
123
+ </dl>
124
+ <div className="mt-6 flex justify-end">
125
+ <button
126
+ type="button"
127
+ onClick={() => setSelectedWorker(null)}
128
+ className="px-4 py-2 text-sm font-medium text-purple-700 hover:bg-purple-50 rounded-md"
129
+ >
130
+ Close
131
+ </button>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ )}
136
+ </>
137
+ );
138
+ }
@@ -1,114 +1,195 @@
1
- import { useEffect, useState } from "react";
2
- import { getProperties } from "../api/properties.js";
1
+ import { useEffect, useState, useRef, useMemo, useCallback } from "react";
2
+ import { useSearchParams } from "react-router";
3
+ import {
4
+ useObjectListMetadata,
5
+ useRecordListGraphQL,
6
+ type FilterCriteria,
7
+ } from "@salesforce/webapp-template-feature-react-global-search-experimental";
3
8
  import type { Property } from "../lib/types.js";
4
9
  import { PropertyCard } from "../components/PropertyCard.js";
5
10
  import { PropertyDetailsModal } from "../components/PropertyDetailsModal.js";
6
- import { Button } from "@/components/ui/button";
11
+ import { PageContainer } from "../components/layout/PageContainer.js";
12
+ import { PageHeader } from "../components/layout/PageHeader.js";
13
+ import { PageLoadingState } from "../components/feedback/PageLoadingState.js";
14
+ import { PageErrorState } from "../components/feedback/PageErrorState.js";
15
+ import { FilterErrorAlert } from "../components/feedback/FilterErrorAlert.js";
16
+ import { ListPageFilterRow } from "../components/filters/ListPageFilterRow.js";
17
+ import { GLOBAL_SEARCH_OBJECT_API_NAME } from "../lib/globalSearchConstants.js";
18
+ import { nodeToProperty } from "../lib/propertyAdapter.js";
19
+ import { getPropertyListColumns } from "../lib/propertyColumns.js";
20
+ import {
21
+ buildFilterCriteriaFromFormValues,
22
+ getDefaultFilterFormValues,
23
+ getApplicableFilters,
24
+ } from "../lib/filterUtils.js";
25
+ import { PAGE_SIZE_LIST, PROPERTY_FILTER_EXCLUDED_FIELD_PATHS } from "../lib/constants.js";
26
+ import { useAccumulatedListPages } from "../hooks/useAccumulatedListPages.js";
27
+
28
+ const PROPERTIES_DEFAULT_SORT = "CreatedDate DESC";
29
+
30
+ const mapNodeToProperty = (node: unknown) =>
31
+ nodeToProperty(node as Record<string, unknown> | undefined);
7
32
 
8
33
  export default function Properties() {
9
- const [properties, setProperties] = useState<Property[]>([]);
10
- const [loading, setLoading] = useState(true);
11
- const [loadingMore, setLoadingMore] = useState(false);
12
- const [hasNextPage, setHasNextPage] = useState(false);
13
- const [endCursor, setEndCursor] = useState<string | null>(null);
34
+ const [searchParams] = useSearchParams();
35
+ const searchQuery = searchParams.get("q") ?? "";
36
+
37
+ const [afterCursor, setAfterCursor] = useState<string | null>(null);
38
+ const [appliedFilters, setAppliedFilters] = useState<FilterCriteria[]>([]);
39
+ const [filterFormValues, setFilterFormValues] = useState<Record<string, string>>({});
40
+ const [filterError, setFilterError] = useState<string | null>(null);
14
41
  const [selectedProperty, setSelectedProperty] = useState<Property | null>(null);
42
+ const hasInitializedFiltersRef = useRef(false);
43
+
44
+ const listMeta = useObjectListMetadata(GLOBAL_SEARCH_OBJECT_API_NAME);
45
+ const columns = useMemo(() => getPropertyListColumns(listMeta.columns), [listMeta.columns]);
46
+ const filters = useMemo(
47
+ () => getApplicableFilters(listMeta.filters ?? [], PROPERTY_FILTER_EXCLUDED_FIELD_PATHS),
48
+ [listMeta.filters],
49
+ );
50
+ const picklistValues = listMeta.picklistValues ?? {};
51
+
52
+ const {
53
+ edges,
54
+ pageInfo,
55
+ loading: resultsLoading,
56
+ error: resultsError,
57
+ } = useRecordListGraphQL({
58
+ objectApiName: GLOBAL_SEARCH_OBJECT_API_NAME,
59
+ columns,
60
+ columnsLoading: listMeta.loading,
61
+ columnsError: listMeta.error,
62
+ first: PAGE_SIZE_LIST,
63
+ after: afterCursor,
64
+ searchQuery: searchQuery.trim() || undefined,
65
+ sortBy: PROPERTIES_DEFAULT_SORT,
66
+ filters: appliedFilters,
67
+ });
68
+
69
+ const [accumulated, setAccumulated] = useAccumulatedListPages(
70
+ edges,
71
+ resultsLoading,
72
+ afterCursor,
73
+ mapNodeToProperty,
74
+ );
75
+
76
+ useEffect(() => {
77
+ if (filters.length === 0) return;
78
+ if (!hasInitializedFiltersRef.current) {
79
+ hasInitializedFiltersRef.current = true;
80
+ setFilterFormValues(getDefaultFilterFormValues(filters));
81
+ }
82
+ }, [filters]);
15
83
 
16
84
  useEffect(() => {
17
- loadProperties();
85
+ setAfterCursor(null);
86
+ setAccumulated([]);
87
+ }, [searchQuery, appliedFilters, setAccumulated]);
88
+
89
+ const hasNextPage = Boolean(pageInfo?.hasNextPage);
90
+ const endCursor = pageInfo?.endCursor ?? null;
91
+
92
+ const handleLoadMore = useCallback(() => {
93
+ if (endCursor && !searchQuery.trim()) setAfterCursor(endCursor);
94
+ }, [endCursor, searchQuery]);
95
+
96
+ const handlePropertyClick = useCallback((property: Property) => {
97
+ setSelectedProperty(property);
18
98
  }, []);
19
99
 
20
- const loadProperties = async () => {
21
- try {
22
- setLoading(true);
23
- const result = await getProperties(12);
24
- setProperties(result.properties);
25
- setHasNextPage(result.pageInfo.hasNextPage);
26
- setEndCursor(result.pageInfo.endCursor!);
27
- } catch (error) {
28
- console.error("Error loading properties:", error);
29
- } finally {
30
- setLoading(false);
31
- }
32
- };
33
-
34
- const loadMoreProperties = async () => {
35
- if (!endCursor) return;
36
-
37
- try {
38
- setLoadingMore(true);
39
- const result = await getProperties(12, endCursor);
40
- setProperties((prev) => [...prev, ...result.properties]);
41
- setHasNextPage(result.pageInfo.hasNextPage);
42
- setEndCursor(result.pageInfo.endCursor!);
43
- } catch (error) {
44
- console.error("Error loading more properties:", error);
45
- } finally {
46
- setLoadingMore(false);
100
+ const handleApplyFilters = useCallback(() => {
101
+ setFilterError(null);
102
+ const result = buildFilterCriteriaFromFormValues(
103
+ GLOBAL_SEARCH_OBJECT_API_NAME,
104
+ filters,
105
+ filterFormValues,
106
+ );
107
+ if (result.rangeError) {
108
+ setFilterError(result.rangeError);
109
+ return;
47
110
  }
48
- };
111
+ setAppliedFilters(result.criteria);
112
+ setAfterCursor(null);
113
+ }, [filters, filterFormValues]);
49
114
 
50
- const handlePropertyClick = (property: Property) => {
51
- setSelectedProperty(property);
52
- };
115
+ const handleResetFilters = useCallback(() => {
116
+ setFilterFormValues(getDefaultFilterFormValues(filters));
117
+ setAppliedFilters([]);
118
+ setAfterCursor(null);
119
+ setFilterError(null);
120
+ }, [filters]);
53
121
 
54
- const handleCloseModal = () => {
55
- setSelectedProperty(null);
56
- };
122
+ const handleFilterFormValueChange = useCallback((key: string, value: string) => {
123
+ setFilterFormValues((prev) => ({ ...prev, [key]: value }));
124
+ setFilterError(null);
125
+ }, []);
57
126
 
58
- if (loading) {
59
- return (
60
- <div className="flex items-center justify-center h-screen bg-gray-50">
61
- <div className="text-lg text-gray-600">Loading properties...</div>
62
- </div>
63
- );
127
+ const loading = listMeta.loading || resultsLoading;
128
+ const error = listMeta.error ?? resultsError;
129
+
130
+ if (error) {
131
+ return <PageErrorState message={error} />;
132
+ }
133
+
134
+ if (loading && accumulated.length === 0) {
135
+ return <PageLoadingState message="Loading properties..." />;
64
136
  }
65
137
 
66
138
  return (
67
- <div className="min-h-screen bg-gray-50 p-8">
68
- <div className="max-w-7xl mx-auto">
69
- {/* Header */}
70
- <div className="mb-6">
71
- <h1 className="text-2xl font-bold text-gray-900">Properties</h1>
72
- <p className="text-gray-600 mt-1">Browse and manage available properties</p>
73
- </div>
139
+ <>
140
+ <PageHeader title="Properties" description="Browse and manage available properties" />
141
+ <PageContainer>
142
+ <div className="max-w-7xl mx-auto space-y-6">
143
+ <ListPageFilterRow
144
+ filters={filters}
145
+ picklistValues={picklistValues}
146
+ formValues={filterFormValues}
147
+ onFormValueChange={handleFilterFormValueChange}
148
+ onApply={handleApplyFilters}
149
+ onReset={handleResetFilters}
150
+ ariaLabel="Properties filters"
151
+ />
152
+ {filterError && <FilterErrorAlert message={filterError} />}
74
153
 
75
- {/* Properties Grid */}
76
- {properties.length === 0 ? (
77
- <div className="text-center py-12">
78
- <p className="text-gray-500 text-lg">No properties found</p>
79
- </div>
80
- ) : (
81
- <>
82
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
83
- {properties.map((property) => (
84
- <PropertyCard key={property.id} property={property} onClick={handlePropertyClick} />
85
- ))}
154
+ {accumulated.length === 0 ? (
155
+ <div className="text-center py-12">
156
+ <p className="text-gray-500 text-lg">No properties found</p>
86
157
  </div>
87
-
88
- {/* Load More Button */}
89
- {hasNextPage && (
90
- <div className="flex justify-center mt-8">
91
- <Button
92
- onClick={loadMoreProperties}
93
- disabled={loadingMore}
94
- className="px-8 py-3 bg-purple-700 hover:bg-purple-800 text-white rounded-lg font-medium transition-colors"
95
- >
96
- {loadingMore ? "Loading..." : "Load More"}
97
- </Button>
158
+ ) : (
159
+ <>
160
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
161
+ {accumulated.map((property) => (
162
+ <PropertyCard
163
+ key={property.id}
164
+ property={property}
165
+ onClick={handlePropertyClick}
166
+ />
167
+ ))}
98
168
  </div>
99
- )}
100
- </>
101
- )}
169
+ {hasNextPage && !searchQuery.trim() && (
170
+ <div className="flex justify-center mt-6">
171
+ <button
172
+ type="button"
173
+ onClick={handleLoadMore}
174
+ disabled={resultsLoading}
175
+ className="px-6 py-2 bg-purple-700 hover:bg-purple-800 text-white rounded-lg text-sm font-medium disabled:opacity-50"
176
+ >
177
+ {resultsLoading ? "Loading..." : "Load More"}
178
+ </button>
179
+ </div>
180
+ )}
181
+ </>
182
+ )}
183
+ </div>
102
184
 
103
- {/* Property Details Modal */}
104
185
  {selectedProperty && (
105
186
  <PropertyDetailsModal
106
187
  property={selectedProperty}
107
188
  isOpen={!!selectedProperty}
108
- onClose={handleCloseModal}
189
+ onClose={() => setSelectedProperty(null)}
109
190
  />
110
191
  )}
111
- </div>
112
- </div>
192
+ </PageContainer>
193
+ </>
113
194
  );
114
195
  }