@salesforce/ui-bundle-template-feature-angular-search 0.0.0-stage → 12.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +82 -0
- package/README.md +75 -2
- package/dist/.forceignore +15 -0
- package/dist/.husky/pre-commit +4 -0
- package/dist/.prettierignore +11 -0
- package/dist/.prettierrc +17 -0
- package/dist/CHANGELOG.md +5375 -0
- package/dist/README.md +83 -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-angular-search/.forceignore +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/.graphqlrc.yml +2 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/.postcssrc.json +5 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/.prettierignore +8 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/.prettierrc +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/CHANGELOG.md +4 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/README.md +85 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/angular.json +98 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/codegen.yml +95 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/components.json +6 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/e2e/app.spec.ts +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/esbuild/api-version.mjs +17 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/esbuild/graphql-codegen.mjs +13 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/eslint-plugin-angular-graphql.js +148 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/eslint.config.js +77 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/feature-angular-search.uibundle-meta.xml +7 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/middleware/html.mjs +3 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/middleware/proxy.mjs +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/package.json +69 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/playwright.config.ts +25 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/public/favicon.ico +0 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/scripts/get-graphql-schema.mjs +69 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/scripts/rewrite-e2e-assets.mjs +30 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/api/data-client.service.spec.ts +184 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/api/data-client.service.ts +74 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/app.config.ts +20 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/app.html +1 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/app.routes.ts +27 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/app.spec.ts +18 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/app.ts +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/layout/app-layout/app-layout.html +51 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/layout/app-layout/app-layout.spec.ts +63 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/layout/app-layout/app-layout.ts +27 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/alert/alert.html +27 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/alert/alert.ts +41 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/button/button.html +27 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/button/button.ts +59 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/card/card.ts +100 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/collapsible/collapsible.html +38 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/collapsible/collapsible.ts +21 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/date-picker/date-picker.html +20 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/date-picker/date-picker.ts +49 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/date-range-picker/date-range-picker.html +20 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/date-range-picker/date-range-picker.ts +69 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/dialog/dialog.html +20 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/dialog/dialog.ts +40 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/dropdown-menu/dropdown-menu.html +20 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/dropdown-menu/dropdown-menu.ts +100 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/field/field.html +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/field/field.ts +28 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/field-appearance.ts +8 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/field-size.ts +8 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/icon/icon.ts +118 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/input/input.html +34 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/input/input.ts +79 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/label/label.ts +21 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/paginator/paginator.html +74 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/paginator/paginator.ts +93 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/paginator/types.ts +13 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/popover/popover.html +13 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/popover/popover.ts +37 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/select/select.html +36 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/select/select.ts +63 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/separator/separator.ts +31 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/skeleton/skeleton.ts +20 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/components/ui/spinner/spinner.ts +28 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/adapters/registry.ts +28 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/adapters/sobject/index.ts +19 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/adapters/sobject/parse-response.ts +57 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/adapters/sobject/query-fragment.ts +169 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/adapters/types.ts +44 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/api/search-response.spec.ts +114 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/api/search-response.ts +73 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/api/search.service.spec.ts +124 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/api/search.service.ts +80 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/active-filters/active-filters.html +23 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/active-filters/active-filters.spec.ts +73 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/active-filters/active-filters.ts +77 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/filter-panel/default-filter-panel.html +49 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/filter-panel/default-filter-panel.ts +87 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/filter-panel/filter-box.html +13 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/filter-panel/filter-box.ts +39 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/global-search-box/global-search-box.html +16 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/global-search-box/global-search-box.ts +50 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/default-result-row.html +19 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/default-result-row.ts +91 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/merged-search-results.html +99 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/merged-search-results.ts +145 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/result-item.ts +53 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/search-results.html +10 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/search-results.ts +33 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/source-section.html +84 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/results/source-section.ts +146 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/scope/scope-selector.html +9 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/scope/scope-selector.ts +45 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/search/search.html +55 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/search/search.ts +118 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/search-bar/search-bar.html +10 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/search-bar/search-bar.spec.ts +65 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/search-bar/search-bar.ts +72 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/sort-control/sort-control.html +24 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/sort-control/sort-control.spec.ts +67 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/components/sort-control/sort-control.ts +59 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/constants.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/default-search-config.ts +83 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/directives/search-result-template/search-result-template.ts +44 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/boolean-filter.html +8 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/boolean-filter.spec.ts +41 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/boolean-filter.ts +42 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-filter.html +20 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-filter.spec.ts +56 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-filter.ts +78 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-range-filter.html +43 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-range-filter.spec.ts +70 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/date-range-filter.ts +125 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/filter-field-wrapper.html +16 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/filter-field-wrapper.spec.ts +28 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/filter-field-wrapper.ts +24 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/filter-types.spec.ts +27 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/filter-types.ts +64 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/multi-select-filter.html +11 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/multi-select-filter.spec.ts +42 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/multi-select-filter.ts +42 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/numeric-range-filter.html +32 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/numeric-range-filter.spec.ts +75 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/numeric-range-filter.ts +117 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/select-filter.html +8 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/select-filter.spec.ts +43 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/select-filter.ts +40 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/text-filter.html +9 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/text-filter.spec.ts +54 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/filters/text-filter.ts +70 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/index.ts +55 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/query/build-search-query.spec.ts +157 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/query/build-search-query.ts +95 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/state/filter-mapping.ts +48 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/state/search-state.service.spec.ts +293 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/state/search-state.service.ts +735 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/state/search-state.types.ts +48 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/filter.types.ts +57 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/index.ts +11 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/scope.types.ts +10 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/search-config.types.ts +110 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/search-result.types.ts +68 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/types/sort.types.ts +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/debounce.ts +23 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/field-utils.ts +20 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/filter-utils.spec.ts +335 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/filter-utils.ts +354 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/identifier.ts +19 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/merge-utils.spec.ts +199 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/merge-utils.ts +177 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/scope-utils.ts +19 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/sort-utils.ts +32 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/validate-config.spec.ts +48 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/features/search/utils/validate-config.ts +34 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/home/home.html +6 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/home/home.spec.ts +23 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/home/home.ts +13 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/not-found/not-found.html +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/not-found/not-found.spec.ts +33 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/not-found/not-found.ts +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/search/search-page.html +5 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/pages/search/search-page.ts +19 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/alert/hlm-alert-action.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/alert/hlm-alert-description.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/alert/hlm-alert-title.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/alert/hlm-alert.ts +35 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/alert/index.ts +16 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/button/hlm-button.token.ts +22 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/button/hlm-button.ts +72 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/button/index.ts +6 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/calendar/hlm-calendar-multi.ts +195 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/calendar/hlm-calendar-range.ts +194 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/calendar/hlm-calendar.ts +189 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/calendar/hlm-month-year-calendar.ts +119 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/calendar/index.ts +16 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-action.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-avatar.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-content.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-description.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-footer.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-header.ts +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card-title.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card.token.ts +19 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/hlm-card.ts +22 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/card/index.ts +28 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/collapsible/hlm-collapsible-content.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/collapsible/hlm-collapsible-trigger.ts +9 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/collapsible/hlm-collapsible.ts +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/collapsible/index.ts +13 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-multi-input.ts +104 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker-anchor.ts +23 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker-input.ts +118 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker-multi.token.ts +109 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker-multi.ts +220 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker-trigger.ts +104 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker.token.ts +69 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-picker.ts +203 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-range-input.ts +120 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-range-picker.token.ts +86 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-date-range-picker.ts +252 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-month-year-input.ts +105 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-month-year-picker.token.ts +89 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/hlm-month-year-picker.ts +196 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/date-picker/index.ts +38 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-close.ts +9 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-content.ts +74 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-description.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-footer.ts +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-header.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-overlay.ts +27 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-portal.ts +8 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-title.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog-trigger.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog.service.ts +46 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/hlm-dialog.ts +24 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dialog/index.ts +35 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-checkbox-indicator.ts +21 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-checkbox.ts +54 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-focus-on-hover.ts +35 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-group.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-item-sub-indicator.ts +19 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-item.ts +42 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-label.ts +20 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-radio-indicator.ts +21 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-radio.ts +49 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-separator.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-shortcut.ts +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-sub-trigger.ts +52 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-sub.ts +53 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-token.ts +26 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu-trigger.ts +46 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/hlm-dropdown-menu.ts +57 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/dropdown-menu/index.ts +49 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-content.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-description.ts +53 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-error.ts +100 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-group.ts +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-label.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-legend.ts +19 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-separator.ts +24 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-set.ts +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field-title.ts +15 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/hlm-field.ts +47 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/field/index.ts +34 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input/hlm-input.ts +21 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input/index.ts +5 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/hlm-input-group-addon.ts +40 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/hlm-input-group-button.ts +47 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/hlm-input-group-input.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/hlm-input-group-text.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/hlm-input-group.ts +18 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/input-group/index.ts +19 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/label/hlm-label.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/label/index.ts +5 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/label/label.html +11 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-numbered-pagination-query-params.ts +191 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-numbered-pagination.ts +290 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-content.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-ellipsis.ts +27 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-item.ts +7 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-link.ts +51 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-next.ts +65 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination-previous.ts +65 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/hlm-pagination.ts +19 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/pagination/index.ts +31 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-content.ts +25 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-description.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-header.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-portal.ts +8 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-title.ts +12 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover-trigger.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/hlm-popover.ts +23 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/popover/index.ts +25 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-content.ts +44 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-group.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-item.ts +36 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-label.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-multiple.ts +37 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-placeholder.ts +17 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-portal.ts +8 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-scroll-down.ts +22 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-scroll-up.ts +22 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-separator.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-trigger.ts +59 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-value-template.ts +5 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-value.ts +18 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-values-content.ts +9 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select-values.ts +5 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/hlm-select.ts +37 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/select/index.ts +52 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/separator/hlm-separator.ts +19 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/separator/index.ts +5 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/skeleton/hlm-skeleton.ts +14 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/skeleton/index.ts +5 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/spinner/hlm-spinner.ts +26 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/spinner/index.ts +5 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/utils/hlm.ts +313 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/utils/index.ts +2 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/shared/utils/provide-spartan-hlm.ts +34 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/app/utils/async-data.ts +99 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/index.html +13 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/main.ts +5 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/styles.css +139 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/src/types/sf-globals.d.ts +9 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/tsconfig.app.json +11 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/tsconfig.json +36 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/tsconfig.spec.json +10 -0
- package/dist/force-app/main/default/uiBundles/feature-angular-search/ui-bundle.json +7 -0
- package/dist/jest.config.js +6 -0
- package/dist/package-lock.json +12554 -0
- package/dist/package.json +46 -0
- package/dist/scripts/apex/hello.apex +10 -0
- package/dist/scripts/gitignore-templates.json +4 -0
- package/dist/scripts/graphql-search.sh +191 -0
- package/dist/scripts/network-senders.mjs +84 -0
- package/dist/scripts/org-setup-config-schema.mjs +132 -0
- package/dist/scripts/org-setup-dev.mjs +114 -0
- package/dist/scripts/org-setup-url.mjs +108 -0
- package/dist/scripts/org-setup-utils.mjs +569 -0
- package/dist/scripts/org-setup-xml.mjs +283 -0
- package/dist/scripts/org-setup.config.json +5 -0
- package/dist/scripts/org-setup.mjs +2545 -0
- package/dist/scripts/sf-project-setup.mjs +103 -0
- package/dist/scripts/soql/account.soql +6 -0
- package/dist/scripts/validate-org-setup-config.mjs +38 -0
- package/dist/sfdx-project.json +12 -0
- package/package.json +43 -4
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
computed,
|
|
8
|
+
DestroyRef,
|
|
9
|
+
effect,
|
|
10
|
+
inject,
|
|
11
|
+
Injectable,
|
|
12
|
+
signal,
|
|
13
|
+
type Signal,
|
|
14
|
+
untracked,
|
|
15
|
+
type WritableSignal,
|
|
16
|
+
} from "@angular/core";
|
|
17
|
+
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
18
|
+
import { ActivatedRoute, Router } from "@angular/router";
|
|
19
|
+
import { Subject } from "rxjs";
|
|
20
|
+
import { debounceTime } from "rxjs/operators";
|
|
21
|
+
import { SearchService } from "../api/search.service";
|
|
22
|
+
import { MIN_QUERY_LENGTH, URL_SYNC_DEBOUNCE_MS } from "../constants";
|
|
23
|
+
import type { FilterValue } from "../filters/filter-types";
|
|
24
|
+
import type { ActiveFilterValue, FilterFieldConfig, PicklistOption } from "../types/filter.types";
|
|
25
|
+
import { ALL_SCOPE, type SearchScope } from "../types/scope.types";
|
|
26
|
+
import type { SearchConfig } from "../types/search-config.types";
|
|
27
|
+
import type {
|
|
28
|
+
GlobalPaginationState,
|
|
29
|
+
MergedResultItem,
|
|
30
|
+
RunSearchOutcome,
|
|
31
|
+
SourceRequest,
|
|
32
|
+
} from "../types/search-result.types";
|
|
33
|
+
import type { SortState } from "../types/sort.types";
|
|
34
|
+
import { createAsyncData } from "../../../utils/async-data";
|
|
35
|
+
import { GLOBAL_QUERY_KEY, readSourceParams, writeSourceParams } from "../utils/filter-utils";
|
|
36
|
+
import {
|
|
37
|
+
computeMergedPagination,
|
|
38
|
+
computePerSourcePagination,
|
|
39
|
+
filterToGlobalPage,
|
|
40
|
+
mergedFetchSize,
|
|
41
|
+
mergeNodes,
|
|
42
|
+
resolvePagination,
|
|
43
|
+
validatePageSize,
|
|
44
|
+
windowMergedPage,
|
|
45
|
+
type ResolvedPagination,
|
|
46
|
+
} from "../utils/merge-utils";
|
|
47
|
+
import { validateSearchConfig } from "../utils/validate-config";
|
|
48
|
+
import { isSourceInScope, validateScope } from "../utils/scope-utils";
|
|
49
|
+
import { activeTypeFor, toActiveFilter, toFilterValue } from "./filter-mapping";
|
|
50
|
+
import type { SearchInitOptions, SourceLocalState, SourceView } from "./search-state.types";
|
|
51
|
+
|
|
52
|
+
const EMPTY_OUTCOME: RunSearchOutcome = { results: {}, errors: {} };
|
|
53
|
+
|
|
54
|
+
/** Whether a query-string key belongs to the search state (vs. the host app). */
|
|
55
|
+
function isOwnParam(key: string): boolean {
|
|
56
|
+
return (
|
|
57
|
+
key === GLOBAL_QUERY_KEY ||
|
|
58
|
+
key === GLOBAL_PAGE_KEY ||
|
|
59
|
+
key === GLOBAL_SIZE_KEY ||
|
|
60
|
+
key.startsWith("s.")
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function freshSourceState(pageSize: number, sort: SortState | null): SourceLocalState {
|
|
65
|
+
return {
|
|
66
|
+
filters: [],
|
|
67
|
+
sort,
|
|
68
|
+
pageSize,
|
|
69
|
+
pageIndex: 0,
|
|
70
|
+
afterCursor: undefined,
|
|
71
|
+
cursorStack: [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Back to page one: every cursor is invalidated. */
|
|
76
|
+
function withFirstPage(state: SourceLocalState): SourceLocalState {
|
|
77
|
+
return { ...state, pageIndex: 0, afterCursor: undefined, cursorStack: [] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** URL param holding the 1-based merged-mode page. */
|
|
81
|
+
const GLOBAL_PAGE_KEY = "page";
|
|
82
|
+
/** URL param holding the merged-mode page size (omitted when it is the default). */
|
|
83
|
+
const GLOBAL_SIZE_KEY = "size";
|
|
84
|
+
/**
|
|
85
|
+
* Upper bound on a restored per-source page number; a crafted URL cannot push
|
|
86
|
+
* the index to an absurd value.
|
|
87
|
+
*/
|
|
88
|
+
const MAX_RESTORED_PAGE = 1000;
|
|
89
|
+
/**
|
|
90
|
+
* Merged mode fetches a front-anchored window of `(page + 1) * pageSize` rows
|
|
91
|
+
* per source. The window is capped here (the uiapi `first` limit), so pages
|
|
92
|
+
* beyond `MAX_MERGED_WINDOW / pageSize` are not reachable in merged mode.
|
|
93
|
+
*/
|
|
94
|
+
export const MAX_MERGED_WINDOW = 200;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* State, URL sync and fetching for one multi-source search — the Angular
|
|
98
|
+
* analogue of React's `useSearch` + `FilterProvider`.
|
|
99
|
+
*
|
|
100
|
+
* ## State model
|
|
101
|
+
* Signals drive instant UI updates; the URL query string (`q`, plus
|
|
102
|
+
* `s.<key>.*` per source) is the durable seed so a refresh or shared link
|
|
103
|
+
* restores the same view. URL writes are debounced and use `replaceUrl`.
|
|
104
|
+
* The current page index and its cursor are persisted per source, so a reload
|
|
105
|
+
* restores the page (earlier cursors are padded with the current one).
|
|
106
|
+
* The scope is likewise never persisted: a fresh load starts in "All".
|
|
107
|
+
*
|
|
108
|
+
* ## Fetching
|
|
109
|
+
* One batched request per change of the fetch-relevant inputs, run through
|
|
110
|
+
* `createAsyncData` (latest-wins). The keyword gate ({@link MIN_QUERY_LENGTH})
|
|
111
|
+
* and the in-scope source set are applied client-side, so out-of-scope edits
|
|
112
|
+
* and sub-minimum keystrokes never hit the network.
|
|
113
|
+
*
|
|
114
|
+
* ## Pagination
|
|
115
|
+
* `pagination.mode` decides the model. `"per-source"` keeps one cursor stack
|
|
116
|
+
* per source; the "global" page is the furthest-advanced in-scope source.
|
|
117
|
+
* `"merged"` windows the cumulative result set into fixed pages: every
|
|
118
|
+
* in-scope source fetches its first `(page + 1) * pageSize` rows and the page
|
|
119
|
+
* slice is taken after ordering.
|
|
120
|
+
*
|
|
121
|
+
* ## Scope
|
|
122
|
+
* Provide per search host (`providers: [SearchStateService]`), NOT in root, and
|
|
123
|
+
* call {@link init} once with the config.
|
|
124
|
+
*/
|
|
125
|
+
@Injectable()
|
|
126
|
+
export class SearchStateService {
|
|
127
|
+
private readonly router = inject(Router);
|
|
128
|
+
private readonly route = inject(ActivatedRoute);
|
|
129
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
130
|
+
private readonly searchService = inject(SearchService);
|
|
131
|
+
|
|
132
|
+
// -- Writable state ---------------------------------------------------------
|
|
133
|
+
private readonly _config = signal<SearchConfig | null>(null);
|
|
134
|
+
private readonly _pagination = signal<ResolvedPagination>(resolvePagination({ sources: [] }));
|
|
135
|
+
private readonly _lockedScope = signal<SearchScope | undefined>(undefined);
|
|
136
|
+
private readonly _configError = signal<string | null>(null);
|
|
137
|
+
private readonly _q = signal("");
|
|
138
|
+
private readonly _scope = signal<SearchScope>(ALL_SCOPE);
|
|
139
|
+
private readonly _sourceStates = signal<Record<string, SourceLocalState>>({});
|
|
140
|
+
private readonly _globalPageIndex = signal(0);
|
|
141
|
+
private readonly _globalPageSize = signal(resolvePagination({ sources: [] }).pageSize);
|
|
142
|
+
|
|
143
|
+
/** Coalesces rapid state changes into a single debounced URL write. */
|
|
144
|
+
private readonly urlSync$ = new Subject<void>();
|
|
145
|
+
/** The `q` last seeded from, or written to, the URL by this service. */
|
|
146
|
+
private lastSyncedQ = "";
|
|
147
|
+
private readonly distinctCache = new Map<string, WritableSignal<readonly PicklistOption[]>>();
|
|
148
|
+
|
|
149
|
+
// -- Public read signals ----------------------------------------------------
|
|
150
|
+
readonly config: Signal<SearchConfig | null> = this._config.asReadonly();
|
|
151
|
+
/** Set when {@link init} rejected the config; nothing is fetched in that case. */
|
|
152
|
+
readonly configError: Signal<string | null> = this._configError.asReadonly();
|
|
153
|
+
/** Raw search term (the search bar debounces before calling {@link setQ}). */
|
|
154
|
+
readonly q: Signal<string> = this._q.asReadonly();
|
|
155
|
+
readonly scope: Signal<SearchScope> = this._scope.asReadonly();
|
|
156
|
+
readonly scopeLocked = computed(() => this._lockedScope() !== undefined);
|
|
157
|
+
readonly pagination: Signal<ResolvedPagination> = this._pagination.asReadonly();
|
|
158
|
+
readonly mergedMode = computed(() => this._pagination().mode === "merged");
|
|
159
|
+
/** True while the search term is below {@link MIN_QUERY_LENGTH} (no request fires). */
|
|
160
|
+
readonly isShortQuery = computed(() => this._q().trim().length < MIN_QUERY_LENGTH);
|
|
161
|
+
|
|
162
|
+
// -- Fetch ------------------------------------------------------------------
|
|
163
|
+
/** Requests for the current state; empty when gated (short query / no source). */
|
|
164
|
+
private readonly requests = computed<SourceRequest[]>(() => {
|
|
165
|
+
const config = this._config();
|
|
166
|
+
if (!config || this.isShortQuery()) return [];
|
|
167
|
+
const q = this._q();
|
|
168
|
+
const scope = this._scope();
|
|
169
|
+
const states = this._sourceStates();
|
|
170
|
+
const merged = this.mergedMode();
|
|
171
|
+
// Merged mode fetches the whole front-anchored window with no cursor.
|
|
172
|
+
const pageSize = this._globalPageSize();
|
|
173
|
+
const windowSize = Math.min(
|
|
174
|
+
mergedFetchSize(this._globalPageIndex(), pageSize),
|
|
175
|
+
Math.max(MAX_MERGED_WINDOW, pageSize),
|
|
176
|
+
);
|
|
177
|
+
return config.sources
|
|
178
|
+
.filter((source) => isSourceInScope(scope, source.key))
|
|
179
|
+
.flatMap((source) => {
|
|
180
|
+
const st = states[source.key];
|
|
181
|
+
if (!st) return [];
|
|
182
|
+
return [
|
|
183
|
+
{
|
|
184
|
+
source,
|
|
185
|
+
q,
|
|
186
|
+
filters: st.filters,
|
|
187
|
+
sort: st.sort,
|
|
188
|
+
pageSize: merged ? windowSize : st.pageSize,
|
|
189
|
+
afterCursor: merged ? undefined : st.afterCursor,
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Fetch-relevant fingerprint. A string so that edits which do not change any
|
|
197
|
+
* request (e.g. to an out-of-scope source) do not refetch.
|
|
198
|
+
*/
|
|
199
|
+
private readonly requestKey = computed(() =>
|
|
200
|
+
JSON.stringify(
|
|
201
|
+
this.requests().map((r) => ({
|
|
202
|
+
k: r.source.key,
|
|
203
|
+
q: r.q,
|
|
204
|
+
f: r.filters,
|
|
205
|
+
o: r.sort,
|
|
206
|
+
p: r.pageSize,
|
|
207
|
+
a: r.afterCursor ?? null,
|
|
208
|
+
})),
|
|
209
|
+
),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
private readonly fetched = createAsyncData<string, RunSearchOutcome>(this.requestKey, () =>
|
|
213
|
+
this.fetch(this.requests()),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
/** True while a request for the current state is in flight. */
|
|
217
|
+
readonly loading = computed(() => this.requests().length > 0 && this.fetched.loading());
|
|
218
|
+
|
|
219
|
+
private readonly outcome = computed(() => this.fetched.data() ?? EMPTY_OUTCOME);
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Page-level error: a whole-request failure, or every requested source
|
|
223
|
+
* failed. A partial failure surfaces per source instead (see {@link SourceView.error}).
|
|
224
|
+
*/
|
|
225
|
+
readonly error = computed<string | null>(() => {
|
|
226
|
+
const invalid = this._configError();
|
|
227
|
+
if (invalid) return invalid;
|
|
228
|
+
if (this.loading()) return null;
|
|
229
|
+
const whole = this.fetched.error();
|
|
230
|
+
if (whole) return whole;
|
|
231
|
+
const requested = this.requests();
|
|
232
|
+
if (requested.length === 0) return null;
|
|
233
|
+
const errors = this.outcome().errors;
|
|
234
|
+
if (!requested.every((r) => errors[r.source.key] != null)) return null;
|
|
235
|
+
const combined = [...new Set(requested.map((r) => errors[r.source.key]).filter(Boolean))];
|
|
236
|
+
return combined.join("; ") || "Unified search failed";
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// -- Derived views ----------------------------------------------------------
|
|
240
|
+
/** Every configured source, keyed by `source.key`. */
|
|
241
|
+
readonly sources = computed<Record<string, SourceView>>(() => {
|
|
242
|
+
const config = this._config();
|
|
243
|
+
if (!config) return {};
|
|
244
|
+
const states = this._sourceStates();
|
|
245
|
+
const { results, errors } = this.outcome();
|
|
246
|
+
const loading = this.loading();
|
|
247
|
+
const out: Record<string, SourceView> = {};
|
|
248
|
+
for (const source of config.sources) {
|
|
249
|
+
const st = states[source.key];
|
|
250
|
+
if (!st) continue;
|
|
251
|
+
const result = results[source.key] ?? null;
|
|
252
|
+
out[source.key] = {
|
|
253
|
+
config: source,
|
|
254
|
+
result,
|
|
255
|
+
error: loading ? null : (errors[source.key] ?? null),
|
|
256
|
+
filters: st.filters,
|
|
257
|
+
sort: st.sort,
|
|
258
|
+
pageSize: st.pageSize,
|
|
259
|
+
pageIndex: st.pageIndex,
|
|
260
|
+
hasNextPage: result?.pageInfo?.hasNextPage ?? false,
|
|
261
|
+
hasPreviousPage: st.pageIndex > 0,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
/** Views of the sources in the current scope, in config order. */
|
|
268
|
+
readonly inScopeSources = computed<SourceView[]>(() => {
|
|
269
|
+
const scope = this._scope();
|
|
270
|
+
const sources = this.sources();
|
|
271
|
+
return (this._config()?.sources ?? [])
|
|
272
|
+
.filter((s) => isSourceInScope(scope, s.key))
|
|
273
|
+
.map((s) => sources[s.key])
|
|
274
|
+
.filter((v): v is SourceView => !!v);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
/** Cumulative in-scope result count (`totalCount`, else rows fetched). */
|
|
278
|
+
readonly totalCount = computed(() =>
|
|
279
|
+
this.inScopeSources().reduce(
|
|
280
|
+
(sum, v) => sum + (v.result?.totalCount ?? v.result?.nodes.length ?? 0),
|
|
281
|
+
0,
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
private readonly concatenatedNodes = computed<MergedResultItem[]>(() =>
|
|
286
|
+
mergeNodes(
|
|
287
|
+
this.inScopeSources().map((v) => ({
|
|
288
|
+
key: v.config.key,
|
|
289
|
+
nodes: v.result?.nodes ?? [],
|
|
290
|
+
weight: v.result?.totalCount ?? v.result?.nodes.length ?? 0,
|
|
291
|
+
})),
|
|
292
|
+
this._pagination().mergeOrder,
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
/** Aggregate pagination across the in-scope sources (model per `pagination.mode`). */
|
|
297
|
+
readonly globalPagination = computed<GlobalPaginationState>(() => {
|
|
298
|
+
const pagination = this._pagination();
|
|
299
|
+
if (this.mergedMode()) {
|
|
300
|
+
const total = this.totalCount();
|
|
301
|
+
const size = this._globalPageSize();
|
|
302
|
+
// Only the capped window is reachable, so page math uses the capped total.
|
|
303
|
+
const reachable = Math.min(total, Math.max(MAX_MERGED_WINDOW, size));
|
|
304
|
+
return {
|
|
305
|
+
...computeMergedPagination(
|
|
306
|
+
reachable,
|
|
307
|
+
size,
|
|
308
|
+
this._globalPageIndex(),
|
|
309
|
+
pagination.pageSizeOptions,
|
|
310
|
+
),
|
|
311
|
+
totalCount: total,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const inScope = this.inScopeSources();
|
|
315
|
+
return computePerSourcePagination(
|
|
316
|
+
inScope.map((v) => ({
|
|
317
|
+
key: v.config.key,
|
|
318
|
+
pageIndex: v.pageIndex,
|
|
319
|
+
hasNextPage: v.hasNextPage,
|
|
320
|
+
totalCount: v.result?.totalCount ?? null,
|
|
321
|
+
nodeCount: v.result?.nodes.length ?? 0,
|
|
322
|
+
})),
|
|
323
|
+
inScope[0]?.pageSize ?? pagination.pageSize,
|
|
324
|
+
pagination.pageSizeOptions,
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
/** The nodes of the current page, tagged by source, for a combined list. */
|
|
329
|
+
readonly mergedResults = computed<MergedResultItem[]>(() => {
|
|
330
|
+
const page = this.globalPagination();
|
|
331
|
+
if (this.mergedMode()) {
|
|
332
|
+
return windowMergedPage(this.concatenatedNodes(), page.pageIndex, page.pageSize);
|
|
333
|
+
}
|
|
334
|
+
const indexBySource: Record<string, number> = {};
|
|
335
|
+
for (const v of this.inScopeSources()) indexBySource[v.config.key] = v.pageIndex;
|
|
336
|
+
return filterToGlobalPage(this.concatenatedNodes(), indexBySource, page.pageIndex);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
constructor() {
|
|
340
|
+
this.urlSync$
|
|
341
|
+
.pipe(debounceTime(URL_SYNC_DEBOUNCE_MS), takeUntilDestroyed(this.destroyRef))
|
|
342
|
+
.subscribe(() => this.flushToUrl());
|
|
343
|
+
|
|
344
|
+
// An external change of `q` (e.g. GlobalSearchBox navigating while this page
|
|
345
|
+
// is already open) re-seeds the term. Our own URL writes are ignored by
|
|
346
|
+
// comparing against the last `q` we wrote or seeded.
|
|
347
|
+
this.route.queryParamMap.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((map) => {
|
|
348
|
+
if (!this._config()) return;
|
|
349
|
+
const q = map.get(GLOBAL_QUERY_KEY) ?? "";
|
|
350
|
+
if (q === this.lastSyncedQ) return;
|
|
351
|
+
this.lastSyncedQ = q;
|
|
352
|
+
this._q.set(q);
|
|
353
|
+
this._globalPageIndex.set(0);
|
|
354
|
+
this.resetAllCursors();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// A restored (or stale) merged page past the last page is pulled back once
|
|
358
|
+
// the totals are known, which also shrinks the fetch window.
|
|
359
|
+
effect(() => {
|
|
360
|
+
if (!this.mergedMode() || this.loading() || this.requests().length === 0) return;
|
|
361
|
+
if (this.fetched.data() === undefined) return;
|
|
362
|
+
const clamped = this.globalPagination().pageIndex;
|
|
363
|
+
if (untracked(this._globalPageIndex) > clamped) {
|
|
364
|
+
this._globalPageIndex.set(clamped);
|
|
365
|
+
this.scheduleUrlSync();
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Seeds state from the config and the current URL. Safe to call once; later
|
|
372
|
+
* calls are ignored (the config is stable for the service's lifetime).
|
|
373
|
+
*/
|
|
374
|
+
init(config: SearchConfig, options?: SearchInitOptions): void {
|
|
375
|
+
if (this._config()) return;
|
|
376
|
+
try {
|
|
377
|
+
validateSearchConfig(config);
|
|
378
|
+
} catch (err) {
|
|
379
|
+
this._configError.set(err instanceof Error ? err.message : String(err));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const pagination = resolvePagination(config);
|
|
383
|
+
|
|
384
|
+
const locked = options?.lockedScope;
|
|
385
|
+
const lockedScope: SearchScope | undefined =
|
|
386
|
+
locked === undefined
|
|
387
|
+
? undefined
|
|
388
|
+
: locked !== ALL_SCOPE && config.sources.some((s) => s.key === locked)
|
|
389
|
+
? locked
|
|
390
|
+
: ALL_SCOPE;
|
|
391
|
+
|
|
392
|
+
const params = new URLSearchParams();
|
|
393
|
+
for (const [key, raw] of Object.entries(this.route.snapshot.queryParams)) {
|
|
394
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
395
|
+
if (typeof value === "string") params.set(key, value);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const states: Record<string, SourceLocalState> = {};
|
|
399
|
+
for (const source of config.sources) {
|
|
400
|
+
const read = readSourceParams(params, source.key, source.filterBy ?? [], {
|
|
401
|
+
sortBy: source.sortBy,
|
|
402
|
+
});
|
|
403
|
+
const fresh = freshSourceState(
|
|
404
|
+
validatePageSize(pagination, read.pageSize ?? pagination.pageSize),
|
|
405
|
+
read.sort ?? source.defaultSort ?? null,
|
|
406
|
+
);
|
|
407
|
+
// Only the current page's cursor is known after a reload; the stack holds
|
|
408
|
+
// just that one entry, so Previous falls back to page one (see
|
|
409
|
+
// goToPreviousSourcePage).
|
|
410
|
+
states[source.key] = {
|
|
411
|
+
...fresh,
|
|
412
|
+
pageIndex: read.afterCursor ? Math.min(read.pageIndex, MAX_RESTORED_PAGE) : 0,
|
|
413
|
+
afterCursor: read.afterCursor,
|
|
414
|
+
cursorStack: read.afterCursor ? [read.afterCursor] : [],
|
|
415
|
+
filters: read.filters.map((f) => ({ ...f, type: activeTypeFor(f.type) })),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
this._pagination.set(pagination);
|
|
420
|
+
let globalSize = pagination.pageSize;
|
|
421
|
+
if (pagination.mode === "merged") {
|
|
422
|
+
const size = parseInt(params.get(GLOBAL_SIZE_KEY) ?? "", 10);
|
|
423
|
+
if (!isNaN(size)) globalSize = validatePageSize(pagination, size);
|
|
424
|
+
}
|
|
425
|
+
this._globalPageSize.set(globalSize);
|
|
426
|
+
this._lockedScope.set(lockedScope);
|
|
427
|
+
this._scope.set(lockedScope ?? ALL_SCOPE);
|
|
428
|
+
this._q.set(params.get(GLOBAL_QUERY_KEY) ?? "");
|
|
429
|
+
if (pagination.mode === "merged") {
|
|
430
|
+
const page = parseInt(params.get(GLOBAL_PAGE_KEY) ?? "", 10);
|
|
431
|
+
const maxIndex = Math.max(
|
|
432
|
+
0,
|
|
433
|
+
Math.floor(Math.max(MAX_MERGED_WINDOW, globalSize) / globalSize) - 1,
|
|
434
|
+
);
|
|
435
|
+
this._globalPageIndex.set(!isNaN(page) && page > 1 ? Math.min(page - 1, maxIndex) : 0);
|
|
436
|
+
}
|
|
437
|
+
this.lastSyncedQ = params.get(GLOBAL_QUERY_KEY) ?? "";
|
|
438
|
+
this._sourceStates.set(states);
|
|
439
|
+
this._config.set(config);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// -- Global term / scope ----------------------------------------------------
|
|
443
|
+
|
|
444
|
+
setQ(q: string): void {
|
|
445
|
+
this._q.set(q);
|
|
446
|
+
this._globalPageIndex.set(0);
|
|
447
|
+
this.resetAllCursors();
|
|
448
|
+
this.scheduleUrlSync();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** No-op when the scope is locked; unknown values fall back to "all". */
|
|
452
|
+
setScope(scope: SearchScope): void {
|
|
453
|
+
const config = this._config();
|
|
454
|
+
if (!config || this._lockedScope() !== undefined) return;
|
|
455
|
+
this._scope.set(validateScope(config, scope));
|
|
456
|
+
this._globalPageIndex.set(0);
|
|
457
|
+
this.resetAllCursors();
|
|
458
|
+
this.scheduleUrlSync();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Clears the term and every source's filters / sort / paging (scope stays locked if locked). */
|
|
462
|
+
resetAll(): void {
|
|
463
|
+
const config = this._config();
|
|
464
|
+
if (!config) return;
|
|
465
|
+
const pagination = this._pagination();
|
|
466
|
+
this._q.set("");
|
|
467
|
+
this._scope.set(this._lockedScope() ?? ALL_SCOPE);
|
|
468
|
+
this._globalPageIndex.set(0);
|
|
469
|
+
this._globalPageSize.set(pagination.pageSize);
|
|
470
|
+
const states: Record<string, SourceLocalState> = {};
|
|
471
|
+
for (const source of config.sources) {
|
|
472
|
+
states[source.key] = freshSourceState(pagination.pageSize, source.defaultSort ?? null);
|
|
473
|
+
}
|
|
474
|
+
this._sourceStates.set(states);
|
|
475
|
+
this.flushToUrl();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// -- Per-source filters / sort ----------------------------------------------
|
|
479
|
+
|
|
480
|
+
/** The input-shaped value of a source's active filter (for filter components). */
|
|
481
|
+
filterValue(sourceKey: string, field: string): FilterValue | undefined {
|
|
482
|
+
return toFilterValue(this._sourceStates()[sourceKey]?.filters.find((f) => f.field === field));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Sets the filter from an input value; an empty value removes it. */
|
|
486
|
+
setFilterValue(
|
|
487
|
+
sourceKey: string,
|
|
488
|
+
config: FilterFieldConfig,
|
|
489
|
+
value: FilterValue | undefined,
|
|
490
|
+
): void {
|
|
491
|
+
this.setFilter(sourceKey, config.field, toActiveFilter(config, value));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Adds or replaces the filter for `field`; `undefined` removes it. */
|
|
495
|
+
setFilter(sourceKey: string, field: string, value: ActiveFilterValue | undefined): void {
|
|
496
|
+
this.updateSource(
|
|
497
|
+
sourceKey,
|
|
498
|
+
(prev) => {
|
|
499
|
+
const filters = prev.filters.filter((f) => f.field !== field);
|
|
500
|
+
if (value) filters.push(value);
|
|
501
|
+
return { ...prev, filters };
|
|
502
|
+
},
|
|
503
|
+
true,
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
removeFilter(sourceKey: string, field: string): void {
|
|
508
|
+
this.setFilter(sourceKey, field, undefined);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** Removes every active filter of one source. */
|
|
512
|
+
clearFilters(sourceKey: string): void {
|
|
513
|
+
this.updateSource(sourceKey, (prev) => ({ ...prev, filters: [] }), true);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
setSort(sourceKey: string, sort: SortState | null): void {
|
|
517
|
+
this.updateSource(sourceKey, (prev) => ({ ...prev, sort }), true);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// -- Per-source pagination --------------------------------------------------
|
|
521
|
+
|
|
522
|
+
setSourcePageSize(sourceKey: string, size: number): void {
|
|
523
|
+
const valid = validatePageSize(this._pagination(), size);
|
|
524
|
+
this.updateSource(sourceKey, (prev) => ({ ...prev, pageSize: valid }), true);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
goToNextSourcePage(sourceKey: string): void {
|
|
528
|
+
const cursor = this.sources()[sourceKey]?.result?.pageInfo?.endCursor;
|
|
529
|
+
if (!cursor) return;
|
|
530
|
+
this.updateSource(
|
|
531
|
+
sourceKey,
|
|
532
|
+
(prev) => ({
|
|
533
|
+
...prev,
|
|
534
|
+
cursorStack: [...prev.cursorStack, cursor],
|
|
535
|
+
afterCursor: cursor,
|
|
536
|
+
pageIndex: prev.pageIndex + 1,
|
|
537
|
+
}),
|
|
538
|
+
false,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
goToPreviousSourcePage(sourceKey: string): void {
|
|
543
|
+
this.updateSource(
|
|
544
|
+
sourceKey,
|
|
545
|
+
(prev) => {
|
|
546
|
+
if (prev.pageIndex === 0) return prev;
|
|
547
|
+
// The cursor of the previous page is the second-from-last stack entry.
|
|
548
|
+
// After a reload only the current cursor is known, so go to page one.
|
|
549
|
+
if (prev.pageIndex === 1 || prev.cursorStack.length < 2) return withFirstPage(prev);
|
|
550
|
+
const cursorStack = prev.cursorStack.slice(0, -1);
|
|
551
|
+
return {
|
|
552
|
+
...prev,
|
|
553
|
+
cursorStack,
|
|
554
|
+
afterCursor: cursorStack[cursorStack.length - 1],
|
|
555
|
+
pageIndex: Math.max(0, prev.pageIndex - 1),
|
|
556
|
+
};
|
|
557
|
+
},
|
|
558
|
+
false,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// -- Global pagination ------------------------------------------------------
|
|
563
|
+
|
|
564
|
+
goToNextPage(): void {
|
|
565
|
+
if (this.mergedMode()) {
|
|
566
|
+
if (this.globalPagination().hasNextPage) {
|
|
567
|
+
this._globalPageIndex.update((p) => p + 1);
|
|
568
|
+
this.scheduleUrlSync();
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
for (const v of this.inScopeSources()) {
|
|
573
|
+
if (v.hasNextPage) this.goToNextSourcePage(v.config.key);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
goToPreviousPage(): void {
|
|
578
|
+
if (this.mergedMode()) {
|
|
579
|
+
this._globalPageIndex.update((p) => Math.max(0, p - 1));
|
|
580
|
+
this.scheduleUrlSync();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const current = this.globalPagination().pageIndex;
|
|
584
|
+
for (const v of this.inScopeSources()) {
|
|
585
|
+
if (v.pageIndex === current) this.goToPreviousSourcePage(v.config.key);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/** Jumps to a 0-based page. Merged: any page. Per-source: only the adjacent pages. */
|
|
590
|
+
goToPage(pageIndex: number): void {
|
|
591
|
+
if (!Number.isFinite(pageIndex)) return;
|
|
592
|
+
const target = Math.max(0, Math.floor(pageIndex));
|
|
593
|
+
if (this.mergedMode()) {
|
|
594
|
+
const last = Math.max(0, this.globalPagination().pageCount - 1);
|
|
595
|
+
this._globalPageIndex.set(Math.min(target, last));
|
|
596
|
+
this.scheduleUrlSync();
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const page = this.globalPagination();
|
|
600
|
+
if (target === page.pageIndex + 1 && page.hasNextPage) this.goToNextPage();
|
|
601
|
+
else if (target === page.pageIndex - 1 && page.hasPreviousPage) this.goToPreviousPage();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/** Sets the page size for the whole search (every source in per-source mode). */
|
|
605
|
+
setPageSize(size: number): void {
|
|
606
|
+
const valid = validatePageSize(this._pagination(), size);
|
|
607
|
+
if (this.mergedMode()) {
|
|
608
|
+
this._globalPageSize.set(valid);
|
|
609
|
+
this._globalPageIndex.set(0);
|
|
610
|
+
this.scheduleUrlSync();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
// Every source, not only the in-scope ones, so sizes cannot diverge after a
|
|
614
|
+
// later scope change.
|
|
615
|
+
this._globalPageIndex.set(0);
|
|
616
|
+
this._sourceStates.update((prev) => {
|
|
617
|
+
const next: Record<string, SourceLocalState> = {};
|
|
618
|
+
for (const [key, st] of Object.entries(prev)) {
|
|
619
|
+
next[key] = withFirstPage({ ...st, pageSize: valid });
|
|
620
|
+
}
|
|
621
|
+
return next;
|
|
622
|
+
});
|
|
623
|
+
this.scheduleUrlSync();
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// -- Distinct picklist values -----------------------------------------------
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Distinct values of a picklist field, fetched once (per object + field) on
|
|
630
|
+
* first read. Empty until loaded, and on failure.
|
|
631
|
+
*/
|
|
632
|
+
distinctOptions(objectName: string, fieldName: string): Signal<readonly PicklistOption[]> {
|
|
633
|
+
const cacheKey = `${objectName}.${fieldName}`;
|
|
634
|
+
let options = this.distinctCache.get(cacheKey);
|
|
635
|
+
if (!options) {
|
|
636
|
+
const created = signal<readonly PicklistOption[]>([]);
|
|
637
|
+
options = created;
|
|
638
|
+
this.distinctCache.set(cacheKey, created);
|
|
639
|
+
this.searchService
|
|
640
|
+
.fetchDistinctValues(objectName, fieldName)
|
|
641
|
+
.then((values) => created.set(values))
|
|
642
|
+
.catch((err: unknown) => {
|
|
643
|
+
console.error(err);
|
|
644
|
+
// Drop the entry so a later read retries instead of caching "empty" forever.
|
|
645
|
+
this.distinctCache.delete(cacheKey);
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
return options.asReadonly();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// -- Internals --------------------------------------------------------------
|
|
652
|
+
|
|
653
|
+
private async fetch(requests: SourceRequest[]): Promise<RunSearchOutcome> {
|
|
654
|
+
if (requests.length === 0) return EMPTY_OUTCOME;
|
|
655
|
+
return this.searchService.runSearch(requests);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private updateSource(
|
|
659
|
+
sourceKey: string,
|
|
660
|
+
updater: (prev: SourceLocalState) => SourceLocalState,
|
|
661
|
+
resetPagination: boolean,
|
|
662
|
+
): void {
|
|
663
|
+
if (!this._sourceStates()[sourceKey]) return;
|
|
664
|
+
// A filter / sort / page-size change reshapes the result set, so the
|
|
665
|
+
// global merged page returns to the first page too.
|
|
666
|
+
if (resetPagination) this._globalPageIndex.set(0);
|
|
667
|
+
this._sourceStates.update((prev) => {
|
|
668
|
+
const prior = prev[sourceKey];
|
|
669
|
+
if (!prior) return prev;
|
|
670
|
+
const next = updater(prior);
|
|
671
|
+
return { ...prev, [sourceKey]: resetPagination ? withFirstPage(next) : next };
|
|
672
|
+
});
|
|
673
|
+
this.scheduleUrlSync();
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private resetAllCursors(): void {
|
|
677
|
+
this._sourceStates.update((prev) => {
|
|
678
|
+
const next: Record<string, SourceLocalState> = {};
|
|
679
|
+
for (const [key, st] of Object.entries(prev)) next[key] = withFirstPage(st);
|
|
680
|
+
return next;
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private scheduleUrlSync(): void {
|
|
685
|
+
this.urlSync$.next();
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private flushToUrl(): void {
|
|
689
|
+
const config = this._config();
|
|
690
|
+
if (!config) return;
|
|
691
|
+
const params = new URLSearchParams();
|
|
692
|
+
const q = this._q();
|
|
693
|
+
if (q) params.set(GLOBAL_QUERY_KEY, q);
|
|
694
|
+
const globalPage = this._globalPageIndex();
|
|
695
|
+
if (this.mergedMode()) {
|
|
696
|
+
if (globalPage > 0) params.set(GLOBAL_PAGE_KEY, String(globalPage + 1));
|
|
697
|
+
const size = this._globalPageSize();
|
|
698
|
+
if (size !== this._pagination().pageSize) params.set(GLOBAL_SIZE_KEY, String(size));
|
|
699
|
+
}
|
|
700
|
+
const states = this._sourceStates();
|
|
701
|
+
const defaultPageSize = this._pagination().pageSize;
|
|
702
|
+
for (const source of config.sources) {
|
|
703
|
+
const st = states[source.key];
|
|
704
|
+
if (!st) continue;
|
|
705
|
+
// Sort / page size equal to the defaults are omitted so a reset leaves a
|
|
706
|
+
// clean query string.
|
|
707
|
+
writeSourceParams(
|
|
708
|
+
params,
|
|
709
|
+
source.key,
|
|
710
|
+
st.filters,
|
|
711
|
+
st.sort,
|
|
712
|
+
st.pageSize,
|
|
713
|
+
st.pageIndex,
|
|
714
|
+
st.afterCursor,
|
|
715
|
+
{
|
|
716
|
+
sort: source.defaultSort ?? null,
|
|
717
|
+
pageSize: defaultPageSize,
|
|
718
|
+
},
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
const queryParams: Record<string, string | null> = Object.fromEntries(params.entries());
|
|
722
|
+
// Merge so host-app params survive; our own now-unused keys are nulled out.
|
|
723
|
+
for (const key of Object.keys(this.route.snapshot.queryParams)) {
|
|
724
|
+
if (!(key in queryParams) && isOwnParam(key)) queryParams[key] = null;
|
|
725
|
+
}
|
|
726
|
+
this.lastSyncedQ = q;
|
|
727
|
+
void this.router.navigate([], {
|
|
728
|
+
relativeTo: this.route,
|
|
729
|
+
queryParams,
|
|
730
|
+
queryParamsHandling: "merge",
|
|
731
|
+
// Replace so typing does not flood browser history.
|
|
732
|
+
replaceUrl: true,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
}
|