@salesforce/ui-bundle-template-feature-react-global-search 1.117.2

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 (231) hide show
  1. package/LICENSE.txt +82 -0
  2. package/README.md +250 -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/AGENT.md +193 -0
  8. package/dist/CHANGELOG.md +2128 -0
  9. package/dist/README.md +28 -0
  10. package/dist/config/project-scratch-def.json +13 -0
  11. package/dist/eslint.config.js +7 -0
  12. package/dist/force-app/main/default/uiBundles/feature-react-global-search/.forceignore +15 -0
  13. package/dist/force-app/main/default/uiBundles/feature-react-global-search/.graphqlrc.yml +2 -0
  14. package/dist/force-app/main/default/uiBundles/feature-react-global-search/.prettierignore +9 -0
  15. package/dist/force-app/main/default/uiBundles/feature-react-global-search/.prettierrc +11 -0
  16. package/dist/force-app/main/default/uiBundles/feature-react-global-search/CHANGELOG.md +10 -0
  17. package/dist/force-app/main/default/uiBundles/feature-react-global-search/README.md +75 -0
  18. package/dist/force-app/main/default/uiBundles/feature-react-global-search/codegen.yml +95 -0
  19. package/dist/force-app/main/default/uiBundles/feature-react-global-search/components.json +18 -0
  20. package/dist/force-app/main/default/uiBundles/feature-react-global-search/e2e/app.spec.ts +17 -0
  21. package/dist/force-app/main/default/uiBundles/feature-react-global-search/eslint.config.js +169 -0
  22. package/dist/force-app/main/default/uiBundles/feature-react-global-search/feature-react-global-search.uibundle-meta.xml +7 -0
  23. package/dist/force-app/main/default/uiBundles/feature-react-global-search/index.html +12 -0
  24. package/dist/force-app/main/default/uiBundles/feature-react-global-search/package.json +70 -0
  25. package/dist/force-app/main/default/uiBundles/feature-react-global-search/playwright.config.ts +24 -0
  26. package/dist/force-app/main/default/uiBundles/feature-react-global-search/scripts/get-graphql-schema.mjs +68 -0
  27. package/dist/force-app/main/default/uiBundles/feature-react-global-search/scripts/rewrite-e2e-assets.mjs +23 -0
  28. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/api/graphqlClient.ts +25 -0
  29. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/app.tsx +17 -0
  30. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/appLayout.tsx +83 -0
  31. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/icons/book.svg +3 -0
  32. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/icons/copy.svg +4 -0
  33. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/icons/rocket.svg +3 -0
  34. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/icons/star.svg +3 -0
  35. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/images/codey-1.png +0 -0
  36. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/images/codey-2.png +0 -0
  37. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/images/codey-3.png +0 -0
  38. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/images/vibe-codey.svg +194 -0
  39. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/alerts/status-alert.tsx +49 -0
  40. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/layouts/card-layout.tsx +29 -0
  41. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/alert.tsx +76 -0
  42. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/badge.tsx +48 -0
  43. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/breadcrumb.tsx +109 -0
  44. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/button.tsx +67 -0
  45. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/calendar.tsx +232 -0
  46. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/card.tsx +103 -0
  47. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/checkbox.tsx +32 -0
  48. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/collapsible.tsx +33 -0
  49. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/datePicker.tsx +127 -0
  50. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/dialog.tsx +162 -0
  51. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/field.tsx +237 -0
  52. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/index.ts +84 -0
  53. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/input.tsx +19 -0
  54. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/label.tsx +22 -0
  55. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/pagination.tsx +132 -0
  56. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/popover.tsx +89 -0
  57. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/select.tsx +193 -0
  58. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/separator.tsx +26 -0
  59. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/skeleton.tsx +14 -0
  60. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/sonner.tsx +20 -0
  61. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/spinner.tsx +16 -0
  62. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/table.tsx +114 -0
  63. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/tabs.tsx +88 -0
  64. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectDetailService.ts +102 -0
  65. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectInfoGraphQLService.ts +137 -0
  66. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectInfoService.ts +95 -0
  67. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/recordListGraphQLService.ts +364 -0
  68. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailFields.tsx +55 -0
  69. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailForm.tsx +146 -0
  70. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
  71. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
  72. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/Section.tsx +108 -0
  73. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/SectionRow.tsx +20 -0
  74. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
  75. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
  76. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
  77. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
  78. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
  79. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
  80. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
  81. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterField.tsx +54 -0
  82. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterInput.tsx +55 -0
  83. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
  84. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
  85. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/forms/filters-form.tsx +114 -0
  86. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/forms/submit-button.tsx +47 -0
  87. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
  88. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
  89. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchHeader.tsx +31 -0
  90. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchPagination.tsx +144 -0
  91. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchResultCard.tsx +138 -0
  92. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
  93. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
  94. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/constants.ts +39 -0
  95. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/filters/FilterInput.tsx +55 -0
  96. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/filters/FilterSelect.tsx +72 -0
  97. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/form.tsx +209 -0
  98. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useObjectInfoBatch.ts +72 -0
  99. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useObjectSearchData.ts +174 -0
  100. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useRecordDetailLayout.ts +137 -0
  101. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
  102. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/pages/DetailPage.tsx +109 -0
  103. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/pages/GlobalSearch.tsx +235 -0
  104. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/filters/filters.ts +121 -0
  105. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/filters/picklist.ts +6 -0
  106. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/objectInfo/objectInfo.ts +49 -0
  107. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
  108. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/schema.d.ts +200 -0
  109. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/search/searchResults.ts +229 -0
  110. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/apiUtils.ts +59 -0
  111. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/cacheUtils.ts +76 -0
  112. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/debounce.ts +90 -0
  113. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/fieldUtils.ts +354 -0
  114. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
  115. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/filterUtils.ts +32 -0
  116. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
  117. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/formUtils.ts +142 -0
  118. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
  119. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +77 -0
  120. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
  121. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
  122. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/linkUtils.ts +14 -0
  123. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/paginationUtils.ts +49 -0
  124. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/recordUtils.ts +159 -0
  125. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/sanitizationUtils.ts +50 -0
  126. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/index.ts +120 -0
  127. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/lib/utils.ts +6 -0
  128. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/navigationMenu.tsx +80 -0
  129. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/pages/Home.tsx +13 -0
  130. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/pages/NotFound.tsx +18 -0
  131. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/router-utils.tsx +35 -0
  132. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/routes.tsx +44 -0
  133. package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/styles/global.css +135 -0
  134. package/dist/force-app/main/default/uiBundles/feature-react-global-search/tsconfig.json +42 -0
  135. package/dist/force-app/main/default/uiBundles/feature-react-global-search/tsconfig.node.json +13 -0
  136. package/dist/force-app/main/default/uiBundles/feature-react-global-search/ui-bundle.json +7 -0
  137. package/dist/force-app/main/default/uiBundles/feature-react-global-search/vite-env.d.ts +1 -0
  138. package/dist/force-app/main/default/uiBundles/feature-react-global-search/vite.config.ts +106 -0
  139. package/dist/force-app/main/default/uiBundles/feature-react-global-search/vitest-env.d.ts +2 -0
  140. package/dist/force-app/main/default/uiBundles/feature-react-global-search/vitest.config.ts +11 -0
  141. package/dist/force-app/main/default/uiBundles/feature-react-global-search/vitest.setup.ts +1 -0
  142. package/dist/jest.config.js +6 -0
  143. package/dist/package-lock.json +9995 -0
  144. package/dist/package.json +40 -0
  145. package/dist/scripts/apex/hello.apex +10 -0
  146. package/dist/scripts/graphql-search.sh +191 -0
  147. package/dist/scripts/prepare-import-unique-fields.js +122 -0
  148. package/dist/scripts/setup-cli.mjs +563 -0
  149. package/dist/scripts/sf-project-setup.mjs +66 -0
  150. package/dist/scripts/soql/account.soql +6 -0
  151. package/dist/sfdx-project.json +12 -0
  152. package/package.json +53 -0
  153. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/__inherit__appLayout.tsx +9 -0
  154. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/alerts/__inherit__status-alert.tsx +27 -0
  155. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/layouts/__inherit__card-layout.tsx +9 -0
  156. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__alert.tsx +41 -0
  157. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__button.tsx +45 -0
  158. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__card.tsx +33 -0
  159. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__field.tsx +62 -0
  160. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__input.tsx +5 -0
  161. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__label.tsx +8 -0
  162. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__pagination.tsx +47 -0
  163. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__select.tsx +57 -0
  164. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__skeleton.tsx +5 -0
  165. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__spinner.tsx +7 -0
  166. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectDetailService.ts +102 -0
  167. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectInfoGraphQLService.ts +137 -0
  168. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectInfoService.ts +95 -0
  169. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/recordListGraphQLService.ts +364 -0
  170. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailFields.tsx +55 -0
  171. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailForm.tsx +146 -0
  172. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
  173. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
  174. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/Section.tsx +108 -0
  175. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/SectionRow.tsx +20 -0
  176. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
  177. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
  178. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
  179. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
  180. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
  181. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
  182. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
  183. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterField.tsx +54 -0
  184. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterInput.tsx +55 -0
  185. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
  186. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
  187. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/forms/filters-form.tsx +114 -0
  188. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/forms/submit-button.tsx +47 -0
  189. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
  190. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
  191. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchHeader.tsx +31 -0
  192. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchPagination.tsx +144 -0
  193. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchResultCard.tsx +138 -0
  194. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
  195. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
  196. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/constants.ts +39 -0
  197. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/filters/FilterInput.tsx +55 -0
  198. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/filters/FilterSelect.tsx +72 -0
  199. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/form.tsx +209 -0
  200. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useObjectInfoBatch.ts +72 -0
  201. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useObjectSearchData.ts +174 -0
  202. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useRecordDetailLayout.ts +137 -0
  203. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
  204. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/pages/DetailPage.tsx +109 -0
  205. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/pages/GlobalSearch.tsx +235 -0
  206. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/filters/filters.ts +121 -0
  207. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/filters/picklist.ts +6 -0
  208. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/objectInfo/objectInfo.ts +49 -0
  209. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
  210. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/schema.d.ts +200 -0
  211. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/search/searchResults.ts +229 -0
  212. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/apiUtils.ts +59 -0
  213. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/cacheUtils.ts +76 -0
  214. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/debounce.ts +90 -0
  215. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/fieldUtils.ts +354 -0
  216. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
  217. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/filterUtils.ts +32 -0
  218. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
  219. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/formUtils.ts +142 -0
  220. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
  221. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +77 -0
  222. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
  223. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
  224. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/linkUtils.ts +14 -0
  225. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/paginationUtils.ts +49 -0
  226. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/recordUtils.ts +159 -0
  227. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/sanitizationUtils.ts +50 -0
  228. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/index.ts +120 -0
  229. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/lib/__inherit__utils.ts +5 -0
  230. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/pages/Home.tsx +13 -0
  231. package/src/force-app/main/default/uiBundles/feature-react-global-search/src/routes.tsx +52 -0
@@ -0,0 +1,114 @@
1
+ import { FieldGroup } from "../../../../components/ui/field";
2
+ import { StatusAlert } from "../../../../components/alerts/status-alert";
3
+ import { CardLayout } from "../../../../components/layouts/card-layout";
4
+ import { SubmitButton } from "./submit-button";
5
+ import { Button } from "../../../../components/ui/button";
6
+ import { useFormContext } from "../../hooks/form";
7
+ import { useId, useEffect, useRef } from "react";
8
+
9
+ const SUCCESS_AUTO_DISMISS_DELAY = 3000;
10
+
11
+ interface FiltersFormProps extends Omit<React.ComponentProps<"form">, "onSubmit"> {
12
+ title: string;
13
+ description?: string;
14
+ error?: React.ReactNode;
15
+ success?: React.ReactNode;
16
+ submit: {
17
+ text: string;
18
+ loadingText?: string;
19
+ };
20
+ reset?: {
21
+ text: string;
22
+ onReset: () => void;
23
+ };
24
+ onSuccessDismiss?: () => void;
25
+ }
26
+
27
+ /**
28
+ * Wrapper component that provides consistent layout and error/success alert positioning
29
+ * for all filter forms.
30
+ */
31
+ export function FiltersForm({
32
+ id: providedId,
33
+ title,
34
+ description,
35
+ error,
36
+ success,
37
+ children,
38
+ submit,
39
+ reset,
40
+ onSuccessDismiss,
41
+ ...props
42
+ }: FiltersFormProps) {
43
+ const form = useFormContext();
44
+ const generatedId = useId();
45
+ const id = providedId ?? generatedId;
46
+
47
+ const isSubmittingSelector = (state: { isSubmitting: boolean }) => state.isSubmitting;
48
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
49
+
50
+ useEffect(() => {
51
+ if (timeoutRef.current) {
52
+ clearTimeout(timeoutRef.current);
53
+ timeoutRef.current = null;
54
+ }
55
+
56
+ if (success && onSuccessDismiss) {
57
+ timeoutRef.current = setTimeout(() => {
58
+ onSuccessDismiss();
59
+ timeoutRef.current = null;
60
+ }, SUCCESS_AUTO_DISMISS_DELAY);
61
+ }
62
+
63
+ return () => {
64
+ if (timeoutRef.current) {
65
+ clearTimeout(timeoutRef.current);
66
+ timeoutRef.current = null;
67
+ }
68
+ };
69
+ }, [success, onSuccessDismiss]);
70
+
71
+ return (
72
+ <CardLayout title={title} description={description}>
73
+ <div className="space-y-6">
74
+ {error && <StatusAlert variant="error">{error}</StatusAlert>}
75
+ {success && <StatusAlert variant="success">{success}</StatusAlert>}
76
+
77
+ <form
78
+ id={id}
79
+ onSubmit={(e) => {
80
+ e.preventDefault();
81
+ e.stopPropagation();
82
+ form.handleSubmit();
83
+ }}
84
+ {...props}
85
+ >
86
+ <FieldGroup>{children}</FieldGroup>
87
+ <div className="flex flex-col sm:flex-row gap-2 pt-4">
88
+ <SubmitButton
89
+ form={id}
90
+ label={submit.text}
91
+ loadingLabel={submit.loadingText}
92
+ className="flex-1"
93
+ />
94
+ {reset && (
95
+ <form.Subscribe selector={isSubmittingSelector}>
96
+ {(isSubmitting: boolean) => (
97
+ <Button
98
+ type="button"
99
+ variant="outline"
100
+ onClick={reset.onReset}
101
+ className="flex-1"
102
+ disabled={isSubmitting}
103
+ >
104
+ {reset.text}
105
+ </Button>
106
+ )}
107
+ </form.Subscribe>
108
+ )}
109
+ </div>
110
+ </form>
111
+ </div>
112
+ </CardLayout>
113
+ );
114
+ }
@@ -0,0 +1,47 @@
1
+ import { Button } from "../../../../components/ui/button";
2
+ import { Spinner } from "../../../../components/ui/spinner";
3
+ import { cn } from "../../../../lib/utils";
4
+ import { useFormContext } from "../../hooks/form";
5
+
6
+ interface SubmitButtonProps extends Omit<React.ComponentProps<typeof Button>, "type" | "disabled"> {
7
+ /** Button text when not submitting */
8
+ label: string;
9
+ /** Button text while submitting */
10
+ loadingLabel?: string;
11
+ /** Form id to associate with (for buttons outside form element) */
12
+ form?: string;
13
+ }
14
+
15
+ const isSubmittingSelector = (state: { isSubmitting: boolean }) => state.isSubmitting;
16
+
17
+ /**
18
+ * Submit button that subscribes to form submission state.
19
+ * Disables interaction during submission and provides visual feedback.
20
+ */
21
+ export function SubmitButton({
22
+ label,
23
+ loadingLabel = "Applying…",
24
+ className,
25
+ form: formId,
26
+ ...props
27
+ }: SubmitButtonProps) {
28
+ const form = useFormContext();
29
+ return (
30
+ <form.Subscribe selector={isSubmittingSelector}>
31
+ {(isSubmitting: boolean) => (
32
+ <Button
33
+ type="submit"
34
+ form={formId}
35
+ className={cn("w-full", className)}
36
+ disabled={isSubmitting}
37
+ aria-label={isSubmitting ? loadingLabel : label}
38
+ aria-busy={isSubmitting}
39
+ {...props}
40
+ >
41
+ {isSubmitting && <Spinner className="mr-2" aria-hidden="true" />}
42
+ <span aria-live="polite">{isSubmitting ? loadingLabel : label}</span>
43
+ </Button>
44
+ )}
45
+ </form.Subscribe>
46
+ );
47
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * GlobalSearchInput Component
3
+ *
4
+ * Search input with two actions: Search (navigate to results for query) and
5
+ * Browse All (navigate to same results UI with all records for the object).
6
+ */
7
+ import { useState, useCallback, useMemo, useId } from "react";
8
+ import type { KeyboardEvent, ChangeEvent } from "react";
9
+ import { useNavigate } from "react-router";
10
+ import { Card, CardContent } from "../../../../components/ui/card";
11
+ import { Input } from "../../../../components/ui/input";
12
+ import { Button } from "../../../../components/ui/button";
13
+ import { Search } from "lucide-react";
14
+ import { OBJECT_API_NAMES } from "../../constants";
15
+ import { useObjectInfoBatch } from "../../hooks/useObjectInfoBatch";
16
+
17
+ const BROWSE_SEGMENT = "browse__all";
18
+
19
+ const FALLBACK_LABEL_PLURAL = "records";
20
+
21
+ export function GlobalSearchInput() {
22
+ const [searchQuery, setSearchQuery] = useState("");
23
+ const navigate = useNavigate();
24
+ const inputId = useId();
25
+ const searchButtonId = useId();
26
+ const browseButtonId = useId();
27
+ const inputDescriptionId = `${inputId}-description`;
28
+ const { objectInfos } = useObjectInfoBatch([...OBJECT_API_NAMES]);
29
+ const labelPlural = (objectInfos[0]?.labelPlural as string | undefined) ?? FALLBACK_LABEL_PLURAL;
30
+
31
+ const handleInputChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
32
+ setSearchQuery(e.target.value);
33
+ }, []);
34
+
35
+ const handleSearch = useCallback(() => {
36
+ const trimmed = searchQuery.trim();
37
+ if (trimmed) {
38
+ navigate(`/global-search/${encodeURIComponent(trimmed)}`);
39
+ }
40
+ }, [searchQuery, navigate]);
41
+
42
+ const handleBrowseAll = useCallback(() => {
43
+ navigate(`/global-search/${BROWSE_SEGMENT}`);
44
+ }, [navigate]);
45
+
46
+ const handleKeyDown = useCallback(
47
+ (e: KeyboardEvent<HTMLInputElement>) => {
48
+ if (e.key === "Enter") {
49
+ e.preventDefault();
50
+ handleSearch();
51
+ }
52
+ },
53
+ [handleSearch],
54
+ );
55
+
56
+ const isSearchDisabled = useMemo(() => !searchQuery.trim(), [searchQuery]);
57
+
58
+ return (
59
+ <div className="w-full max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
60
+ <Card className="w-full">
61
+ <CardContent className="pt-6">
62
+ <div className="flex flex-col sm:flex-row gap-3">
63
+ <div className="flex-1 relative">
64
+ <Search
65
+ className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-600"
66
+ aria-hidden="true"
67
+ />
68
+ <Input
69
+ id={inputId}
70
+ type="search"
71
+ placeholder={`Search for ${labelPlural}`}
72
+ value={searchQuery}
73
+ onChange={handleInputChange}
74
+ onKeyDown={handleKeyDown}
75
+ className="pl-10"
76
+ aria-label={`Search for ${labelPlural}`}
77
+ aria-describedby={inputDescriptionId}
78
+ />
79
+ <p id={inputDescriptionId} className="sr-only">
80
+ Enter your search query and press Enter or click Search. Or click Browse All to see
81
+ all {labelPlural}.
82
+ </p>
83
+ </div>
84
+
85
+ <div className="flex flex-col sm:flex-row gap-2 sm:gap-3">
86
+ <Button
87
+ id={searchButtonId}
88
+ onClick={handleSearch}
89
+ disabled={isSearchDisabled}
90
+ className="w-full sm:w-auto"
91
+ aria-label="Search"
92
+ aria-describedby={inputDescriptionId}
93
+ variant="default"
94
+ >
95
+ <Search className="h-4 w-4 mr-2" aria-hidden="true" />
96
+ Search
97
+ </Button>
98
+ <Button
99
+ id={browseButtonId}
100
+ variant="outline"
101
+ onClick={handleBrowseAll}
102
+ className="w-full sm:w-auto"
103
+ aria-label={`Browse all ${labelPlural}`}
104
+ aria-describedby={inputDescriptionId}
105
+ >
106
+ Browse All {labelPlural}
107
+ </Button>
108
+ </div>
109
+ </div>
110
+ </CardContent>
111
+ </Card>
112
+ </div>
113
+ );
114
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * ResultCardFields Component
3
+ *
4
+ * Displays secondary fields (up to 3) for a search result card.
5
+ * Excludes the primary field and handles nested field values.
6
+ *
7
+ * @param record - The search result record data
8
+ * @param columns - Array of column definitions
9
+ * @param excludeFieldApiName - Field API name to exclude (usually the primary field)
10
+ *
11
+ * @remarks
12
+ * - Displays up to 3 secondary fields
13
+ * - Handles nested field paths (e.g., "Owner.Alias")
14
+ * - Skips fields with null/undefined/empty values
15
+ * - Responsive layout (vertical on mobile, horizontal on desktop)
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <ResultCardFields
20
+ * record={searchResult}
21
+ * columns={columns}
22
+ * excludeFieldApiName="Name"
23
+ * />
24
+ * ```
25
+ */
26
+ import type { Column, SearchResultRecordData } from "../../types/search/searchResults";
27
+ import { getNestedFieldValue } from "../../utils/fieldUtils";
28
+
29
+ interface ResultCardFieldsProps {
30
+ record: SearchResultRecordData;
31
+ columns: Column[];
32
+ excludeFieldApiName?: string;
33
+ }
34
+
35
+ export default function ResultCardFields({
36
+ record,
37
+ columns,
38
+ excludeFieldApiName,
39
+ }: ResultCardFieldsProps) {
40
+ const secondaryFields = columns.filter(
41
+ (col) => col && col.fieldApiName && col.fieldApiName !== excludeFieldApiName,
42
+ );
43
+
44
+ return (
45
+ <dl className="space-y-2" aria-label="Additional record information">
46
+ {secondaryFields.map((column) => {
47
+ if (!column || !column.fieldApiName) {
48
+ return null;
49
+ }
50
+
51
+ const displayValue = getNestedFieldValue(record.fields, column.fieldApiName);
52
+
53
+ if (displayValue === null || displayValue === undefined || displayValue === "") {
54
+ return null;
55
+ }
56
+
57
+ return (
58
+ <div
59
+ key={column.fieldApiName}
60
+ className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2"
61
+ >
62
+ <dt className="text-sm font-medium text-muted-foreground min-w-[100px]">
63
+ {column.label || column.fieldApiName}:
64
+ </dt>
65
+ <dd className="text-sm text-foreground">{displayValue}</dd>
66
+ </div>
67
+ );
68
+ })}
69
+ </dl>
70
+ );
71
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * SearchHeader Component
3
+ *
4
+ * Displays the header for search results or browse-all (same UI).
5
+ * labelPlural comes from object metadata (e.g. useObjectInfoBatch) so it is not hard-coded.
6
+ */
7
+ interface SearchHeaderProps {
8
+ query?: string;
9
+ isBrowseAll?: boolean;
10
+ /** Plural label for the primary object (e.g. "Accounts"). From object metadata. */
11
+ labelPlural?: string;
12
+ }
13
+
14
+ export default function SearchHeader({
15
+ query,
16
+ isBrowseAll,
17
+ labelPlural = "records",
18
+ }: SearchHeaderProps) {
19
+ return (
20
+ <header className="mb-6" aria-label="Search results header">
21
+ <h1 className="text-3xl font-bold mb-2">
22
+ {isBrowseAll ? `Browse All ${labelPlural}` : "Search Results"}
23
+ </h1>
24
+ {!isBrowseAll && query && (
25
+ <p className="text-lg" aria-live="polite">
26
+ Results for: <span className="font-semibold">"{query}"</span>
27
+ </p>
28
+ )}
29
+ </header>
30
+ );
31
+ }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * SearchPagination Component
3
+ *
4
+ * Displays pagination controls for search results.
5
+ * Previous/Next are disabled using hasPreviousPage/hasNextPage from the API so no request is made when there is no page.
6
+ *
7
+ * @remarks
8
+ * - Layout: page size selector on the left, prev/page/next controls on the right (corners).
9
+ * - Previous disabled when !hasPreviousPage (cursor stack enables prev when pageIndex > 0).
10
+ * - Next disabled when !hasNextPage or nextPageToken is null.
11
+ */
12
+ import { Button } from "../../../../components/ui/button";
13
+ import {
14
+ Select,
15
+ SelectContent,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ SelectValue,
19
+ } from "../../../../components/ui/select";
20
+ import { Label } from "../../../../components/ui/label";
21
+ import { PAGE_SIZE_OPTIONS, getValidPageSize, isValidPageSize } from "../../utils/paginationUtils";
22
+ import { ChevronLeft, ChevronRight } from "lucide-react";
23
+
24
+ interface SearchPaginationProps {
25
+ currentPageToken: string;
26
+ nextPageToken: string | null;
27
+ previousPageToken: string | null;
28
+ hasNextPage?: boolean;
29
+ hasPreviousPage?: boolean;
30
+ pageSize: number;
31
+ /** direction: 'prev' | 'next' | 'first'. When 'first' (e.g. page size change), parent typically resets pagination; token may be '' for prev when going to page 0. */
32
+ onPageChange: (newPageToken: string, direction?: "next" | "prev" | "first") => void;
33
+ onPageSizeChange: (newPageSize: number) => void;
34
+ }
35
+
36
+ export default function SearchPagination({
37
+ currentPageToken,
38
+ nextPageToken,
39
+ previousPageToken,
40
+ hasNextPage = false,
41
+ hasPreviousPage = false,
42
+ pageSize,
43
+ onPageChange,
44
+ onPageSizeChange,
45
+ }: SearchPaginationProps) {
46
+ const validPageSize = getValidPageSize(pageSize);
47
+
48
+ const currentPageTokenNum = parseInt(currentPageToken, 10) || 0;
49
+ const currentPage = currentPageTokenNum + 1;
50
+
51
+ const canGoPrevious = Boolean(hasPreviousPage);
52
+ const canGoNext = Boolean(hasNextPage && nextPageToken != null);
53
+
54
+ const handlePrevious = () => {
55
+ if (canGoPrevious) {
56
+ onPageChange(previousPageToken ?? "", "prev");
57
+ }
58
+ };
59
+
60
+ const handleNext = () => {
61
+ if (canGoNext && nextPageToken != null) {
62
+ onPageChange(nextPageToken, "next");
63
+ }
64
+ };
65
+
66
+ const handlePageSizeChange = (newPageSize: string) => {
67
+ const newSize = parseInt(newPageSize, 10);
68
+ if (!isNaN(newSize) && isValidPageSize(newSize) && newSize !== validPageSize) {
69
+ onPageSizeChange(newSize);
70
+ onPageChange("0", "first");
71
+ }
72
+ };
73
+
74
+ return (
75
+ <nav
76
+ className="w-full flex flex-row flex-wrap items-center justify-between gap-4 py-2"
77
+ aria-label="Search results pagination"
78
+ >
79
+ <div
80
+ className="flex items-center gap-2 shrink-0"
81
+ role="group"
82
+ aria-label="Page size selector"
83
+ >
84
+ <Label htmlFor="page-size-select" className="text-sm font-normal whitespace-nowrap">
85
+ Results per page:
86
+ </Label>
87
+ <Select value={validPageSize.toString()} onValueChange={handlePageSizeChange}>
88
+ <SelectTrigger
89
+ id="page-size-select"
90
+ className="w-[70px]"
91
+ aria-label="Select number of results per page"
92
+ >
93
+ <SelectValue />
94
+ </SelectTrigger>
95
+ <SelectContent>
96
+ {PAGE_SIZE_OPTIONS.map((option) => (
97
+ <SelectItem key={option.value} value={option.value}>
98
+ {option.label}
99
+ </SelectItem>
100
+ ))}
101
+ </SelectContent>
102
+ </Select>
103
+ </div>
104
+
105
+ <div className="flex items-center gap-1 shrink-0" role="group" aria-label="Page navigation">
106
+ <Button
107
+ type="button"
108
+ variant="outline"
109
+ size="sm"
110
+ disabled={!canGoPrevious}
111
+ onClick={handlePrevious}
112
+ aria-label={
113
+ canGoPrevious
114
+ ? `Go to previous page (Page ${currentPage - 1})`
115
+ : "Previous page (disabled)"
116
+ }
117
+ >
118
+ <ChevronLeft className="size-4" aria-hidden />
119
+ Previous
120
+ </Button>
121
+ <span
122
+ className="min-w-[4rem] text-center text-sm text-muted-foreground px-2"
123
+ aria-label={`Page ${currentPage}, current page`}
124
+ aria-current="page"
125
+ >
126
+ Page {currentPage}
127
+ </span>
128
+ <Button
129
+ type="button"
130
+ variant="outline"
131
+ size="sm"
132
+ disabled={!canGoNext}
133
+ onClick={handleNext}
134
+ aria-label={
135
+ canGoNext ? `Go to next page (Page ${currentPage + 1})` : "Next page (disabled)"
136
+ }
137
+ >
138
+ Next
139
+ <ChevronRight className="size-4" aria-hidden />
140
+ </Button>
141
+ </div>
142
+ </nav>
143
+ );
144
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * SearchResultCard Component
3
+ *
4
+ * Displays a single search result as a card with primary and secondary fields.
5
+ * Clicking the card navigates to the detail page for that record.
6
+ *
7
+ * @param record - The search result record data to display
8
+ * @param columns - Array of column definitions for field display
9
+ * @param objectApiName - API name of the object (path param in detail URL: /object/:objectApiName/:recordId)
10
+ *
11
+ * @remarks
12
+ * - Automatically identifies the primary field (usually "Name")
13
+ * - Displays up to 3 secondary fields
14
+ * - Supports keyboard navigation (Enter/Space to navigate)
15
+ * - Handles nested field values (e.g., "Owner.Alias")
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <SearchResultCard
20
+ * record={searchResult}
21
+ * columns={columns}
22
+ * objectApiName="Account"
23
+ * />
24
+ * ```
25
+ */
26
+ import React from "react";
27
+ import { useNavigate } from "react-router";
28
+ import { useMemo, useCallback } from "react";
29
+ import {
30
+ Card,
31
+ CardContent,
32
+ CardHeader,
33
+ CardTitle,
34
+ } from "../../../../components/ui/card";
35
+ import type { Column, SearchResultRecordData } from "../../types/search/searchResults";
36
+ import { getNestedFieldValue } from "../../utils/fieldUtils";
37
+ import ResultCardFields from "./ResultCardFields";
38
+ import { OBJECT_API_NAMES } from "../../constants";
39
+
40
+ interface SearchResultCardProps {
41
+ record: SearchResultRecordData;
42
+ columns: Column[];
43
+ objectApiName?: string;
44
+ }
45
+
46
+ export default function SearchResultCard({
47
+ record,
48
+ columns,
49
+ objectApiName,
50
+ }: SearchResultCardProps) {
51
+ const navigate = useNavigate();
52
+
53
+ const validColumns = useMemo(
54
+ () => (columns && Array.isArray(columns) && columns.length > 0 ? columns : []),
55
+ [columns],
56
+ );
57
+ const validRecord =
58
+ record?.id && record?.fields && typeof record.fields === "object" ? record : null;
59
+
60
+ const detailPath = useMemo(
61
+ () =>
62
+ validRecord
63
+ ? `/object/${objectApiName?.trim() || OBJECT_API_NAMES[0]}/${validRecord.id}`
64
+ : "",
65
+ [validRecord, objectApiName],
66
+ );
67
+
68
+ const handleClick = useCallback(() => {
69
+ if (validRecord?.id) navigate(detailPath);
70
+ }, [validRecord?.id, detailPath, navigate]);
71
+
72
+ const handleKeyDown = useCallback(
73
+ (e: React.KeyboardEvent) => {
74
+ if (e.key === "Enter" || e.key === " ") {
75
+ e.preventDefault();
76
+ handleClick();
77
+ }
78
+ },
79
+ [handleClick],
80
+ );
81
+
82
+ const primaryField = useMemo(() => {
83
+ return (
84
+ validColumns.find(
85
+ (col) =>
86
+ col &&
87
+ col.fieldApiName &&
88
+ (col.fieldApiName.toLowerCase() === "name" ||
89
+ col.fieldApiName.toLowerCase().includes("name")),
90
+ ) ||
91
+ validColumns[0] ||
92
+ null
93
+ );
94
+ }, [validColumns]);
95
+
96
+ const primaryValue = useMemo(() => {
97
+ return primaryField && primaryField.fieldApiName && validRecord?.fields
98
+ ? getNestedFieldValue(validRecord.fields, primaryField.fieldApiName) || "Untitled"
99
+ : "Untitled";
100
+ }, [primaryField, validRecord]);
101
+
102
+ const secondaryColumns = useMemo(() => {
103
+ return validColumns.filter(
104
+ (col) => col && col.fieldApiName && col.fieldApiName !== primaryField?.fieldApiName,
105
+ );
106
+ }, [validColumns, primaryField]);
107
+
108
+ if (!validRecord) return null;
109
+ if (validColumns.length === 0) return null;
110
+
111
+ return (
112
+ <Card
113
+ className="cursor-pointer hover:shadow-md transition-shadow focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2"
114
+ onClick={handleClick}
115
+ onKeyDown={handleKeyDown}
116
+ role="button"
117
+ tabIndex={0}
118
+ aria-label={`View details for ${primaryValue}`}
119
+ aria-describedby={`result-${validRecord.id}-description`}
120
+ >
121
+ <CardHeader>
122
+ <CardTitle className="text-lg" id={`result-${validRecord.id}-title`}>
123
+ {primaryValue}
124
+ </CardTitle>
125
+ </CardHeader>
126
+ <CardContent>
127
+ <div id={`result-${validRecord.id}-description`} className="sr-only">
128
+ Search result: {primaryValue}
129
+ </div>
130
+ <ResultCardFields
131
+ record={validRecord}
132
+ columns={secondaryColumns}
133
+ excludeFieldApiName={primaryField?.fieldApiName}
134
+ />
135
+ </CardContent>
136
+ </Card>
137
+ );
138
+ }