@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
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Adapter: maps Property__c GraphQL node (from feature-global-search) to app Property type.
3
+ */
4
+ import type { Property } from "./types.js";
5
+
6
+ interface PropertyNode {
7
+ Id?: string;
8
+ Name?: { value?: string };
9
+ Address__c?: { value?: string };
10
+ Description__c?: { value?: string };
11
+ Type__c?: { value?: string };
12
+ Status__c?: { value?: string };
13
+ Monthly_Rent__c?: { value?: number };
14
+ Bedrooms__c?: { value?: number };
15
+ Bathrooms__c?: { value?: number };
16
+ Sq_Ft__c?: { value?: number };
17
+ Year_Built__c?: { value?: number };
18
+ Hero_Image__c?: { value?: string };
19
+ Deposit__c?: { value?: number };
20
+ Parking__c?: { value?: string };
21
+ Pet_Friendly__c?: { value?: boolean };
22
+ Available_Date__c?: { value?: string };
23
+ Lease_Term__c?: { value?: number };
24
+ Features__c?: { value?: string };
25
+ Utilities__c?: { value?: string };
26
+ Tour_URL__c?: { value?: string };
27
+ CreatedDate?: { value?: string };
28
+ [key: string]: unknown;
29
+ }
30
+
31
+ export function nodeToProperty(node: Record<string, unknown> | undefined): Property {
32
+ const n = (node ?? {}) as PropertyNode;
33
+ const createdYear = n.CreatedDate?.value
34
+ ? new Date(n.CreatedDate.value).getFullYear().toString()
35
+ : undefined;
36
+ const features = n.Features__c?.value ? n.Features__c.value.split(";") : undefined;
37
+ const utilities = n.Utilities__c?.value ? n.Utilities__c.value.split(";") : undefined;
38
+
39
+ return {
40
+ id: n.Id ?? "",
41
+ name: n.Name?.value ?? "Unnamed Property",
42
+ address: n.Address__c?.value ?? "Address not available",
43
+ type: (n.Type__c?.value?.toLowerCase() as "apartment" | "house" | "commercial") ?? "apartment",
44
+ status:
45
+ (n.Status__c?.value?.toLowerCase() as "available" | "rented" | "maintenance") ?? "available",
46
+ monthlyRent: n.Monthly_Rent__c?.value ?? 0,
47
+ bedrooms: n.Bedrooms__c?.value,
48
+ bathrooms: n.Bathrooms__c?.value,
49
+ heroImage: n.Hero_Image__c?.value,
50
+ description: n.Description__c?.value,
51
+ sqFt: n.Sq_Ft__c?.value,
52
+ yearBuilt: n.Year_Built__c?.value,
53
+ deposit: n.Deposit__c?.value,
54
+ parking:
55
+ typeof n.Parking__c?.value === "number"
56
+ ? n.Parking__c.value
57
+ : n.Parking__c?.value != null
58
+ ? Number(n.Parking__c.value)
59
+ : undefined,
60
+ petFriendly: n.Pet_Friendly__c?.value,
61
+ availableDate: n.Available_Date__c?.value,
62
+ leaseTerm: n.Lease_Term__c?.value,
63
+ features,
64
+ utilities,
65
+ tourUrl: n.Tour_URL__c?.value,
66
+ createdDate: createdYear,
67
+ };
68
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Property list column config for useRecordListGraphQL.
3
+ */
4
+ import type { Column } from "@salesforce/webapp-template-feature-react-global-search-experimental";
5
+
6
+ const PROPERTY_EXTRA_COLUMNS: Column[] = [
7
+ { fieldApiName: "Description__c", label: "Description", searchable: true, sortable: false },
8
+ { fieldApiName: "Hero_Image__c", label: "Hero Image", searchable: true, sortable: false },
9
+ { fieldApiName: "Address__c", label: "Address", searchable: true, sortable: false },
10
+ { fieldApiName: "CreatedDate", label: "Created Date", searchable: true, sortable: false },
11
+ ];
12
+
13
+ export function getPropertyListColumns(columns: Column[]): Column[] {
14
+ const existing = new Set(columns.map((c) => c.fieldApiName));
15
+ const toAdd = PROPERTY_EXTRA_COLUMNS.filter((c) => !existing.has(c.fieldApiName));
16
+ return toAdd.length === 0 ? columns : [...columns, ...toAdd];
17
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Central route configuration for list pages and navigation.
3
+ * Use these paths for links and search redirects to avoid duplication.
4
+ */
5
+ export const PATHS = {
6
+ HOME: "/",
7
+ PROPERTIES: "/properties",
8
+ MAINTENANCE_REQUESTS: "/maintenance/requests",
9
+ MAINTENANCE_WORKERS: "/maintenance/workers",
10
+ APPLICATIONS: "/applications",
11
+ } as const;
12
+
13
+ export interface ListPageRoute {
14
+ path: string;
15
+ label: string;
16
+ searchParamKey?: string;
17
+ }
18
+
19
+ /** List pages that appear in the Home search object dropdown and in nav */
20
+ export const LIST_PAGE_ROUTES: Record<string, ListPageRoute> = {
21
+ properties: { path: PATHS.PROPERTIES, label: "Properties", searchParamKey: "q" },
22
+ maintenance_requests: {
23
+ path: PATHS.MAINTENANCE_REQUESTS,
24
+ label: "Maintenance Requests",
25
+ searchParamKey: "q",
26
+ },
27
+ maintenance_workers: {
28
+ path: PATHS.MAINTENANCE_WORKERS,
29
+ label: "Maintenance Workers",
30
+ searchParamKey: "q",
31
+ },
32
+ applications: { path: PATHS.APPLICATIONS, label: "Applications", searchParamKey: "q" },
33
+ } as const;
34
+
35
+ export type ListPageKey = keyof typeof LIST_PAGE_ROUTES;
@@ -59,3 +59,13 @@ export interface Property {
59
59
  tourUrl?: string;
60
60
  createdDate?: string;
61
61
  }
62
+
63
+ export interface MaintenanceWorker {
64
+ id: string;
65
+ name: string;
66
+ email?: string;
67
+ phone?: string;
68
+ organization?: string;
69
+ activeRequestsCount?: number;
70
+ status?: string;
71
+ }
@@ -1,45 +1,28 @@
1
1
  import { useEffect, useState } from "react";
2
+ import { useListPage } from "../hooks/useListPage.js";
3
+ import { applicationsListConfig } from "../lib/listPageConfig.js";
4
+ import { ListPageWithFilters } from "../components/list/ListPageWithFilters.js";
5
+ import { PageHeader } from "../components/layout/PageHeader.js";
2
6
  import { ApplicationsTable } from "../components/ApplicationsTable.js";
3
7
  import { ApplicationDetailsModal } from "../components/ApplicationDetailsModal.js";
4
- import { getApplications, updateApplicationStatus } from "../api/applications.js";
8
+ import { updateApplicationStatus } from "../api/applications.js";
5
9
  import type { Application } from "../lib/types.js";
6
10
 
7
11
  export default function Applications() {
8
- const [applications, setApplications] = useState<Application[]>([]);
9
- const [loading, setLoading] = useState(true);
12
+ const list = useListPage(applicationsListConfig);
10
13
  const [selectedApplication, setSelectedApplication] = useState<Application | null>(null);
11
14
  const [notification, setNotification] = useState<{
12
15
  message: string;
13
16
  type: "success" | "error";
14
17
  } | null>(null);
15
18
 
16
- useEffect(() => {
17
- loadApplications();
18
- }, []);
19
-
20
- // Clear notification after 5 seconds
21
19
  useEffect(() => {
22
20
  if (notification) {
23
- const timer = setTimeout(() => {
24
- setNotification(null);
25
- }, 5000);
21
+ const timer = setTimeout(() => setNotification(null), 5000);
26
22
  return () => clearTimeout(timer);
27
23
  }
28
24
  }, [notification]);
29
25
 
30
- const loadApplications = async () => {
31
- try {
32
- setLoading(true);
33
- const data = await getApplications();
34
- setApplications(data);
35
- } catch (error) {
36
- console.error("Error loading applications:", error);
37
- showNotification("Failed to load applications", "error");
38
- } finally {
39
- setLoading(false);
40
- }
41
- };
42
-
43
26
  const handleRowClick = (application: Application) => {
44
27
  setSelectedApplication(application);
45
28
  };
@@ -51,43 +34,22 @@ export default function Applications() {
51
34
  const handleSaveStatus = async (applicationId: string, status: string) => {
52
35
  try {
53
36
  const success = await updateApplicationStatus(applicationId, status);
54
-
55
37
  if (success) {
56
- // Update the local state
57
- setApplications((prev) =>
58
- prev.map((app) => (app.id === applicationId ? { ...app, status } : app)),
59
- );
60
-
61
- // Update selected application if it's the one being edited
62
38
  if (selectedApplication?.id === applicationId) {
63
39
  setSelectedApplication({ ...selectedApplication, status });
64
40
  }
65
-
66
- showNotification("Application status updated successfully!", "success");
41
+ setNotification({ message: "Application status updated successfully!", type: "success" });
67
42
  } else {
68
- showNotification("Failed to update application status", "error");
43
+ setNotification({ message: "Failed to update application status", type: "error" });
69
44
  }
70
45
  } catch (error) {
71
46
  console.error("Error updating application:", error);
72
- showNotification("An error occurred while updating the status", "error");
47
+ setNotification({ message: "An error occurred while updating the status", type: "error" });
73
48
  }
74
49
  };
75
50
 
76
- const showNotification = (message: string, type: "success" | "error") => {
77
- setNotification({ message, type });
78
- };
79
-
80
- if (loading) {
81
- return (
82
- <div className="flex items-center justify-center h-screen bg-gray-50">
83
- <div className="text-lg text-gray-600">Loading applications...</div>
84
- </div>
85
- );
86
- }
87
-
88
51
  return (
89
- <div className="min-h-screen bg-gray-50">
90
- {/* Notification */}
52
+ <>
91
53
  {notification && (
92
54
  <div className="fixed top-4 right-4 z-50 animate-slide-in">
93
55
  <div
@@ -103,19 +65,42 @@ export default function Applications() {
103
65
  </div>
104
66
  )}
105
67
 
106
- {/* Main Content */}
107
- <div className="max-w-7xl mx-auto p-8">
108
- {/* Header */}
109
- <div className="mb-6">
110
- <h1 className="text-2xl font-bold text-gray-900">Applications</h1>
111
- <p className="text-gray-600 mt-1">Manage and review rental applications</p>
112
- </div>
113
-
114
- {/* Applications Table */}
115
- <ApplicationsTable applications={applications} onRowClick={handleRowClick} />
116
- </div>
68
+ <PageHeader title="Applications" description="Manage and review rental applications" />
69
+
70
+ <ListPageWithFilters
71
+ filterProps={{
72
+ filters: list.filters,
73
+ picklistValues: list.picklistValues,
74
+ formValues: list.formValues,
75
+ onFormValueChange: list.onFormValueChange,
76
+ onApply: list.onApplyFilters,
77
+ onReset: list.onResetFilters,
78
+ ariaLabel: applicationsListConfig.filtersAriaLabel,
79
+ }}
80
+ filterError={list.filterError}
81
+ loading={list.loading}
82
+ error={list.error}
83
+ loadingMessage={applicationsListConfig.loadingMessage}
84
+ isEmpty={list.items.length === 0}
85
+ searchPlaceholder="Search by applicant, property, status..."
86
+ searchAriaLabel="Search applications"
87
+ >
88
+ <ApplicationsTable applications={list.items} onRowClick={handleRowClick} />
89
+
90
+ {list.canLoadMore && (
91
+ <div className="flex justify-center mt-6">
92
+ <button
93
+ type="button"
94
+ onClick={list.onLoadMore}
95
+ disabled={list.loadMoreLoading}
96
+ className="px-6 py-2 bg-purple-700 hover:bg-purple-800 text-white rounded-lg text-sm font-medium disabled:opacity-50"
97
+ >
98
+ {list.loadMoreLoading ? "Loading..." : "Load More"}
99
+ </button>
100
+ </div>
101
+ )}
102
+ </ListPageWithFilters>
117
103
 
118
- {/* Application Details Modal */}
119
104
  {selectedApplication && (
120
105
  <ApplicationDetailsModal
121
106
  application={selectedApplication}
@@ -124,6 +109,6 @@ export default function Applications() {
124
109
  onSave={handleSaveStatus}
125
110
  />
126
111
  )}
127
- </div>
112
+ </>
128
113
  );
129
114
  }
@@ -1,12 +1,55 @@
1
- import { useEffect, useState } from "react";
2
- import { StatCard } from "../components/StatCard.js";
1
+ import { useEffect, useState, useCallback, useMemo } from "react";
2
+ import { useNavigate } from "react-router";
3
+ import {
4
+ useObjectInfoBatch,
5
+ useObjectListMetadata,
6
+ useRecordListGraphQL,
7
+ } from "@salesforce/webapp-template-feature-react-global-search-experimental";
3
8
  import { IssuesDonutChart } from "../components/IssuesDonutChart.js";
4
9
  import { MaintenanceTable } from "../components/MaintenanceTable.js";
10
+ import { GlobalSearchBar } from "../components/dashboard/GlobalSearchBar.js";
11
+ import { StatCard } from "../components/StatCard.js";
12
+ import { PageContainer } from "../components/layout/PageContainer.js";
13
+ import { PageLoadingState } from "../components/feedback/PageLoadingState.js";
5
14
  import { getDashboardMetrics, calculateMetrics } from "../api/dashboard.js";
6
- import { getMaintenanceRequests } from "../api/maintenance.js";
7
15
  import type { DashboardMetrics, MaintenanceRequest } from "../lib/types.js";
16
+ import {
17
+ GLOBAL_SEARCH_OBJECT_API_NAME,
18
+ SEARCHABLE_OBJECTS,
19
+ MAINTENANCE_OBJECT_API_NAME,
20
+ type SearchableObjectConfig,
21
+ } from "../lib/globalSearchConstants.js";
22
+ import { getMaintenanceColumns } from "../lib/maintenanceColumns.js";
23
+ import { nodeToMaintenanceRequest } from "../lib/maintenanceAdapter.js";
24
+ import { DASHBOARD_MAINTENANCE_LIMIT } from "../lib/constants.js";
25
+ import { PATHS } from "../lib/routeConfig.js";
26
+
27
+ const CHART_ISSUE_TYPES = ["Plumbing", "HVAC", "Electrical"] as const;
28
+ const CHART_OTHER_LABEL = "Other";
29
+ const CHART_COLORS = ["#7C3AED", "#EC4899", "#14B8A6", "#06B6D4"] as const;
8
30
 
9
31
  export default function Home() {
32
+ const navigate = useNavigate();
33
+ const objectApiNames = useMemo(() => SEARCHABLE_OBJECTS.map((o) => o.objectApiName), []);
34
+ const { objectInfos } = useObjectInfoBatch(objectApiNames);
35
+
36
+ const [searchQuery, setSearchQuery] = useState("");
37
+ const [selectedObjectApiName, setSelectedObjectApiName] = useState<
38
+ SearchableObjectConfig["objectApiName"]
39
+ >(GLOBAL_SEARCH_OBJECT_API_NAME);
40
+
41
+ const selectedConfig = useMemo(
42
+ () => SEARCHABLE_OBJECTS.find((o) => o.objectApiName === selectedObjectApiName),
43
+ [selectedObjectApiName],
44
+ );
45
+ const labelPlural = useMemo(() => {
46
+ const idx = objectApiNames.indexOf(selectedObjectApiName);
47
+ const info = idx >= 0 ? objectInfos[idx] : null;
48
+ return (
49
+ (info?.labelPlural as string | undefined) ?? selectedConfig?.fallbackLabelPlural ?? "Records"
50
+ );
51
+ }, [selectedObjectApiName, objectApiNames, objectInfos, selectedConfig?.fallbackLabelPlural]);
52
+
10
53
  const [metrics, setMetrics] = useState<DashboardMetrics>({
11
54
  totalProperties: 0,
12
55
  unitsAvailable: 0,
@@ -14,67 +57,91 @@ export default function Home() {
14
57
  topMaintenanceIssue: "",
15
58
  topMaintenanceIssueCount: 0,
16
59
  });
17
- const [maintenanceRequests, setMaintenanceRequests] = useState<MaintenanceRequest[]>([]);
18
- const [loading, setLoading] = useState(true);
60
+ const [metricsLoading, setMetricsLoading] = useState(true);
61
+
62
+ const listMeta = useObjectListMetadata(MAINTENANCE_OBJECT_API_NAME);
63
+ const columns = useMemo(() => getMaintenanceColumns(listMeta.columns), [listMeta.columns]);
64
+ const { edges, loading: maintenanceLoading } = useRecordListGraphQL({
65
+ objectApiName: MAINTENANCE_OBJECT_API_NAME,
66
+ columns,
67
+ columnsLoading: listMeta.loading,
68
+ columnsError: listMeta.error,
69
+ first: DASHBOARD_MAINTENANCE_LIMIT,
70
+ after: null,
71
+ searchQuery: undefined,
72
+ sortBy: "Priority__c DESC",
73
+ });
74
+
75
+ const maintenanceRequests: MaintenanceRequest[] = useMemo(
76
+ () => edges.map((e) => nodeToMaintenanceRequest(e.node as Record<string, unknown>)),
77
+ [edges],
78
+ );
19
79
 
20
80
  useEffect(() => {
21
- loadDashboardData();
81
+ let cancelled = false;
82
+ (async () => {
83
+ try {
84
+ setMetricsLoading(true);
85
+ const { properties } = await getDashboardMetrics();
86
+ if (!cancelled) setMetrics(calculateMetrics(properties));
87
+ } catch (error) {
88
+ if (!cancelled) console.error("Error loading dashboard metrics:", error);
89
+ } finally {
90
+ if (!cancelled) setMetricsLoading(false);
91
+ }
92
+ })();
93
+ return () => {
94
+ cancelled = true;
95
+ };
22
96
  }, []);
23
97
 
24
- const loadDashboardData = async () => {
25
- try {
26
- setLoading(true);
27
-
28
- // Load metrics
29
- const { properties } = await getDashboardMetrics();
30
- setMetrics(calculateMetrics(properties));
31
-
32
- // Load maintenance requests
33
- const maintenanceData = await getMaintenanceRequests(5);
34
- setMaintenanceRequests(maintenanceData);
35
- } catch (error) {
36
- console.error("Error loading dashboard data:", error);
37
- } finally {
38
- setLoading(false);
98
+ const loading = metricsLoading || listMeta.loading || maintenanceLoading;
99
+
100
+ const handleSearchSubmit = useCallback(() => {
101
+ const trimmed = searchQuery.trim();
102
+ const path = selectedConfig?.path ?? SEARCHABLE_OBJECTS[0].path;
103
+ if (trimmed) {
104
+ navigate(`${path}?q=${encodeURIComponent(trimmed)}`);
105
+ } else {
106
+ navigate(path);
39
107
  }
40
- };
108
+ }, [searchQuery, navigate, selectedConfig?.path]);
109
+
110
+ const handleBrowseAll = useCallback(() => {
111
+ navigate(selectedConfig?.path ?? SEARCHABLE_OBJECTS[0].path);
112
+ }, [navigate, selectedConfig?.path]);
41
113
 
42
- const handleViewMaintenance = (id: string) => {
43
- console.log("View maintenance request:", id);
44
- };
114
+ const handleViewMaintenance = useCallback(() => {
115
+ navigate(PATHS.MAINTENANCE_REQUESTS);
116
+ }, [navigate]);
45
117
 
46
- // Calculate chart data from maintenance requests
47
- const calculateChartData = () => {
48
- const issueCounts: Record<string, number> = {
118
+ const chartData = useMemo(() => {
119
+ const counts: Record<string, number> = {
49
120
  Plumbing: 0,
50
121
  HVAC: 0,
51
122
  Electrical: 0,
52
- Other: 0,
123
+ [CHART_OTHER_LABEL]: 0,
53
124
  };
54
-
55
125
  maintenanceRequests.forEach((request) => {
56
126
  const type = request.issueType;
57
- if (type === "Plumbing" || type === "HVAC" || type === "Electrical") {
58
- issueCounts[type]++;
127
+ if (CHART_ISSUE_TYPES.includes(type as (typeof CHART_ISSUE_TYPES)[number])) {
128
+ counts[type]++;
59
129
  } else {
60
- issueCounts.Other++;
130
+ counts[CHART_OTHER_LABEL]++;
61
131
  }
62
132
  });
63
-
64
133
  return [
65
- { name: "Plumbing", value: issueCounts.Plumbing, color: "#7C3AED" },
66
- { name: "HVAC", value: issueCounts.HVAC, color: "#EC4899" },
67
- { name: "Electrical", value: issueCounts.Electrical, color: "#14B8A6" },
68
- { name: "Other", value: issueCounts.Other, color: "#06B6D4" },
134
+ { name: "Plumbing", value: counts.Plumbing, color: CHART_COLORS[0] },
135
+ { name: "HVAC", value: counts.HVAC, color: CHART_COLORS[1] },
136
+ { name: "Electrical", value: counts.Electrical, color: CHART_COLORS[2] },
137
+ { name: CHART_OTHER_LABEL, value: counts[CHART_OTHER_LABEL], color: CHART_COLORS[3] },
69
138
  ];
70
- };
139
+ }, [maintenanceRequests]);
71
140
 
72
- // Calculate trends based on current metrics (assuming 10% growth for properties and occupied, 10% decrease for available)
73
- const calculateTrends = () => {
141
+ const trends = useMemo(() => {
74
142
  const totalPropertiesTrend = Math.round(metrics.totalProperties * 0.1);
75
143
  const unitsAvailableTrend = Math.round(metrics.unitsAvailable * 0.1);
76
144
  const occupiedUnitsTrend = Math.round(metrics.occupiedUnits * 0.1);
77
-
78
145
  return {
79
146
  totalProperties: {
80
147
  trend: totalPropertiesTrend,
@@ -89,92 +156,57 @@ export default function Home() {
89
156
  previous: metrics.occupiedUnits - occupiedUnitsTrend,
90
157
  },
91
158
  };
92
- };
93
-
94
- const trends = calculateTrends();
159
+ }, [metrics]);
95
160
 
96
161
  if (loading) {
97
- return (
98
- <div className="flex items-center justify-center h-screen bg-gray-50">
99
- <div className="text-lg text-gray-600">Loading dashboard...</div>
100
- </div>
101
- );
162
+ return <PageLoadingState message="Loading dashboard..." />;
102
163
  }
103
164
 
104
165
  return (
105
- <div className="min-h-screen bg-gray-50">
106
- {/* Main Content Area */}
107
- <div className="max-w-7xl mx-auto p-8 space-y-6">
108
- {/* Search Bar (Desktop) */}
109
- <div className="hidden md:flex justify-end mb-4">
110
- <div className="w-96 bg-white rounded-full px-6 py-3 shadow-sm border border-gray-200 flex items-center">
111
- <input
112
- type="text"
113
- placeholder="Search"
114
- className="flex-1 outline-none text-gray-600"
115
- disabled
116
- />
117
- <svg
118
- className="w-5 h-5 text-gray-400"
119
- fill="none"
120
- stroke="currentColor"
121
- viewBox="0 0 24 24"
122
- >
123
- <path
124
- strokeLinecap="round"
125
- strokeLinejoin="round"
126
- strokeWidth={2}
127
- d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
128
- />
129
- </svg>
130
- </div>
131
- </div>
166
+ <PageContainer>
167
+ <div className="max-w-7xl mx-auto space-y-6">
168
+ <GlobalSearchBar
169
+ objectApiNames={objectApiNames}
170
+ objectInfos={objectInfos}
171
+ searchableObjects={SEARCHABLE_OBJECTS}
172
+ selectedObjectApiName={selectedObjectApiName}
173
+ onSelectedObjectChange={setSelectedObjectApiName}
174
+ searchQuery={searchQuery}
175
+ onSearchQueryChange={setSearchQuery}
176
+ onSearchSubmit={handleSearchSubmit}
177
+ onBrowseAll={handleBrowseAll}
178
+ labelPlural={labelPlural}
179
+ />
132
180
 
133
- {/* Main Layout: 70/30 split */}
134
181
  <div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-6">
135
- {/* Left Column: Stat Cards + Maintenance Table */}
136
182
  <div className="space-y-6">
137
- {/* Stat Cards Row */}
138
183
  <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
139
184
  <StatCard
140
185
  title="Total Properties"
141
186
  value={metrics.totalProperties}
142
- trend={{
143
- value: trends.totalProperties.trend,
144
- isPositive: true,
145
- }}
187
+ trend={{ value: trends.totalProperties.trend, isPositive: true }}
146
188
  subtitle={`Last month total ${trends.totalProperties.previous}`}
147
189
  />
148
190
  <StatCard
149
191
  title="Units Available"
150
192
  value={metrics.unitsAvailable}
151
- trend={{
152
- value: trends.unitsAvailable.trend,
153
- isPositive: false,
154
- }}
193
+ trend={{ value: trends.unitsAvailable.trend, isPositive: false }}
155
194
  subtitle={`Last month total ${trends.unitsAvailable.previous}/${metrics.totalProperties}`}
156
195
  />
157
196
  <StatCard
158
197
  title="Occupied Units"
159
198
  value={metrics.occupiedUnits}
160
- trend={{
161
- value: trends.occupiedUnits.trend,
162
- isPositive: true,
163
- }}
199
+ trend={{ value: trends.occupiedUnits.trend, isPositive: true }}
164
200
  subtitle={`Last month total ${trends.occupiedUnits.previous}`}
165
201
  />
166
202
  </div>
167
-
168
- {/* Maintenance Requests Table */}
169
203
  <MaintenanceTable requests={maintenanceRequests} onView={handleViewMaintenance} />
170
204
  </div>
171
-
172
- {/* Right Column: Donut Chart */}
173
205
  <div>
174
- <IssuesDonutChart data={calculateChartData()} />
206
+ <IssuesDonutChart data={chartData} />
175
207
  </div>
176
208
  </div>
177
209
  </div>
178
- </div>
210
+ </PageContainer>
179
211
  );
180
212
  }