@salesforce/ui-bundle-template-feature-react-search 11.10.1 → 11.11.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 (35) hide show
  1. package/README.md +435 -93
  2. package/dist/CHANGELOG.md +8 -0
  3. package/dist/force-app/main/default/uiBundles/feature-react-search/package.json +4 -4
  4. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/MergedSearchResults.tsx +263 -0
  5. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/Search.tsx +40 -15
  6. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SearchResults.tsx +2 -2
  7. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SourceSection.tsx +12 -13
  8. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/PaginationControls.tsx +75 -5
  9. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/DefaultFilterPanel.tsx +5 -2
  10. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/DateRangeFilter.tsx +128 -18
  11. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +59 -18
  12. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/config.json +15 -14
  13. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useSearch.ts +287 -26
  14. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/index.ts +11 -1
  15. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/queryBuilder.ts +1 -1
  16. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/types.ts +146 -8
  17. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/filterUtils.ts +13 -0
  18. package/dist/package-lock.json +2 -2
  19. package/dist/package.json +1 -1
  20. package/package.json +2 -2
  21. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__popover.tsx +40 -0
  22. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/MergedSearchResults.tsx +263 -0
  23. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/Search.tsx +40 -15
  24. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SearchResults.tsx +2 -2
  25. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SourceSection.tsx +12 -13
  26. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/PaginationControls.tsx +75 -5
  27. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/DefaultFilterPanel.tsx +5 -2
  28. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/DateRangeFilter.tsx +128 -18
  29. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +59 -18
  30. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/config.json +15 -14
  31. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useSearch.ts +287 -26
  32. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/index.ts +11 -1
  33. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/queryBuilder.ts +1 -1
  34. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/types.ts +146 -8
  35. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/filterUtils.ts +13 -0
@@ -40,8 +40,17 @@ export interface SObjectSourceConfig {
40
40
  key: string;
41
41
  /** GraphQL object name (e.g. "Account", "Contact"). */
42
42
  objectName: string;
43
- /** Display label used in section headers. */
43
+ /**
44
+ * Plural display label — used in section headers, the scope dropdown, and
45
+ * empty-state messages (e.g. "Accounts", "Maintenance Requests").
46
+ */
44
47
  label: string;
48
+ /**
49
+ * Singular display label for a single record of this type (e.g. "Account",
50
+ * "Maintenance Request"). Used for the per-card source badge in the merged
51
+ * grid. Falls back to `label` when omitted.
52
+ */
53
+ labelSingular?: string;
45
54
  /** Field name that uniquely identifies a record. Defaults to "Id". */
46
55
  idField?: string;
47
56
  /**
@@ -67,16 +76,20 @@ export interface SObjectSourceConfig {
67
76
  * automatically; do not list it.
68
77
  */
69
78
  displayFields: DisplayField[];
70
- /** Optional structured filters surfaced in the per-source filter panel. */
71
- filterFields?: FilterFieldConfig[];
79
+ /**
80
+ * Optional structured filters for this source.
81
+ *
82
+ * Named `filterBy` (rather than "the filters shown in the panel") because a
83
+ * future entry may constrain the query without rendering a control — e.g. a
84
+ * default `Status != Completed` that hides completed items from results while
85
+ * never appearing in the UI. Today every entry renders an input via
86
+ * `DefaultFilterPanel`.
87
+ */
88
+ filterBy?: FilterFieldConfig[];
72
89
  /** Optional sort options surfaced in the per-source sort dropdown. */
73
- sortFields?: SortFieldConfig[];
90
+ sortBy?: SortFieldConfig[];
74
91
  /** Initial sort applied when no URL sort is present. */
75
92
  defaultSort?: SortState;
76
- /** Default page size for this source. */
77
- pageSize?: number;
78
- /** Allowed page sizes. */
79
- validPageSizes?: number[];
80
93
  /**
81
94
  * GraphQL type name for the `where` variable.
82
95
  * Defaults to `${objectName}_Filter` — override only for non-conventional schemas.
@@ -86,9 +99,48 @@ export interface SObjectSourceConfig {
86
99
  orderByTypeName?: string;
87
100
  }
88
101
 
102
+ /**
103
+ * One global pagination config for the whole search experience. It is the
104
+ * single source of truth — there is no per-source pagination. The same
105
+ * `pageSize` / `pageSizeOptions` apply across every object type and in every
106
+ * scope, including single-object pages (`restrictTo` / `lockedScope`) and when
107
+ * the scope dropdown narrows to one source.
108
+ */
109
+ export interface SearchPaginationConfig {
110
+ /**
111
+ * How results are paginated:
112
+ * - `"per-source"` (default): each source advances its own cursor; a page
113
+ * can hold up to `sources × pageSize` items. This is what the drop-in
114
+ * `<Search>` (one section per source) expects.
115
+ * - `"merged"`: treat the cumulative result set as one list and window it
116
+ * into fixed pages of `pageSize`. Each page shows exactly `pageSize` items
117
+ * across all sources — use for a single combined grid.
118
+ */
119
+ mode?: "per-source" | "merged";
120
+ /**
121
+ * In `"merged"` mode, how nodes from different sources are ordered:
122
+ * - `"sequential"` (default): source 1 in full, then source 2, …
123
+ * - `"interleaved"`: round-robin — every source gets equal priority.
124
+ * - `"proportional"`: each source spread by its result count, so larger
125
+ * sources appear more often. Ignored in `"per-source"` mode.
126
+ */
127
+ mergeOrder?: "sequential" | "interleaved" | "proportional";
128
+ /** Items per page (across all sources in `"merged"` mode; per source otherwise). */
129
+ pageSize: number;
130
+ /** Page sizes offered in the "results per page" control. */
131
+ pageSizeOptions: number[];
132
+ }
133
+
89
134
  /** Top-level config passed to {@link useSearch}. */
90
135
  export interface SearchConfig {
91
136
  sources: SObjectSourceConfig[];
137
+ /**
138
+ * Global pagination config — the single source of truth for page size, page
139
+ * options, mode, and merge order. See {@link SearchPaginationConfig}. When
140
+ * omitted, pagination defaults to per-source mode, page size 10, options
141
+ * `[10, 25, 50]`.
142
+ */
143
+ pagination?: SearchPaginationConfig;
92
144
  }
93
145
 
94
146
  // ---------------------------------------------------------------------------
@@ -129,6 +181,8 @@ export interface SourceController {
129
181
  };
130
182
  pagination: {
131
183
  pageSize: number;
184
+ /** Allowed page sizes — from the global pagination config. */
185
+ pageSizeOptions: readonly number[];
132
186
  pageIndex: number;
133
187
  hasNextPage: boolean;
134
188
  hasPreviousPage: boolean;
@@ -149,6 +203,70 @@ export type SearchScope = "all" | string;
149
203
 
150
204
  export const ALL_SCOPE = "all" as const;
151
205
 
206
+ /**
207
+ * One result node tagged with the source it came from. Produced by
208
+ * {@link SearchHandle.mergedResults} so a single combined grid can render
209
+ * cross-source results and still pick the right card per node.
210
+ */
211
+ export interface MergedResultItem {
212
+ /** The `key` of the source this node came from. */
213
+ sourceKey: string;
214
+ /** The raw result node (cast to your typed node in the renderer). */
215
+ node: unknown;
216
+ }
217
+
218
+ /**
219
+ * Aggregate pagination across every in-scope source, exposed by
220
+ * {@link SearchHandle.globalPagination}. Its exact behaviour depends on the
221
+ * `pagination` mode passed to {@link useSearch}:
222
+ *
223
+ * - **`"merged"`** (recommended for a single combined grid): the cumulative
224
+ * result set is treated as one list and windowed into fixed pages of
225
+ * `pageSize`. Page `P` always shows **exactly** `pageSize` items (the last
226
+ * page may show fewer), `pageCount` is `ceil(totalCount / pageSize)`, and
227
+ * `goToPage` can jump to any page. Under the hood each in-scope source fetches
228
+ * `(P + 1) * pageSize` rows from its front, the results are concatenated in
229
+ * config order, and the page-`P` slice is taken — so the math is exact, not an
230
+ * estimate.
231
+ * - **`"per-source"`** (default): each source paginates with its own cursor and
232
+ * a "global page" is the furthest-advanced in-scope source's page. `Next`
233
+ * advances only the sources still on the current global page that have more
234
+ * data; a source that runs out is left behind. `goToPage` is best-effort
235
+ * (adjacent pages only) because cursors can't seek to an arbitrary offset.
236
+ */
237
+ export interface GlobalPagination {
238
+ /** Current global page, 0-based. */
239
+ pageIndex: number;
240
+ /**
241
+ * Total number of global pages. In `"merged"` mode this is exactly
242
+ * `ceil(totalCount / pageSize)`. In `"per-source"` mode it is the `max` over
243
+ * in-scope sources of `ceil(totalCount / pageSize)` and may be an estimate if
244
+ * a source omits `totalCount`. `0` when there are no results, otherwise `>= 1`.
245
+ */
246
+ pageCount: number;
247
+ /** Shared page size. */
248
+ pageSize: number;
249
+ /** Allowed page sizes (from the first in-scope source). */
250
+ pageSizeOptions: readonly number[];
251
+ /** True when there is a page after the current one. */
252
+ hasNextPage: boolean;
253
+ /** True when the current global page is past the first. */
254
+ hasPreviousPage: boolean;
255
+ /** Cumulative result count summed across in-scope sources. */
256
+ totalCount: number;
257
+ /** Advance to the next global page. */
258
+ goToNextPage: () => void;
259
+ /** Step back to the previous global page. */
260
+ goToPreviousPage: () => void;
261
+ /**
262
+ * Jump to a specific 0-based page. Exact in `"merged"` mode; in
263
+ * `"per-source"` mode only adjacent pages are honoured (others are a no-op).
264
+ */
265
+ goToPage: (pageIndex: number) => void;
266
+ /** Set the page size (resets to the first page). */
267
+ setPageSize: (size: number) => void;
268
+ }
269
+
152
270
  export interface SearchHandle {
153
271
  q: string;
154
272
  setQ: (q: string) => void;
@@ -160,6 +278,26 @@ export interface SearchHandle {
160
278
  */
161
279
  scopeLocked: boolean;
162
280
  sources: Record<string, SourceController>;
281
+ /**
282
+ * Source controllers that participate in the current scope, in config
283
+ * declaration order. In `"all"` scope this is every source; otherwise just
284
+ * the selected one. Convenience for callers building aggregate UI.
285
+ */
286
+ inScopeSources: SourceController[];
287
+ /**
288
+ * In-scope nodes flattened into one list (config order, source by source),
289
+ * each tagged with its `sourceKey` — for rendering a single combined grid
290
+ * instead of one section per source. In `"merged"` pagination mode this is
291
+ * exactly the current page's window (at most `globalPagination.pageSize`
292
+ * items); in `"per-source"` mode it is every in-scope source's current page
293
+ * concatenated.
294
+ */
295
+ mergedResults: MergedResultItem[];
296
+ /**
297
+ * Aggregate pagination across all in-scope sources — drive one shared
298
+ * pager over the cumulative result set. See {@link GlobalPagination}.
299
+ */
300
+ globalPagination: GlobalPagination;
163
301
  loading: boolean;
164
302
  error: string | null;
165
303
  resetAll: () => void;
@@ -28,6 +28,17 @@ export type FilterFieldType =
28
28
  | "datetimerange"
29
29
  | "multipicklist";
30
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
+
31
42
  export type FilterFieldConfig<TFieldName extends string = string> = {
32
43
  field: TFieldName;
33
44
  label: string;
@@ -35,6 +46,8 @@ export type FilterFieldConfig<TFieldName extends string = string> = {
35
46
  placeholder?: string;
36
47
  /** Required for picklist / multipicklist. */
37
48
  options?: Array<{ value: string; label: string }>;
49
+ /** Date-family only — see {@link DateFilterMode}. Defaults to `"comparison"`. */
50
+ dateMode?: DateFilterMode;
38
51
  helpText?: string;
39
52
  };
40
53