@salesforce/webapp-template-app-react-sample-b2e-experimental 1.73.0 → 1.73.1

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 +8 -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
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [1.73.1](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.73.0...v1.73.1) (2026-03-05)
7
+
8
+ **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
9
+
10
+
11
+
12
+
13
+
6
14
  # [1.73.0](https://github.com/salesforce-experience-platform-emu/webapps/compare/v1.72.0...v1.73.0) (2026-03-05)
7
15
 
8
16
  **Note:** Version bump only for package @salesforce/webapp-template-base-sfdx-project-experimental
@@ -61,7 +61,17 @@
61
61
  <type>AutoNumber</type>
62
62
  </nameField>
63
63
  <pluralLabel>Maintenance Requests</pluralLabel>
64
- <searchLayouts/>
64
+ <searchLayouts>
65
+ <searchResultsAdditionalFields>Description__c</searchResultsAdditionalFields>
66
+ <searchResultsAdditionalFields>Type__c</searchResultsAdditionalFields>
67
+ <searchResultsAdditionalFields>Priority__c</searchResultsAdditionalFields>
68
+ <searchResultsAdditionalFields>Status__c</searchResultsAdditionalFields>
69
+ <searchResultsAdditionalFields>Scheduled__c</searchResultsAdditionalFields>
70
+ <searchResultsAdditionalFields>Property__r.Address__c</searchResultsAdditionalFields>
71
+ <searchResultsAdditionalFields>Property__r.Name</searchResultsAdditionalFields>
72
+ <searchResultsAdditionalFields>User__r.Name</searchResultsAdditionalFields>
73
+ <searchResultsAdditionalFields>Owner.Name</searchResultsAdditionalFields>
74
+ </searchLayouts>
65
75
  <sharingModel>ReadWrite</sharingModel>
66
76
  <visibility>Public</visibility>
67
77
  </CustomObject>
@@ -60,7 +60,12 @@
60
60
  <type>Text</type>
61
61
  </nameField>
62
62
  <pluralLabel>Maintenance Workers</pluralLabel>
63
- <searchLayouts/>
63
+ <searchLayouts>
64
+ <searchResultsAdditionalFields>Hourly_Rate__c</searchResultsAdditionalFields>
65
+ <searchResultsAdditionalFields>Type__c</searchResultsAdditionalFields>
66
+ <searchResultsAdditionalFields>Rating__c</searchResultsAdditionalFields>
67
+ <searchResultsAdditionalFields>Employment_Type__c</searchResultsAdditionalFields>
68
+ </searchLayouts>
64
69
  <sharingModel>ReadWrite</sharingModel>
65
70
  <visibility>Public</visibility>
66
71
  </CustomObject>
@@ -60,7 +60,12 @@
60
60
  <type>Text</type>
61
61
  </nameField>
62
62
  <pluralLabel>Properties</pluralLabel>
63
- <searchLayouts/>
63
+ <searchLayouts>
64
+ <searchResultsAdditionalFields>Hero_Image__c</searchResultsAdditionalFields>
65
+ <searchResultsAdditionalFields>Address__c</searchResultsAdditionalFields>
66
+ <searchResultsAdditionalFields>Description__c</searchResultsAdditionalFields>
67
+ <searchResultsAdditionalFields>CREATED_DATE</searchResultsAdditionalFields>
68
+ </searchLayouts>
64
69
  <sharingModel>ReadWrite</sharingModel>
65
70
  <visibility>Public</visibility>
66
71
  </CustomObject>
@@ -15,10 +15,11 @@
15
15
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
16
16
  },
17
17
  "dependencies": {
18
- "@salesforce/agentforce-conversation-client": "^1.73.0",
19
- "@salesforce/sdk-data": "^1.73.0",
20
- "@salesforce/webapp-experimental": "^1.73.0",
18
+ "@salesforce/agentforce-conversation-client": "^1.73.1",
19
+ "@salesforce/sdk-data": "^1.73.1",
20
+ "@salesforce/webapp-experimental": "^1.73.1",
21
21
  "@tailwindcss/vite": "^4.1.17",
22
+ "@tanstack/react-form": "^1.28.4",
22
23
  "class-variance-authority": "^0.7.1",
23
24
  "clsx": "^2.1.1",
24
25
  "date-fns": "^3.6.0",
@@ -30,7 +31,8 @@
30
31
  "shadcn": "^3.8.5",
31
32
  "tailwind-merge": "^3.4.0",
32
33
  "tailwindcss": "^4.1.17",
33
- "tw-animate-css": "^1.4.0"
34
+ "tw-animate-css": "^1.4.0",
35
+ "zod": "^4.3.6"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@eslint/js": "^9.39.1",
@@ -40,7 +42,7 @@
40
42
  "@graphql-eslint/eslint-plugin": "^4.1.0",
41
43
  "@graphql-tools/utils": "^11.0.0",
42
44
  "@playwright/test": "^1.49.0",
43
- "@salesforce/vite-plugin-webapp-experimental": "^1.73.0",
45
+ "@salesforce/vite-plugin-webapp-experimental": "^1.73.1",
44
46
  "@testing-library/jest-dom": "^6.6.3",
45
47
  "@testing-library/react": "^16.1.0",
46
48
  "@testing-library/user-event": "^14.5.2",
@@ -0,0 +1,60 @@
1
+ import type { MaintenanceWorker } from "../lib/types.js";
2
+ import { getAllMaintenanceRequests } from "./maintenance.js";
3
+
4
+ /**
5
+ * Fetches maintenance workers. Derives unique workers from maintenance requests
6
+ * (assigned workers) and adds fallback placeholder entries so the list is never empty.
7
+ */
8
+ export async function getMaintenanceWorkers(): Promise<MaintenanceWorker[]> {
9
+ try {
10
+ const requests = await getAllMaintenanceRequests(200);
11
+ const byName = new Map<string, MaintenanceWorker>();
12
+ let idCounter = 1;
13
+ for (const req of requests) {
14
+ const name = req.assignedWorkerName || req.assignedWorker || "Unassigned";
15
+ if (!name || name === "Unassigned") continue;
16
+ if (!byName.has(name)) {
17
+ byName.set(name, {
18
+ id: `worker-${idCounter++}`,
19
+ name,
20
+ organization: req.assignedWorkerOrg,
21
+ activeRequestsCount: 0,
22
+ status: "Active",
23
+ });
24
+ }
25
+ const w = byName.get(name)!;
26
+ w.activeRequestsCount = (w.activeRequestsCount ?? 0) + 1;
27
+ }
28
+ let workers = Array.from(byName.values());
29
+ if (workers.length === 0) {
30
+ workers = [
31
+ {
32
+ id: "worker-1",
33
+ name: "ABC Diamond Technicians",
34
+ organization: "ABC Diamond",
35
+ activeRequestsCount: 0,
36
+ status: "Active",
37
+ },
38
+ {
39
+ id: "worker-2",
40
+ name: "Maintenance Team",
41
+ organization: "Property Mgmt",
42
+ activeRequestsCount: 0,
43
+ status: "Active",
44
+ },
45
+ ];
46
+ }
47
+ return workers;
48
+ } catch (error) {
49
+ console.error("Error fetching maintenance workers:", error);
50
+ return [
51
+ {
52
+ id: "worker-1",
53
+ name: "ABC Diamond Technicians",
54
+ organization: "ABC Diamond",
55
+ status: "Active",
56
+ },
57
+ { id: "worker-2", name: "Maintenance Team", organization: "Property Mgmt", status: "Active" },
58
+ ];
59
+ }
60
+ }
@@ -1,5 +1,4 @@
1
1
  import React from "react";
2
- import { Card } from "@/components/ui/card";
3
2
  import type { Application } from "../lib/types.js";
4
3
 
5
4
  interface ApplicationsTableProps {
@@ -35,71 +34,69 @@ export const ApplicationsTable: React.FC<ApplicationsTableProps> = ({
35
34
  };
36
35
 
37
36
  return (
38
- <Card className="border-gray-200 shadow-sm">
39
- <div className="overflow-x-auto">
40
- <table className="w-full">
41
- <thead className="bg-gray-50 border-b border-gray-200">
37
+ <div className="border border-gray-200 rounded-lg shadow-sm overflow-x-auto bg-white">
38
+ <table className="w-full">
39
+ <thead className="bg-gray-50 border-b border-gray-200">
40
+ <tr>
41
+ <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
42
+ User
43
+ </th>
44
+ <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
45
+ Start Date
46
+ </th>
47
+ <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
48
+ Status
49
+ </th>
50
+ </tr>
51
+ </thead>
52
+ <tbody className="bg-white divide-y divide-gray-200">
53
+ {applications.length === 0 ? (
42
54
  <tr>
43
- <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
44
- User
45
- </th>
46
- <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
47
- Start Date
48
- </th>
49
- <th className="px-6 py-4 text-left text-xs font-semibold text-gray-600 uppercase tracking-wider">
50
- Status
51
- </th>
55
+ <td colSpan={3} className="px-6 py-8 text-center text-gray-500">
56
+ No applications found
57
+ </td>
52
58
  </tr>
53
- </thead>
54
- <tbody className="bg-white divide-y divide-gray-200">
55
- {applications.length === 0 ? (
56
- <tr>
57
- <td colSpan={3} className="px-6 py-8 text-center text-gray-500">
58
- No applications found
59
- </td>
60
- </tr>
61
- ) : (
62
- applications.map((application) => (
63
- <tr
64
- key={application.id}
65
- onClick={() => onRowClick(application)}
66
- className="hover:bg-gray-50 cursor-pointer transition-colors"
67
- >
68
- <td className="px-6 py-4 whitespace-nowrap">
69
- <div className="flex items-center">
70
- <div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center flex-shrink-0">
71
- <span className="text-sm font-medium text-purple-700">
72
- {application.applicantName?.charAt(0) || "?"}
73
- </span>
59
+ ) : (
60
+ applications.map((application) => (
61
+ <tr
62
+ key={application.id}
63
+ onClick={() => onRowClick(application)}
64
+ className="hover:bg-gray-50 cursor-pointer transition-colors"
65
+ >
66
+ <td className="px-6 py-4 whitespace-nowrap">
67
+ <div className="flex items-center">
68
+ <div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center flex-shrink-0">
69
+ <span className="text-sm font-medium text-purple-700">
70
+ {application.applicantName?.charAt(0) || "?"}
71
+ </span>
72
+ </div>
73
+ <div className="ml-4">
74
+ <div className="text-sm font-medium text-gray-900">
75
+ {application.applicantName || "Unknown"}
74
76
  </div>
75
- <div className="ml-4">
76
- <div className="text-sm font-medium text-gray-900">
77
- {application.applicantName || "Unknown"}
78
- </div>
79
- <div className="text-sm text-gray-500">
80
- {application.propertyName || application.propertyAddress}
81
- </div>
77
+ <div className="text-sm text-gray-500">
78
+ {application.propertyName || application.propertyAddress}
82
79
  </div>
83
80
  </div>
84
- </td>
85
- <td className="px-6 py-4 whitespace-nowrap">
86
- <div className="text-sm text-gray-900">
87
- {formatDate(application.startDate || application.submittedDate)}
88
- </div>
89
- </td>
90
- <td className="px-6 py-4 whitespace-nowrap">
91
- <span
92
- className={`inline-flex px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(application.status)}`}
93
- >
94
- {application.status}
95
- </span>
96
- </td>
97
- </tr>
98
- ))
99
- )}
100
- </tbody>
101
- </table>
102
- </div>
103
- </Card>
81
+ </div>
82
+ </td>
83
+ <td className="px-6 py-4 whitespace-nowrap">
84
+ <div className="text-sm text-gray-900">
85
+ {formatDate(application.startDate || application.submittedDate)}
86
+ </div>
87
+ </td>
88
+ <td className="px-6 py-4 whitespace-nowrap">
89
+ <span
90
+ className={`inline-flex px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(application.status)}`}
91
+ >
92
+ {application.status}
93
+ </span>
94
+ </td>
95
+ </tr>
96
+ ))
97
+ )}
98
+ </tbody>
99
+ </table>
100
+ </div>
104
101
  );
105
102
  };
@@ -0,0 +1,200 @@
1
+ import { useState, useCallback } from "react";
2
+ import {
3
+ useObjectListMetadata,
4
+ parseFilterValue,
5
+ sanitizeFilterValue,
6
+ type FilterCriteria,
7
+ } from "@salesforce/webapp-template-feature-react-global-search-experimental";
8
+
9
+ const inputClass =
10
+ "h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-purple/20 focus:border-primary-purple";
11
+
12
+ interface FiltersFromApiProps {
13
+ objectApiName: string | null;
14
+ onApplyFilters: (criteria: FilterCriteria[]) => void;
15
+ }
16
+
17
+ /**
18
+ * Renders filter UI from the react-global-search filters API (getObjectListFilters).
19
+ * Uses useObjectListMetadata to load filters and picklist values, then builds FilterCriteria on Apply.
20
+ */
21
+ export function FiltersFromApi({ objectApiName, onApplyFilters }: FiltersFromApiProps) {
22
+ const { filters, picklistValues, loading, error } = useObjectListMetadata(objectApiName);
23
+ const [formValues, setFormValues] = useState<Record<string, string>>({});
24
+ const [rangeMin, setRangeMin] = useState<Record<string, string>>({});
25
+ const [rangeMax, setRangeMax] = useState<Record<string, string>>({});
26
+
27
+ const handleChange = useCallback((fieldPath: string, value: string) => {
28
+ setFormValues((prev) => ({ ...prev, [fieldPath]: value }));
29
+ }, []);
30
+
31
+ const handleRangeChange = useCallback((fieldPath: string, min: string, max: string) => {
32
+ setRangeMin((prev) => ({ ...prev, [fieldPath]: min }));
33
+ setRangeMax((prev) => ({ ...prev, [fieldPath]: max }));
34
+ }, []);
35
+
36
+ const handleApply = useCallback(() => {
37
+ if (!objectApiName || !filters?.length) {
38
+ onApplyFilters([]);
39
+ return;
40
+ }
41
+ const criteria: FilterCriteria[] = [];
42
+ for (const filter of filters) {
43
+ if (!filter?.targetFieldPath) continue;
44
+ const affordance = (filter.affordance ?? "").toLowerCase();
45
+ if (affordance === "range") {
46
+ const minVal = sanitizeFilterValue(rangeMin[filter.targetFieldPath] ?? "");
47
+ const maxVal = sanitizeFilterValue(rangeMax[filter.targetFieldPath] ?? "");
48
+ if (minVal) {
49
+ const parsed = parseFilterValue(minVal);
50
+ if (parsed !== "")
51
+ criteria.push({
52
+ objectApiName,
53
+ fieldPath: filter.targetFieldPath,
54
+ operator: "gte",
55
+ values: [parsed],
56
+ });
57
+ }
58
+ if (maxVal) {
59
+ const parsed = parseFilterValue(maxVal);
60
+ if (parsed !== "")
61
+ criteria.push({
62
+ objectApiName,
63
+ fieldPath: filter.targetFieldPath,
64
+ operator: "lte",
65
+ values: [parsed],
66
+ });
67
+ }
68
+ } else {
69
+ const raw = formValues[filter.targetFieldPath] ?? "";
70
+ const fieldValue = sanitizeFilterValue(raw);
71
+ if (!fieldValue) continue;
72
+ if (affordance === "select") {
73
+ criteria.push({
74
+ objectApiName,
75
+ fieldPath: filter.targetFieldPath,
76
+ operator: "eq",
77
+ values: [fieldValue],
78
+ });
79
+ } else {
80
+ criteria.push({
81
+ objectApiName,
82
+ fieldPath: filter.targetFieldPath,
83
+ operator: "like",
84
+ values: [`%${fieldValue}%`],
85
+ });
86
+ }
87
+ }
88
+ }
89
+ onApplyFilters(criteria);
90
+ }, [objectApiName, filters, formValues, rangeMin, rangeMax, onApplyFilters]);
91
+
92
+ const handleReset = useCallback(() => {
93
+ setFormValues({});
94
+ setRangeMin({});
95
+ setRangeMax({});
96
+ onApplyFilters([]);
97
+ }, [onApplyFilters]);
98
+
99
+ if (!objectApiName) return null;
100
+ if (loading) {
101
+ return (
102
+ <div className="mb-4 flex flex-wrap gap-2 items-center text-sm text-gray-500">
103
+ Loading filters…
104
+ </div>
105
+ );
106
+ }
107
+ if (error || !filters?.length) return null;
108
+
109
+ return (
110
+ <div className="mb-4 flex flex-wrap items-end gap-3">
111
+ {filters.map((filter) => {
112
+ if (!filter.targetFieldPath) return null;
113
+ const affordance = (filter.affordance ?? "").toLowerCase();
114
+ if (affordance === "range") {
115
+ return (
116
+ <div key={filter.targetFieldPath} className="flex items-center gap-2 flex-wrap">
117
+ <span className="text-sm text-gray-600">{filter.label}</span>
118
+ <input
119
+ type="text"
120
+ className={inputClass}
121
+ placeholder="Min"
122
+ value={rangeMin[filter.targetFieldPath] ?? ""}
123
+ onChange={(e) =>
124
+ handleRangeChange(
125
+ filter.targetFieldPath,
126
+ e.target.value,
127
+ rangeMax[filter.targetFieldPath] ?? "",
128
+ )
129
+ }
130
+ aria-label={`${filter.label} min`}
131
+ />
132
+ <input
133
+ type="text"
134
+ className={inputClass}
135
+ placeholder="Max"
136
+ value={rangeMax[filter.targetFieldPath] ?? ""}
137
+ onChange={(e) =>
138
+ handleRangeChange(
139
+ filter.targetFieldPath,
140
+ rangeMin[filter.targetFieldPath] ?? "",
141
+ e.target.value,
142
+ )
143
+ }
144
+ aria-label={`${filter.label} max`}
145
+ />
146
+ </div>
147
+ );
148
+ }
149
+ if (affordance === "select") {
150
+ const options = picklistValues?.[filter.targetFieldPath] ?? [];
151
+ return (
152
+ <div key={filter.targetFieldPath} className="flex items-center gap-2">
153
+ <label className="text-sm text-gray-600">{filter.label}</label>
154
+ <select
155
+ className={inputClass}
156
+ value={formValues[filter.targetFieldPath] ?? ""}
157
+ onChange={(e) => handleChange(filter.targetFieldPath, e.target.value)}
158
+ aria-label={filter.label}
159
+ >
160
+ <option value="">Any</option>
161
+ {options.map((opt) => (
162
+ <option key={opt.value} value={opt.value}>
163
+ {opt.label}
164
+ </option>
165
+ ))}
166
+ </select>
167
+ </div>
168
+ );
169
+ }
170
+ return (
171
+ <div key={filter.targetFieldPath} className="flex items-center gap-2">
172
+ <label className="text-sm text-gray-600">{filter.label}</label>
173
+ <input
174
+ type="text"
175
+ className={inputClass}
176
+ placeholder={filter.attributes?.placeholder ?? filter.label}
177
+ value={formValues[filter.targetFieldPath] ?? ""}
178
+ onChange={(e) => handleChange(filter.targetFieldPath, e.target.value)}
179
+ aria-label={filter.label}
180
+ />
181
+ </div>
182
+ );
183
+ })}
184
+ <button
185
+ type="button"
186
+ onClick={handleApply}
187
+ className="h-9 px-3 rounded-md bg-purple-700 text-white text-sm font-medium hover:bg-purple-800"
188
+ >
189
+ Apply Filters
190
+ </button>
191
+ <button
192
+ type="button"
193
+ onClick={handleReset}
194
+ className="h-9 px-3 text-sm text-gray-600 hover:text-gray-800"
195
+ >
196
+ Reset
197
+ </button>
198
+ </div>
199
+ );
200
+ }
@@ -0,0 +1,97 @@
1
+ import type { ActiveFilter, FilterFieldConfig } from "../lib/listFilters.js";
2
+
3
+ const inputClass =
4
+ "h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-purple/20 focus:border-primary-purple";
5
+
6
+ interface ListPageFiltersProps {
7
+ fieldConfigs: FilterFieldConfig[];
8
+ activeFilters: ActiveFilter[];
9
+ onFiltersChange: (filters: ActiveFilter[]) => void;
10
+ }
11
+
12
+ export function ListPageFilters({
13
+ fieldConfigs,
14
+ activeFilters,
15
+ onFiltersChange,
16
+ }: ListPageFiltersProps) {
17
+ const addFilter = () => {
18
+ const first = fieldConfigs[0];
19
+ if (!first) return;
20
+ onFiltersChange([...activeFilters, { fieldKey: first.key, value: "" }]);
21
+ };
22
+
23
+ const updateFilter = (index: number, updates: Partial<ActiveFilter>) => {
24
+ const next = [...activeFilters];
25
+ next[index] = { ...next[index]!, ...updates };
26
+ onFiltersChange(next);
27
+ };
28
+
29
+ const removeFilter = (index: number) => {
30
+ onFiltersChange(activeFilters.filter((_, i) => i !== index));
31
+ };
32
+
33
+ if (fieldConfigs.length === 0) return null;
34
+
35
+ return (
36
+ <div className="flex flex-wrap items-center gap-2 mb-4">
37
+ {activeFilters.map((filter, index) => {
38
+ const config = fieldConfigs.find((c) => c.key === filter.fieldKey) ?? fieldConfigs[0]!;
39
+ return (
40
+ <div key={index} className="flex items-center gap-2 flex-wrap">
41
+ <select
42
+ className={inputClass}
43
+ value={filter.fieldKey}
44
+ onChange={(e) => updateFilter(index, { fieldKey: e.target.value })}
45
+ aria-label="Filter field"
46
+ >
47
+ {fieldConfigs.map((c) => (
48
+ <option key={c.key} value={c.key}>
49
+ {c.label}
50
+ </option>
51
+ ))}
52
+ </select>
53
+ {config.type === "select" && config.options && config.options.length > 0 ? (
54
+ <select
55
+ className={inputClass}
56
+ value={filter.value}
57
+ onChange={(e) => updateFilter(index, { value: e.target.value })}
58
+ aria-label={`Filter by ${config.label}`}
59
+ >
60
+ <option value="">Any</option>
61
+ {config.options.map((opt) => (
62
+ <option key={opt.value} value={opt.value}>
63
+ {opt.label}
64
+ </option>
65
+ ))}
66
+ </select>
67
+ ) : (
68
+ <input
69
+ type="text"
70
+ className={inputClass}
71
+ value={filter.value}
72
+ onChange={(e) => updateFilter(index, { value: e.target.value })}
73
+ placeholder={config.label}
74
+ aria-label={`Filter by ${config.label}`}
75
+ />
76
+ )}
77
+ <button
78
+ type="button"
79
+ onClick={() => removeFilter(index)}
80
+ className="text-gray-500 hover:text-gray-700 text-sm px-1"
81
+ aria-label="Remove filter"
82
+ >
83
+
84
+ </button>
85
+ </div>
86
+ );
87
+ })}
88
+ <button
89
+ type="button"
90
+ onClick={addFilter}
91
+ className="text-sm text-purple-700 hover:text-purple-800 font-medium"
92
+ >
93
+ + Add filter
94
+ </button>
95
+ </div>
96
+ );
97
+ }
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { useNavigate } from "react-router";
3
+ import { PATHS } from "../lib/routeConfig.js";
3
4
  import { Card } from "@/components/ui/card";
4
5
  import { Button } from "@/components/ui/button";
5
6
  import type { MaintenanceRequest } from "../lib/types.js";
@@ -34,7 +35,7 @@ export const MaintenanceTable: React.FC<MaintenanceTableProps> = ({ requests })
34
35
  const navigate = useNavigate();
35
36
 
36
37
  const handleSeeAll = () => {
37
- navigate("/maintenance");
38
+ navigate(PATHS.MAINTENANCE_REQUESTS);
38
39
  };
39
40
 
40
41
  return (
@@ -0,0 +1,39 @@
1
+ const selectClass =
2
+ "h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary-purple/20 focus:border-primary-purple min-w-[180px]";
3
+
4
+ export interface ObjectSelectOption {
5
+ value: string;
6
+ label: string;
7
+ }
8
+
9
+ interface ObjectSelectProps {
10
+ options: ObjectSelectOption[];
11
+ value: string;
12
+ onChange: (value: string) => void;
13
+ "aria-label"?: string;
14
+ }
15
+
16
+ /**
17
+ * Shadcn-style select for object type (Properties, Maintenance Requests, Maintenance Workers).
18
+ */
19
+ export function ObjectSelect({
20
+ options,
21
+ value,
22
+ onChange,
23
+ "aria-label": ariaLabel = "Search in",
24
+ }: ObjectSelectProps) {
25
+ return (
26
+ <select
27
+ className={selectClass}
28
+ value={value}
29
+ onChange={(e) => onChange(e.target.value)}
30
+ aria-label={ariaLabel}
31
+ >
32
+ {options.map((opt) => (
33
+ <option key={opt.value} value={opt.value}>
34
+ {opt.label}
35
+ </option>
36
+ ))}
37
+ </select>
38
+ );
39
+ }
@@ -4,6 +4,7 @@ import dashboardIcon from "../assets/icons/dashboard.svg";
4
4
  import filesIcon from "../assets/icons/files.svg";
5
5
  import propertiesIcon from "../assets/icons/properties.svg";
6
6
  import maintenanceIcon from "../assets/icons/maintenance.svg";
7
+ import { PATHS } from "../lib/routeConfig.js";
7
8
 
8
9
  interface NavItem {
9
10
  path: string;
@@ -12,10 +13,11 @@ interface NavItem {
12
13
  }
13
14
 
14
15
  const navItems: NavItem[] = [
15
- { path: "/", icon: dashboardIcon, label: "Dashboard" },
16
- { path: "/applications", icon: filesIcon, label: "Applications" },
17
- { path: "/properties", icon: propertiesIcon, label: "Properties" },
18
- { path: "/maintenance", icon: maintenanceIcon, label: "Maintenance" },
16
+ { path: PATHS.HOME, icon: dashboardIcon, label: "Dashboard" },
17
+ { path: PATHS.APPLICATIONS, icon: filesIcon, label: "Applications" },
18
+ { path: PATHS.PROPERTIES, icon: propertiesIcon, label: "Properties" },
19
+ { path: PATHS.MAINTENANCE_REQUESTS, icon: maintenanceIcon, label: "Maintenance Requests" },
20
+ { path: PATHS.MAINTENANCE_WORKERS, icon: maintenanceIcon, label: "Maintenance Workers" },
19
21
  ];
20
22
 
21
23
  export const VerticalNav: React.FC = () => {