@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,55 @@
1
+ /**
2
+ * Alternative detail rendering: columns + record → label/value list.
3
+ *
4
+ * Use when you have list columns + record (e.g. from filters-derived columns + searchResults)
5
+ * and do not need the Layout API. The primary detail view (DetailPage) uses DetailForm
6
+ * via UiApiDetailForm (layout + GraphQL record).
7
+ *
8
+ * @param record - Record data to display
9
+ * @param columns - Column definitions (e.g. derived from getObjectListFilters)
10
+ */
11
+ import type { Column, SearchResultRecordData } from "../../types/search/searchResults";
12
+ import { getNestedFieldValue } from "../../utils/fieldUtils";
13
+
14
+ interface DetailFieldsProps {
15
+ record: SearchResultRecordData;
16
+ columns: Column[];
17
+ }
18
+
19
+ function hasVisibleValue(value: string | number | boolean | null | undefined): boolean {
20
+ return value !== null && value !== undefined && value !== "";
21
+ }
22
+
23
+ export default function DetailFields({ record, columns }: DetailFieldsProps) {
24
+ const rows = columns.filter(
25
+ (col) =>
26
+ col?.fieldApiName && hasVisibleValue(getNestedFieldValue(record.fields, col.fieldApiName)),
27
+ );
28
+
29
+ if (columns.length > 0 && rows.length === 0) {
30
+ return (
31
+ <div role="status" className="text-sm text-muted-foreground py-4">
32
+ No field values to display
33
+ </div>
34
+ );
35
+ }
36
+
37
+ return (
38
+ <dl className="space-y-4" role="list">
39
+ {rows.map((column) => {
40
+ const fieldApiName = column.fieldApiName as string;
41
+ const displayValue = getNestedFieldValue(record.fields, fieldApiName);
42
+ return (
43
+ <div key={fieldApiName} className="border-b pb-4 last:border-0" role="listitem">
44
+ <div className="flex flex-col sm:flex-row sm:items-start gap-2">
45
+ <dt className="font-semibold text-sm text-muted-foreground min-w-[150px]">
46
+ {column.label || fieldApiName}:
47
+ </dt>
48
+ <dd className="text-sm text-foreground flex-1">{displayValue}</dd>
49
+ </div>
50
+ </div>
51
+ );
52
+ })}
53
+ </dl>
54
+ );
55
+ }
@@ -0,0 +1,146 @@
1
+ import { useState, useCallback, useMemo, useId } from "react";
2
+ import type { LayoutResponse } from "../../types/recordDetail/recordDetail";
3
+ import type { GraphQLRecordNode } from "../../api/recordListGraphQLService";
4
+ import {
5
+ getDisplayValueForLayoutItemFromNode,
6
+ getDisplayValueForDetailFieldFromNode,
7
+ } from "../../utils/graphQLNodeFieldUtils";
8
+ import type { ObjectInfoMetadata } from "../../utils/formDataTransformUtils";
9
+ import {
10
+ getTransformedSections,
11
+ type LayoutTransformContext,
12
+ type ObjectInfo,
13
+ type PicklistOption,
14
+ type TransformedLayoutItem,
15
+ } from "../../utils/layoutTransformUtils";
16
+ import { FieldValueDisplay } from "./formatted/FieldValueDisplay";
17
+ import { Section } from "./Section";
18
+ import { SectionRow } from "./SectionRow";
19
+
20
+ export interface DetailFormProps {
21
+ layout: LayoutResponse;
22
+ record: GraphQLRecordNode;
23
+ metadata?: ObjectInfoMetadata | null;
24
+ objectInfo?: ObjectInfo | null;
25
+ lookupRecords?: Record<string, PicklistOption[] | null> | null;
26
+ showSectionHeaders?: boolean;
27
+ collapsibleSections?: boolean;
28
+ }
29
+
30
+ function FieldCell({
31
+ item,
32
+ record,
33
+ metadata,
34
+ }: {
35
+ item: TransformedLayoutItem;
36
+ record: GraphQLRecordNode;
37
+ metadata?: ObjectInfoMetadata | null;
38
+ }) {
39
+ const labelId = useId();
40
+ const valueId = useId();
41
+ if (!item.isField || item.apiName == null) return null;
42
+ const label = item.label ?? item.apiName;
43
+ const hasComponents = item.layoutComponentApiNames && item.layoutComponentApiNames.length > 0;
44
+ const layoutResult = hasComponents
45
+ ? getDisplayValueForLayoutItemFromNode(
46
+ record,
47
+ item.layoutComponentApiNames as string[],
48
+ metadata,
49
+ )
50
+ : null;
51
+ const value = hasComponents
52
+ ? (layoutResult?.value ?? null)
53
+ : getDisplayValueForDetailFieldFromNode(record, item.apiName, metadata);
54
+ const dataType =
55
+ (hasComponents ? layoutResult?.dataType : undefined) ?? item.dataType ?? undefined;
56
+ return (
57
+ <div
58
+ className="flex flex-col gap-1"
59
+ role="group"
60
+ aria-labelledby={labelId}
61
+ aria-describedby={valueId}
62
+ >
63
+ <dt id={labelId} className="text-sm font-medium text-muted-foreground">
64
+ {label}
65
+ </dt>
66
+ <dd id={valueId} className="text-sm text-foreground">
67
+ <FieldValueDisplay value={value} dataType={dataType} />
68
+ </dd>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Read-only detail form: layout API + record (+ optional object info) drive sections, rows, and
75
+ * field values. Uses layoutComponents to club multi-component items (address, Created By, etc.).
76
+ */
77
+ export function DetailForm({
78
+ layout,
79
+ record,
80
+ metadata = null,
81
+ objectInfo = null,
82
+ lookupRecords = null,
83
+ showSectionHeaders = true,
84
+ collapsibleSections = true,
85
+ }: DetailFormProps) {
86
+ const [collapsedSections, setCollapsedSections] = useState<Record<string, boolean>>({});
87
+
88
+ const recordId = (record.Id as string) ?? "";
89
+
90
+ const layoutObjectInfo = objectInfo ?? metadata;
91
+
92
+ const transformContext: LayoutTransformContext = useMemo(
93
+ () => ({
94
+ recordId,
95
+ objectInfo: layoutObjectInfo,
96
+ lookupRecords,
97
+ getSectionCollapsedState: (sectionId: string) => Boolean(collapsedSections[sectionId]),
98
+ }),
99
+ [recordId, layoutObjectInfo, lookupRecords, collapsedSections],
100
+ );
101
+
102
+ const computedSections = useMemo(
103
+ () => getTransformedSections(layout.sections, transformContext),
104
+ [layout.sections, transformContext],
105
+ );
106
+
107
+ const handleSectionToggle = useCallback((sectionId: string, collapsed: boolean) => {
108
+ setCollapsedSections((prev) => ({ ...prev, [sectionId]: collapsed }));
109
+ }, []);
110
+
111
+ return (
112
+ <div
113
+ className="space-y-6"
114
+ role="region"
115
+ aria-label="Record details"
116
+ aria-roledescription="Detail form"
117
+ >
118
+ {computedSections.map((section) => (
119
+ <Section
120
+ key={section.key}
121
+ sectionId={section.id}
122
+ titleLabel={section.heading}
123
+ showHeader={showSectionHeaders && section.useHeading}
124
+ collapsible={collapsibleSections && section.collapsible}
125
+ collapsed={section.collapsed}
126
+ onToggle={handleSectionToggle}
127
+ >
128
+ <div className="space-y-4">
129
+ {section.layoutRows.map((row) => (
130
+ <SectionRow key={row.key}>
131
+ {row.layoutItems.map((item) => {
132
+ const cellKey = `${section.key}-${row.key}-${item.apiName ?? item.key}`;
133
+ return item.isField ? (
134
+ <FieldCell key={cellKey} item={item} record={record} metadata={metadata} />
135
+ ) : (
136
+ <div key={cellKey} className="min-h-[2.5rem]" aria-hidden="true" />
137
+ );
138
+ })}
139
+ </SectionRow>
140
+ ))}
141
+ </div>
142
+ </Section>
143
+ ))}
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Back button and title for the record detail page.
3
+ *
4
+ * @param title - Record title (e.g. record name) shown next to the back control.
5
+ * @param onBack - Called when the user activates the back control.
6
+ */
7
+ import { Button } from "../../../../components/ui/button";
8
+ import { ArrowLeft } from "lucide-react";
9
+
10
+ interface DetailHeaderProps {
11
+ title: string;
12
+ onBack: () => void;
13
+ }
14
+
15
+ export default function DetailHeader({ title, onBack }: DetailHeaderProps) {
16
+ return (
17
+ <div className="mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-4">
18
+ <Button
19
+ variant="ghost"
20
+ onClick={onBack}
21
+ className="w-fit"
22
+ aria-label="Go back to search results"
23
+ >
24
+ <ArrowLeft className="h-4 w-4 mr-2" aria-hidden="true" />
25
+ Back
26
+ </Button>
27
+ {title ? (
28
+ <h1 className="text-xl font-semibold text-foreground truncate" id="detail-page-title">
29
+ {title}
30
+ </h1>
31
+ ) : null}
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Alternative detail rendering: layout sections → rows → items → label/value grid.
3
+ *
4
+ * Use when you have raw Layout API response + record and do not need the full
5
+ * layoutTransformUtils + formDataTransformUtils pipeline. The primary detail view
6
+ * (DetailPage) uses DetailForm via UiApiDetailForm; use this component for other
7
+ * entry points that already have layout + record in hand.
8
+ */
9
+ import type { LayoutResponse } from "../../types/recordDetail/recordDetail";
10
+ import type { SearchResultRecordData } from "../../types/search/searchResults";
11
+ import { getNestedFieldValue } from "../../utils/fieldUtils";
12
+
13
+ interface DetailLayoutSectionsProps {
14
+ layout: LayoutResponse;
15
+ record: SearchResultRecordData;
16
+ }
17
+
18
+ interface FieldEntry {
19
+ key: string;
20
+ label: string;
21
+ value: string | number | boolean | null;
22
+ }
23
+
24
+ function getSectionFieldEntries(
25
+ section: LayoutResponse["sections"][number],
26
+ record: SearchResultRecordData,
27
+ ): FieldEntry[] {
28
+ const entries: FieldEntry[] = [];
29
+ section.layoutRows.forEach((row, rowIdx) => {
30
+ row.layoutItems.forEach((item, itemIdx) => {
31
+ item.layoutComponents.forEach((comp, compIdx) => {
32
+ if (comp.componentType !== "Field" || !comp.apiName) return;
33
+ const value = getNestedFieldValue(record.fields, comp.apiName);
34
+ const label = comp.label ?? item.label;
35
+ entries.push({
36
+ key: `${section.id}-${rowIdx}-${itemIdx}-${comp.apiName ?? compIdx}`,
37
+ label: label || comp.apiName,
38
+ value: value ?? null,
39
+ });
40
+ });
41
+ });
42
+ });
43
+ return entries;
44
+ }
45
+
46
+ export default function DetailLayoutSections({ layout, record }: DetailLayoutSectionsProps) {
47
+ return (
48
+ <div className="space-y-8" role="region" aria-label="Record details">
49
+ {layout.sections.map((section) => {
50
+ const entries = getSectionFieldEntries(section, record);
51
+ if (entries.length === 0) return null;
52
+
53
+ return (
54
+ <section
55
+ key={section.id}
56
+ className="space-y-4"
57
+ aria-labelledby={section.useHeading ? `section-${section.id}` : undefined}
58
+ >
59
+ {section.useHeading && section.heading ? (
60
+ <h3
61
+ id={`section-${section.id}`}
62
+ className="text-base font-semibold text-foreground border-b pb-2"
63
+ >
64
+ {section.heading}
65
+ </h3>
66
+ ) : null}
67
+ <dl className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-4">
68
+ {entries.map(({ key, label, value }) => (
69
+ <div key={key} className="flex flex-col gap-1">
70
+ <dt className="text-sm font-medium text-muted-foreground">{label}</dt>
71
+ <dd className="text-sm text-foreground">{value || "—"}</dd>
72
+ </div>
73
+ ))}
74
+ </dl>
75
+ </section>
76
+ );
77
+ })}
78
+ </div>
79
+ );
80
+ }
@@ -0,0 +1,108 @@
1
+ import { useState, useCallback, useEffect, useRef } from "react";
2
+ import { ChevronDown, ChevronRight } from "lucide-react";
3
+ import type { ReactNode } from "react";
4
+
5
+ export interface SectionProps {
6
+ sectionId: string;
7
+ titleLabel: string;
8
+ showHeader: boolean;
9
+ collapsible: boolean;
10
+ /** When provided, section is controlled (parent owns state). When undefined, section is uncontrolled (internal state). */
11
+ collapsed?: boolean;
12
+ onToggle?: (sectionId: string, collapsed: boolean) => void;
13
+ children: ReactNode;
14
+ }
15
+
16
+ /**
17
+ * Section block with optional heading and collapsible content. Controlled when
18
+ * `collapsed` is passed; uncontrolled otherwise. Accessible: aria-expanded, aria-controls, keyboard (Enter/Space).
19
+ */
20
+ export function Section({
21
+ sectionId,
22
+ titleLabel,
23
+ showHeader,
24
+ collapsible,
25
+ collapsed: controlledCollapsed,
26
+ onToggle,
27
+ children,
28
+ }: SectionProps) {
29
+ const [internalCollapsed, setInternalCollapsed] = useState(false);
30
+ const isControlled = controlledCollapsed !== undefined;
31
+ const collapsed = isControlled ? controlledCollapsed : internalCollapsed;
32
+
33
+ const warnedUncontrolledRef = useRef(false);
34
+ useEffect(() => {
35
+ if (
36
+ process.env.NODE_ENV === "development" &&
37
+ onToggle != null &&
38
+ !isControlled &&
39
+ !warnedUncontrolledRef.current
40
+ ) {
41
+ warnedUncontrolledRef.current = true;
42
+ console.warn(
43
+ "[Section] onToggle is passed but collapsed is undefined; section is uncontrolled. Pass collapsed to control from parent.",
44
+ );
45
+ }
46
+ }, [onToggle, isControlled]);
47
+
48
+ const contentId = `section-content-${sectionId}`;
49
+ const headerId = `section-header-${sectionId}`;
50
+
51
+ const handleToggle = useCallback(() => {
52
+ const next = !collapsed;
53
+ if (!isControlled) setInternalCollapsed(next);
54
+ onToggle?.(sectionId, next);
55
+ }, [collapsed, isControlled, onToggle, sectionId]);
56
+
57
+ const handleKeyDown = useCallback(
58
+ (e: React.KeyboardEvent) => {
59
+ if (!collapsible) return;
60
+ if (e.key === "Enter" || e.key === " ") {
61
+ e.preventDefault();
62
+ handleToggle();
63
+ }
64
+ },
65
+ [collapsible, handleToggle],
66
+ );
67
+
68
+ return (
69
+ <section
70
+ className="border-b border-border last:border-b-0 pb-6 last:pb-0"
71
+ aria-labelledby={showHeader ? headerId : undefined}
72
+ >
73
+ {showHeader && titleLabel && (
74
+ <h3 id={headerId} className="text-base font-semibold text-foreground mb-4">
75
+ {collapsible ? (
76
+ <button
77
+ type="button"
78
+ className="flex items-center gap-2 w-full text-left hover:opacity-80 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 rounded"
79
+ onClick={handleToggle}
80
+ onKeyDown={handleKeyDown}
81
+ aria-expanded={!collapsed}
82
+ aria-controls={contentId}
83
+ aria-label={`${titleLabel}, ${collapsed ? "expand" : "collapse"} section`}
84
+ aria-roledescription="Section toggle"
85
+ >
86
+ {collapsed ? (
87
+ <ChevronRight className="h-4 w-4 shrink-0" aria-hidden />
88
+ ) : (
89
+ <ChevronDown className="h-4 w-4 shrink-0" aria-hidden />
90
+ )}
91
+ <span>{titleLabel}</span>
92
+ </button>
93
+ ) : (
94
+ <span className="block">{titleLabel}</span>
95
+ )}
96
+ </h3>
97
+ )}
98
+ <div
99
+ id={contentId}
100
+ className={showHeader && collapsible ? "mt-2" : ""}
101
+ aria-hidden={collapsible ? collapsed : undefined}
102
+ hidden={collapsible && collapsed}
103
+ >
104
+ {children}
105
+ </div>
106
+ </section>
107
+ );
108
+ }
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export interface SectionRowProps {
4
+ children: ReactNode;
5
+ }
6
+
7
+ /**
8
+ * One row of the detail form: definition list (dl) with two-column grid. Each child
9
+ * is a layout item (field cell or placeholder) from the layout API row.
10
+ */
11
+ export function SectionRow({ children }: SectionRowProps) {
12
+ return (
13
+ <dl
14
+ className="grid grid-cols-1 sm:grid-cols-2 gap-x-8 gap-y-4 sm:gap-y-2"
15
+ aria-label="Row of fields"
16
+ >
17
+ {children}
18
+ </dl>
19
+ );
20
+ }
@@ -0,0 +1,140 @@
1
+ import { useEffect, useMemo } from "react";
2
+ import type { LayoutResponse } from "../../types/recordDetail/recordDetail";
3
+ import { useRecordDetailLayout } from "../../hooks/useRecordDetailLayout";
4
+ import { toRecordDisplayNameMetadata } from "../../utils/fieldUtils";
5
+ import { DetailForm } from "./DetailForm";
6
+ import type { ObjectInfoResult } from "../../types/objectInfo/objectInfo";
7
+ import type { ObjectInfoMetadata } from "../../utils/formDataTransformUtils";
8
+ import type { ObjectInfo } from "../../utils/layoutTransformUtils";
9
+ import type { GraphQLRecordNode } from "../../api/recordListGraphQLService";
10
+ import { getGraphQLRecordDisplayName } from "../../utils/graphQLNodeFieldUtils";
11
+
12
+ export interface UiApiDetailFormProps {
13
+ objectApiName: string;
14
+ recordId: string;
15
+ recordTypeId?: string | null;
16
+ /** When provided, skips fetching and uses this layout (controlled mode). */
17
+ layout?: LayoutResponse | null;
18
+ /** When provided with layout, skips fetching and uses this record (controlled mode). */
19
+ record?: GraphQLRecordNode | null;
20
+ /** When provided, skips fetching and uses this object metadata (controlled mode). */
21
+ objectMetadata?: ObjectInfoResult | null;
22
+ /** When true, shows a loading spinner until layout and record are ready. */
23
+ loadsWithSpinner?: boolean;
24
+ /** Reserved for future edit mode; no-op in read-only. */
25
+ hideFooter?: boolean;
26
+ /** Callback when layout and record are ready (e.g. for parent to show record title). */
27
+ onRecordDataUpdate?: (payload: { recordName: string; record: unknown }) => void;
28
+ }
29
+
30
+ /**
31
+ * Entry component for the record detail view. When layout/record are not provided,
32
+ * fetches them via useRecordDetailLayout. Shows optional loading spinner and renders
33
+ * a read-only DetailForm when ready. Mirrors LWC uiApiDetailForm (read-only).
34
+ *
35
+ * Passes objectInfo (mapped from object metadata) to DetailForm for layout transform.
36
+ * lookupRecords (picklist/lookup options) are not fetched in this flow; DetailForm
37
+ * accepts them when provided (e.g. from a future picklist API). Omit for read-only
38
+ * display without API-driven picklist labels.
39
+ */
40
+ export function UiApiDetailForm({
41
+ objectApiName,
42
+ recordId,
43
+ recordTypeId = null,
44
+ layout: layoutProp,
45
+ record: recordProp,
46
+ objectMetadata: objectMetadataProp,
47
+ loadsWithSpinner = false,
48
+ onRecordDataUpdate,
49
+ }: UiApiDetailFormProps) {
50
+ // Memoize so hook dependency doesn't change every render (avoids duplicate fetches)
51
+ const initialData = useMemo(
52
+ () =>
53
+ layoutProp && recordProp && objectMetadataProp
54
+ ? {
55
+ layout: layoutProp,
56
+ record: recordProp,
57
+ objectMetadata: objectMetadataProp,
58
+ }
59
+ : null,
60
+ [layoutProp, recordProp, objectMetadataProp],
61
+ );
62
+
63
+ const fetched = useRecordDetailLayout({
64
+ objectApiName,
65
+ recordId,
66
+ recordTypeId,
67
+ initialData,
68
+ });
69
+
70
+ const layout = layoutProp ?? fetched.layout;
71
+ const record = recordProp ?? fetched.record;
72
+ const metadata = objectMetadataProp ?? fetched.objectMetadata;
73
+ const loading = layoutProp == null || recordProp == null ? fetched.loading : false;
74
+ const error = layoutProp == null || recordProp == null ? fetched.error : null;
75
+
76
+ const objectInfo: ObjectInfo | null = useMemo(() => {
77
+ if (!metadata?.fields) return null;
78
+ const apiName = metadata.ApiName;
79
+ return {
80
+ apiName,
81
+ fields: Object.fromEntries(
82
+ Object.entries(metadata.fields).map(([name, f]) => [
83
+ name,
84
+ {
85
+ compoundFieldName: f.compoundFieldName ?? undefined,
86
+ dataType: f.dataType ?? "",
87
+ },
88
+ ]),
89
+ ),
90
+ };
91
+ }, [metadata]);
92
+
93
+ const isReadyToRender = Boolean(layout && record && layout.sections?.length);
94
+
95
+ const showSpinner = !isReadyToRender && loadsWithSpinner && loading;
96
+
97
+ useEffect(() => {
98
+ if (!record || !onRecordDataUpdate || !isReadyToRender) return;
99
+ onRecordDataUpdate({
100
+ recordName: getGraphQLRecordDisplayName(record, toRecordDisplayNameMetadata(metadata)),
101
+ record,
102
+ });
103
+ }, [record, metadata, onRecordDataUpdate, isReadyToRender]);
104
+
105
+ if (showSpinner) {
106
+ return (
107
+ <div
108
+ className="min-h-[80px] flex items-center justify-center"
109
+ role="status"
110
+ aria-live="polite"
111
+ aria-label="Loading record details"
112
+ >
113
+ <span className="sr-only">Loading record details</span>
114
+ <div
115
+ className="h-8 w-8 animate-spin rounded-full border-2 border-primary border-t-transparent"
116
+ aria-hidden
117
+ />
118
+ </div>
119
+ );
120
+ }
121
+
122
+ if (error || !layout || !record) {
123
+ return null;
124
+ }
125
+
126
+ if (!isReadyToRender) {
127
+ return null;
128
+ }
129
+
130
+ return (
131
+ <DetailForm
132
+ layout={layout}
133
+ record={record}
134
+ metadata={metadata as ObjectInfoMetadata}
135
+ objectInfo={objectInfo}
136
+ showSectionHeaders
137
+ collapsibleSections
138
+ />
139
+ );
140
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Picks formatter by dataType (Address, Phone, Url, Email) or plain text. Empty values show "—" with a11y label.
3
+ */
4
+ import { FormattedAddress } from "./FormattedAddress";
5
+ import { FormattedEmail } from "./FormattedEmail";
6
+ import { FormattedPhone } from "./FormattedPhone";
7
+ import { FormattedText } from "./FormattedText";
8
+ import { FormattedUrl } from "./FormattedUrl";
9
+
10
+ /** Salesforce UI API dataType values that have dedicated formatters. */
11
+ const DATA_TYPES = {
12
+ Address: "Address",
13
+ Email: "Email",
14
+ Phone: "Phone",
15
+ Url: "Url",
16
+ } as const;
17
+
18
+ /** Normalize dataType to canonical casing so "PHONE" / "phone" match Phone, etc. */
19
+ function normalizeDataType(dataType: string | undefined): string | undefined {
20
+ if (dataType == null || dataType === "") return dataType;
21
+ const lower = dataType.toLowerCase();
22
+ const canonical: Record<string, string> = {
23
+ phone: DATA_TYPES.Phone,
24
+ email: DATA_TYPES.Email,
25
+ url: DATA_TYPES.Url,
26
+ address: DATA_TYPES.Address,
27
+ };
28
+ return canonical[lower] ?? dataType;
29
+ }
30
+
31
+ export interface FieldValueDisplayProps {
32
+ /** Resolved display value (string, number, boolean, or null). */
33
+ value: string | number | boolean | null | undefined;
34
+ /** Field dataType from object info (e.g. Phone, Email, Url, Address). */
35
+ dataType?: string;
36
+ className?: string;
37
+ }
38
+
39
+ const DEFAULT_CLASS = "text-sm text-foreground";
40
+ const LINK_CLASS =
41
+ "text-sm text-foreground text-primary underline underline-offset-2 hover:opacity-80";
42
+
43
+ export function FieldValueDisplay({
44
+ value,
45
+ dataType,
46
+ className = DEFAULT_CLASS,
47
+ }: FieldValueDisplayProps) {
48
+ const str = value || null;
49
+
50
+ if (str === null) {
51
+ return (
52
+ <span className={className} aria-label="No value">
53
+
54
+ </span>
55
+ );
56
+ }
57
+
58
+ const linkClassName = className === DEFAULT_CLASS ? LINK_CLASS : className;
59
+ const normalizedType = normalizeDataType(dataType);
60
+
61
+ switch (normalizedType) {
62
+ case DATA_TYPES.Address:
63
+ return <FormattedAddress value={str as string} className={linkClassName} />;
64
+ case DATA_TYPES.Phone:
65
+ return <FormattedPhone value={str as string} className={linkClassName} />;
66
+ case DATA_TYPES.Url:
67
+ return <FormattedUrl value={str as string} className={linkClassName} />;
68
+ case DATA_TYPES.Email:
69
+ return <FormattedEmail value={str as string} className={linkClassName} />;
70
+ default:
71
+ return <FormattedText value={value} className={className} />;
72
+ }
73
+ }
@@ -0,0 +1,29 @@
1
+ /** Address as link to Google Maps search. External link: target _blank, rel noopener noreferrer. */
2
+
3
+ const GOOGLE_MAPS_SEARCH_BASE = "https://www.google.com/maps/search/?api=1&query=";
4
+
5
+ export interface FormattedAddressProps {
6
+ /** Full address string (e.g. "10 Main Rd.\nNew York, NY 31349\nUSA"). */
7
+ value: string;
8
+ className?: string;
9
+ }
10
+
11
+ export function FormattedAddress({ value, className }: FormattedAddressProps) {
12
+ if (!value || !value.trim()) return null;
13
+ const url = GOOGLE_MAPS_SEARCH_BASE + encodeURIComponent(value.trim());
14
+ return (
15
+ <a
16
+ href={url}
17
+ target="_blank"
18
+ rel="noopener noreferrer"
19
+ className={className}
20
+ aria-label="Open address in Google Maps"
21
+ >
22
+ {value.split("\n").map((line, i) => (
23
+ <span key={i} className="block">
24
+ {line}
25
+ </span>
26
+ ))}
27
+ </a>
28
+ );
29
+ }