@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.
- package/LICENSE.txt +82 -0
- package/README.md +250 -0
- package/dist/.forceignore +15 -0
- package/dist/.husky/pre-commit +4 -0
- package/dist/.prettierignore +11 -0
- package/dist/.prettierrc +17 -0
- package/dist/AGENT.md +193 -0
- package/dist/CHANGELOG.md +2128 -0
- package/dist/README.md +28 -0
- package/dist/config/project-scratch-def.json +13 -0
- package/dist/eslint.config.js +7 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/.forceignore +15 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/.graphqlrc.yml +2 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/.prettierignore +9 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/.prettierrc +11 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/CHANGELOG.md +10 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/README.md +75 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/codegen.yml +95 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/components.json +18 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/e2e/app.spec.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/eslint.config.js +169 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/feature-react-global-search.uibundle-meta.xml +7 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/index.html +12 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/package.json +70 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/playwright.config.ts +24 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/scripts/get-graphql-schema.mjs +68 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/scripts/rewrite-e2e-assets.mjs +23 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/api/graphqlClient.ts +25 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/app.tsx +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/appLayout.tsx +83 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/icons/book.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/icons/copy.svg +4 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/icons/rocket.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/icons/star.svg +3 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/images/codey-1.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/images/codey-2.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/images/codey-3.png +0 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/assets/images/vibe-codey.svg +194 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/alerts/status-alert.tsx +49 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/layouts/card-layout.tsx +29 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/alert.tsx +76 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/badge.tsx +48 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/breadcrumb.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/button.tsx +67 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/calendar.tsx +232 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/card.tsx +103 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/checkbox.tsx +32 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/collapsible.tsx +33 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/datePicker.tsx +127 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/dialog.tsx +162 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/field.tsx +237 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/index.ts +84 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/input.tsx +19 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/label.tsx +22 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/pagination.tsx +132 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/popover.tsx +89 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/select.tsx +193 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/separator.tsx +26 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/skeleton.tsx +14 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/sonner.tsx +20 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/spinner.tsx +16 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/table.tsx +114 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/tabs.tsx +88 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectDetailService.ts +102 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectInfoGraphQLService.ts +137 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectInfoService.ts +95 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/recordListGraphQLService.ts +364 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailFields.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailForm.tsx +146 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/Section.tsx +108 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/SectionRow.tsx +20 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterField.tsx +54 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/forms/filters-form.tsx +114 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/forms/submit-button.tsx +47 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchHeader.tsx +31 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchPagination.tsx +144 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchResultCard.tsx +138 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/constants.ts +39 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/filters/FilterInput.tsx +55 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/filters/FilterSelect.tsx +72 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/form.tsx +209 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useObjectInfoBatch.ts +72 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useObjectSearchData.ts +174 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useRecordDetailLayout.ts +137 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/pages/DetailPage.tsx +109 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/pages/GlobalSearch.tsx +235 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/filters/filters.ts +121 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/filters/picklist.ts +6 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/objectInfo/objectInfo.ts +49 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/schema.d.ts +200 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/search/searchResults.ts +229 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/apiUtils.ts +59 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/cacheUtils.ts +76 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/debounce.ts +90 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/fieldUtils.ts +354 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/filterUtils.ts +32 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/formUtils.ts +142 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +77 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/linkUtils.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/paginationUtils.ts +49 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/recordUtils.ts +159 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/sanitizationUtils.ts +50 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/index.ts +120 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/lib/utils.ts +6 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/navigationMenu.tsx +80 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/pages/Home.tsx +13 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/pages/NotFound.tsx +18 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/router-utils.tsx +35 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/routes.tsx +44 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/src/styles/global.css +135 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/tsconfig.json +42 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/tsconfig.node.json +13 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/ui-bundle.json +7 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/vite-env.d.ts +1 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/vite.config.ts +106 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/vitest-env.d.ts +2 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/vitest.config.ts +11 -0
- package/dist/force-app/main/default/uiBundles/feature-react-global-search/vitest.setup.ts +1 -0
- package/dist/jest.config.js +6 -0
- package/dist/package-lock.json +9995 -0
- package/dist/package.json +40 -0
- package/dist/scripts/apex/hello.apex +10 -0
- package/dist/scripts/graphql-search.sh +191 -0
- package/dist/scripts/prepare-import-unique-fields.js +122 -0
- package/dist/scripts/setup-cli.mjs +563 -0
- package/dist/scripts/sf-project-setup.mjs +66 -0
- package/dist/scripts/soql/account.soql +6 -0
- package/dist/sfdx-project.json +12 -0
- package/package.json +53 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/__inherit__appLayout.tsx +9 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/alerts/__inherit__status-alert.tsx +27 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/layouts/__inherit__card-layout.tsx +9 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__alert.tsx +41 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__button.tsx +45 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__card.tsx +33 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__field.tsx +62 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__input.tsx +5 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__label.tsx +8 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__pagination.tsx +47 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__select.tsx +57 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__skeleton.tsx +5 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/components/ui/__inherit__spinner.tsx +7 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectDetailService.ts +102 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectInfoGraphQLService.ts +137 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/objectInfoService.ts +95 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/api/recordListGraphQLService.ts +364 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailFields.tsx +55 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailForm.tsx +146 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailHeader.tsx +34 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/DetailLayoutSections.tsx +80 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/Section.tsx +108 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/SectionRow.tsx +20 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/UiApiDetailForm.tsx +140 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FieldValueDisplay.tsx +73 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedAddress.tsx +29 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedEmail.tsx +17 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedPhone.tsx +24 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedText.tsx +11 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/detail/formatted/FormattedUrl.tsx +29 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterField.tsx +54 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterInput.tsx +55 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FilterSelect.tsx +72 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/filters/FiltersPanel.tsx +380 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/forms/filters-form.tsx +114 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/forms/submit-button.tsx +47 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/GlobalSearchInput.tsx +114 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/ResultCardFields.tsx +71 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchHeader.tsx +31 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchPagination.tsx +144 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchResultCard.tsx +138 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/search/SearchResultsPanel.tsx +197 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/components/shared/LoadingFallback.tsx +61 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/constants.ts +39 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/filters/FilterInput.tsx +55 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/filters/FilterSelect.tsx +72 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/form.tsx +209 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useObjectInfoBatch.ts +72 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useObjectSearchData.ts +174 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useRecordDetailLayout.ts +137 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/hooks/useRecordListGraphQL.ts +135 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/pages/DetailPage.tsx +109 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/pages/GlobalSearch.tsx +235 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/filters/filters.ts +121 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/filters/picklist.ts +6 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/objectInfo/objectInfo.ts +49 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/recordDetail/recordDetail.ts +61 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/schema.d.ts +200 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/types/search/searchResults.ts +229 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/apiUtils.ts +59 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/cacheUtils.ts +76 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/debounce.ts +90 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/fieldUtils.ts +354 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/fieldValueExtractor.ts +67 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/filterUtils.ts +32 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/formDataTransformUtils.ts +260 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/formUtils.ts +142 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLNodeFieldUtils.ts +186 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLObjectInfoAdapter.ts +77 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/graphQLRecordAdapter.ts +90 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/layoutTransformUtils.ts +236 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/linkUtils.ts +14 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/paginationUtils.ts +49 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/recordUtils.ts +159 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/features/global-search/utils/sanitizationUtils.ts +50 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/index.ts +120 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/lib/__inherit__utils.ts +5 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/pages/Home.tsx +13 -0
- package/src/force-app/main/default/uiBundles/feature-react-global-search/src/routes.tsx +52 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { objectDetailService } from "../api/objectDetailService";
|
|
3
|
+
import type { LayoutResponse } from "../types/recordDetail/recordDetail";
|
|
4
|
+
import type { ObjectInfoResult } from "../types/objectInfo/objectInfo";
|
|
5
|
+
import type { GraphQLRecordNode } from "../api/recordListGraphQLService";
|
|
6
|
+
|
|
7
|
+
export interface UseRecordDetailLayoutReturn {
|
|
8
|
+
layout: LayoutResponse | null;
|
|
9
|
+
record: GraphQLRecordNode | null;
|
|
10
|
+
objectMetadata: ObjectInfoResult | null;
|
|
11
|
+
loading: boolean;
|
|
12
|
+
error: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseRecordDetailLayoutParams {
|
|
16
|
+
objectApiName: string | null;
|
|
17
|
+
recordId: string | null;
|
|
18
|
+
recordTypeId?: string | null;
|
|
19
|
+
initialData?: {
|
|
20
|
+
layout: LayoutResponse;
|
|
21
|
+
record: GraphQLRecordNode;
|
|
22
|
+
objectMetadata: ObjectInfoResult;
|
|
23
|
+
} | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const MAX_CACHE_SIZE = 50;
|
|
27
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
28
|
+
|
|
29
|
+
type CacheEntry = {
|
|
30
|
+
layout: LayoutResponse;
|
|
31
|
+
record: GraphQLRecordNode;
|
|
32
|
+
objectMetadata: ObjectInfoResult;
|
|
33
|
+
cachedAt: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function useRecordDetailLayout({
|
|
37
|
+
objectApiName,
|
|
38
|
+
recordId,
|
|
39
|
+
recordTypeId = null,
|
|
40
|
+
initialData = null,
|
|
41
|
+
}: UseRecordDetailLayoutParams): UseRecordDetailLayoutReturn {
|
|
42
|
+
const [layout, setLayout] = useState<LayoutResponse | null>(initialData?.layout ?? null);
|
|
43
|
+
const [record, setRecord] = useState<GraphQLRecordNode | null>(initialData?.record ?? null);
|
|
44
|
+
const [objectMetadata, setObjectMetadata] = useState<ObjectInfoResult | null>(
|
|
45
|
+
initialData?.objectMetadata ?? null,
|
|
46
|
+
);
|
|
47
|
+
const [loading, setLoading] = useState(!initialData);
|
|
48
|
+
const [error, setError] = useState<string | null>(null);
|
|
49
|
+
|
|
50
|
+
const cacheKey =
|
|
51
|
+
objectApiName && recordId ? `${objectApiName}:${recordId}:${recordTypeId ?? "default"}` : null;
|
|
52
|
+
const cacheRef = useRef<Map<string, CacheEntry>>(new Map());
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!objectApiName || !recordId) {
|
|
56
|
+
setError("Invalid object or record ID");
|
|
57
|
+
setLoading(false);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (
|
|
62
|
+
initialData?.layout != null &&
|
|
63
|
+
initialData?.record != null &&
|
|
64
|
+
initialData?.objectMetadata != null
|
|
65
|
+
) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const cached = cacheRef.current.get(cacheKey!);
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
if (cached && now - cached.cachedAt < CACHE_TTL_MS) {
|
|
72
|
+
setLayout(cached.layout);
|
|
73
|
+
setRecord(cached.record);
|
|
74
|
+
setObjectMetadata(cached.objectMetadata);
|
|
75
|
+
setLoading(false);
|
|
76
|
+
setError(null);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let isCancelled = false;
|
|
81
|
+
|
|
82
|
+
const fetchDetail = async () => {
|
|
83
|
+
setLoading(true);
|
|
84
|
+
setError(null);
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const {
|
|
88
|
+
layout: layoutData,
|
|
89
|
+
record: recordData,
|
|
90
|
+
objectMetadata: objectMetadataData,
|
|
91
|
+
} = await objectDetailService.getRecordDetail(
|
|
92
|
+
objectApiName,
|
|
93
|
+
recordId,
|
|
94
|
+
recordTypeId ?? undefined,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (isCancelled) return;
|
|
98
|
+
|
|
99
|
+
const cache = cacheRef.current;
|
|
100
|
+
if (cache.size >= MAX_CACHE_SIZE) {
|
|
101
|
+
const firstKey = cache.keys().next().value;
|
|
102
|
+
if (firstKey != null) cache.delete(firstKey);
|
|
103
|
+
}
|
|
104
|
+
cache.set(cacheKey!, {
|
|
105
|
+
layout: layoutData,
|
|
106
|
+
record: recordData,
|
|
107
|
+
objectMetadata: objectMetadataData,
|
|
108
|
+
cachedAt: Date.now(),
|
|
109
|
+
});
|
|
110
|
+
setLayout(layoutData);
|
|
111
|
+
setRecord(recordData);
|
|
112
|
+
setObjectMetadata(objectMetadataData);
|
|
113
|
+
} catch {
|
|
114
|
+
if (isCancelled) return;
|
|
115
|
+
setError("Failed to load record details");
|
|
116
|
+
} finally {
|
|
117
|
+
if (!isCancelled) {
|
|
118
|
+
setLoading(false);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
fetchDetail();
|
|
124
|
+
|
|
125
|
+
return () => {
|
|
126
|
+
isCancelled = true;
|
|
127
|
+
};
|
|
128
|
+
}, [objectApiName, recordId, recordTypeId, cacheKey, initialData]);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
layout,
|
|
132
|
+
record,
|
|
133
|
+
objectMetadata,
|
|
134
|
+
loading,
|
|
135
|
+
error,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Record list hook: GraphQL records with filter, sort, pagination, search.
|
|
3
|
+
* Use for list/search views; detail view uses useRecordDetailLayout instead.
|
|
4
|
+
*
|
|
5
|
+
* @module hooks/useRecordListGraphQL
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback } from "react";
|
|
9
|
+
import { useObjectColumns } from "./useObjectSearchData";
|
|
10
|
+
import {
|
|
11
|
+
getRecordsGraphQL,
|
|
12
|
+
buildOrderByFromSort,
|
|
13
|
+
type RecordListGraphQLResult,
|
|
14
|
+
} from "../api/recordListGraphQLService";
|
|
15
|
+
import type { Column } from "../types/search/searchResults";
|
|
16
|
+
import type { FilterCriteria } from "../types/filters/filters";
|
|
17
|
+
|
|
18
|
+
const EMPTY_FILTERS: FilterCriteria[] = [];
|
|
19
|
+
|
|
20
|
+
export interface UseRecordListGraphQLOptions {
|
|
21
|
+
objectApiName: string;
|
|
22
|
+
first?: number;
|
|
23
|
+
after?: string | null;
|
|
24
|
+
filters?: FilterCriteria[];
|
|
25
|
+
sortBy?: string;
|
|
26
|
+
searchQuery?: string;
|
|
27
|
+
/** When provided, skips useObjectColumns (use from parent e.g. useObjectListMetadata). */
|
|
28
|
+
columns?: Column[];
|
|
29
|
+
columnsLoading?: boolean;
|
|
30
|
+
columnsError?: string | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UseRecordListGraphQLReturn {
|
|
34
|
+
data: RecordListGraphQLResult | null;
|
|
35
|
+
edges: Array<{ node?: Record<string, unknown> }>;
|
|
36
|
+
pageInfo: {
|
|
37
|
+
hasNextPage?: boolean;
|
|
38
|
+
hasPreviousPage?: boolean;
|
|
39
|
+
endCursor?: string | null;
|
|
40
|
+
startCursor?: string | null;
|
|
41
|
+
} | null;
|
|
42
|
+
loading: boolean;
|
|
43
|
+
error: string | null;
|
|
44
|
+
columnsLoading: boolean;
|
|
45
|
+
columnsError: string | null;
|
|
46
|
+
refetch: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Fetches records via GraphQL for the given object with filter, sort, pagination, and search.
|
|
51
|
+
*/
|
|
52
|
+
export function useRecordListGraphQL(
|
|
53
|
+
options: UseRecordListGraphQLOptions,
|
|
54
|
+
): UseRecordListGraphQLReturn {
|
|
55
|
+
const {
|
|
56
|
+
objectApiName,
|
|
57
|
+
first = 50,
|
|
58
|
+
after = null,
|
|
59
|
+
filters = EMPTY_FILTERS,
|
|
60
|
+
sortBy = "",
|
|
61
|
+
searchQuery = "",
|
|
62
|
+
columns: columnsProp,
|
|
63
|
+
columnsLoading: columnsLoadingProp,
|
|
64
|
+
columnsError: columnsErrorProp,
|
|
65
|
+
} = options;
|
|
66
|
+
|
|
67
|
+
const fromParent = columnsProp !== undefined;
|
|
68
|
+
const fromHook = useObjectColumns(fromParent ? null : objectApiName);
|
|
69
|
+
|
|
70
|
+
const columns = fromParent ? columnsProp : fromHook.columns;
|
|
71
|
+
const columnsLoading = fromParent ? (columnsLoadingProp ?? false) : fromHook.columnsLoading;
|
|
72
|
+
const columnsError = fromParent ? (columnsErrorProp ?? null) : fromHook.columnsError;
|
|
73
|
+
|
|
74
|
+
const [data, setData] = useState<RecordListGraphQLResult | null>(null);
|
|
75
|
+
const [loading, setLoading] = useState(false);
|
|
76
|
+
const [error, setError] = useState<string | null>(null);
|
|
77
|
+
|
|
78
|
+
const fetchRecords = useCallback(() => {
|
|
79
|
+
if (columnsLoading || columnsError || columns.length === 0) return;
|
|
80
|
+
|
|
81
|
+
setLoading(true);
|
|
82
|
+
setError(null);
|
|
83
|
+
const orderBy = buildOrderByFromSort(sortBy);
|
|
84
|
+
|
|
85
|
+
getRecordsGraphQL({
|
|
86
|
+
objectApiName,
|
|
87
|
+
columns,
|
|
88
|
+
first,
|
|
89
|
+
after,
|
|
90
|
+
filters,
|
|
91
|
+
orderBy,
|
|
92
|
+
searchQuery: searchQuery.trim() || undefined,
|
|
93
|
+
})
|
|
94
|
+
.then((result) => {
|
|
95
|
+
setData(result);
|
|
96
|
+
})
|
|
97
|
+
.catch((err) => {
|
|
98
|
+
setError(err instanceof Error ? err.message : "Failed to load records");
|
|
99
|
+
})
|
|
100
|
+
.finally(() => {
|
|
101
|
+
setLoading(false);
|
|
102
|
+
});
|
|
103
|
+
}, [
|
|
104
|
+
objectApiName,
|
|
105
|
+
columns,
|
|
106
|
+
columnsLoading,
|
|
107
|
+
columnsError,
|
|
108
|
+
first,
|
|
109
|
+
after,
|
|
110
|
+
filters,
|
|
111
|
+
sortBy,
|
|
112
|
+
searchQuery,
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!objectApiName || columnsLoading || columnsError) return;
|
|
117
|
+
if (columns.length === 0 && !columnsLoading) return;
|
|
118
|
+
queueMicrotask(() => fetchRecords());
|
|
119
|
+
}, [objectApiName, columns, columnsLoading, columnsError, fetchRecords]);
|
|
120
|
+
|
|
121
|
+
const objectData = data?.uiapi?.query?.[objectApiName];
|
|
122
|
+
const edges = objectData?.edges ?? [];
|
|
123
|
+
const pageInfo = objectData?.pageInfo ?? null;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
data,
|
|
127
|
+
edges,
|
|
128
|
+
pageInfo,
|
|
129
|
+
loading: columnsLoading || loading,
|
|
130
|
+
error: columnsError || error,
|
|
131
|
+
columnsLoading,
|
|
132
|
+
columnsError,
|
|
133
|
+
refetch: fetchRecords,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useParams, useNavigate } from "react-router";
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../../../components/ui/__inherit__card";
|
|
4
|
+
import { Skeleton } from "../../../components/ui/__inherit__skeleton";
|
|
5
|
+
import { Alert, AlertDescription, AlertTitle } from "../../../components/ui/__inherit__alert";
|
|
6
|
+
import { AlertCircle } from "lucide-react";
|
|
7
|
+
import DetailHeader from "../components/detail/DetailHeader";
|
|
8
|
+
import { UiApiDetailForm } from "../components/detail/UiApiDetailForm";
|
|
9
|
+
import { OBJECT_API_NAMES, DEFAULT_DETAIL_PAGE_TITLE } from "../constants";
|
|
10
|
+
import { toRecordDisplayNameMetadata } from "../utils/fieldUtils";
|
|
11
|
+
import { useRecordDetailLayout } from "../hooks/useRecordDetailLayout";
|
|
12
|
+
import { getGraphQLRecordDisplayName } from "../utils/graphQLNodeFieldUtils";
|
|
13
|
+
|
|
14
|
+
export default function DetailPage() {
|
|
15
|
+
const { objectApiName: objectApiNameParam, recordId } = useParams<{
|
|
16
|
+
objectApiName: string;
|
|
17
|
+
recordId: string;
|
|
18
|
+
}>();
|
|
19
|
+
const navigate = useNavigate();
|
|
20
|
+
const objectApiName = objectApiNameParam ?? OBJECT_API_NAMES[0];
|
|
21
|
+
|
|
22
|
+
const { layout, record, objectMetadata, loading, error } = useRecordDetailLayout({
|
|
23
|
+
objectApiName,
|
|
24
|
+
recordId: recordId ?? null,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const recordTitle = useMemo(
|
|
28
|
+
() =>
|
|
29
|
+
record
|
|
30
|
+
? getGraphQLRecordDisplayName(record, toRecordDisplayNameMetadata(objectMetadata))
|
|
31
|
+
: DEFAULT_DETAIL_PAGE_TITLE,
|
|
32
|
+
[record, objectMetadata],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const handleBack = () => navigate(-1);
|
|
36
|
+
|
|
37
|
+
if (loading) {
|
|
38
|
+
return (
|
|
39
|
+
<div
|
|
40
|
+
className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12"
|
|
41
|
+
role="status"
|
|
42
|
+
aria-live="polite"
|
|
43
|
+
aria-label="Loading record details"
|
|
44
|
+
>
|
|
45
|
+
<span className="sr-only">Loading record details</span>
|
|
46
|
+
<Skeleton className="h-10 w-32 mb-6" aria-hidden="true" />
|
|
47
|
+
<Card aria-hidden="true">
|
|
48
|
+
<CardHeader>
|
|
49
|
+
<Skeleton className="h-8 w-3/4" />
|
|
50
|
+
</CardHeader>
|
|
51
|
+
<CardContent className="space-y-4">
|
|
52
|
+
{[1, 2, 3, 4].map((i) => (
|
|
53
|
+
<div key={i} className="space-y-2">
|
|
54
|
+
<Skeleton className="h-4 w-24" />
|
|
55
|
+
<Skeleton className="h-4 w-full" />
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
</CardContent>
|
|
59
|
+
</Card>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (error) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
67
|
+
<DetailHeader title="" onBack={handleBack} />
|
|
68
|
+
<Alert variant="destructive" role="alert">
|
|
69
|
+
<AlertCircle className="h-4 w-4" aria-hidden="true" />
|
|
70
|
+
<AlertTitle>Error</AlertTitle>
|
|
71
|
+
<AlertDescription>{error}</AlertDescription>
|
|
72
|
+
</Alert>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!layout || !record) {
|
|
78
|
+
return (
|
|
79
|
+
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
80
|
+
<DetailHeader title="" onBack={handleBack} />
|
|
81
|
+
<Alert role="alert">
|
|
82
|
+
<AlertCircle className="h-4 w-4" aria-hidden="true" />
|
|
83
|
+
<AlertTitle>Not Found</AlertTitle>
|
|
84
|
+
<AlertDescription>Record not found</AlertDescription>
|
|
85
|
+
</Alert>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12" aria-label="Record details">
|
|
92
|
+
<DetailHeader title={recordTitle} onBack={handleBack} />
|
|
93
|
+
<Card>
|
|
94
|
+
<CardHeader>
|
|
95
|
+
<CardTitle className="text-2xl">{recordTitle}</CardTitle>
|
|
96
|
+
</CardHeader>
|
|
97
|
+
<CardContent>
|
|
98
|
+
<UiApiDetailForm
|
|
99
|
+
objectApiName={objectApiName}
|
|
100
|
+
recordId={recordId!}
|
|
101
|
+
layout={layout}
|
|
102
|
+
record={record}
|
|
103
|
+
objectMetadata={objectMetadata}
|
|
104
|
+
/>
|
|
105
|
+
</CardContent>
|
|
106
|
+
</Card>
|
|
107
|
+
</main>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GlobalSearch Page Component
|
|
3
|
+
*
|
|
4
|
+
* Main page component for displaying global search results.
|
|
5
|
+
* Uses GraphQL API (useRecordListGraphQL) for list data; results are adapted to the
|
|
6
|
+
* same record shape as before so SearchResultCard and filters/sort/pagination work unchanged.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* - Supports single object search (no tabs)
|
|
10
|
+
* - Displays filters panel on the left and results on the right
|
|
11
|
+
* - Pagination uses a cursor stack: we only query forward (first + after) and store endCursor per page;
|
|
12
|
+
* Previous re-queries using the stored cursor for the previous page so both Next and Previous work.
|
|
13
|
+
*/
|
|
14
|
+
import { useMemo, useState, useCallback, useEffect, useRef } from "react";
|
|
15
|
+
import { useParams } from "react-router";
|
|
16
|
+
import { OBJECT_API_NAMES, DEFAULT_PAGE_SIZE } from "../constants";
|
|
17
|
+
import { useObjectListMetadata } from "../hooks/useObjectSearchData";
|
|
18
|
+
import { useObjectInfoBatch } from "../hooks/useObjectInfoBatch";
|
|
19
|
+
import { useRecordListGraphQL } from "../hooks/useRecordListGraphQL";
|
|
20
|
+
import FiltersPanel from "../components/filters/FiltersPanel";
|
|
21
|
+
import SearchHeader from "../components/search/SearchHeader";
|
|
22
|
+
import SearchResultsPanel from "../components/search/SearchResultsPanel";
|
|
23
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../../../components/ui/__inherit__card";
|
|
24
|
+
import { Skeleton } from "../../../components/ui/__inherit__skeleton";
|
|
25
|
+
import type { FilterCriteria } from "../types/filters/filters";
|
|
26
|
+
import type { SearchResultRecord } from "../types/search/searchResults";
|
|
27
|
+
import { graphQLNodeToSearchResultRecordData } from "../utils/graphQLRecordAdapter";
|
|
28
|
+
|
|
29
|
+
const EMPTY_HIGHLIGHT = { fields: {}, snippet: null };
|
|
30
|
+
const EMPTY_SEARCH_INFO = { isPromoted: false, isSpellCorrected: false };
|
|
31
|
+
|
|
32
|
+
export default function GlobalSearch() {
|
|
33
|
+
const { query } = useParams<{ query: string }>();
|
|
34
|
+
|
|
35
|
+
const objectApiName = OBJECT_API_NAMES[0];
|
|
36
|
+
|
|
37
|
+
const [searchPageSize, setSearchPageSize] = useState(DEFAULT_PAGE_SIZE);
|
|
38
|
+
const [afterCursor, setAfterCursor] = useState<string | null>(null);
|
|
39
|
+
const [pageIndex, setPageIndex] = useState(0);
|
|
40
|
+
/** Cursor stack: cursorStack[i] is the `after` value that returns page i. cursorStack[0] = null (first page). */
|
|
41
|
+
const [cursorStack, setCursorStack] = useState<(string | null)[]>([null]);
|
|
42
|
+
const [appliedFilters, setAppliedFilters] = useState<FilterCriteria[]>([]);
|
|
43
|
+
const [sortBy, setSortBy] = useState("Name");
|
|
44
|
+
|
|
45
|
+
const decodedQuery = useMemo(() => {
|
|
46
|
+
if (!query) return "";
|
|
47
|
+
try {
|
|
48
|
+
return decodeURIComponent(query);
|
|
49
|
+
} catch {
|
|
50
|
+
return query;
|
|
51
|
+
}
|
|
52
|
+
}, [query]);
|
|
53
|
+
|
|
54
|
+
const isBrowseAll = decodedQuery === "browse__all";
|
|
55
|
+
const searchQuery = isBrowseAll ? "" : decodedQuery.trim();
|
|
56
|
+
|
|
57
|
+
// Reset pagination when the URL search query changes so we don't use an old cursor with a new result set
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
queueMicrotask(() => {
|
|
60
|
+
setAfterCursor(null);
|
|
61
|
+
setPageIndex(0);
|
|
62
|
+
setCursorStack([null]);
|
|
63
|
+
});
|
|
64
|
+
}, [query]);
|
|
65
|
+
|
|
66
|
+
const listMeta = useObjectListMetadata(objectApiName);
|
|
67
|
+
const { objectInfos } = useObjectInfoBatch([...OBJECT_API_NAMES]);
|
|
68
|
+
const labelPlural = (objectInfos[0]?.labelPlural as string | undefined) ?? "records";
|
|
69
|
+
const {
|
|
70
|
+
edges,
|
|
71
|
+
pageInfo,
|
|
72
|
+
loading: resultsLoading,
|
|
73
|
+
error: resultsError,
|
|
74
|
+
} = useRecordListGraphQL({
|
|
75
|
+
objectApiName,
|
|
76
|
+
first: searchPageSize,
|
|
77
|
+
after: afterCursor,
|
|
78
|
+
filters: appliedFilters,
|
|
79
|
+
sortBy: sortBy === "relevance" ? "Name" : sortBy,
|
|
80
|
+
searchQuery: searchQuery || undefined,
|
|
81
|
+
columns: listMeta.columns,
|
|
82
|
+
columnsLoading: listMeta.loading,
|
|
83
|
+
columnsError: listMeta.error,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Store endCursor for the next page so we can re-query when user clicks Next; also enables Previous via stack.
|
|
87
|
+
// Only update when not loading so a stale response cannot write a cursor into the wrong stack index (e.g. after rapid Next clicks).
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (resultsLoading) return;
|
|
90
|
+
const cursor = pageInfo?.endCursor ?? null;
|
|
91
|
+
if (cursor == null) return;
|
|
92
|
+
queueMicrotask(() => {
|
|
93
|
+
setCursorStack((prev) => {
|
|
94
|
+
const next = [...prev];
|
|
95
|
+
next[pageIndex + 1] = cursor;
|
|
96
|
+
return next;
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}, [resultsLoading, pageInfo?.endCursor, pageIndex]);
|
|
100
|
+
|
|
101
|
+
const results: SearchResultRecord[] = useMemo(
|
|
102
|
+
() =>
|
|
103
|
+
(edges ?? []).map((edge) => ({
|
|
104
|
+
record: graphQLNodeToSearchResultRecordData(
|
|
105
|
+
edge?.node as Record<string, unknown>,
|
|
106
|
+
objectApiName,
|
|
107
|
+
),
|
|
108
|
+
highlightInfo: EMPTY_HIGHLIGHT,
|
|
109
|
+
searchInfo: EMPTY_SEARCH_INFO,
|
|
110
|
+
})),
|
|
111
|
+
[edges, objectApiName],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const nextPageToken = pageInfo?.endCursor ?? null;
|
|
115
|
+
/** Entry cursor for the previous page; used when user clicks Previous to re-query with after=cursorStack[pageIndex-1]. */
|
|
116
|
+
const previousPageToken = pageIndex > 0 ? (cursorStack[pageIndex - 1] ?? null) : null;
|
|
117
|
+
const hasNextPage = pageInfo?.hasNextPage === true;
|
|
118
|
+
const hasPreviousPage = pageIndex > 0;
|
|
119
|
+
const currentPageToken = pageIndex.toString();
|
|
120
|
+
|
|
121
|
+
const cursorStackRef = useRef(cursorStack);
|
|
122
|
+
const pageIndexRef = useRef(pageIndex);
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
cursorStackRef.current = cursorStack;
|
|
125
|
+
pageIndexRef.current = pageIndex;
|
|
126
|
+
}, [cursorStack, pageIndex]);
|
|
127
|
+
|
|
128
|
+
const canRenderFilters =
|
|
129
|
+
!listMeta.loading && listMeta.filters !== undefined && listMeta.picklistValues !== undefined;
|
|
130
|
+
|
|
131
|
+
const handleApplyFilters = useCallback((filterCriteria: FilterCriteria[]) => {
|
|
132
|
+
setAppliedFilters(filterCriteria);
|
|
133
|
+
setAfterCursor(null);
|
|
134
|
+
setPageIndex(0);
|
|
135
|
+
setCursorStack([null]);
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
const handlePageChange = useCallback(
|
|
139
|
+
(newPageToken: string, direction?: "next" | "prev" | "first") => {
|
|
140
|
+
if (direction === "first" || newPageToken === "0") {
|
|
141
|
+
setAfterCursor(null);
|
|
142
|
+
setPageIndex(0);
|
|
143
|
+
} else if (direction === "prev") {
|
|
144
|
+
const idx = pageIndexRef.current;
|
|
145
|
+
const stack = cursorStackRef.current;
|
|
146
|
+
const prevCursor = idx > 0 ? (stack[idx - 1] ?? null) : null;
|
|
147
|
+
setAfterCursor(prevCursor);
|
|
148
|
+
setPageIndex((prev) => Math.max(0, prev - 1));
|
|
149
|
+
} else {
|
|
150
|
+
setAfterCursor(newPageToken);
|
|
151
|
+
setPageIndex((prev) => prev + 1);
|
|
152
|
+
}
|
|
153
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
154
|
+
},
|
|
155
|
+
[],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const handlePageSizeChange = useCallback((newPageSize: number) => {
|
|
159
|
+
setSearchPageSize(newPageSize);
|
|
160
|
+
setAfterCursor(null);
|
|
161
|
+
setPageIndex(0);
|
|
162
|
+
setCursorStack([null]);
|
|
163
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
const handleSortByChange = useCallback((newSortBy: string) => {
|
|
167
|
+
setSortBy(newSortBy);
|
|
168
|
+
setAfterCursor(null);
|
|
169
|
+
setPageIndex(0);
|
|
170
|
+
setCursorStack([null]);
|
|
171
|
+
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
176
|
+
<SearchHeader query={decodedQuery} isBrowseAll={isBrowseAll} labelPlural={labelPlural} />
|
|
177
|
+
|
|
178
|
+
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
179
|
+
<aside className="lg:col-span-1" aria-label="Filters panel">
|
|
180
|
+
{canRenderFilters ? (
|
|
181
|
+
<FiltersPanel
|
|
182
|
+
filters={listMeta.filters}
|
|
183
|
+
picklistValues={listMeta.picklistValues}
|
|
184
|
+
loading={listMeta.loading}
|
|
185
|
+
objectApiName={objectApiName}
|
|
186
|
+
onApplyFilters={handleApplyFilters}
|
|
187
|
+
/>
|
|
188
|
+
) : (
|
|
189
|
+
<Card className="w-full" role="region" aria-label="Filters panel">
|
|
190
|
+
<CardHeader>
|
|
191
|
+
<CardTitle>Filters</CardTitle>
|
|
192
|
+
</CardHeader>
|
|
193
|
+
<CardContent
|
|
194
|
+
className="space-y-4"
|
|
195
|
+
role="status"
|
|
196
|
+
aria-live="polite"
|
|
197
|
+
aria-label="Loading filters"
|
|
198
|
+
>
|
|
199
|
+
<span className="sr-only">Loading filters</span>
|
|
200
|
+
{[1, 2, 3].map((i) => (
|
|
201
|
+
<div key={i} className="space-y-2" aria-hidden="true">
|
|
202
|
+
<Skeleton className="h-4 w-24" />
|
|
203
|
+
<Skeleton className="h-9 w-full" />
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
206
|
+
</CardContent>
|
|
207
|
+
</Card>
|
|
208
|
+
)}
|
|
209
|
+
</aside>
|
|
210
|
+
|
|
211
|
+
<section className="lg:col-span-3" aria-label="Search results">
|
|
212
|
+
<SearchResultsPanel
|
|
213
|
+
objectApiName={objectApiName}
|
|
214
|
+
columns={listMeta.columns}
|
|
215
|
+
results={results}
|
|
216
|
+
columnsLoading={listMeta.loading}
|
|
217
|
+
resultsLoading={resultsLoading}
|
|
218
|
+
columnsError={listMeta.error}
|
|
219
|
+
resultsError={resultsError}
|
|
220
|
+
currentPageToken={currentPageToken}
|
|
221
|
+
nextPageToken={nextPageToken}
|
|
222
|
+
previousPageToken={previousPageToken}
|
|
223
|
+
hasNextPage={hasNextPage}
|
|
224
|
+
hasPreviousPage={hasPreviousPage}
|
|
225
|
+
pageSize={searchPageSize}
|
|
226
|
+
sortBy={sortBy}
|
|
227
|
+
onPageChange={handlePageChange}
|
|
228
|
+
onPageSizeChange={handlePageSizeChange}
|
|
229
|
+
onSortByChange={handleSortByChange}
|
|
230
|
+
/>
|
|
231
|
+
</section>
|
|
232
|
+
</div>
|
|
233
|
+
</main>
|
|
234
|
+
);
|
|
235
|
+
}
|