@salesforce/ui-bundle-template-app-react-sample-b2e 11.20.0 → 11.22.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 (102) hide show
  1. package/dist/CHANGELOG.md +16 -0
  2. package/dist/force-app/main/default/uiBundles/propertymanagementapp/package.json +4 -4
  3. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/applications/applicationSearchService.ts +25 -43
  4. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/maintenanceRequestSearchService.ts +31 -65
  5. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceWorkers/maintenanceWorkerSearchService.ts +21 -43
  6. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/propertySearchService.ts +33 -53
  7. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/applications/ApplicationCard.tsx +80 -0
  8. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/dashboard/GlobalSearchBar.tsx +2 -2
  9. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/layout/VerticalNav.tsx +28 -18
  10. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/maintenanceRequests/MaintenanceRequestCard.tsx +79 -52
  11. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/maintenanceWorkers/WorkerCard.tsx +87 -0
  12. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/search/UnifiedSearch.tsx +213 -0
  13. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/api/distinctValuesService.ts +84 -0
  14. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/api/searchService.ts +71 -0
  15. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/MergedSearchResults.tsx +263 -0
  16. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/Search.tsx +185 -0
  17. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/SearchResults.tsx +77 -0
  18. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/SourceSection.tsx +166 -0
  19. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/PaginationControls.tsx +185 -0
  20. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/ScopeSelector.tsx +55 -0
  21. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/SearchBar.tsx +55 -0
  22. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/controls/SortControl.tsx +62 -0
  23. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/ActiveFilters.tsx +61 -0
  24. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/DefaultFilterPanel.tsx +125 -0
  25. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/{object-search/components → search/components/filters}/FilterContext.tsx +7 -20
  26. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
  27. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/DateRangeFilter.tsx +160 -0
  28. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
  29. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +88 -0
  30. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
  31. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
  32. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
  33. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/components/results/DefaultResultRow.tsx +109 -0
  34. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/config.json +83 -0
  35. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/{object-search → search}/hooks/useAsyncData.ts +3 -10
  36. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/hooks/useDistinctValues.ts +59 -0
  37. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/hooks/useSearch.ts +703 -0
  38. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/index.ts +90 -0
  39. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/loadConfig.ts +14 -0
  40. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/queryBuilder.ts +156 -0
  41. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/types.ts +307 -0
  42. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/debounce.ts +17 -0
  43. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/fieldUtils.ts +14 -0
  44. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/filterUtils.ts +285 -0
  45. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/search/utils/sortUtils.ts +34 -0
  46. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/constants.ts +16 -29
  47. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/routeConfig.ts +1 -0
  48. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/searchConfig.ts +190 -0
  49. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/ApplicationSearch.tsx +8 -385
  50. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/Home.tsx +4 -2
  51. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/MaintenanceRequestSearch.tsx +8 -458
  52. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/MaintenanceWorkerSearch.tsx +8 -341
  53. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/PropertySearch.tsx +7 -243
  54. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/pages/Search.tsx +16 -0
  55. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/routes.tsx +6 -0
  56. package/dist/force-app/main/default/uiBundles/propertymanagementapp/tsconfig.tsbuildinfo +1 -1
  57. package/dist/package-lock.json +2 -2
  58. package/dist/package.json +1 -1
  59. package/package.json +2 -2
  60. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/applications/query/distinctApplicationStatus.graphql +0 -19
  61. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/applications/query/searchApplications.graphql +0 -65
  62. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/distinctMaintenanceRequestPriority.graphql +0 -19
  63. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/distinctMaintenanceRequestStatus.graphql +0 -19
  64. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/distinctMaintenanceRequestType.graphql +0 -19
  65. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceRequests/query/searchMaintenanceRequests.graphql +0 -82
  66. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceWorkers/query/distinctMaintenanceWorkerEmploymentType.graphql +0 -19
  67. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/maintenanceWorkers/query/searchMaintenanceWorkers.graphql +0 -64
  68. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/query/distinctPropertyStatus.graphql +0 -19
  69. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/query/distinctPropertyType.graphql +0 -19
  70. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/api/properties/query/searchProperties.graphql +0 -105
  71. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/shared/ObjectSearchErrorState.tsx +0 -45
  72. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/components/shared/UserAvatar.tsx +0 -35
  73. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/accountSearchService.ts +0 -46
  74. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/distinctAccountIndustries.graphql +0 -19
  75. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/distinctAccountTypes.graphql +0 -19
  76. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/getAccountDetail.graphql +0 -121
  77. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/api/query/searchAccounts.graphql +0 -51
  78. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/pages/AccountObjectDetailPage.tsx +0 -355
  79. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/pages/AccountSearch.tsx +0 -304
  80. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/__examples__/pages/Home.tsx +0 -34
  81. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/api/objectSearchService.ts +0 -82
  82. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/ActiveFilters.tsx +0 -89
  83. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/ObjectBreadcrumb.tsx +0 -66
  84. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/PaginationControls.tsx +0 -109
  85. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/SearchBar.tsx +0 -41
  86. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/SortControl.tsx +0 -143
  87. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/BooleanFilter.tsx +0 -78
  88. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/DateFilter.tsx +0 -128
  89. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/DateRangeFilter.tsx +0 -70
  90. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/FilterFieldWrapper.tsx +0 -43
  91. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/MultiSelectFilter.tsx +0 -105
  92. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/NumericRangeFilter.tsx +0 -171
  93. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/SearchFilter.tsx +0 -52
  94. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/SelectFilter.tsx +0 -97
  95. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/components/filters/TextFilter.tsx +0 -93
  96. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/hooks/useDebouncedCallback.ts +0 -34
  97. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/hooks/useObjectSearchParams.ts +0 -254
  98. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/debounce.ts +0 -25
  99. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/fieldUtils.ts +0 -29
  100. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/filterUtils.ts +0 -404
  101. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/features/object-search/utils/sortUtils.ts +0 -38
  102. package/dist/force-app/main/default/uiBundles/propertymanagementapp/src/lib/filterUtils.ts +0 -11
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Filter primitives for search.
3
+ *
4
+ * Two responsibilities:
5
+ * 1. Build a GraphQL `where` clause from a list of active filter values.
6
+ * 2. Serialize/deserialize active filters to/from URLSearchParams under a
7
+ * per-source namespace prefix (so multiple sources can coexist on one URL).
8
+ *
9
+ * The URL helpers are namespaced: each source declares its key, and filter
10
+ * params are written as `s.<key>.f.<field>=...`. Sort uses
11
+ * `s.<key>.sort=<field>&s.<key>.dir=ASC|DESC`. Page size and page index use
12
+ * `s.<key>.ps=<size>` and `s.<key>.page=<n>`.
13
+ *
14
+ * The global search term `q=...` is **not** scoped to a source — it's broadcast
15
+ * to every source's `searchableFields` by the query builder.
16
+ */
17
+
18
+ import type { SortState } from "./sortUtils";
19
+
20
+ export type FilterFieldType =
21
+ | "text"
22
+ | "picklist"
23
+ | "numeric"
24
+ | "boolean"
25
+ | "date"
26
+ | "daterange"
27
+ | "datetime"
28
+ | "datetimerange"
29
+ | "multipicklist";
30
+
31
+ /**
32
+ * Which comparison UI a date/datetime filter exposes:
33
+ * - `"range"`: a single Between control (two date inputs, lower–upper bound).
34
+ * - `"comparison"`: an After / Before selector with one date input.
35
+ * - `"both"`: a Between / After / Before selector that switches between the two.
36
+ *
37
+ * Applies only to the date-family `type`s (`date`, `daterange`, `datetime`,
38
+ * `datetimerange`); ignored for other field types. Defaults to `"comparison"`.
39
+ */
40
+ export type DateFilterMode = "range" | "comparison" | "both";
41
+
42
+ export type FilterFieldConfig<TFieldName extends string = string> = {
43
+ field: TFieldName;
44
+ label: string;
45
+ type: FilterFieldType;
46
+ placeholder?: string;
47
+ /** Required for picklist / multipicklist. */
48
+ options?: Array<{ value: string; label: string }>;
49
+ /** Date-family only — see {@link DateFilterMode}. Defaults to `"comparison"`. */
50
+ dateMode?: DateFilterMode;
51
+ helpText?: string;
52
+ };
53
+
54
+ export type ActiveFilterValue<TFieldName extends string = string> = {
55
+ field: TFieldName;
56
+ label: string;
57
+ type: FilterFieldType;
58
+ value?: string;
59
+ min?: string;
60
+ max?: string;
61
+ };
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // URL serialization (per-source namespace)
65
+ // ---------------------------------------------------------------------------
66
+
67
+ const SOURCE_PREFIX = "s.";
68
+ const FILTER_INFIX = ".f.";
69
+ const SORT_SUFFIX = ".sort";
70
+ const DIR_SUFFIX = ".dir";
71
+ const PAGE_SIZE_SUFFIX = ".ps";
72
+ const PAGE_SUFFIX = ".page";
73
+
74
+ /** Global search term key (not source-scoped). */
75
+ export const GLOBAL_QUERY_KEY = "q";
76
+
77
+ function filterKey(sourceKey: string, field: string, suffix?: string) {
78
+ return `${SOURCE_PREFIX}${sourceKey}${FILTER_INFIX}${field}${suffix ? `.${suffix}` : ""}`;
79
+ }
80
+
81
+ /**
82
+ * The source's own default sort / page size. When a source's current state
83
+ * matches these, the corresponding URL params are omitted so the query string
84
+ * only ever reflects an *explicit* user choice — resetting filters or returning
85
+ * a source to its defaults yields a clean URL instead of one littered with the
86
+ * defaults. The read path falls back to these same defaults when the params are
87
+ * absent, so the round-trip is lossless.
88
+ */
89
+ export interface SourceParamDefaults {
90
+ sort?: SortState | null;
91
+ pageSize?: number;
92
+ }
93
+
94
+ function sortMatchesDefault(sort: SortState, defaultSort: SortState | null | undefined): boolean {
95
+ return (
96
+ !!defaultSort && sort.field === defaultSort.field && sort.direction === defaultSort.direction
97
+ );
98
+ }
99
+
100
+ export function writeSourceParams(
101
+ params: URLSearchParams,
102
+ sourceKey: string,
103
+ filters: ActiveFilterValue[],
104
+ sort: SortState | null,
105
+ pageSize?: number,
106
+ pageIndex?: number,
107
+ defaults?: SourceParamDefaults,
108
+ ): void {
109
+ for (const filter of filters) {
110
+ if (filter.value !== undefined && filter.value !== "") {
111
+ params.set(filterKey(sourceKey, filter.field), filter.value);
112
+ }
113
+ if (filter.min !== undefined && filter.min !== "") {
114
+ params.set(filterKey(sourceKey, filter.field, "min"), filter.min);
115
+ }
116
+ if (filter.max !== undefined && filter.max !== "") {
117
+ params.set(filterKey(sourceKey, filter.field, "max"), filter.max);
118
+ }
119
+ }
120
+ // Only serialize sort when it differs from the source default — otherwise
121
+ // the URL would carry the default the user never chose.
122
+ if (sort && !sortMatchesDefault(sort, defaults?.sort)) {
123
+ params.set(`${SOURCE_PREFIX}${sourceKey}${SORT_SUFFIX}`, sort.field);
124
+ params.set(`${SOURCE_PREFIX}${sourceKey}${DIR_SUFFIX}`, sort.direction);
125
+ }
126
+ if (pageSize !== undefined && pageSize !== defaults?.pageSize) {
127
+ params.set(`${SOURCE_PREFIX}${sourceKey}${PAGE_SIZE_SUFFIX}`, String(pageSize));
128
+ }
129
+ if (pageIndex !== undefined && pageIndex > 0) {
130
+ params.set(`${SOURCE_PREFIX}${sourceKey}${PAGE_SUFFIX}`, String(pageIndex + 1));
131
+ }
132
+ }
133
+
134
+ export interface ReadSourceParamsResult {
135
+ filters: ActiveFilterValue[];
136
+ sort: SortState | null;
137
+ pageSize: number | undefined;
138
+ pageIndex: number;
139
+ }
140
+
141
+ export function readSourceParams(
142
+ params: URLSearchParams,
143
+ sourceKey: string,
144
+ configs: FilterFieldConfig[],
145
+ ): ReadSourceParamsResult {
146
+ const filters: ActiveFilterValue[] = [];
147
+ for (const config of configs) {
148
+ const { field, label, type } = config;
149
+ const value = params.get(filterKey(sourceKey, field)) ?? undefined;
150
+ const min = params.get(filterKey(sourceKey, field, "min")) ?? undefined;
151
+ const max = params.get(filterKey(sourceKey, field, "max")) ?? undefined;
152
+
153
+ const hasValue = value !== undefined && value !== "";
154
+ const hasRange = (min !== undefined && min !== "") || (max !== undefined && max !== "");
155
+ if (hasValue || hasRange) {
156
+ filters.push({ field, label, type, value, min, max });
157
+ }
158
+ }
159
+
160
+ let sort: SortState | null = null;
161
+ const sortField = params.get(`${SOURCE_PREFIX}${sourceKey}${SORT_SUFFIX}`);
162
+ const sortDir = params.get(`${SOURCE_PREFIX}${sourceKey}${DIR_SUFFIX}`);
163
+ if (sortField) {
164
+ sort = { field: sortField, direction: sortDir === "DESC" ? "DESC" : "ASC" };
165
+ }
166
+
167
+ const pageSizeRaw = params.get(`${SOURCE_PREFIX}${sourceKey}${PAGE_SIZE_SUFFIX}`);
168
+ const pageSize = pageSizeRaw ? parseInt(pageSizeRaw, 10) : undefined;
169
+ const pageRaw = params.get(`${SOURCE_PREFIX}${sourceKey}${PAGE_SUFFIX}`);
170
+ const page = pageRaw ? parseInt(pageRaw, 10) : 1;
171
+ const pageIndex = !isNaN(page) && page > 1 ? page - 1 : 0;
172
+
173
+ return {
174
+ filters,
175
+ sort,
176
+ pageSize: pageSize && !isNaN(pageSize) ? pageSize : undefined,
177
+ pageIndex,
178
+ };
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // GraphQL filter building
183
+ // ---------------------------------------------------------------------------
184
+
185
+ /**
186
+ * Combines active filter values into a GraphQL `where` clause for one source.
187
+ * Returns `undefined` when no constraints are active.
188
+ */
189
+ export function buildFilter(filters: ActiveFilterValue[], configs: FilterFieldConfig[]): unknown {
190
+ const configMap = new Map(configs.map((c) => [c.field, c]));
191
+ const clauses: unknown[] = [];
192
+ for (const filter of filters) {
193
+ const clause = buildSingleFilter(filter, configMap.get(filter.field));
194
+ if (clause) clauses.push(clause);
195
+ }
196
+ if (clauses.length === 0) return undefined;
197
+ if (clauses.length === 1) return clauses[0];
198
+ return { and: clauses };
199
+ }
200
+
201
+ function toStartOfDay(d: string) {
202
+ return `${d}T00:00:00.000Z`;
203
+ }
204
+ function toEndOfDay(d: string) {
205
+ return `${d}T23:59:59.999Z`;
206
+ }
207
+
208
+ function buildSingleFilter(filter: ActiveFilterValue, _config?: FilterFieldConfig): unknown {
209
+ const { field, type, value, min, max } = filter;
210
+ switch (type) {
211
+ case "text": {
212
+ if (!value) return null;
213
+ return { [field]: { like: `%${value}%` } };
214
+ }
215
+ case "picklist": {
216
+ if (!value) return null;
217
+ return { [field]: { eq: value } };
218
+ }
219
+ case "numeric": {
220
+ if (!min && !max) return null;
221
+ const ops: Record<string, number> = {};
222
+ if (min) ops.gte = Number(min);
223
+ if (max) ops.lte = Number(max);
224
+ return { [field]: ops };
225
+ }
226
+ case "boolean": {
227
+ if (value === undefined || value === "") return null;
228
+ return { [field]: { eq: value === "true" } };
229
+ }
230
+ case "multipicklist": {
231
+ if (!value) return null;
232
+ const values = value.split(",").filter(Boolean);
233
+ if (values.length === 0) return null;
234
+ if (values.length === 1) return { [field]: { eq: values[0] } };
235
+ return { [field]: { in: values } };
236
+ }
237
+ case "date": {
238
+ if (!min && !max) return null;
239
+ const op = min ? "gte" : "lte";
240
+ const dateStr = min ?? max;
241
+ return { [field]: { [op]: { value: dateStr } } };
242
+ }
243
+ case "daterange": {
244
+ if (!min && !max) return null;
245
+ const clauses: unknown[] = [];
246
+ if (min) clauses.push({ [field]: { gte: { value: min } } });
247
+ if (max) clauses.push({ [field]: { lte: { value: max } } });
248
+ return clauses.length === 1 ? clauses[0] : { and: clauses };
249
+ }
250
+ case "datetime": {
251
+ if (!min && !max) return null;
252
+ const op = min ? "gte" : "lte";
253
+ const dateStr = min ?? max;
254
+ const isoStr = op === "gte" ? toStartOfDay(dateStr!) : toEndOfDay(dateStr!);
255
+ return { [field]: { [op]: { value: isoStr } } };
256
+ }
257
+ case "datetimerange": {
258
+ if (!min && !max) return null;
259
+ const clauses: unknown[] = [];
260
+ if (min) clauses.push({ [field]: { gte: { value: toStartOfDay(min) } } });
261
+ if (max) clauses.push({ [field]: { lte: { value: toEndOfDay(max) } } });
262
+ return clauses.length === 1 ? clauses[0] : { and: clauses };
263
+ }
264
+ default:
265
+ return null;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Builds a global-q `or` clause across the source's searchable fields.
271
+ * Returns `undefined` when q is empty or the source has no searchable fields.
272
+ */
273
+ export function buildGlobalQueryClause(q: string, searchableFields: string[]): unknown {
274
+ const trimmed = q.trim();
275
+ if (!trimmed || searchableFields.length === 0) return undefined;
276
+ const clauses = searchableFields.map((path) => {
277
+ const parts = path.split(".");
278
+ let clause: Record<string, unknown> = { like: `%${trimmed}%` };
279
+ for (let i = parts.length - 1; i >= 0; i--) {
280
+ clause = { [parts[i]]: clause };
281
+ }
282
+ return clause;
283
+ });
284
+ return clauses.length === 1 ? clauses[0] : { or: clauses };
285
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Sort state and order-by builder for search.
3
+ *
4
+ * The order-by output shape matches the Salesforce uiapi GraphQL convention:
5
+ * { [field]: { order: ASC|DESC, nulls: LAST } }
6
+ *
7
+ * The constants are stringly-typed here (rather than imported from codegen)
8
+ * because search builds its query at runtime and isn't tied to a
9
+ * specific generated schema.
10
+ */
11
+
12
+ export type SortFieldConfig<TFieldName extends string = string> = {
13
+ field: TFieldName;
14
+ label: string;
15
+ };
16
+
17
+ export type SortState<TFieldName extends string = string> = {
18
+ field: TFieldName;
19
+ direction: "ASC" | "DESC";
20
+ };
21
+
22
+ const ORDER_ASC = "ASC";
23
+ const ORDER_DESC = "DESC";
24
+ const NULLS_LAST = "LAST";
25
+
26
+ export function buildOrderBy(sort: SortState | null): unknown {
27
+ if (!sort) return undefined;
28
+ return {
29
+ [sort.field]: {
30
+ order: sort.direction === "ASC" ? ORDER_ASC : ORDER_DESC,
31
+ nulls: NULLS_LAST,
32
+ },
33
+ };
34
+ }
@@ -2,34 +2,26 @@
2
2
  * Application-wide constants.
3
3
  */
4
4
 
5
- import type { PaginationConfig } from "../features/object-search/hooks/useObjectSearchParams";
5
+ const MAINTENANCE_WORKER_OBJECT_API_NAME = "Maintenance_Worker__c" as const;
6
6
 
7
- export const PAGE_SIZE_LIST = 12;
8
- export const DASHBOARD_MAINTENANCE_LIMIT = 5;
7
+ const FALLBACK_LABEL_PROPERTIES_PLURAL = "Properties";
8
+ const FALLBACK_LABEL_MAINTENANCE_PLURAL = "Maintenance Requests";
9
+ const FALLBACK_LABEL_MAINTENANCE_WORKERS_PLURAL = "Maintenance Workers";
10
+ const FALLBACK_LABEL_APPLICATIONS_PLURAL = "Applications";
9
11
 
10
- export const PROPERTY_FILTER_EXCLUDED_FIELD_PATHS = new Set([
11
- "CreatedDate",
12
- "Hero_Image__c",
13
- "Year_Built__c",
14
- "Available_Date__c",
15
- ]);
16
-
17
- export const MAINTENANCE_WORKER_FILTER_EXCLUDED_FIELD_PATHS = new Set<string>([]);
18
- export const MAINTENANCE_FILTER_EXCLUDED_FIELD_PATHS = new Set(["Scheduled__c"]);
19
- export const APPLICATION_FILTER_EXCLUDED_FIELD_PATHS = new Set<string>([]);
20
-
21
- export const MAINTENANCE_WORKER_OBJECT_API_NAME = "Maintenance_Worker__c" as const;
22
-
23
- export const FALLBACK_LABEL_PROPERTIES_PLURAL = "Properties";
24
- export const FALLBACK_LABEL_MAINTENANCE_PLURAL = "Maintenance Requests";
25
- export const FALLBACK_LABEL_MAINTENANCE_WORKERS_PLURAL = "Maintenance Workers";
26
- export const FALLBACK_LABEL_APPLICATIONS_PLURAL = "Applications";
27
-
28
- export const PROPERTY_OBJECT_API_NAME = "Property__c" as const;
29
- export const MAINTENANCE_OBJECT_API_NAME = "Maintenance_Request__c" as const;
30
- export const APPLICATION_OBJECT_API_NAME = "Application__c" as const;
12
+ /**
13
+ * Sentinel value for the "search across everything" option in the dashboard
14
+ * search dropdown. Selecting it routes to the unified `/search` page rather
15
+ * than a single-object list page.
16
+ */
17
+ export const ALL_SEARCH_OBJECT_VALUE = "All" as const;
31
18
 
32
19
  export const SEARCHABLE_OBJECTS = [
20
+ {
21
+ objectApiName: ALL_SEARCH_OBJECT_VALUE,
22
+ path: "/search",
23
+ fallbackLabelPlural: "All",
24
+ },
33
25
  {
34
26
  objectApiName: "Property__c" as const,
35
27
  path: "/properties",
@@ -53,8 +45,3 @@ export const SEARCHABLE_OBJECTS = [
53
45
  ] as const;
54
46
 
55
47
  export type SearchableObjectConfig = (typeof SEARCHABLE_OBJECTS)[number];
56
-
57
- export const PAGINATION_CONFIG: PaginationConfig = {
58
- defaultPageSize: 6,
59
- validPageSizes: [6, 12, 24, 48],
60
- };
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export const PATHS = {
6
6
  HOME: "/",
7
+ SEARCH: "/search",
7
8
  PROPERTIES: "/properties",
8
9
  MAINTENANCE_REQUESTS: "/maintenance/requests",
9
10
  MAINTENANCE_WORKERS: "/maintenance/workers",
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Property-management search configuration for feature-react-search.
3
+ *
4
+ * The search feature builds its GraphQL queries at runtime from this config, so
5
+ * the app no longer ships per-object `.graphql` search documents or
6
+ * `searchObjects()` services. One `SearchConfig` describes all four searchable
7
+ * objects; `<UnifiedSearch>` consumes it for both the unified `/search` page and
8
+ * each single-object page (via `restrictTo`).
9
+ *
10
+ * `displayFields` must cover every field the result cards and detail modals read
11
+ * — see the matching `*SearchNode` types in `src/api/**`. Relationships that
12
+ * require arguments (e.g. `ContentDocumentLinks(first: 1)` thumbnails or the
13
+ * worker's open-request `totalCount`) cannot be expressed here and are
14
+ * intentionally omitted; the cards degrade gracefully when they are absent.
15
+ */
16
+
17
+ import type { SearchConfig } from "../features/search";
18
+
19
+ /** Stable source keys — used as GraphQL aliases, URL namespaces, and result-map keys. */
20
+ export const SOURCE_KEYS = {
21
+ PROPERTIES: "properties",
22
+ MAINTENANCE: "maintenance",
23
+ WORKERS: "workers",
24
+ APPLICATIONS: "applications",
25
+ } as const;
26
+
27
+ export type SourceKey = (typeof SOURCE_KEYS)[keyof typeof SOURCE_KEYS];
28
+
29
+ /** Page-size options shared by every source (cards lay out 3-up). */
30
+ export const SEARCH_PAGE_SIZE = 6;
31
+ export const SEARCH_PAGE_SIZE_OPTIONS = [6, 12, 24, 48];
32
+
33
+ export const searchConfig: SearchConfig = {
34
+ // One global pagination config — the single source of truth for the whole
35
+ // search experience. `merged` mode windows the cumulative result set into
36
+ // fixed pages of `pageSize`, and `proportional` spreads each object type by
37
+ // its result count so larger sources appear more often. These settings apply
38
+ // to every object type, in every scope — the unified `/search` page, when the
39
+ // dropdown narrows to one source, and single-object pages (via `restrictTo`).
40
+ pagination: {
41
+ mode: "merged",
42
+ mergeOrder: "proportional",
43
+ pageSize: SEARCH_PAGE_SIZE,
44
+ pageSizeOptions: SEARCH_PAGE_SIZE_OPTIONS,
45
+ },
46
+ sources: [
47
+ {
48
+ kind: "sobject",
49
+ key: SOURCE_KEYS.PROPERTIES,
50
+ objectName: "Property__c",
51
+ label: "Properties",
52
+ labelSingular: "Property",
53
+ searchableFields: ["Name", "Address__c"],
54
+ displayFields: [
55
+ "Name",
56
+ "Address__c",
57
+ "Status__c",
58
+ "Type__c",
59
+ "Monthly_Rent__c",
60
+ "Bedrooms__c",
61
+ "Bathrooms__c",
62
+ "Description__c",
63
+ "Hero_Image__c",
64
+ "Sq_Ft__c",
65
+ "Year_Built__c",
66
+ "Deposit__c",
67
+ "Parking__c",
68
+ "Pet_Friendly__c",
69
+ "Available_Date__c",
70
+ "Lease_Term__c",
71
+ "Features__c",
72
+ "Utilities__c",
73
+ "Tour_URL__c",
74
+ "CreatedDate",
75
+ ],
76
+ filterBy: [
77
+ { field: "Status__c", label: "Status", type: "multipicklist" },
78
+ { field: "Type__c", label: "Type", type: "multipicklist" },
79
+ { field: "Monthly_Rent__c", label: "Monthly Rent", type: "numeric" },
80
+ { field: "Bedrooms__c", label: "Bedrooms", type: "numeric" },
81
+ ],
82
+ sortBy: [
83
+ { field: "Name", label: "Name" },
84
+ { field: "Monthly_Rent__c", label: "Monthly Rent" },
85
+ { field: "Status__c", label: "Status" },
86
+ { field: "CreatedDate", label: "Created Date" },
87
+ ],
88
+ },
89
+ {
90
+ kind: "sobject",
91
+ key: SOURCE_KEYS.MAINTENANCE,
92
+ objectName: "Maintenance_Request__c",
93
+ label: "Maintenance Requests",
94
+ labelSingular: "Maintenance Request",
95
+ searchableFields: [
96
+ "Name",
97
+ "User__r.Name",
98
+ "Property__r.Address__c",
99
+ "Assigned_Worker__r.Name",
100
+ ],
101
+ displayFields: [
102
+ "Name",
103
+ "Description__c",
104
+ "Type__c",
105
+ "Priority__c",
106
+ "Status__c",
107
+ "Scheduled__c",
108
+ { name: "Property__r", subfields: ["Address__c", "Name"] },
109
+ { name: "User__r", subfields: ["Name"] },
110
+ { name: "Assigned_Worker__r", subfields: ["Name"] },
111
+ ],
112
+ filterBy: [
113
+ { field: "Status__c", label: "Status", type: "multipicklist" },
114
+ { field: "Type__c", label: "Type", type: "multipicklist" },
115
+ { field: "Priority__c", label: "Priority", type: "multipicklist" },
116
+ { field: "Scheduled__c", label: "Scheduled", type: "datetime" },
117
+ ],
118
+ sortBy: [
119
+ { field: "Name", label: "Subject" },
120
+ { field: "Status__c", label: "Status" },
121
+ { field: "Priority__c", label: "Priority" },
122
+ { field: "Scheduled__c", label: "Scheduled" },
123
+ { field: "CreatedDate", label: "Created Date" },
124
+ ],
125
+ defaultSort: { field: "CreatedDate", direction: "DESC" },
126
+ },
127
+ {
128
+ kind: "sobject",
129
+ key: SOURCE_KEYS.WORKERS,
130
+ objectName: "Maintenance_Worker__c",
131
+ label: "Maintenance Workers",
132
+ labelSingular: "Maintenance Worker",
133
+ searchableFields: ["Name", "Phone__c"],
134
+ displayFields: [
135
+ "Name",
136
+ "Type__c",
137
+ "Employment_Type__c",
138
+ "Phone__c",
139
+ "Location__c",
140
+ "IsActive__c",
141
+ "Rating__c",
142
+ "Hourly_Rate__c",
143
+ "CreatedDate",
144
+ ],
145
+ filterBy: [
146
+ { field: "Employment_Type__c", label: "Employment Type", type: "multipicklist" },
147
+ { field: "Location__c", label: "Location", type: "text", placeholder: "Location" },
148
+ { field: "Hourly_Rate__c", label: "Hourly Rate", type: "numeric" },
149
+ { field: "CreatedDate", label: "Created Date", type: "datetime" },
150
+ ],
151
+ sortBy: [
152
+ { field: "Name", label: "Name" },
153
+ { field: "Employment_Type__c", label: "Employment Type" },
154
+ { field: "Hourly_Rate__c", label: "Hourly Rate" },
155
+ { field: "Rating__c", label: "Rating" },
156
+ { field: "CreatedDate", label: "Created Date" },
157
+ ],
158
+ },
159
+ {
160
+ kind: "sobject",
161
+ key: SOURCE_KEYS.APPLICATIONS,
162
+ objectName: "Application__c",
163
+ label: "Applications",
164
+ labelSingular: "Application",
165
+ searchableFields: ["Name", "User__r.Name", "Property__r.Address__c"],
166
+ displayFields: [
167
+ "Name",
168
+ "Status__c",
169
+ "Start_Date__c",
170
+ "Employment__c",
171
+ "References__c",
172
+ { name: "Property__r", subfields: ["Name", "Address__c"] },
173
+ { name: "User__r", subfields: ["Name"] },
174
+ "CreatedDate",
175
+ ],
176
+ filterBy: [
177
+ { field: "Status__c", label: "Status", type: "multipicklist" },
178
+ { field: "Start_Date__c", label: "Start Date", type: "date" },
179
+ { field: "CreatedDate", label: "Created Date", type: "datetime" },
180
+ ],
181
+ sortBy: [
182
+ { field: "Name", label: "Application" },
183
+ { field: "Status__c", label: "Status" },
184
+ { field: "Start_Date__c", label: "Start Date" },
185
+ { field: "CreatedDate", label: "Created Date" },
186
+ ],
187
+ defaultSort: { field: "CreatedDate", direction: "DESC" },
188
+ },
189
+ ],
190
+ };