@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,165 @@
1
+ /**
2
+ * Filter utilities: build FilterCriteria from form values and derive default form state.
3
+ * Multi-select filter values are stored as delimiter-separated; "in" operator is used when multiple values.
4
+ */
5
+ import type {
6
+ Filter,
7
+ FilterCriteria,
8
+ } from "@salesforce/webapp-template-feature-react-global-search-experimental";
9
+ import {
10
+ parseFilterValue,
11
+ sanitizeFilterValue,
12
+ } from "@salesforce/webapp-template-feature-react-global-search-experimental";
13
+
14
+ /** Delimiter for multi-select form values. Picklist values must not contain this character. */
15
+ export const MULTI_VALUE_SEP = "|";
16
+
17
+ /** Placeholder value for "All" in single-select (Radix does not accept empty string). */
18
+ export const ALL_PLACEHOLDER_VALUE = "__all__";
19
+
20
+ /** Parses a stored select value into an array (single or multi). Excludes ALL_PLACEHOLDER_VALUE. */
21
+ export function parseMultiSelectValue(raw: string): string[] {
22
+ if (!raw?.trim() || raw === ALL_PLACEHOLDER_VALUE) return [];
23
+ return raw
24
+ .split(MULTI_VALUE_SEP)
25
+ .map((s) => s.trim())
26
+ .filter((s) => s && s !== ALL_PLACEHOLDER_VALUE);
27
+ }
28
+
29
+ /** Form state key for range filter minimum value (suffix _min). */
30
+ export function getRangeMinKey(fieldPath: string): string {
31
+ return `${fieldPath}_min`;
32
+ }
33
+
34
+ /** Form state key for range filter maximum value (suffix _max). */
35
+ export function getRangeMaxKey(fieldPath: string): string {
36
+ return `${fieldPath}_max`;
37
+ }
38
+
39
+ /** Error message when range min > max. */
40
+ export const RANGE_VALIDATION_ERROR_MESSAGE =
41
+ "Minimum value must be less than or equal to maximum value";
42
+
43
+ /**
44
+ * Validates that min is less than or equal to max (when both are numeric).
45
+ */
46
+ export function validateRangeValues(minRaw: string, maxRaw: string): string | null {
47
+ const minVal = parseFilterValue(minRaw);
48
+ const maxVal = parseFilterValue(maxRaw);
49
+ if (minVal === "" || maxVal === "") return null;
50
+ const minNum = typeof minVal === "number" ? minVal : Number(minVal);
51
+ const maxNum = typeof maxVal === "number" ? maxVal : Number(maxVal);
52
+ if (Number.isNaN(minNum) || Number.isNaN(maxNum)) return null;
53
+ if (minNum > maxNum) return RANGE_VALIDATION_ERROR_MESSAGE;
54
+ return null;
55
+ }
56
+
57
+ export interface BuildFilterCriteriaResult {
58
+ criteria: FilterCriteria[];
59
+ rangeError?: string;
60
+ }
61
+
62
+ /**
63
+ * Builds FilterCriteria[] from current filter form values.
64
+ * Validates range filters (min <= max). Returns rangeError when invalid.
65
+ */
66
+ export function buildFilterCriteriaFromFormValues(
67
+ objectApiName: string,
68
+ filters: Filter[],
69
+ formValues: Record<string, string>,
70
+ ): BuildFilterCriteriaResult {
71
+ const criteria: FilterCriteria[] = [];
72
+ let rangeError: string | undefined;
73
+ for (const filter of filters) {
74
+ if (!filter?.targetFieldPath) continue;
75
+ const affordance = (filter.affordance ?? "").toLowerCase();
76
+
77
+ if (affordance === "range") {
78
+ const minKey = getRangeMinKey(filter.targetFieldPath);
79
+ const maxKey = getRangeMaxKey(filter.targetFieldPath);
80
+ const minValue = sanitizeFilterValue(formValues[minKey] ?? "");
81
+ const maxValue = sanitizeFilterValue(formValues[maxKey] ?? "");
82
+ if (minValue && maxValue) {
83
+ const err = validateRangeValues(minValue, maxValue);
84
+ if (err) {
85
+ rangeError = err;
86
+ continue;
87
+ }
88
+ }
89
+ if (minValue) {
90
+ const parsed = parseFilterValue(minValue);
91
+ if (parsed !== "") {
92
+ criteria.push({
93
+ objectApiName,
94
+ fieldPath: filter.targetFieldPath,
95
+ operator: "gte",
96
+ values: [parsed],
97
+ });
98
+ }
99
+ }
100
+ if (maxValue) {
101
+ const parsed = parseFilterValue(maxValue);
102
+ if (parsed !== "") {
103
+ criteria.push({
104
+ objectApiName,
105
+ fieldPath: filter.targetFieldPath,
106
+ operator: "lte",
107
+ values: [parsed],
108
+ });
109
+ }
110
+ }
111
+ } else {
112
+ const raw = formValues[filter.targetFieldPath] ?? "";
113
+ if (affordance === "select") {
114
+ const values = parseMultiSelectValue(raw)
115
+ .map((v) => sanitizeFilterValue(v))
116
+ .filter(Boolean);
117
+ if (values.length > 0) {
118
+ criteria.push({
119
+ objectApiName,
120
+ fieldPath: filter.targetFieldPath,
121
+ operator: values.length > 1 ? "in" : "eq",
122
+ values,
123
+ });
124
+ }
125
+ } else {
126
+ const fieldValue = sanitizeFilterValue(raw);
127
+ if (fieldValue) {
128
+ criteria.push({
129
+ objectApiName,
130
+ fieldPath: filter.targetFieldPath,
131
+ operator: "like",
132
+ values: [`%${fieldValue}%`],
133
+ });
134
+ }
135
+ }
136
+ }
137
+ }
138
+ return rangeError ? { criteria, rangeError } : { criteria };
139
+ }
140
+
141
+ /**
142
+ * Returns initial form values for the filter row (empty or from filter.defaultValues).
143
+ */
144
+ export function getDefaultFilterFormValues(filters: Filter[]): Record<string, string> {
145
+ const values: Record<string, string> = {};
146
+ if (!filters?.length) return values;
147
+ for (const filter of filters) {
148
+ if (!filter?.targetFieldPath) continue;
149
+ const affordance = (filter.affordance ?? "").toLowerCase();
150
+ if (affordance === "range") {
151
+ values[getRangeMinKey(filter.targetFieldPath)] = filter.defaultValues?.[0] ?? "";
152
+ values[getRangeMaxKey(filter.targetFieldPath)] = filter.defaultValues?.[1] ?? "";
153
+ } else {
154
+ values[filter.targetFieldPath] = filter.defaultValues?.[0] ?? "";
155
+ }
156
+ }
157
+ return values;
158
+ }
159
+
160
+ /**
161
+ * Returns only filters whose targetFieldPath is not in the excluded set.
162
+ */
163
+ export function getApplicableFilters(filters: Filter[], excludedFieldPaths: Set<string>): Filter[] {
164
+ return filters.filter((f) => f?.targetFieldPath && !excludedFieldPaths.has(f.targetFieldPath));
165
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Object API names and routes for list/search.
3
+ */
4
+ import {
5
+ MAINTENANCE_WORKER_OBJECT_API_NAME,
6
+ FALLBACK_LABEL_PROPERTIES_PLURAL,
7
+ FALLBACK_LABEL_MAINTENANCE_PLURAL,
8
+ FALLBACK_LABEL_MAINTENANCE_WORKERS_PLURAL,
9
+ FALLBACK_LABEL_APPLICATIONS_PLURAL,
10
+ } from "./constants.js";
11
+
12
+ export const GLOBAL_SEARCH_OBJECT_API_NAME = "Property__c" as const;
13
+ export const MAINTENANCE_OBJECT_API_NAME = "Maintenance_Request__c" as const;
14
+ export const APPLICATION_OBJECT_API_NAME = "Application__c" as const;
15
+ export { MAINTENANCE_WORKER_OBJECT_API_NAME };
16
+
17
+ export const SEARCHABLE_OBJECTS = [
18
+ {
19
+ objectApiName: "Property__c" as const,
20
+ path: "/properties",
21
+ fallbackLabelPlural: FALLBACK_LABEL_PROPERTIES_PLURAL,
22
+ },
23
+ {
24
+ objectApiName: "Maintenance_Request__c" as const,
25
+ path: "/maintenance/requests",
26
+ fallbackLabelPlural: FALLBACK_LABEL_MAINTENANCE_PLURAL,
27
+ },
28
+ {
29
+ objectApiName: MAINTENANCE_WORKER_OBJECT_API_NAME,
30
+ path: "/maintenance/workers",
31
+ fallbackLabelPlural: FALLBACK_LABEL_MAINTENANCE_WORKERS_PLURAL,
32
+ },
33
+ {
34
+ objectApiName: "Application__c" as const,
35
+ path: "/applications",
36
+ fallbackLabelPlural: FALLBACK_LABEL_APPLICATIONS_PLURAL,
37
+ },
38
+ ] as const;
39
+
40
+ export type SearchableObjectConfig = (typeof SEARCHABLE_OBJECTS)[number];
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Shared filter types and client-side filter/search helpers for list pages.
3
+ * Used by Properties, Maintenance Requests, Maintenance Workers, and Applications.
4
+ * Supports both static filter config and FilterCriteria from the react-global-search filters API.
5
+ */
6
+
7
+ import type { FilterCriteria } from "@salesforce/webapp-template-feature-react-global-search-experimental";
8
+
9
+ export type FilterFieldType = "text" | "select";
10
+
11
+ export interface FilterOption {
12
+ value: string;
13
+ label: string;
14
+ }
15
+
16
+ export interface FilterFieldConfig {
17
+ key: string;
18
+ label: string;
19
+ type: FilterFieldType;
20
+ options?: FilterOption[];
21
+ getValue?: (record: Record<string, unknown>) => string | number | undefined;
22
+ }
23
+
24
+ export interface ActiveFilter {
25
+ fieldKey: string;
26
+ value: string;
27
+ }
28
+
29
+ /**
30
+ * Returns true if the record matches the given filter (case-insensitive contains for text, exact for select).
31
+ */
32
+ function recordMatchesFilter(
33
+ record: Record<string, unknown>,
34
+ fieldKey: string,
35
+ value: string,
36
+ getValue?: (record: Record<string, unknown>) => string | number | undefined,
37
+ ): boolean {
38
+ const raw = getValue ? getValue(record) : record[fieldKey];
39
+ const str = raw == null ? "" : String(raw).toLowerCase();
40
+ const v = value.trim().toLowerCase();
41
+ if (!v) return true;
42
+ return str.includes(v);
43
+ }
44
+
45
+ /**
46
+ * Filters a list of records by active filters. All filters must match (AND).
47
+ */
48
+ export function applyFilters<T extends Record<string, unknown>>(
49
+ items: T[],
50
+ filters: ActiveFilter[],
51
+ fieldConfigs: Map<string, FilterFieldConfig>,
52
+ ): T[] {
53
+ if (filters.length === 0) return items;
54
+ return items.filter((item) =>
55
+ filters.every((f) => {
56
+ const config = fieldConfigs.get(f.fieldKey);
57
+ return recordMatchesFilter(
58
+ item as Record<string, unknown>,
59
+ f.fieldKey,
60
+ f.value,
61
+ config?.getValue,
62
+ );
63
+ }),
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Filters items by a search query: matches if any of the searchFields (values from the record) contain the query.
69
+ */
70
+ export function applySearch<T extends Record<string, unknown>>(
71
+ items: T[],
72
+ query: string,
73
+ searchFields: (keyof T)[],
74
+ getValue?: (item: T, key: keyof T) => string | number | undefined,
75
+ ): T[] {
76
+ const q = query.trim().toLowerCase();
77
+ if (!q) return items;
78
+ const get = getValue ?? ((item: T, key: keyof T) => item[key]);
79
+ return items.filter((item) =>
80
+ searchFields.some((key) => {
81
+ const val = get(item, key);
82
+ return val != null && String(val).toLowerCase().includes(q);
83
+ }),
84
+ );
85
+ }
86
+
87
+ /**
88
+ * Applies both search and filters: first search, then filters.
89
+ */
90
+ export function applySearchAndFilters<T extends Record<string, unknown>>(
91
+ items: T[],
92
+ searchQuery: string,
93
+ searchFields: (keyof T)[],
94
+ activeFilters: ActiveFilter[],
95
+ fieldConfigs: Map<string, FilterFieldConfig>,
96
+ getSearchValue?: (item: T, key: keyof T) => string | number | undefined,
97
+ ): T[] {
98
+ const afterSearch = applySearch(items, searchQuery, searchFields, getSearchValue);
99
+ return applyFilters(afterSearch, activeFilters, fieldConfigs);
100
+ }
101
+
102
+ /**
103
+ * Returns true if the record value matches the FilterCriteria (operator + values).
104
+ * Used when filters come from the react-global-search getObjectListFilters API.
105
+ */
106
+ function recordMatchesCriterion(
107
+ recordValue: string | number | undefined,
108
+ criterion: FilterCriteria,
109
+ ): boolean {
110
+ const raw = recordValue == null ? "" : String(recordValue);
111
+ const val = raw.toLowerCase();
112
+ const values = criterion.values.map((v) => (v == null ? "" : String(v).toLowerCase()));
113
+ if (values.length === 0 || values.every((v) => !v)) return true;
114
+ const op = criterion.operator;
115
+ if (op === "eq") return values.some((v) => val === v);
116
+ if (op === "ne") return values.every((v) => val !== v);
117
+ if (op === "like") {
118
+ // values may be like "%x%" from the API
119
+ return values.some((v) => {
120
+ const pattern = v.replace(/%/g, "");
121
+ return pattern ? val.includes(pattern) : true;
122
+ });
123
+ }
124
+ // numeric comparison
125
+ const numVal = parseFloat(raw);
126
+ const numValues = values.map((v) => parseFloat(v)).filter((n) => !Number.isNaN(n));
127
+ if (numValues.length === 0) return true;
128
+ if (op === "gt") return numValues.some((n) => numVal > n);
129
+ if (op === "gte") return numValues.some((n) => numVal >= n);
130
+ if (op === "lt") return numValues.some((n) => numVal < n);
131
+ if (op === "lte") return numValues.some((n) => numVal <= n);
132
+ return true;
133
+ }
134
+
135
+ /**
136
+ * Filters a list using FilterCriteria from the feature's filters API.
137
+ * getRecordValue(record, fieldPath) should return the record's value for the API field path
138
+ * (e.g. Status__c -> record.status, Type__c -> record.issueType for maintenance).
139
+ */
140
+ export function applyFilterCriteria<T>(
141
+ items: T[],
142
+ criteria: FilterCriteria[],
143
+ getRecordValue: (record: T, fieldPath: string) => string | number | undefined,
144
+ ): T[] {
145
+ if (criteria.length === 0) return items;
146
+ return items.filter((item) =>
147
+ criteria.every((c) => {
148
+ const recordValue = getRecordValue(item, c.fieldPath);
149
+ return recordMatchesCriterion(recordValue, c);
150
+ }),
151
+ );
152
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Data-driven config for list pages with filters.
3
+ */
4
+ import type { Column } from "@salesforce/webapp-template-feature-react-global-search-experimental";
5
+ import {
6
+ MAINTENANCE_OBJECT_API_NAME,
7
+ MAINTENANCE_WORKER_OBJECT_API_NAME,
8
+ APPLICATION_OBJECT_API_NAME,
9
+ } from "./globalSearchConstants.js";
10
+ import { getMaintenanceColumns } from "./maintenanceColumns.js";
11
+ import { getMaintenanceWorkerColumns } from "./maintenanceWorkerColumns.js";
12
+ import { getApplicationListColumns } from "./applicationColumns.js";
13
+ import {
14
+ MAINTENANCE_FILTER_EXCLUDED_FIELD_PATHS,
15
+ MAINTENANCE_WORKER_FILTER_EXCLUDED_FIELD_PATHS,
16
+ APPLICATION_FILTER_EXCLUDED_FIELD_PATHS,
17
+ } from "./constants.js";
18
+ import { nodeToMaintenanceRequest } from "./maintenanceAdapter.js";
19
+ import { nodeToMaintenanceWorker } from "./maintenanceWorkerAdapter.js";
20
+ import { nodeToApplication } from "./applicationAdapter.js";
21
+ import type { MaintenanceRequest, MaintenanceWorker, Application } from "./types.js";
22
+
23
+ export interface ListPageConfig<T> {
24
+ objectApiName: string;
25
+ getColumns: (listMetaColumns: Column[]) => Column[];
26
+ filterExcludedFieldPaths: Set<string>;
27
+ defaultSort: string;
28
+ nodeToItem: (node: unknown) => T;
29
+ loadingMessage: string;
30
+ filtersAriaLabel: string;
31
+ sortable: boolean;
32
+ }
33
+
34
+ export const maintenanceRequestsListConfig: ListPageConfig<MaintenanceRequest> = {
35
+ objectApiName: MAINTENANCE_OBJECT_API_NAME,
36
+ getColumns: getMaintenanceColumns,
37
+ filterExcludedFieldPaths: MAINTENANCE_FILTER_EXCLUDED_FIELD_PATHS,
38
+ defaultSort: "Priority__c DESC",
39
+ nodeToItem: (node) => nodeToMaintenanceRequest(node as Record<string, unknown>),
40
+ loadingMessage: "Loading maintenance requests...",
41
+ filtersAriaLabel: "Maintenance Requests filters",
42
+ sortable: false,
43
+ };
44
+
45
+ export const maintenanceWorkersListConfig: ListPageConfig<MaintenanceWorker> = {
46
+ objectApiName: MAINTENANCE_WORKER_OBJECT_API_NAME,
47
+ getColumns: getMaintenanceWorkerColumns,
48
+ filterExcludedFieldPaths: MAINTENANCE_WORKER_FILTER_EXCLUDED_FIELD_PATHS,
49
+ defaultSort: "Name ASC",
50
+ nodeToItem: (node) => nodeToMaintenanceWorker(node as Record<string, unknown>),
51
+ loadingMessage: "Loading maintenance workers...",
52
+ filtersAriaLabel: "Maintenance Workers filters",
53
+ sortable: true,
54
+ };
55
+
56
+ export const applicationsListConfig: ListPageConfig<Application> = {
57
+ objectApiName: APPLICATION_OBJECT_API_NAME,
58
+ getColumns: getApplicationListColumns,
59
+ filterExcludedFieldPaths: APPLICATION_FILTER_EXCLUDED_FIELD_PATHS,
60
+ defaultSort: "CreatedDate DESC",
61
+ nodeToItem: (node) => nodeToApplication(node as Record<string, unknown>),
62
+ loadingMessage: "Loading applications...",
63
+ filtersAriaLabel: "Applications filters",
64
+ sortable: false,
65
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Adapter: maps Maintenance_Request__c GraphQL node to app MaintenanceRequest type.
3
+ */
4
+ import type { MaintenanceRequest } from "./types.js";
5
+
6
+ interface MaintenanceNode {
7
+ Id?: string;
8
+ Name?: { value?: string };
9
+ Description__c?: { value?: string };
10
+ Type__c?: { value?: string };
11
+ Priority__c?: { value?: string };
12
+ Status__c?: { value?: string };
13
+ Scheduled__c?: { value?: string };
14
+ Property__r?: {
15
+ Address__c?: { value?: string };
16
+ Name?: { value?: string };
17
+ };
18
+ User__r?: { Name?: { value?: string }; DisplayValue?: { value?: string } };
19
+ Owner?: { Name?: { value?: string } };
20
+ ContentDocumentLinks?: {
21
+ edges?: Array<{
22
+ node?: {
23
+ ContentDocument?: { LatestPublishedVersionId?: { value?: string } };
24
+ };
25
+ }>;
26
+ };
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ function getTenantNameFromNode(n: MaintenanceNode): string {
31
+ const userRef = n.User__r;
32
+ if (!userRef || typeof userRef !== "object") return "Unknown";
33
+ const nameObj =
34
+ (userRef as Record<string, unknown>).Name ?? (userRef as Record<string, unknown>).name;
35
+ if (nameObj != null && typeof nameObj === "object" && "value" in nameObj) {
36
+ const v = (nameObj as { value?: string }).value;
37
+ if (typeof v === "string" && v.trim() !== "") return v;
38
+ }
39
+ const displayVal =
40
+ (userRef as Record<string, unknown>).DisplayValue ??
41
+ (userRef as Record<string, unknown>).displayValue;
42
+ if (displayVal != null && typeof displayVal === "object" && "value" in displayVal) {
43
+ const v = (displayVal as { value?: string }).value;
44
+ if (typeof v === "string" && v.trim() !== "") return v;
45
+ }
46
+ return "Unknown";
47
+ }
48
+
49
+ function mapPriority(priority: string | undefined): MaintenanceRequest["priority"] {
50
+ if (!priority) return "medium";
51
+ const p = priority.toLowerCase();
52
+ if (p.includes("emergency")) return "emergency";
53
+ if (p.includes("high") || p.includes("same day")) return "high";
54
+ if (p.includes("medium") || p.includes("standard")) return "medium";
55
+ if (p.includes("low")) return "low";
56
+ return "medium";
57
+ }
58
+
59
+ function mapStatus(status: string | undefined): string {
60
+ if (!status) return "new";
61
+ const s = status.toLowerCase().replace(/\s+/g, "_");
62
+ if (s === "completed" || s === "resolved") return "completed";
63
+ if (s === "in_progress") return "in_progress";
64
+ if (s === "assigned") return "assigned";
65
+ if (s === "scheduled") return "scheduled";
66
+ if (s === "new") return "new";
67
+ return "new";
68
+ }
69
+
70
+ export function nodeToMaintenanceRequest(
71
+ node: Record<string, unknown> | undefined,
72
+ ): MaintenanceRequest {
73
+ const n = (node ?? {}) as MaintenanceNode;
74
+ const scheduledDate = n.Scheduled__c?.value ? new Date(n.Scheduled__c.value) : null;
75
+ const formattedDate = scheduledDate
76
+ ? scheduledDate.toLocaleDateString("en-US", { month: "short", day: "numeric" }) +
77
+ ", " +
78
+ scheduledDate.toLocaleTimeString("en-US", {
79
+ hour: "numeric",
80
+ minute: "2-digit",
81
+ hour12: true,
82
+ })
83
+ : undefined;
84
+
85
+ const imageVersionId =
86
+ n.ContentDocumentLinks?.edges?.[0]?.node?.ContentDocument?.LatestPublishedVersionId?.value;
87
+ const imageUrl = imageVersionId
88
+ ? `/sfc/servlet.shepherd/version/download/${imageVersionId}`
89
+ : undefined;
90
+
91
+ const tenantUnit = n.Property__r?.Name?.value ?? n.Property__r?.Address__c?.value;
92
+ const assignedWorkerName = n.Owner?.Name?.value ?? n.User__r?.Name?.value;
93
+
94
+ return {
95
+ id: n.Id ?? "",
96
+ propertyAddress: n.Property__r?.Address__c?.value ?? "Unknown Address",
97
+ issueType: n.Type__c?.value ?? "General",
98
+ priority: mapPriority(n.Priority__c?.value),
99
+ status: mapStatus(n.Status__c?.value),
100
+ assignedWorker: assignedWorkerName,
101
+ scheduledDateTime: scheduledDate?.toLocaleString(),
102
+ description: n.Description__c?.value ?? "",
103
+ tenantName: getTenantNameFromNode(n),
104
+ imageUrl,
105
+ tenantUnit,
106
+ assignedWorkerName: assignedWorkerName ?? undefined,
107
+ assignedWorkerOrg: undefined,
108
+ formattedDate,
109
+ };
110
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Column config for Maintenance_Request__c list.
3
+ */
4
+ import type { Column } from "@salesforce/webapp-template-feature-react-global-search-experimental";
5
+
6
+ export const MAINTENANCE_EXTRA_COLUMNS: Column[] = [
7
+ { fieldApiName: "Description__c", label: "Description", searchable: true, sortable: false },
8
+ {
9
+ fieldApiName: "Property__r.Address__c",
10
+ label: "Property Address",
11
+ searchable: false,
12
+ sortable: false,
13
+ },
14
+ { fieldApiName: "Property__r.Name", label: "Property Name", searchable: false, sortable: false },
15
+ { fieldApiName: "User__r.Name", label: "User Name", searchable: false, sortable: false },
16
+ { fieldApiName: "OwnerId", label: "Owner Id", searchable: false, sortable: false },
17
+ { fieldApiName: "Owner", label: "Owner", searchable: false, sortable: false },
18
+ ];
19
+
20
+ export function getMaintenanceColumns(columns: Column[]): Column[] {
21
+ const existing = new Set(columns.map((c) => c.fieldApiName));
22
+ const toAdd = MAINTENANCE_EXTRA_COLUMNS.filter((c) => !existing.has(c.fieldApiName));
23
+ return toAdd.length === 0 ? columns : [...columns, ...toAdd];
24
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Adapter: maps Maintenance_Worker__c GraphQL node to app MaintenanceWorker type.
3
+ */
4
+ import type { MaintenanceWorker } from "./types.js";
5
+
6
+ interface MaintenanceWorkerNode {
7
+ Id?: string;
8
+ Name?: { value?: string };
9
+ Type__c?: { value?: string };
10
+ Phone__c?: { value?: string };
11
+ Location__c?: { value?: string };
12
+ IsActive__c?: { value?: boolean };
13
+ Employment_Type__c?: { value?: string };
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ export function nodeToMaintenanceWorker(
18
+ node: Record<string, unknown> | undefined,
19
+ ): MaintenanceWorker {
20
+ const n = (node ?? {}) as MaintenanceWorkerNode;
21
+ const isActive = n.IsActive__c?.value;
22
+ return {
23
+ id: n.Id ?? "",
24
+ name: n.Name?.value ?? "",
25
+ phone: n.Phone__c?.value,
26
+ organization: n.Employment_Type__c?.value ?? n.Type__c?.value,
27
+ status: typeof isActive === "boolean" ? (isActive ? "Active" : "Inactive") : undefined,
28
+ };
29
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Column config for Maintenance_Worker__c list.
3
+ */
4
+ import type { Column } from "@salesforce/webapp-template-feature-react-global-search-experimental";
5
+
6
+ export const MAINTENANCE_WORKER_EXTRA_COLUMNS: Column[] = [
7
+ { fieldApiName: "Name", label: "Worker Name", searchable: true, sortable: true },
8
+ { fieldApiName: "Type__c", label: "Primary Skill", searchable: true, sortable: true },
9
+ { fieldApiName: "Phone__c", label: "Phone", searchable: true, sortable: false },
10
+ { fieldApiName: "Location__c", label: "Location", searchable: true, sortable: true },
11
+ { fieldApiName: "IsActive__c", label: "Active", searchable: false, sortable: true },
12
+ {
13
+ fieldApiName: "Employment_Type__c",
14
+ label: "Employment Type",
15
+ searchable: true,
16
+ sortable: true,
17
+ },
18
+ ];
19
+
20
+ export function getMaintenanceWorkerColumns(columns: Column[]): Column[] {
21
+ if (columns.length === 0) return MAINTENANCE_WORKER_EXTRA_COLUMNS;
22
+ const existing = new Set(columns.map((c) => c.fieldApiName));
23
+ const toAdd = MAINTENANCE_WORKER_EXTRA_COLUMNS.filter((c) => !existing.has(c.fieldApiName));
24
+ return toAdd.length === 0 ? columns : [...columns, ...toAdd];
25
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Salesforce object API names for list pages. Used to fetch filters from the
3
+ * react-global-search feature's getObjectListFilters API.
4
+ * Maintenance Workers has no SObject; use null and fall back to static filters.
5
+ */
6
+ export const OBJECT_API_NAMES = {
7
+ properties: "Property__c",
8
+ maintenance_requests: "Maintenance_Request__c",
9
+ applications: "Application__c",
10
+ maintenance_workers: null as string | null,
11
+ } as const;
12
+
13
+ export type ObjectApiNameKey = keyof typeof OBJECT_API_NAMES;