@salesforce/ui-bundle-template-feature-react-search 11.3.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.
- package/LICENSE.txt +82 -0
- package/README.md +692 -0
- package/dist/.forceignore +15 -0
- package/dist/.husky/pre-commit +4 -0
- package/dist/.prettierignore +11 -0
- package/dist/.prettierrc +17 -0
- package/dist/CHANGELOG.md +3499 -0
- package/dist/README.md +28 -0
- package/dist/config/project-scratch-def.json +13 -0
- package/dist/eslint.config.js +7 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/.forceignore +15 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/.graphqlrc.yml +2 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/.prettierignore +9 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/.prettierrc +11 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/CHANGELOG.md +10 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/README.md +75 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/codegen.yml +95 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/components.json +18 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/e2e/app.spec.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/eslint.config.js +169 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/feature-react-search.uibundle-meta.xml +7 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/index.html +12 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/package.json +76 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/playwright.config.ts +24 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/scripts/get-graphql-schema.mjs +71 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/scripts/rewrite-e2e-assets.mjs +23 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/api/graphqlClient.ts +44 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/app.tsx +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/appLayout.tsx +83 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/book.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/copy.svg +4 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/rocket.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/star.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-1.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-2.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-3.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/vibe-codey.svg +194 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/alerts/status-alert.tsx +52 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/layouts/card-layout.tsx +29 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/alert.tsx +76 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/avatar.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/badge.tsx +48 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/breadcrumb.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/button.tsx +67 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/calendar.tsx +232 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/card.tsx +103 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/checkbox.tsx +32 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/collapsible.tsx +33 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/datePicker.tsx +127 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/dialog.tsx +162 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/dropdown-menu.tsx +257 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/field.tsx +237 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/index.ts +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/input.tsx +19 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/label.tsx +22 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/pagination.tsx +132 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/popover.tsx +89 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/select.tsx +193 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/separator.tsx +26 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/skeleton.tsx +14 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/sonner.tsx +20 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/spinner.tsx +16 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/table.tsx +114 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/tabs.tsx +88 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/distinctValuesService.ts +84 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/searchService.ts +71 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/Search.tsx +160 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SearchResults.tsx +77 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SourceSection.tsx +167 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/PaginationControls.tsx +115 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/ScopeSelector.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SearchBar.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SortControl.tsx +62 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/ActiveFilters.tsx +61 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/DefaultFilterPanel.tsx +122 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/FilterContext.tsx +70 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/DateRangeFilter.tsx +50 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +47 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/results/DefaultResultRow.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/config.json +82 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useAsyncData.ts +56 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useDistinctValues.ts +59 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useSearch.ts +442 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/index.ts +80 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/loadConfig.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/queryBuilder.ts +156 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/types.ts +169 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/debounce.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/fieldUtils.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/filterUtils.ts +272 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/sortUtils.ts +34 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/hooks/useAsyncData.ts +67 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/lib/utils.ts +6 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/navigationMenu.tsx +80 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/pages/Home.tsx +11 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/pages/NotFound.tsx +18 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/router-utils.tsx +35 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/routes.tsx +22 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/src/styles/global.css +135 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/tsconfig.json +45 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/tsconfig.node.json +13 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/ui-bundle.json +7 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/vite-env.d.ts +4 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/vite.config.ts +106 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/vitest-env.d.ts +2 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/vitest.config.ts +11 -0
- package/dist/force-app/main/default/uiBundles/feature-react-search/vitest.setup.ts +1 -0
- package/dist/jest.config.js +6 -0
- package/dist/package-lock.json +9995 -0
- package/dist/package.json +44 -0
- package/dist/scripts/apex/hello.apex +10 -0
- package/dist/scripts/gitignore-templates.json +4 -0
- package/dist/scripts/graphql-search.sh +191 -0
- package/dist/scripts/org-setup-config-schema.mjs +96 -0
- package/dist/scripts/org-setup.config.json +5 -0
- package/dist/scripts/org-setup.mjs +1392 -0
- package/dist/scripts/sf-project-setup.mjs +103 -0
- package/dist/scripts/soql/account.soql +6 -0
- package/dist/scripts/validate-org-setup-config.mjs +38 -0
- package/dist/sfdx-project.json +12 -0
- package/package.json +51 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/__inherit__appLayout.tsx +9 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__alert.tsx +39 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__button.tsx +45 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__checkbox.tsx +8 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__input.tsx +5 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__label.tsx +8 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__pagination.tsx +47 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__select.tsx +57 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__skeleton.tsx +5 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/distinctValuesService.ts +84 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/searchService.ts +71 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/Search.tsx +160 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SearchResults.tsx +77 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SourceSection.tsx +167 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/PaginationControls.tsx +115 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/ScopeSelector.tsx +55 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SearchBar.tsx +55 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SortControl.tsx +62 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/ActiveFilters.tsx +61 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/DefaultFilterPanel.tsx +122 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/FilterContext.tsx +70 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/DateRangeFilter.tsx +50 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +47 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/results/DefaultResultRow.tsx +109 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/config.json +82 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useAsyncData.ts +56 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useDistinctValues.ts +59 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useSearch.ts +442 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/index.ts +80 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/loadConfig.ts +14 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/queryBuilder.ts +156 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/types.ts +169 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/debounce.ts +17 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/fieldUtils.ts +14 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/filterUtils.ts +272 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/sortUtils.ts +34 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/pages/Home.tsx +11 -0
- package/src/force-app/main/default/uiBundles/feature-react-search/src/routes.tsx +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
# @salesforce/ui-bundle-template-feature-react-search
|
|
2
|
+
|
|
3
|
+
Configuration-driven, multi-source search for **React** applications on the
|
|
4
|
+
Salesforce platform.
|
|
5
|
+
|
|
6
|
+
> ⚛️ **React only.** This package ships React components and hooks built on
|
|
7
|
+
> `react` 19 and `react-router` 7. Use it in a React application; it is not
|
|
8
|
+
> compatible with LWC or other non-React UIs — see [Requirements](#requirements).
|
|
9
|
+
|
|
10
|
+
You describe **what** is searchable in a single config object; the package owns
|
|
11
|
+
**how** it's searched — GraphQL query construction, filter/sort/pagination
|
|
12
|
+
state, URL sync, default rendering, and the orchestration UI. Most apps drop in
|
|
13
|
+
one component and get a complete search page with zero custom code:
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Search, config } from "@salesforce/ui-bundle-template-feature-react-search";
|
|
17
|
+
|
|
18
|
+
export default function SearchPage() {
|
|
19
|
+
return <Search config={config} />;
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Adding a new searchable object is a single entry in your config — no code
|
|
24
|
+
changes.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Table of contents
|
|
29
|
+
|
|
30
|
+
- [Requirements](#requirements)
|
|
31
|
+
- [Installation](#installation)
|
|
32
|
+
- [Quick start](#quick-start)
|
|
33
|
+
- [The `<Search>` component](#the-search-component)
|
|
34
|
+
- [Props](#props)
|
|
35
|
+
- [Creating your own config](#creating-your-own-config)
|
|
36
|
+
- [Source fields](#source-fields-sobjectsourceconfig)
|
|
37
|
+
- [`displayFields` shapes](#displayfields-shapes)
|
|
38
|
+
- [`filterFields` types](#filterfields-types)
|
|
39
|
+
- [`sortFields` and `defaultSort`](#sortfields-and-defaultsort)
|
|
40
|
+
- [`routePattern` tokens](#routepattern-tokens)
|
|
41
|
+
- [A complete source, annotated](#a-complete-source-annotated)
|
|
42
|
+
- [Use cases](#use-cases)
|
|
43
|
+
- [1. Unified search across many objects](#1-unified-search-across-many-objects)
|
|
44
|
+
- [2. Single-object search (`restrictTo`)](#2-single-object-search-restrictto)
|
|
45
|
+
- [3. Hand off a term from another page](#3-hand-off-a-term-from-another-page)
|
|
46
|
+
- [4. Custom row layout for one source](#4-custom-row-layout-for-one-source)
|
|
47
|
+
- [5. Hide a source from the results](#5-hide-a-source-from-the-results)
|
|
48
|
+
- [6. Replace a source's filter sidebar](#6-replace-a-sources-filter-sidebar)
|
|
49
|
+
- [7. Headless — drive your own UI with `useSearch`](#7-headless--drive-your-own-ui-with-usesearch)
|
|
50
|
+
- [8. Mixed card layouts per source](#8-mixed-card-layouts-per-source)
|
|
51
|
+
- [9. Global pagination across all sources](#9-global-pagination-across-all-sources)
|
|
52
|
+
- [URL and state conventions](#url-and-state-conventions)
|
|
53
|
+
- [Public API](#public-api)
|
|
54
|
+
- [Common mistakes](#common-mistakes)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
This is a **React-only** package. Your application must provide:
|
|
61
|
+
|
|
62
|
+
- **React 19** and **react-router 7** — the components render React elements and
|
|
63
|
+
use react-router for navigation, URL sync, and result links.
|
|
64
|
+
- **`@salesforce/platform-sdk`** — data is fetched via
|
|
65
|
+
`createDataSDK().graphql.query()` against the Salesforce uiapi GraphQL bridge.
|
|
66
|
+
Every source today is an SObject queried through this bridge.
|
|
67
|
+
- **lucide-react** — used for the control icons.
|
|
68
|
+
|
|
69
|
+
Your app must be wrapped in a react-router router (e.g. a `<BrowserRouter>` or a
|
|
70
|
+
data router), since the search components read and write the URL.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install @salesforce/ui-bundle-template-feature-react-search
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Then import what you need from the package entry point:
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
import { Search, config } from "@salesforce/ui-bundle-template-feature-react-search";
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Quick start
|
|
89
|
+
|
|
90
|
+
The package ships an example `config` you can import to try things out
|
|
91
|
+
immediately. Render the drop-in `<Search>` against it:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { Search, config } from "@salesforce/ui-bundle-template-feature-react-search";
|
|
95
|
+
|
|
96
|
+
export default function SearchPage() {
|
|
97
|
+
return <Search config={config} title="Search" searchPlaceholder="Search across Salesforce…" />;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
This renders: a search bar, a Reset button, a live total-result count, a scope
|
|
102
|
+
dropdown (when the config has 2+ sources), and one results section per source —
|
|
103
|
+
each with auto-rendered rows, a filter sidebar, a sort dropdown, and
|
|
104
|
+
pagination. Everything is driven by the config you pass in.
|
|
105
|
+
|
|
106
|
+
For a real application you'll typically pass [your own config](#creating-your-own-config).
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## The `<Search>` component
|
|
111
|
+
|
|
112
|
+
`<Search>` is the top of a layered API. Use it as-is, override individual
|
|
113
|
+
slots, or drop down to the `useSearch` hook for full control:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
<Search config /> ← drop-in (most apps)
|
|
117
|
+
│
|
|
118
|
+
├── <SearchBar /> + <SearchResults /> ← compose primitives + own header
|
|
119
|
+
│ └── <SourceSection /> per source
|
|
120
|
+
│ ├── <DefaultResultRow /> ← override per source
|
|
121
|
+
│ └── <DefaultFilterPanel /> ← override per source
|
|
122
|
+
│
|
|
123
|
+
└── useSearch(config) ← headless: build your own UI
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Props
|
|
127
|
+
|
|
128
|
+
| Prop | Type | Default | Purpose |
|
|
129
|
+
| ------------------- | ------------------------------------------------ | ------------------------------ | ------------------------------------------------------------------------------------------- |
|
|
130
|
+
| `config` | `SearchConfig` | — (required) | The source list. Pass your own, or the example `config` export. |
|
|
131
|
+
| `title` | `string` | `"Search"` | Heading shown by the default header. |
|
|
132
|
+
| `subtitle` | `string` | — | Sub-heading shown by the default header. |
|
|
133
|
+
| `searchPlaceholder` | `string` | `"Search…"` | Placeholder for the search input. |
|
|
134
|
+
| `restrictTo` | `{ kind: "sobject"; key: string }` | — | Lock the page to one source. Hides the scope dropdown and only fetches/renders that source. |
|
|
135
|
+
| `showScopeSelector` | `boolean` | `true` when 2+ sources | Force-show or hide the scope dropdown. Always hidden when `restrictTo` is set. |
|
|
136
|
+
| `allScopeLabel` | `string` | `"All"` | Label for the "search everything" entry in the scope dropdown. |
|
|
137
|
+
| `renderResult` | `Record<string, ((node) => ReactNode) \| false>` | — | Per source key: custom row renderer, or `false` to hide that source. |
|
|
138
|
+
| `renderFilters` | `Record<string, (() => ReactNode) \| false>` | — | Per source key: custom filter sidebar, or `false` to suppress filters for that source. |
|
|
139
|
+
| `emptyMessages` | `Record<string, string>` | — | Per source key: message shown when that source has no results. |
|
|
140
|
+
| `renderHeader` | `(handle: SearchHandle) => ReactNode` | built-in title/subtitle header | Replace the entire header region. |
|
|
141
|
+
| `className` | `string` | — | Extra classes on the outer container. |
|
|
142
|
+
|
|
143
|
+
`renderResult` / `renderFilters` / `emptyMessages` are keyed by the source
|
|
144
|
+
`key` from your config (e.g. `accounts`, `contacts`).
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Creating your own config
|
|
149
|
+
|
|
150
|
+
A `SearchConfig` is just `{ sources: SObjectSourceConfig[] }`. Define it in
|
|
151
|
+
your own code and pass it to `<Search config={...} />`:
|
|
152
|
+
|
|
153
|
+
```tsx
|
|
154
|
+
import { Search } from "@salesforce/ui-bundle-template-feature-react-search";
|
|
155
|
+
import type { SearchConfig } from "@salesforce/ui-bundle-template-feature-react-search";
|
|
156
|
+
|
|
157
|
+
const config: SearchConfig = {
|
|
158
|
+
sources: [
|
|
159
|
+
{
|
|
160
|
+
kind: "sobject",
|
|
161
|
+
key: "accounts",
|
|
162
|
+
objectName: "Account",
|
|
163
|
+
label: "Accounts",
|
|
164
|
+
routePattern: "/accounts/:id",
|
|
165
|
+
searchableFields: ["Name", "Phone", "Industry"],
|
|
166
|
+
displayFields: ["Name", "Industry", "Phone"],
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export default function SearchPage() {
|
|
172
|
+
return <Search config={config} />;
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
You can also keep the config as a JSON file and import it — cast it to
|
|
177
|
+
`SearchConfig` since the structure is not validated at runtime (see the note at
|
|
178
|
+
the end of this section).
|
|
179
|
+
|
|
180
|
+
Today every source is `kind: "sobject"`, queried through the platform-sdk
|
|
181
|
+
uiapi GraphQL bridge.
|
|
182
|
+
|
|
183
|
+
### Source fields (`SObjectSourceConfig`)
|
|
184
|
+
|
|
185
|
+
| Field | Required | Type | Description |
|
|
186
|
+
| ------------------ | -------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
187
|
+
| `kind` | ✅ | `"sobject"` | Discriminator selecting the runtime adapter. |
|
|
188
|
+
| `key` | ✅ | `string` | Stable id — used as the GraphQL alias, URL namespace, and result-map key. Must match `/^[A-Za-z_][A-Za-z0-9_]*$/`. |
|
|
189
|
+
| `objectName` | ✅ | `string` | GraphQL object name (`"Account"`, `"Contact"`, …). |
|
|
190
|
+
| `label` | ✅ | `string` | Display label for the section header and scope dropdown. |
|
|
191
|
+
| `searchableFields` | ✅ | `string[]` | Fields the global `q` term matches. OR-ed as `like %q%`. Supports dot-paths (`"Owner.Name"`). |
|
|
192
|
+
| `displayFields` | ✅ | `DisplayField[]` | Fields selected in the GraphQL query and used by the default row layout. (See below.) |
|
|
193
|
+
| `routePattern` | | `string` | Makes the default row a `<Link>`. Tokens like `:id` / `:Name` are substituted per record. Omit for non-clickable rows. |
|
|
194
|
+
| `idField` | | `string` (default `"Id"`) | Unique-id field. Drives the `:id` route token. Always selected automatically — **do not** list it in `displayFields`. |
|
|
195
|
+
| `filterFields` | | `FilterFieldConfig[]` | Structured filters shown in the per-source sidebar. |
|
|
196
|
+
| `sortFields` | | `SortFieldConfig[]` | Options in the per-source sort dropdown. |
|
|
197
|
+
| `defaultSort` | | `{ field, direction }` | Initial sort applied when the URL has none. `direction` is `"ASC"` or `"DESC"`. |
|
|
198
|
+
| `pageSize` | | `number` (default `10`) | Default page size. |
|
|
199
|
+
| `validPageSizes` | | `number[]` | Allowed page sizes for the page-size selector. |
|
|
200
|
+
| `whereTypeName` | | `string` | GraphQL `where` variable type. Defaults to `${objectName}_Filter`. Override only for non-conventional schemas. |
|
|
201
|
+
| `orderByTypeName` | | `string` | GraphQL `orderBy` variable type. Defaults to `${objectName}_OrderBy`. |
|
|
202
|
+
|
|
203
|
+
### `displayFields` shapes
|
|
204
|
+
|
|
205
|
+
Each entry controls both the GraphQL selection set **and** the default row
|
|
206
|
+
layout (first entry → row title; the rest → subtitle, joined with `·`):
|
|
207
|
+
|
|
208
|
+
| Form | GraphQL emitted | Use for |
|
|
209
|
+
| ---------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------- |
|
|
210
|
+
| `"Name"` (string) | `Name @optional { value displayValue }` | Ordinary scalar fields. |
|
|
211
|
+
| `{ name: "Owner", subfields: ["Name"] }` | `Owner @optional { Name @optional { … } }` | Relationship traversal (parent/lookup). |
|
|
212
|
+
| `{ name: "SomeId", raw: true }` | `SomeId` | Fields not wrapped in `{ value displayValue }` (Id-like scalars). |
|
|
213
|
+
|
|
214
|
+
> The `idField` (default `"Id"`) is always emitted — never list it explicitly.
|
|
215
|
+
|
|
216
|
+
### `filterFields` types
|
|
217
|
+
|
|
218
|
+
Each `FilterFieldConfig` is `{ field, label, type, options?, placeholder?, helpText? }`.
|
|
219
|
+
The `type` drives which input renders and how the `where` clause is built:
|
|
220
|
+
|
|
221
|
+
| `type` | UI | `where` clause |
|
|
222
|
+
| --------------- | -------------------------- | -------------------------------------------- |
|
|
223
|
+
| `text` | Text input | `{ field: { like: "%value%" } }` |
|
|
224
|
+
| `picklist` | Single-select dropdown | `{ field: { eq: value } }` |
|
|
225
|
+
| `multipicklist` | Multi-select | `{ field: { in: [...] } }` (or `eq` for one) |
|
|
226
|
+
| `numeric` | min / max number inputs | `{ field: { gte, lte } }` |
|
|
227
|
+
| `boolean` | Tri-state (any/true/false) | `{ field: { eq: true \| false } }` |
|
|
228
|
+
| `date` | Single date | `{ field: { gte \| lte: { value } } }` |
|
|
229
|
+
| `daterange` | Date min / max | `and` of `gte` / `lte` |
|
|
230
|
+
| `datetime` | Single datetime | start/end-of-day ISO bounds |
|
|
231
|
+
| `datetimerange` | Datetime min / max | `and` of start/end-of-day ISO bounds |
|
|
232
|
+
|
|
233
|
+
For `picklist` / `multipicklist`, options are **auto-fetched** from the GraphQL
|
|
234
|
+
aggregate API (`groupBy`) on first render. Skip the fetch by supplying inline
|
|
235
|
+
`options: [{ value, label }]` — required for fields that aren't group-by-able
|
|
236
|
+
(formulas, long text, etc.).
|
|
237
|
+
|
|
238
|
+
### `sortFields` and `defaultSort`
|
|
239
|
+
|
|
240
|
+
`sortFields` is a list of `{ field, label }`. Each becomes an option in the
|
|
241
|
+
per-source sort dropdown (with an ASC/DESC toggle). `defaultSort` sets the
|
|
242
|
+
initial order when no sort is present in the URL:
|
|
243
|
+
|
|
244
|
+
```jsonc
|
|
245
|
+
"sortFields": [
|
|
246
|
+
{ "field": "CloseDate", "label": "Close Date" },
|
|
247
|
+
{ "field": "Amount", "label": "Amount" }
|
|
248
|
+
],
|
|
249
|
+
"defaultSort": { "field": "CloseDate", "direction": "DESC" }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Omit `sortFields` entirely to hide the sort dropdown for that source.
|
|
253
|
+
|
|
254
|
+
### `routePattern` tokens
|
|
255
|
+
|
|
256
|
+
When set, the default row becomes a react-router `<Link>`:
|
|
257
|
+
|
|
258
|
+
- `:id` → the configured `idField` value.
|
|
259
|
+
- `:fieldName` → that field's value; supports dot-paths (`:Owner.Name`).
|
|
260
|
+
|
|
261
|
+
If **any** token resolves to `null` for a record, that row falls back to a
|
|
262
|
+
plain, non-clickable layout. Make sure tokenized fields are present in
|
|
263
|
+
`displayFields`.
|
|
264
|
+
|
|
265
|
+
### A complete source, annotated
|
|
266
|
+
|
|
267
|
+
```jsonc
|
|
268
|
+
{
|
|
269
|
+
"kind": "sobject", // discriminator
|
|
270
|
+
"key": "leads", // GraphQL alias + URL namespace + result key
|
|
271
|
+
"objectName": "Lead", // GraphQL type
|
|
272
|
+
"label": "Leads", // section header + scope option
|
|
273
|
+
"routePattern": "/leads/:id", // default row → <Link to="/leads/<id>">
|
|
274
|
+
"idField": "Id", // default; drives :id (don't add to displayFields)
|
|
275
|
+
"searchableFields": ["Name", "Company", "Email"], // global q is OR-ed across these
|
|
276
|
+
"displayFields": [
|
|
277
|
+
"Name", // row title
|
|
278
|
+
"Title", // ┐
|
|
279
|
+
"Company", // ├ subtitle (joined with " · ")
|
|
280
|
+
"Email", // ┘
|
|
281
|
+
{ "name": "Owner", "subfields": ["Name"] }, // relationship traversal
|
|
282
|
+
],
|
|
283
|
+
"filterFields": [
|
|
284
|
+
{ "field": "Status", "label": "Status", "type": "picklist" },
|
|
285
|
+
{ "field": "Industry", "label": "Industry", "type": "picklist" },
|
|
286
|
+
],
|
|
287
|
+
"sortFields": [
|
|
288
|
+
{ "field": "Name", "label": "Name" },
|
|
289
|
+
{ "field": "CreatedDate", "label": "Created Date" },
|
|
290
|
+
],
|
|
291
|
+
"defaultSort": { "field": "CreatedDate", "direction": "DESC" },
|
|
292
|
+
"pageSize": 10,
|
|
293
|
+
"validPageSizes": [5, 10, 20],
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
That single entry gives Leads a full search experience: a global-term match
|
|
298
|
+
across name/company/email, two auto-populated picklist filters, a sort
|
|
299
|
+
dropdown, pagination, and clickable result rows linking to `/leads/<id>`.
|
|
300
|
+
|
|
301
|
+
> **No runtime schema validation.** A config object is trusted as-is; a typo
|
|
302
|
+
> surfaces at GraphQL query time, not when the config loads. Layer your own
|
|
303
|
+
> validation (e.g. a zod schema) if you need stricter guarantees.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Use cases
|
|
308
|
+
|
|
309
|
+
### 1. Unified search across many objects
|
|
310
|
+
|
|
311
|
+
The default. Every source in the config is searched from one box; a scope
|
|
312
|
+
dropdown lets the user narrow to a single object. Best for a global
|
|
313
|
+
"search everything" page.
|
|
314
|
+
|
|
315
|
+
```tsx
|
|
316
|
+
<Search config={config} title="Search" />
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### 2. Single-object search (`restrictTo`)
|
|
320
|
+
|
|
321
|
+
Reuse the same config but lock the page to one source. The scope dropdown is
|
|
322
|
+
hidden and only the matching source is fetched and rendered — ideal for a
|
|
323
|
+
dedicated "Account search" or "Browse Contacts" page where the object is
|
|
324
|
+
implied by the route.
|
|
325
|
+
|
|
326
|
+
```tsx
|
|
327
|
+
<Search
|
|
328
|
+
config={config}
|
|
329
|
+
title="Search Accounts"
|
|
330
|
+
searchPlaceholder="Search by name, phone, or industry…"
|
|
331
|
+
restrictTo={{ kind: "sobject", key: "accounts" }}
|
|
332
|
+
/>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### 3. Hand off a term from another page
|
|
336
|
+
|
|
337
|
+
The global term lives in the URL under `?q=` (the exported `GLOBAL_QUERY_KEY`).
|
|
338
|
+
Navigate to the search page with `?q=` pre-filled and the input + results
|
|
339
|
+
populate on arrival — handy for a simple search box on a Home page:
|
|
340
|
+
|
|
341
|
+
```tsx
|
|
342
|
+
import { useNavigate } from "react-router";
|
|
343
|
+
|
|
344
|
+
function HomeSearchBox() {
|
|
345
|
+
const navigate = useNavigate();
|
|
346
|
+
const onSubmit = (term: string) => {
|
|
347
|
+
// `q` matches GLOBAL_QUERY_KEY, so the search page pre-fills on arrival.
|
|
348
|
+
navigate(`/accounts${term ? `?q=${encodeURIComponent(term)}` : ""}`);
|
|
349
|
+
};
|
|
350
|
+
// …render an input that calls onSubmit…
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### 4. Custom row layout for one source
|
|
355
|
+
|
|
356
|
+
Override the default renderer for specific sources by key; others keep the
|
|
357
|
+
default. Return `ReactNode` from `(node) => …` — narrow `node` to your own
|
|
358
|
+
type. Use the exported `fieldValue()` to read `{ value, displayValue }` fields:
|
|
359
|
+
|
|
360
|
+
```tsx
|
|
361
|
+
import { Search, fieldValue } from "@salesforce/ui-bundle-template-feature-react-search";
|
|
362
|
+
|
|
363
|
+
<Search
|
|
364
|
+
config={config}
|
|
365
|
+
renderResult={{
|
|
366
|
+
opportunities: (node) => {
|
|
367
|
+
const o = node as OpportunityNode;
|
|
368
|
+
return (
|
|
369
|
+
<div className="flex justify-between">
|
|
370
|
+
<span>{fieldValue(o.Name)}</span>
|
|
371
|
+
<span className="text-muted-foreground">{fieldValue(o.StageName)}</span>
|
|
372
|
+
</div>
|
|
373
|
+
);
|
|
374
|
+
},
|
|
375
|
+
}}
|
|
376
|
+
/>;
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 5. Hide a source from the results
|
|
380
|
+
|
|
381
|
+
Pass `false` for that source's `renderResult`. The source skips both the
|
|
382
|
+
network request and its UI section:
|
|
383
|
+
|
|
384
|
+
```tsx
|
|
385
|
+
<Search config={config} renderResult={{ accounts: false }} />
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 6. Replace a source's filter sidebar
|
|
389
|
+
|
|
390
|
+
Supply your own filter UI built from the exported filter inputs (they wire into
|
|
391
|
+
the search state via `FilterContext`). Pass `false` to suppress filters
|
|
392
|
+
entirely for a source.
|
|
393
|
+
|
|
394
|
+
```tsx
|
|
395
|
+
import {
|
|
396
|
+
Search,
|
|
397
|
+
TextFilter,
|
|
398
|
+
SelectFilter,
|
|
399
|
+
} from "@salesforce/ui-bundle-template-feature-react-search";
|
|
400
|
+
|
|
401
|
+
<Search
|
|
402
|
+
config={config}
|
|
403
|
+
renderFilters={{
|
|
404
|
+
accounts: () => (
|
|
405
|
+
<>
|
|
406
|
+
<TextFilter field="Name" label="Account Name" />
|
|
407
|
+
{/* SelectFilter / MultiSelectFilter take an explicit `options` list. */}
|
|
408
|
+
<SelectFilter
|
|
409
|
+
field="Industry"
|
|
410
|
+
label="Industry"
|
|
411
|
+
options={[
|
|
412
|
+
{ value: "Technology", label: "Technology" },
|
|
413
|
+
{ value: "Finance", label: "Finance" },
|
|
414
|
+
]}
|
|
415
|
+
/>
|
|
416
|
+
</>
|
|
417
|
+
),
|
|
418
|
+
}}
|
|
419
|
+
/>;
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### 7. Headless — drive your own UI with `useSearch`
|
|
423
|
+
|
|
424
|
+
Skip the orchestration components entirely and build a bespoke layout. The hook
|
|
425
|
+
owns state, URL sync, and fetching; you render whatever you like from the
|
|
426
|
+
returned `SearchHandle`.
|
|
427
|
+
|
|
428
|
+
```tsx
|
|
429
|
+
import { useSearch } from "@salesforce/ui-bundle-template-feature-react-search";
|
|
430
|
+
|
|
431
|
+
function MySearch() {
|
|
432
|
+
const { q, setQ, sources, loading, error, resetAll } = useSearch(config);
|
|
433
|
+
// sources[key].result.nodes, .filters, .sort, .pagination …
|
|
434
|
+
// build your own UI from these.
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
Lock the headless hook to a single source with
|
|
439
|
+
`useSearch(config, { lockedScope: "accounts" })`.
|
|
440
|
+
|
|
441
|
+
### 8. Mixed card layouts per source
|
|
442
|
+
|
|
443
|
+
`renderResult` is keyed by source, so each object type can render a completely
|
|
444
|
+
different card while the search bar, scope, and pagination stay shared. Sources
|
|
445
|
+
without an entry fall back to the default row. Here Accounts render as a stat
|
|
446
|
+
card and Contacts as an avatar row:
|
|
447
|
+
|
|
448
|
+
```tsx
|
|
449
|
+
import { Search, fieldValue } from "@salesforce/ui-bundle-template-feature-react-search";
|
|
450
|
+
|
|
451
|
+
<Search
|
|
452
|
+
config={config}
|
|
453
|
+
renderResult={{
|
|
454
|
+
// Accounts → a bordered "stat" card.
|
|
455
|
+
accounts: (node) => {
|
|
456
|
+
const a = node as AccountNode;
|
|
457
|
+
return (
|
|
458
|
+
<div className="rounded-lg border p-4 flex items-center justify-between">
|
|
459
|
+
<div>
|
|
460
|
+
<p className="font-semibold">{fieldValue(a.Name)}</p>
|
|
461
|
+
<p className="text-sm text-muted-foreground">{fieldValue(a.Industry)}</p>
|
|
462
|
+
</div>
|
|
463
|
+
<span className="text-sm tabular-nums">{fieldValue(a.AnnualRevenue) ?? "—"}</span>
|
|
464
|
+
</div>
|
|
465
|
+
);
|
|
466
|
+
},
|
|
467
|
+
// Contacts → an avatar + two-line card.
|
|
468
|
+
contacts: (node) => {
|
|
469
|
+
const c = node as ContactNode;
|
|
470
|
+
const name = fieldValue(c.Name) ?? "—";
|
|
471
|
+
return (
|
|
472
|
+
<div className="flex items-center gap-3 py-1">
|
|
473
|
+
<span className="flex size-9 items-center justify-center rounded-full bg-muted text-sm font-medium">
|
|
474
|
+
{name.slice(0, 1)}
|
|
475
|
+
</span>
|
|
476
|
+
<div>
|
|
477
|
+
<p className="font-medium">{name}</p>
|
|
478
|
+
<p className="text-sm text-muted-foreground">{fieldValue(c.Email)}</p>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
},
|
|
483
|
+
// `opportunities` omitted → keeps the built-in default row.
|
|
484
|
+
}}
|
|
485
|
+
/>;
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
To render every source's results as cards in a responsive grid instead of the
|
|
489
|
+
default stacked list, drop down to the headless hook (next example shows the
|
|
490
|
+
same pattern) and wrap each source's `result.nodes` in your own grid container.
|
|
491
|
+
|
|
492
|
+
### 9. Global pagination across all sources
|
|
493
|
+
|
|
494
|
+
The drop-in `<Search>` paginates each source independently. If you want a
|
|
495
|
+
single, shared pagination control that advances **every** in-scope source at
|
|
496
|
+
once, build a headless layout with `useSearch` and the exported
|
|
497
|
+
`PaginationControls`. Each source exposes its own
|
|
498
|
+
`controller.pagination` (`goToNextPage` / `goToPreviousPage` / `setPageSize`
|
|
499
|
+
plus `pageIndex` / `hasNextPage` / `hasPreviousPage`); a global control simply
|
|
500
|
+
fans an action out across all of them:
|
|
501
|
+
|
|
502
|
+
```tsx
|
|
503
|
+
import {
|
|
504
|
+
useSearch,
|
|
505
|
+
SearchBar,
|
|
506
|
+
DefaultResultRow,
|
|
507
|
+
PaginationControls,
|
|
508
|
+
} from "@salesforce/ui-bundle-template-feature-react-search";
|
|
509
|
+
|
|
510
|
+
function GloballyPaginatedSearch() {
|
|
511
|
+
const handle = useSearch(config);
|
|
512
|
+
const controllers = Object.values(handle.sources);
|
|
513
|
+
|
|
514
|
+
// Aggregate paging state: there's a next page if ANY source has one;
|
|
515
|
+
// you can go back only once every source is past its first page.
|
|
516
|
+
const hasNextPage = controllers.some((c) => c.pagination.hasNextPage);
|
|
517
|
+
const hasPreviousPage =
|
|
518
|
+
controllers.length > 0 && controllers.every((c) => c.pagination.hasPreviousPage);
|
|
519
|
+
const pageSize = controllers[0]?.pagination.pageSize ?? 10;
|
|
520
|
+
const pageIndex = controllers[0]?.pagination.pageIndex ?? 0;
|
|
521
|
+
|
|
522
|
+
const goNext = () => controllers.forEach((c) => c.pagination.goToNextPage());
|
|
523
|
+
const goPrev = () => controllers.forEach((c) => c.pagination.goToPreviousPage());
|
|
524
|
+
const setSize = (size: number) => controllers.forEach((c) => c.pagination.setPageSize(size));
|
|
525
|
+
|
|
526
|
+
return (
|
|
527
|
+
<div className="max-w-4xl mx-auto py-6 space-y-6">
|
|
528
|
+
<SearchBar value={handle.q} onChange={handle.setQ} placeholder="Search…" />
|
|
529
|
+
|
|
530
|
+
{controllers.map((c) => (
|
|
531
|
+
<section key={c.config.key}>
|
|
532
|
+
<h2 className="text-lg font-semibold mb-2">{c.config.label}</h2>
|
|
533
|
+
<ul className="divide-y">
|
|
534
|
+
{(c.result?.nodes ?? []).map((node, i) => (
|
|
535
|
+
<li key={i} className="py-2">
|
|
536
|
+
<DefaultResultRow node={node} source={c.config} />
|
|
537
|
+
</li>
|
|
538
|
+
))}
|
|
539
|
+
</ul>
|
|
540
|
+
</section>
|
|
541
|
+
))}
|
|
542
|
+
|
|
543
|
+
{/* One control drives every source. */}
|
|
544
|
+
<PaginationControls
|
|
545
|
+
pageIndex={pageIndex}
|
|
546
|
+
hasNextPage={hasNextPage}
|
|
547
|
+
hasPreviousPage={hasPreviousPage}
|
|
548
|
+
pageSize={pageSize}
|
|
549
|
+
pageSizeOptions={[5, 10, 20]}
|
|
550
|
+
onNextPage={goNext}
|
|
551
|
+
onPreviousPage={goPrev}
|
|
552
|
+
onPageSizeChange={setSize}
|
|
553
|
+
disabled={handle.loading}
|
|
554
|
+
/>
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
> Each source keeps its own cursor stack, so per-source `pageIndex` values can
|
|
561
|
+
> drift if a source runs out of pages before the others. Read aggregate state
|
|
562
|
+
> from whichever source you treat as the "lead" (above uses the first), or
|
|
563
|
+
> compute a max/min across `controllers` to suit your UX.
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## URL and state conventions
|
|
568
|
+
|
|
569
|
+
State is reflected in the URL (debounced 300 ms), so searches are
|
|
570
|
+
bookmarkable and shareable:
|
|
571
|
+
|
|
572
|
+
| URL param | Meaning |
|
|
573
|
+
| ------------------------------- | --------------------------------------------- |
|
|
574
|
+
| `q` | Global search term (broadcast to all sources) |
|
|
575
|
+
| `scope=<key>` | Narrow to one source (omitted = "all") |
|
|
576
|
+
| `s.<key>.f.<field>=<value>` | Single-value filter for a source |
|
|
577
|
+
| `s.<key>.f.<field>.min=<value>` | Range/numeric/date lower bound |
|
|
578
|
+
| `s.<key>.f.<field>.max=<value>` | Range/numeric/date upper bound |
|
|
579
|
+
| `s.<key>.sort=<field>` | Source-specific sort field |
|
|
580
|
+
| `s.<key>.dir=ASC\|DESC` | Source-specific sort direction |
|
|
581
|
+
| `s.<key>.ps=<n>` | Source-specific page size |
|
|
582
|
+
| `s.<key>.page=<n>` | Source-specific 1-based page index |
|
|
583
|
+
|
|
584
|
+
Changing `q` resets every source's pagination; changing a source's filter or
|
|
585
|
+
sort resets that source's pagination only. Cursors back the prev/next paging
|
|
586
|
+
but are not persisted to the URL (only the page index is).
|
|
587
|
+
|
|
588
|
+
When `restrictTo` / `lockedScope` is set, `?scope=` is **not** written — the
|
|
589
|
+
route already implies the source.
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Public API
|
|
594
|
+
|
|
595
|
+
Everything is exported from the package entry point:
|
|
596
|
+
|
|
597
|
+
```ts
|
|
598
|
+
// Drop-in + example config
|
|
599
|
+
import { Search, config } from "@salesforce/ui-bundle-template-feature-react-search";
|
|
600
|
+
|
|
601
|
+
// Hooks
|
|
602
|
+
import {
|
|
603
|
+
useSearch,
|
|
604
|
+
useAsyncData,
|
|
605
|
+
useDistinctValues,
|
|
606
|
+
} from "@salesforce/ui-bundle-template-feature-react-search";
|
|
607
|
+
|
|
608
|
+
// Composition primitives
|
|
609
|
+
import {
|
|
610
|
+
SearchBar,
|
|
611
|
+
SearchResults,
|
|
612
|
+
SourceSection,
|
|
613
|
+
SortControl,
|
|
614
|
+
PaginationControls,
|
|
615
|
+
ScopeSelector,
|
|
616
|
+
ActiveFilters,
|
|
617
|
+
DefaultResultRow,
|
|
618
|
+
DefaultFilterPanel,
|
|
619
|
+
} from "@salesforce/ui-bundle-template-feature-react-search";
|
|
620
|
+
|
|
621
|
+
// Filter inputs + context
|
|
622
|
+
import {
|
|
623
|
+
FilterProvider,
|
|
624
|
+
FilterResetButton,
|
|
625
|
+
useFilterField,
|
|
626
|
+
useFilterPanel,
|
|
627
|
+
TextFilter,
|
|
628
|
+
SelectFilter,
|
|
629
|
+
MultiSelectFilter,
|
|
630
|
+
NumericRangeFilter,
|
|
631
|
+
BooleanFilter,
|
|
632
|
+
DateRangeFilter,
|
|
633
|
+
FilterFieldWrapper,
|
|
634
|
+
} from "@salesforce/ui-bundle-template-feature-react-search";
|
|
635
|
+
|
|
636
|
+
// Utilities / lower-level building blocks
|
|
637
|
+
import {
|
|
638
|
+
fieldValue,
|
|
639
|
+
buildFilter,
|
|
640
|
+
buildGlobalQueryClause,
|
|
641
|
+
buildOrderBy,
|
|
642
|
+
readSourceParams,
|
|
643
|
+
writeSourceParams,
|
|
644
|
+
GLOBAL_QUERY_KEY,
|
|
645
|
+
buildSearchQuery,
|
|
646
|
+
runSearch,
|
|
647
|
+
fetchDistinctValues,
|
|
648
|
+
ALL_SCOPE,
|
|
649
|
+
} from "@salesforce/ui-bundle-template-feature-react-search";
|
|
650
|
+
|
|
651
|
+
// Types
|
|
652
|
+
import type {
|
|
653
|
+
SearchConfig,
|
|
654
|
+
SObjectSourceConfig,
|
|
655
|
+
DisplayField,
|
|
656
|
+
SourceController,
|
|
657
|
+
SourceResult,
|
|
658
|
+
SourcePageInfo,
|
|
659
|
+
SearchHandle,
|
|
660
|
+
SearchScope,
|
|
661
|
+
RenderResultFn,
|
|
662
|
+
FilterFieldConfig,
|
|
663
|
+
FilterFieldType,
|
|
664
|
+
ActiveFilterValue,
|
|
665
|
+
SortFieldConfig,
|
|
666
|
+
SortState,
|
|
667
|
+
PicklistOption,
|
|
668
|
+
SourceRequest,
|
|
669
|
+
SearchQueryPayload,
|
|
670
|
+
} from "@salesforce/ui-bundle-template-feature-react-search";
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Common mistakes
|
|
676
|
+
|
|
677
|
+
- **Missing `searchableFields`** — the global `q` won't match that source.
|
|
678
|
+
Provide at least one field, or omit the source when `q` is non-empty.
|
|
679
|
+
- **Relationship field without `subfields`** — `"Owner"` as a bare string emits
|
|
680
|
+
`Owner @optional { value displayValue }`, which is wrong for a relationship.
|
|
681
|
+
Use `{ name: "Owner", subfields: ["Name"] }`.
|
|
682
|
+
- **Listing `idField` in `displayFields`** — it's selected automatically; listing
|
|
683
|
+
it again duplicates the selection.
|
|
684
|
+
- **`routePattern` token not in `displayFields`** — the token can't resolve and
|
|
685
|
+
every row silently falls back to a non-clickable layout.
|
|
686
|
+
- **Non-conforming `key`** — must match `/^[A-Za-z_][A-Za-z0-9_]*$/`; it becomes
|
|
687
|
+
a GraphQL alias and variable prefix. Invalid keys produce malformed documents.
|
|
688
|
+
- **Picklist auto-fetch failing** — the aggregate `groupBy` requires a
|
|
689
|
+
group-by-able field. For formulas / long text, supply inline `options`.
|
|
690
|
+
- **Overriding `whereTypeName` / `orderByTypeName` needlessly** — only set these
|
|
691
|
+
when the schema deviates from `${ObjectName}_Filter` / `${ObjectName}_OrderBy`.
|
|
692
|
+
Wrong names cause GraphQL parse errors.
|