@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.
Files changed (169) hide show
  1. package/LICENSE.txt +82 -0
  2. package/README.md +692 -0
  3. package/dist/.forceignore +15 -0
  4. package/dist/.husky/pre-commit +4 -0
  5. package/dist/.prettierignore +11 -0
  6. package/dist/.prettierrc +17 -0
  7. package/dist/CHANGELOG.md +3499 -0
  8. package/dist/README.md +28 -0
  9. package/dist/config/project-scratch-def.json +13 -0
  10. package/dist/eslint.config.js +7 -0
  11. package/dist/force-app/main/default/uiBundles/feature-react-search/.forceignore +15 -0
  12. package/dist/force-app/main/default/uiBundles/feature-react-search/.graphqlrc.yml +2 -0
  13. package/dist/force-app/main/default/uiBundles/feature-react-search/.prettierignore +9 -0
  14. package/dist/force-app/main/default/uiBundles/feature-react-search/.prettierrc +11 -0
  15. package/dist/force-app/main/default/uiBundles/feature-react-search/CHANGELOG.md +10 -0
  16. package/dist/force-app/main/default/uiBundles/feature-react-search/README.md +75 -0
  17. package/dist/force-app/main/default/uiBundles/feature-react-search/codegen.yml +95 -0
  18. package/dist/force-app/main/default/uiBundles/feature-react-search/components.json +18 -0
  19. package/dist/force-app/main/default/uiBundles/feature-react-search/e2e/app.spec.ts +17 -0
  20. package/dist/force-app/main/default/uiBundles/feature-react-search/eslint.config.js +169 -0
  21. package/dist/force-app/main/default/uiBundles/feature-react-search/feature-react-search.uibundle-meta.xml +7 -0
  22. package/dist/force-app/main/default/uiBundles/feature-react-search/index.html +12 -0
  23. package/dist/force-app/main/default/uiBundles/feature-react-search/package.json +76 -0
  24. package/dist/force-app/main/default/uiBundles/feature-react-search/playwright.config.ts +24 -0
  25. package/dist/force-app/main/default/uiBundles/feature-react-search/scripts/get-graphql-schema.mjs +71 -0
  26. package/dist/force-app/main/default/uiBundles/feature-react-search/scripts/rewrite-e2e-assets.mjs +23 -0
  27. package/dist/force-app/main/default/uiBundles/feature-react-search/src/api/graphqlClient.ts +44 -0
  28. package/dist/force-app/main/default/uiBundles/feature-react-search/src/app.tsx +17 -0
  29. package/dist/force-app/main/default/uiBundles/feature-react-search/src/appLayout.tsx +83 -0
  30. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/book.svg +3 -0
  31. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/copy.svg +4 -0
  32. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/rocket.svg +3 -0
  33. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/icons/star.svg +3 -0
  34. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-1.png +0 -0
  35. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-2.png +0 -0
  36. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/codey-3.png +0 -0
  37. package/dist/force-app/main/default/uiBundles/feature-react-search/src/assets/images/vibe-codey.svg +194 -0
  38. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/alerts/status-alert.tsx +52 -0
  39. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/layouts/card-layout.tsx +29 -0
  40. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/alert.tsx +76 -0
  41. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/avatar.tsx +109 -0
  42. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/badge.tsx +48 -0
  43. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/breadcrumb.tsx +109 -0
  44. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/button.tsx +67 -0
  45. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/calendar.tsx +232 -0
  46. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/card.tsx +103 -0
  47. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/checkbox.tsx +32 -0
  48. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/collapsible.tsx +33 -0
  49. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/datePicker.tsx +127 -0
  50. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/dialog.tsx +162 -0
  51. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/dropdown-menu.tsx +257 -0
  52. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/field.tsx +237 -0
  53. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/index.ts +109 -0
  54. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/input.tsx +19 -0
  55. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/label.tsx +22 -0
  56. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/pagination.tsx +132 -0
  57. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/popover.tsx +89 -0
  58. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/select.tsx +193 -0
  59. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/separator.tsx +26 -0
  60. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/skeleton.tsx +14 -0
  61. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/sonner.tsx +20 -0
  62. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/spinner.tsx +16 -0
  63. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/table.tsx +114 -0
  64. package/dist/force-app/main/default/uiBundles/feature-react-search/src/components/ui/tabs.tsx +88 -0
  65. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/distinctValuesService.ts +84 -0
  66. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/searchService.ts +71 -0
  67. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/Search.tsx +160 -0
  68. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SearchResults.tsx +77 -0
  69. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SourceSection.tsx +167 -0
  70. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/PaginationControls.tsx +115 -0
  71. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/ScopeSelector.tsx +55 -0
  72. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SearchBar.tsx +55 -0
  73. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SortControl.tsx +62 -0
  74. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/ActiveFilters.tsx +61 -0
  75. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/DefaultFilterPanel.tsx +122 -0
  76. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/FilterContext.tsx +70 -0
  77. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
  78. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/DateRangeFilter.tsx +50 -0
  79. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
  80. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +47 -0
  81. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
  82. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
  83. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
  84. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/results/DefaultResultRow.tsx +109 -0
  85. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/config.json +82 -0
  86. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useAsyncData.ts +56 -0
  87. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useDistinctValues.ts +59 -0
  88. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useSearch.ts +442 -0
  89. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/index.ts +80 -0
  90. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/loadConfig.ts +14 -0
  91. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/queryBuilder.ts +156 -0
  92. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/types.ts +169 -0
  93. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/debounce.ts +17 -0
  94. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/fieldUtils.ts +14 -0
  95. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/filterUtils.ts +272 -0
  96. package/dist/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/sortUtils.ts +34 -0
  97. package/dist/force-app/main/default/uiBundles/feature-react-search/src/hooks/useAsyncData.ts +67 -0
  98. package/dist/force-app/main/default/uiBundles/feature-react-search/src/lib/utils.ts +6 -0
  99. package/dist/force-app/main/default/uiBundles/feature-react-search/src/navigationMenu.tsx +80 -0
  100. package/dist/force-app/main/default/uiBundles/feature-react-search/src/pages/Home.tsx +11 -0
  101. package/dist/force-app/main/default/uiBundles/feature-react-search/src/pages/NotFound.tsx +18 -0
  102. package/dist/force-app/main/default/uiBundles/feature-react-search/src/router-utils.tsx +35 -0
  103. package/dist/force-app/main/default/uiBundles/feature-react-search/src/routes.tsx +22 -0
  104. package/dist/force-app/main/default/uiBundles/feature-react-search/src/styles/global.css +135 -0
  105. package/dist/force-app/main/default/uiBundles/feature-react-search/tsconfig.json +45 -0
  106. package/dist/force-app/main/default/uiBundles/feature-react-search/tsconfig.node.json +13 -0
  107. package/dist/force-app/main/default/uiBundles/feature-react-search/ui-bundle.json +7 -0
  108. package/dist/force-app/main/default/uiBundles/feature-react-search/vite-env.d.ts +4 -0
  109. package/dist/force-app/main/default/uiBundles/feature-react-search/vite.config.ts +106 -0
  110. package/dist/force-app/main/default/uiBundles/feature-react-search/vitest-env.d.ts +2 -0
  111. package/dist/force-app/main/default/uiBundles/feature-react-search/vitest.config.ts +11 -0
  112. package/dist/force-app/main/default/uiBundles/feature-react-search/vitest.setup.ts +1 -0
  113. package/dist/jest.config.js +6 -0
  114. package/dist/package-lock.json +9995 -0
  115. package/dist/package.json +44 -0
  116. package/dist/scripts/apex/hello.apex +10 -0
  117. package/dist/scripts/gitignore-templates.json +4 -0
  118. package/dist/scripts/graphql-search.sh +191 -0
  119. package/dist/scripts/org-setup-config-schema.mjs +96 -0
  120. package/dist/scripts/org-setup.config.json +5 -0
  121. package/dist/scripts/org-setup.mjs +1392 -0
  122. package/dist/scripts/sf-project-setup.mjs +103 -0
  123. package/dist/scripts/soql/account.soql +6 -0
  124. package/dist/scripts/validate-org-setup-config.mjs +38 -0
  125. package/dist/sfdx-project.json +12 -0
  126. package/package.json +51 -0
  127. package/src/force-app/main/default/uiBundles/feature-react-search/src/__inherit__appLayout.tsx +9 -0
  128. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__alert.tsx +39 -0
  129. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__button.tsx +45 -0
  130. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__checkbox.tsx +8 -0
  131. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__input.tsx +5 -0
  132. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__label.tsx +8 -0
  133. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__pagination.tsx +47 -0
  134. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__select.tsx +57 -0
  135. package/src/force-app/main/default/uiBundles/feature-react-search/src/components/ui/__inherit__skeleton.tsx +5 -0
  136. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/distinctValuesService.ts +84 -0
  137. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/api/searchService.ts +71 -0
  138. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/Search.tsx +160 -0
  139. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SearchResults.tsx +77 -0
  140. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/SourceSection.tsx +167 -0
  141. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/PaginationControls.tsx +115 -0
  142. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/ScopeSelector.tsx +55 -0
  143. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SearchBar.tsx +55 -0
  144. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/controls/SortControl.tsx +62 -0
  145. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/ActiveFilters.tsx +61 -0
  146. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/DefaultFilterPanel.tsx +122 -0
  147. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/FilterContext.tsx +70 -0
  148. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/BooleanFilter.tsx +50 -0
  149. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/DateRangeFilter.tsx +50 -0
  150. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/FilterFieldWrapper.tsx +26 -0
  151. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/MultiSelectFilter.tsx +47 -0
  152. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/NumericRangeFilter.tsx +78 -0
  153. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/SelectFilter.tsx +46 -0
  154. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/filters/inputs/TextFilter.tsx +52 -0
  155. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/components/results/DefaultResultRow.tsx +109 -0
  156. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/config.json +82 -0
  157. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useAsyncData.ts +56 -0
  158. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useDistinctValues.ts +59 -0
  159. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/hooks/useSearch.ts +442 -0
  160. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/index.ts +80 -0
  161. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/loadConfig.ts +14 -0
  162. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/queryBuilder.ts +156 -0
  163. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/types.ts +169 -0
  164. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/debounce.ts +17 -0
  165. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/fieldUtils.ts +14 -0
  166. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/filterUtils.ts +272 -0
  167. package/src/force-app/main/default/uiBundles/feature-react-search/src/features/search/utils/sortUtils.ts +34 -0
  168. package/src/force-app/main/default/uiBundles/feature-react-search/src/pages/Home.tsx +11 -0
  169. 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.