@salesforce/ui-bundle-template-app-angular-template-b2e 11.30.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/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 +3966 -0
- package/dist/README.md +77 -0
- package/dist/config/project-scratch-def.json +13 -0
- package/dist/eslint.config.js +7 -0
- package/dist/force-app/main/default/applications/angularinternalapp.app-meta.xml +17 -0
- package/dist/force-app/main/default/permissionsets/angularinternalapp_Access.permissionset-meta.xml +13 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/.forceignore +15 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/.postcssrc.json +5 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/.prettierignore +8 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/.prettierrc +12 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/CHANGELOG.md +4 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/README.md +85 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/angular.json +98 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/angularinternalapp.uibundle-meta.xml +8 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/e2e/app.spec.ts +15 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/esbuild/api-version.mjs +17 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/eslint.config.js +60 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/middleware/html.mjs +3 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/middleware/proxy.mjs +12 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/package.json +57 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/playwright.config.ts +25 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/public/favicon.ico +0 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/scripts/rewrite-e2e-assets.mjs +30 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/account/account-search.service.ts +196 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/account/distinctAccountIndustries.ts +26 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/account/distinctAccountTypes.ts +26 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/account/getAccountDetail.ts +128 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/account/searchAccounts.ts +58 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/data-client.service.spec.ts +184 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/api/data-client.service.ts +74 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/app.config.ts +22 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/app.html +1 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/app.routes.ts +31 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/app.spec.ts +18 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/app.ts +15 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/layout/app-layout/app-layout.component.html +52 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/layout/app-layout/app-layout.component.spec.ts +90 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/layout/app-layout/app-layout.component.ts +67 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/_field-size.scss +42 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/alert/alert.component.html +31 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/alert/alert.component.ts +53 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/button/button.component.html +18 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/button/button.component.scss +112 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/button/button.component.ts +57 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/card/card.component.scss +34 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/card/card.component.ts +110 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/collapsible/collapsible.component.html +10 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/collapsible/collapsible.component.ts +26 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-picker/date-picker.component.html +25 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-picker/date-picker.component.scss +36 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-picker/date-picker.component.ts +41 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-range-picker/date-range-picker.component.html +34 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-range-picker/date-range-picker.component.scss +33 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/date-range-picker/date-range-picker.component.ts +66 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/dialog/dialog.component.html +37 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/dialog/dialog.component.ts +55 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/field/field.component.html +21 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/field/field.component.ts +36 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/field-size.ts +8 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/input/input.component.html +19 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/input/input.component.scss +35 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/input/input.component.ts +36 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/label/label.component.html +3 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/label/label.component.ts +17 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/paginator/paginator.component.html +10 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/paginator/paginator.component.ts +37 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/popover/popover.component.html +41 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/popover/popover.component.ts +40 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/select/select.component.html +24 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/select/select.component.scss +35 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/select/select.component.ts +72 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/separator/separator.component.html +1 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/separator/separator.component.scss +23 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/separator/separator.component.ts +56 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/skeleton/skeleton.component.html +1 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/skeleton/skeleton.component.scss +28 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/skeleton/skeleton.component.ts +24 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/components/ui/spinner/spinner.component.ts +35 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/agentforce/agentforce-conversation-client/agentforce-conversation-client.component.html +3 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/agentforce/agentforce-conversation-client/agentforce-conversation-client.component.ts +119 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/agentforce/agentforce-embed.service.ts +177 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/active-filters/active-filters.component.html +23 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/active-filters/active-filters.component.ts +78 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/date-filter/date-filter.component.html +18 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/date-filter/date-filter.component.ts +137 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/date-range-filter/date-range-filter.component.html +11 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/date-range-filter/date-range-filter.component.ts +69 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/filter-field-wrapper/filter-field-wrapper.component.html +11 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/filter-field-wrapper/filter-field-wrapper.component.ts +36 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/multi-select-filter/multi-select-filter.component.html +10 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/multi-select-filter/multi-select-filter.component.ts +58 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/numeric-range-filter/numeric-range-filter.component.html +22 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/numeric-range-filter/numeric-range-filter.component.ts +146 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/search-filter/search-filter.component.html +8 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/search-filter/search-filter.component.ts +82 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/select-filter/select-filter.component.html +12 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/select-filter/select-filter.component.ts +65 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/text-filter/text-filter.component.html +12 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/filters/text-filter/text-filter.component.ts +90 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/object-breadcrumb/object-breadcrumb.component.html +55 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/object-breadcrumb/object-breadcrumb.component.ts +40 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/pagination-controls/pagination-controls.component.html +8 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/pagination-controls/pagination-controls.component.ts +61 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/search-bar/search-bar.component.html +59 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/search-bar/search-bar.component.ts +108 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/sort-control/sort-control.component.html +50 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/components/sort-control/sort-control.component.ts +72 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/object-search-state.service.ts +232 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/utils/debounce.ts +31 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/utils/field-utils.ts +54 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/utils/filter-utils.ts +354 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/utils/index.ts +12 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/features/object-search/utils/sort-utils.ts +46 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/account-detail/account-detail-page.component.html +174 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/account-detail/account-detail-page.component.scss +7 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/account-detail/account-detail-page.component.ts +199 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/account-search/account-search-page.component.html +111 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/account-search/account-search-page.component.ts +217 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/home/home-page.component.html +18 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/home/home-page.component.ts +47 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/home/home.component.html +6 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/home/home.component.spec.ts +23 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/home/home.component.ts +13 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/not-found/not-found.component.html +12 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/not-found/not-found.component.spec.ts +33 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/pages/not-found/not-found.component.ts +15 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/types/conversation.ts +55 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/types/globals.d.ts +13 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/app/utils/async-data.ts +99 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/index.html +14 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/index.ts +15 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/main.ts +5 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/styles.scss +170 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/theme.scss +39 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/src/types/sf-globals.d.ts +9 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/tsconfig.app.json +11 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/tsconfig.json +33 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/tsconfig.spec.json +10 -0
- package/dist/force-app/main/default/uiBundles/angularinternalapp/ui-bundle.json +7 -0
- package/dist/jest.config.js +6 -0
- package/dist/package-lock.json +10066 -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/org-setup-config-schema.mjs +103 -0
- package/dist/scripts/org-setup-dev.mjs +87 -0
- package/dist/scripts/org-setup-utils.mjs +369 -0
- package/dist/scripts/org-setup-xml.mjs +182 -0
- package/dist/scripts/org-setup.config.json +9 -0
- package/dist/scripts/org-setup.mjs +1702 -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 +40 -0
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
ChangeDetectionStrategy,
|
|
8
|
+
Component,
|
|
9
|
+
DestroyRef,
|
|
10
|
+
computed,
|
|
11
|
+
effect,
|
|
12
|
+
inject,
|
|
13
|
+
input,
|
|
14
|
+
signal,
|
|
15
|
+
} from "@angular/core";
|
|
16
|
+
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
17
|
+
import { Subject } from "rxjs";
|
|
18
|
+
import { debounceTime } from "rxjs/operators";
|
|
19
|
+
import { MatInputModule } from "@angular/material/input";
|
|
20
|
+
import { MatFormFieldModule } from "@angular/material/form-field";
|
|
21
|
+
import { FormsModule } from "@angular/forms";
|
|
22
|
+
// Base-app UI wrapper, inherited (not recreated) by this feature. Post-compose
|
|
23
|
+
// it lands at uiBundles/<target>/src/app/components/ui/input/..., three levels up.
|
|
24
|
+
import { InputComponent } from "../../../../../components/ui/input/input.component";
|
|
25
|
+
import { ObjectSearchStateService } from "../../../object-search-state.service";
|
|
26
|
+
import { FILTER_DEBOUNCE_MS } from "../../../utils/debounce";
|
|
27
|
+
import type { FilterFieldConfig } from "../../../utils/filter-utils";
|
|
28
|
+
import { FilterFieldWrapperComponent } from "../filter-field-wrapper/filter-field-wrapper.component";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Numeric range filter — paired `min` / `max` inputs whose committed values are
|
|
32
|
+
* written as strings on a `type: 'numeric'` filter (numeric coercion happens
|
|
33
|
+
* in `buildSingleFilter`).
|
|
34
|
+
*
|
|
35
|
+
* Local signals drive the inputs immediately; a trailing debounce commits the
|
|
36
|
+
* pair, but only when the range is self-consistent (`min <= max`) and within
|
|
37
|
+
* `boundMin` / `boundMax` if provided — an inverted or out-of-bounds pair
|
|
38
|
+
* shows the same inline error the React reference renders and skips the write.
|
|
39
|
+
*/
|
|
40
|
+
@Component({
|
|
41
|
+
selector: "app-numeric-range-filter",
|
|
42
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
43
|
+
imports: [
|
|
44
|
+
InputComponent,
|
|
45
|
+
FilterFieldWrapperComponent,
|
|
46
|
+
FormsModule,
|
|
47
|
+
MatFormFieldModule,
|
|
48
|
+
MatInputModule,
|
|
49
|
+
],
|
|
50
|
+
templateUrl: "./numeric-range-filter.component.html",
|
|
51
|
+
})
|
|
52
|
+
export class NumericRangeFilterComponent {
|
|
53
|
+
private readonly state = inject(ObjectSearchStateService);
|
|
54
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
55
|
+
|
|
56
|
+
/** Field config: `field`, `label`, `helpText`, ... */
|
|
57
|
+
readonly config = input.required<FilterFieldConfig>();
|
|
58
|
+
/** Optional lower bound accepted by the field (used for validation only). */
|
|
59
|
+
readonly boundMin = input<number | undefined>(undefined);
|
|
60
|
+
/** Optional upper bound accepted by the field (used for validation only). */
|
|
61
|
+
readonly boundMax = input<number | undefined>(undefined);
|
|
62
|
+
|
|
63
|
+
protected readonly localMin = signal<string>("");
|
|
64
|
+
protected readonly localMax = signal<string>("");
|
|
65
|
+
|
|
66
|
+
private readonly commit$ = new Subject<{ min: string; max: string }>();
|
|
67
|
+
|
|
68
|
+
constructor() {
|
|
69
|
+
this.commit$
|
|
70
|
+
.pipe(debounceTime(FILTER_DEBOUNCE_MS), takeUntilDestroyed(this.destroyRef))
|
|
71
|
+
.subscribe(({ min, max }) => this.commit(min, max));
|
|
72
|
+
|
|
73
|
+
// Sync local inputs from shared state (URL restore / reset-all).
|
|
74
|
+
effect(() => {
|
|
75
|
+
const cfg = this.config();
|
|
76
|
+
const active = this.state.filterFor(cfg.field);
|
|
77
|
+
this.localMin.set(active?.min ?? "");
|
|
78
|
+
this.localMax.set(active?.max ?? "");
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
protected readonly minOutOfBounds = computed(() => this.isOutOfBounds(this.localMin()));
|
|
83
|
+
protected readonly maxOutOfBounds = computed(() => this.isOutOfBounds(this.localMax()));
|
|
84
|
+
protected readonly isRangeInverted = computed(() => {
|
|
85
|
+
const lo = this.localMin();
|
|
86
|
+
const hi = this.localMax();
|
|
87
|
+
return lo !== "" && hi !== "" && Number(lo) > Number(hi);
|
|
88
|
+
});
|
|
89
|
+
protected readonly hasError = computed(
|
|
90
|
+
() => this.minOutOfBounds() || this.maxOutOfBounds() || this.isRangeInverted(),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
protected readonly errorMessage = computed<string | undefined>(() => {
|
|
94
|
+
if (this.isRangeInverted()) return "Min must not exceed max";
|
|
95
|
+
if (!this.minOutOfBounds() && !this.maxOutOfBounds()) return undefined;
|
|
96
|
+
const lo = this.boundMin();
|
|
97
|
+
const hi = this.boundMax();
|
|
98
|
+
if (lo != null && hi != null) return `Value must be between ${lo}–${hi}`;
|
|
99
|
+
if (lo != null) return `Value must be ${lo} or more`;
|
|
100
|
+
if (hi != null) return `Value must be ${hi} or less`;
|
|
101
|
+
return undefined;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
protected onMinChange(v: string): void {
|
|
105
|
+
this.localMin.set(v);
|
|
106
|
+
this.commit$.next({ min: v, max: this.localMax() });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
protected onMaxChange(v: string): void {
|
|
110
|
+
this.localMax.set(v);
|
|
111
|
+
this.commit$.next({ min: this.localMin(), max: v });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private isOutOfBounds(v: string): boolean {
|
|
115
|
+
if (v === "") return false;
|
|
116
|
+
const n = Number(v);
|
|
117
|
+
const lo = this.boundMin();
|
|
118
|
+
const hi = this.boundMax();
|
|
119
|
+
return (lo != null && n < lo) || (hi != null && n > hi);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private commit(min: string, max: string): void {
|
|
123
|
+
const cfg = this.config();
|
|
124
|
+
if (!min && !max) {
|
|
125
|
+
this.state.removeFilter(cfg.field);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const minNum = min !== "" ? Number(min) : null;
|
|
129
|
+
const maxNum = max !== "" ? Number(max) : null;
|
|
130
|
+
// Skip writes that would produce an inverted or out-of-bounds clause;
|
|
131
|
+
// the UI keeps the input contents so the user can correct in place.
|
|
132
|
+
if (minNum != null && maxNum != null && minNum > maxNum) return;
|
|
133
|
+
const lo = this.boundMin();
|
|
134
|
+
const hi = this.boundMax();
|
|
135
|
+
if (minNum != null && ((lo != null && minNum < lo) || (hi != null && minNum > hi))) return;
|
|
136
|
+
if (maxNum != null && ((lo != null && maxNum < lo) || (hi != null && maxNum > hi))) return;
|
|
137
|
+
|
|
138
|
+
this.state.setFilter(cfg.field, {
|
|
139
|
+
field: cfg.field,
|
|
140
|
+
label: cfg.label,
|
|
141
|
+
type: "numeric",
|
|
142
|
+
min,
|
|
143
|
+
max,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
ChangeDetectionStrategy,
|
|
8
|
+
Component,
|
|
9
|
+
DestroyRef,
|
|
10
|
+
effect,
|
|
11
|
+
inject,
|
|
12
|
+
input,
|
|
13
|
+
signal,
|
|
14
|
+
} from "@angular/core";
|
|
15
|
+
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
16
|
+
import { Subject } from "rxjs";
|
|
17
|
+
import { debounceTime } from "rxjs/operators";
|
|
18
|
+
// Base-app UI wrapper, inherited (not recreated) by this feature. Post-compose
|
|
19
|
+
// it lands at uiBundles/<target>/src/app/components/ui/input/..., three levels up.
|
|
20
|
+
import { InputComponent } from "../../../../../components/ui/input/input.component";
|
|
21
|
+
import { ObjectSearchStateService } from "../../../object-search-state.service";
|
|
22
|
+
import { FILTER_DEBOUNCE_MS } from "../../../utils/debounce";
|
|
23
|
+
import type { FilterFieldConfig } from "../../../utils/filter-utils";
|
|
24
|
+
import { FilterFieldWrapperComponent } from "../filter-field-wrapper/filter-field-wrapper.component";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Multi-field search filter — a debounced input whose value is applied via
|
|
28
|
+
* `like %value%` across every field named in `config.searchFields` (combined
|
|
29
|
+
* with a top-level `or`, see {@link buildFilter}).
|
|
30
|
+
*
|
|
31
|
+
* Shape mirrors {@link TextFilterComponent} but emits a `type: 'search'`
|
|
32
|
+
* {@link ActiveFilterValue} so `filter-utils` picks the multi-field path.
|
|
33
|
+
*/
|
|
34
|
+
@Component({
|
|
35
|
+
selector: "app-search-filter",
|
|
36
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
37
|
+
imports: [InputComponent, FilterFieldWrapperComponent],
|
|
38
|
+
templateUrl: "./search-filter.component.html",
|
|
39
|
+
})
|
|
40
|
+
export class SearchFilterComponent {
|
|
41
|
+
private readonly state = inject(ObjectSearchStateService);
|
|
42
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
43
|
+
|
|
44
|
+
/** Field config: `field`, `label`, `placeholder`, `searchFields`, ... */
|
|
45
|
+
readonly config = input.required<FilterFieldConfig>();
|
|
46
|
+
|
|
47
|
+
protected readonly localValue = signal<string>("");
|
|
48
|
+
protected readonly inputId = signal<string>("");
|
|
49
|
+
private readonly commit$ = new Subject<string>();
|
|
50
|
+
|
|
51
|
+
constructor() {
|
|
52
|
+
this.commit$
|
|
53
|
+
.pipe(debounceTime(FILTER_DEBOUNCE_MS), takeUntilDestroyed(this.destroyRef))
|
|
54
|
+
.subscribe((v) => this.commit(v));
|
|
55
|
+
|
|
56
|
+
effect(() => {
|
|
57
|
+
const cfg = this.config();
|
|
58
|
+
this.inputId.set(`filter-${cfg.field}`);
|
|
59
|
+
const external = this.state.filterFor(cfg.field)?.value ?? "";
|
|
60
|
+
this.localValue.set(external);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
protected onValueChange(v: string): void {
|
|
65
|
+
this.localValue.set(v);
|
|
66
|
+
this.commit$.next(v);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private commit(v: string): void {
|
|
70
|
+
const cfg = this.config();
|
|
71
|
+
if (v) {
|
|
72
|
+
this.state.setFilter(cfg.field, {
|
|
73
|
+
field: cfg.field,
|
|
74
|
+
label: cfg.label,
|
|
75
|
+
type: "search",
|
|
76
|
+
value: v,
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
this.state.removeFilter(cfg.field);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<app-filter-field-wrapper
|
|
2
|
+
[label]="config().label"
|
|
3
|
+
[htmlFor]="inputId()"
|
|
4
|
+
[helpText]="config().helpText"
|
|
5
|
+
>
|
|
6
|
+
<app-select
|
|
7
|
+
[placeholder]="'Select ' + config().label.toLowerCase()"
|
|
8
|
+
[options]="options()"
|
|
9
|
+
[value]="currentValue()"
|
|
10
|
+
(valueChange)="onValueChange($event)"
|
|
11
|
+
/>
|
|
12
|
+
</app-filter-field-wrapper>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import { ChangeDetectionStrategy, Component, computed, inject, input } from "@angular/core";
|
|
7
|
+
// Base-app UI wrapper, inherited (not recreated) by this feature. Post-compose
|
|
8
|
+
// it lands at uiBundles/<target>/src/app/components/ui/select/..., three levels up.
|
|
9
|
+
import {
|
|
10
|
+
SelectComponent,
|
|
11
|
+
type AppSelectOption,
|
|
12
|
+
} from "../../../../../components/ui/select/select.component";
|
|
13
|
+
import { ObjectSearchStateService } from "../../../object-search-state.service";
|
|
14
|
+
import type { FilterFieldConfig } from "../../../utils/filter-utils";
|
|
15
|
+
import { FilterFieldWrapperComponent } from "../filter-field-wrapper/filter-field-wrapper.component";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Single-value picklist filter — a select rendered from `config.options` with
|
|
19
|
+
* a leading "All" entry (empty value) that clears the filter.
|
|
20
|
+
*
|
|
21
|
+
* Emits a `type: 'picklist'` filter which `buildSingleFilter` maps to
|
|
22
|
+
* `{ [field]: { eq: value } }`.
|
|
23
|
+
*/
|
|
24
|
+
const ALL_VALUE = "";
|
|
25
|
+
|
|
26
|
+
@Component({
|
|
27
|
+
selector: "app-select-filter",
|
|
28
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
29
|
+
imports: [SelectComponent, FilterFieldWrapperComponent],
|
|
30
|
+
templateUrl: "./select-filter.component.html",
|
|
31
|
+
})
|
|
32
|
+
export class SelectFilterComponent {
|
|
33
|
+
private readonly state = inject(ObjectSearchStateService);
|
|
34
|
+
|
|
35
|
+
/** Field config: `field`, `label`, `options`, `helpText`, ... */
|
|
36
|
+
readonly config = input.required<FilterFieldConfig>();
|
|
37
|
+
|
|
38
|
+
/** DOM id shared by the wrapper label and the underlying control. */
|
|
39
|
+
protected readonly inputId = computed(() => `filter-${this.config().field}`);
|
|
40
|
+
|
|
41
|
+
/** Options rendered in the select, with an "All" (clear) sentinel prepended. */
|
|
42
|
+
protected readonly options = computed<readonly AppSelectOption[]>(() => [
|
|
43
|
+
{ value: ALL_VALUE, label: "All" },
|
|
44
|
+
...(this.config().options ?? []),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** Current selection, reading through to shared state so URL changes reflect. */
|
|
48
|
+
protected readonly currentValue = computed(
|
|
49
|
+
() => this.state.filterFor(this.config().field)?.value ?? ALL_VALUE,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
protected onValueChange(v: string): void {
|
|
53
|
+
const cfg = this.config();
|
|
54
|
+
if (v === ALL_VALUE) {
|
|
55
|
+
this.state.removeFilter(cfg.field);
|
|
56
|
+
} else {
|
|
57
|
+
this.state.setFilter(cfg.field, {
|
|
58
|
+
field: cfg.field,
|
|
59
|
+
label: cfg.label,
|
|
60
|
+
type: "picklist",
|
|
61
|
+
value: v,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<app-filter-field-wrapper
|
|
2
|
+
[label]="config().label"
|
|
3
|
+
[htmlFor]="inputId()"
|
|
4
|
+
[helpText]="config().helpText"
|
|
5
|
+
>
|
|
6
|
+
<app-input
|
|
7
|
+
type="text"
|
|
8
|
+
[placeholder]="config().placeholder ?? 'Filter by ' + config().label.toLowerCase() + '...'"
|
|
9
|
+
[value]="localValue()"
|
|
10
|
+
(valueChange)="onValueChange($event)"
|
|
11
|
+
/>
|
|
12
|
+
</app-filter-field-wrapper>
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
+
ChangeDetectionStrategy,
|
|
8
|
+
Component,
|
|
9
|
+
DestroyRef,
|
|
10
|
+
effect,
|
|
11
|
+
inject,
|
|
12
|
+
input,
|
|
13
|
+
signal,
|
|
14
|
+
} from "@angular/core";
|
|
15
|
+
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
|
16
|
+
import { Subject } from "rxjs";
|
|
17
|
+
import { debounceTime } from "rxjs/operators";
|
|
18
|
+
// Base-app UI wrapper, inherited (not recreated) by this feature. Post-compose
|
|
19
|
+
// it lands at uiBundles/<target>/src/app/components/ui/input/..., three levels up.
|
|
20
|
+
import { InputComponent } from "../../../../../components/ui/input/input.component";
|
|
21
|
+
import { ObjectSearchStateService } from "../../../object-search-state.service";
|
|
22
|
+
import { FILTER_DEBOUNCE_MS } from "../../../utils/debounce";
|
|
23
|
+
import type { FilterFieldConfig } from "../../../utils/filter-utils";
|
|
24
|
+
import { FilterFieldWrapperComponent } from "../filter-field-wrapper/filter-field-wrapper.component";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Text filter — free-text input that writes a `like %value%` clause via
|
|
28
|
+
* {@link ObjectSearchStateService.setFilter}. Keystrokes update the local
|
|
29
|
+
* signal immediately (for a responsive UI) and flush to shared state on a
|
|
30
|
+
* {@link FILTER_DEBOUNCE_MS}ms trailing debounce (to avoid a query per key).
|
|
31
|
+
*
|
|
32
|
+
* Reads the current value from `state.filterFor(config.field)` so URL /
|
|
33
|
+
* reset-all changes reflect back into the input.
|
|
34
|
+
*/
|
|
35
|
+
@Component({
|
|
36
|
+
selector: "app-text-filter",
|
|
37
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
38
|
+
imports: [InputComponent, FilterFieldWrapperComponent],
|
|
39
|
+
templateUrl: "./text-filter.component.html",
|
|
40
|
+
})
|
|
41
|
+
export class TextFilterComponent {
|
|
42
|
+
private readonly state = inject(ObjectSearchStateService);
|
|
43
|
+
private readonly destroyRef = inject(DestroyRef);
|
|
44
|
+
|
|
45
|
+
/** Field config: `field`, `label`, `placeholder`, `helpText`, ... */
|
|
46
|
+
readonly config = input.required<FilterFieldConfig>();
|
|
47
|
+
|
|
48
|
+
/** Local (uncommitted) value that drives the input while the user is typing. */
|
|
49
|
+
protected readonly localValue = signal<string>("");
|
|
50
|
+
|
|
51
|
+
/** DOM id shared by the wrapper label and the underlying input. */
|
|
52
|
+
protected readonly inputId = signal<string>("");
|
|
53
|
+
|
|
54
|
+
/** Coalesces rapid keystrokes into one shared-state write. */
|
|
55
|
+
private readonly commit$ = new Subject<string>();
|
|
56
|
+
|
|
57
|
+
constructor() {
|
|
58
|
+
this.commit$
|
|
59
|
+
.pipe(debounceTime(FILTER_DEBOUNCE_MS), takeUntilDestroyed(this.destroyRef))
|
|
60
|
+
.subscribe((v) => this.commit(v));
|
|
61
|
+
|
|
62
|
+
// Sync the input with the shared state (URL restore, reset-all, ...).
|
|
63
|
+
// Mirrors the React "reset local when external changes" pattern.
|
|
64
|
+
effect(() => {
|
|
65
|
+
const cfg = this.config();
|
|
66
|
+
this.inputId.set(`filter-${cfg.field}`);
|
|
67
|
+
const external = this.state.filterFor(cfg.field)?.value ?? "";
|
|
68
|
+
this.localValue.set(external);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected onValueChange(v: string): void {
|
|
73
|
+
this.localValue.set(v);
|
|
74
|
+
this.commit$.next(v);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private commit(v: string): void {
|
|
78
|
+
const cfg = this.config();
|
|
79
|
+
if (v) {
|
|
80
|
+
this.state.setFilter(cfg.field, {
|
|
81
|
+
field: cfg.field,
|
|
82
|
+
label: cfg.label,
|
|
83
|
+
type: "text",
|
|
84
|
+
value: v,
|
|
85
|
+
});
|
|
86
|
+
} else {
|
|
87
|
+
this.state.removeFilter(cfg.field);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<nav aria-label="Breadcrumb" class="mb-3">
|
|
2
|
+
<ol
|
|
3
|
+
class="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5"
|
|
4
|
+
>
|
|
5
|
+
@if (includeHome()) {
|
|
6
|
+
<li class="inline-flex items-center gap-1.5">
|
|
7
|
+
<a [routerLink]="'/'" class="hover:text-foreground transition-colors">{{ homeLabel() }}</a>
|
|
8
|
+
</li>
|
|
9
|
+
<li aria-hidden="true" class="[&>svg]:size-3.5">
|
|
10
|
+
<svg
|
|
11
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
fill="none"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
stroke-width="2"
|
|
16
|
+
stroke-linecap="round"
|
|
17
|
+
stroke-linejoin="round"
|
|
18
|
+
>
|
|
19
|
+
<polyline points="9 18 15 12 9 6" />
|
|
20
|
+
</svg>
|
|
21
|
+
</li>
|
|
22
|
+
}
|
|
23
|
+
@if (isDetailView()) {
|
|
24
|
+
<li class="inline-flex items-center gap-1.5">
|
|
25
|
+
<a [routerLink]="listPath()" class="hover:text-foreground transition-colors">{{
|
|
26
|
+
listLabel()
|
|
27
|
+
}}</a>
|
|
28
|
+
</li>
|
|
29
|
+
<li aria-hidden="true" class="[&>svg]:size-3.5">
|
|
30
|
+
<svg
|
|
31
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
fill="none"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
stroke-width="2"
|
|
36
|
+
stroke-linecap="round"
|
|
37
|
+
stroke-linejoin="round"
|
|
38
|
+
>
|
|
39
|
+
<polyline points="9 18 15 12 9 6" />
|
|
40
|
+
</svg>
|
|
41
|
+
</li>
|
|
42
|
+
<li class="inline-flex items-center gap-1.5">
|
|
43
|
+
@if (loading() && !recordName()) {
|
|
44
|
+
<app-skeleton class="h-4 w-32" />
|
|
45
|
+
} @else {
|
|
46
|
+
<span class="text-foreground font-normal" aria-current="page">{{ recordName() }}</span>
|
|
47
|
+
}
|
|
48
|
+
</li>
|
|
49
|
+
} @else {
|
|
50
|
+
<li class="inline-flex items-center gap-1.5">
|
|
51
|
+
<span class="text-foreground font-normal" aria-current="page">{{ listLabel() }}</span>
|
|
52
|
+
</li>
|
|
53
|
+
}
|
|
54
|
+
</ol>
|
|
55
|
+
</nav>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core";
|
|
7
|
+
import { RouterLink } from "@angular/router";
|
|
8
|
+
import { SkeletonComponent } from "../../../../components/ui/skeleton/skeleton.component";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Object-search breadcrumb — Home > Records list > (optional) Record name.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors the React `ObjectBreadcrumb`. Renders the list-only crumb by default
|
|
14
|
+
* and switches to the detail-view crumb (with record name + skeleton fallback)
|
|
15
|
+
* as soon as `loading` or `recordName` is set. Uses Angular `routerLink` where
|
|
16
|
+
* the React version uses `react-router`'s `Link`.
|
|
17
|
+
*/
|
|
18
|
+
@Component({
|
|
19
|
+
selector: "app-object-breadcrumb",
|
|
20
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
21
|
+
imports: [RouterLink, SkeletonComponent],
|
|
22
|
+
templateUrl: "./object-breadcrumb.component.html",
|
|
23
|
+
})
|
|
24
|
+
export class ObjectBreadcrumbComponent {
|
|
25
|
+
/** Router path for the object list view (e.g. `/accounts`). */
|
|
26
|
+
readonly listPath = input.required<string>();
|
|
27
|
+
/** Label for the list crumb (e.g. `Accounts`). */
|
|
28
|
+
readonly listLabel = input.required<string>();
|
|
29
|
+
/** Record name for the detail-view leaf crumb; omit for list-only view. */
|
|
30
|
+
readonly recordName = input<string | undefined>(undefined);
|
|
31
|
+
/** Show a skeleton in the leaf crumb while the record is loading. */
|
|
32
|
+
readonly loading = input<boolean>(false);
|
|
33
|
+
/** Prepend a Home crumb (default `true`). */
|
|
34
|
+
readonly includeHome = input<boolean>(true);
|
|
35
|
+
/** Label for the Home crumb (default `Home`). */
|
|
36
|
+
readonly homeLabel = input<string>("Home");
|
|
37
|
+
|
|
38
|
+
/** `true` when the crumb should render list + record leaves. */
|
|
39
|
+
protected readonly isDetailView = computed(() => this.loading() || !!this.recordName());
|
|
40
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import { ChangeDetectionStrategy, Component, inject, input } from "@angular/core";
|
|
7
|
+
import type { PageEvent } from "@angular/material/paginator";
|
|
8
|
+
import { PaginatorComponent } from "../../../../components/ui/paginator/paginator.component";
|
|
9
|
+
import { ObjectSearchStateService } from "../../object-search-state.service";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Cursor-pagination bridge. Feeds the shared cursor state into the presentational
|
|
13
|
+
* {@link PaginatorComponent} (`app-paginator`) and translates its offset-style
|
|
14
|
+
* `(page)` event back into the cursor walk the state service understands.
|
|
15
|
+
*
|
|
16
|
+
* State comes from the shared {@link ObjectSearchStateService} (`pageIndex`,
|
|
17
|
+
* `pageSize`); the hosting page supplies the cursor-derived inputs
|
|
18
|
+
* (`totalCount`, `hasNextPage`, `endCursor`) after each fetch. A next-page step
|
|
19
|
+
* needs the current `endCursor`; a previous step and page-size changes are
|
|
20
|
+
* handled entirely by the state service's cursor stack.
|
|
21
|
+
*/
|
|
22
|
+
@Component({
|
|
23
|
+
selector: "app-pagination-controls",
|
|
24
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
25
|
+
imports: [PaginatorComponent],
|
|
26
|
+
templateUrl: "./pagination-controls.component.html",
|
|
27
|
+
})
|
|
28
|
+
export class PaginationControlsComponent {
|
|
29
|
+
/** Total row count from the latest fetch; drives the paginator's range label. */
|
|
30
|
+
readonly totalCount = input<number | undefined>(undefined);
|
|
31
|
+
/** Whether the API reports another page after the current one. */
|
|
32
|
+
readonly hasNextPage = input<boolean>(false);
|
|
33
|
+
/** `endCursor` from the latest page-info; forwarded to `goToNextPage`. */
|
|
34
|
+
readonly endCursor = input<string | undefined>(undefined);
|
|
35
|
+
/** Allowed page-size choices. */
|
|
36
|
+
readonly pageSizeOptions = input.required<readonly number[]>();
|
|
37
|
+
/** Disable navigation and the size select (e.g. while loading). */
|
|
38
|
+
readonly disabled = input<boolean>(false);
|
|
39
|
+
|
|
40
|
+
private readonly state = inject(ObjectSearchStateService);
|
|
41
|
+
protected readonly pageIndex = this.state.pageIndex;
|
|
42
|
+
protected readonly pageSize = this.state.pageSize;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Route Material's offset-style page event onto the cursor model: a page-size
|
|
46
|
+
* change resets pagination; a forward step walks the `endCursor`; a backward
|
|
47
|
+
* step pops the cursor stack.
|
|
48
|
+
*/
|
|
49
|
+
protected onPage(event: PageEvent): void {
|
|
50
|
+
if (event.pageSize !== this.pageSize()) {
|
|
51
|
+
this.state.setPageSize(event.pageSize);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (event.pageIndex > this.pageIndex()) {
|
|
55
|
+
const cursor = this.endCursor();
|
|
56
|
+
if (cursor && this.hasNextPage()) this.state.goToNextPage(cursor);
|
|
57
|
+
} else if (event.pageIndex < this.pageIndex()) {
|
|
58
|
+
this.state.goToPreviousPage();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<div class="flex items-center gap-2 w-full">
|
|
2
|
+
<div class="relative flex-1">
|
|
3
|
+
<svg
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-10"
|
|
6
|
+
viewBox="0 0 24 24"
|
|
7
|
+
fill="none"
|
|
8
|
+
stroke="currentColor"
|
|
9
|
+
stroke-width="2"
|
|
10
|
+
stroke-linecap="round"
|
|
11
|
+
stroke-linejoin="round"
|
|
12
|
+
aria-hidden="true"
|
|
13
|
+
>
|
|
14
|
+
<circle cx="11" cy="11" r="8" />
|
|
15
|
+
<path d="m21 21-4.3-4.3" />
|
|
16
|
+
</svg>
|
|
17
|
+
<app-input
|
|
18
|
+
class="pl-9 block"
|
|
19
|
+
type="text"
|
|
20
|
+
[placeholder]="searchConfig().placeholder ?? ''"
|
|
21
|
+
[label]="searchConfig().label"
|
|
22
|
+
[(value)]="term"
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
<app-popover [(open)]="popoverOpen" align="end">
|
|
26
|
+
<app-button
|
|
27
|
+
trigger
|
|
28
|
+
appearance="outlined"
|
|
29
|
+
[attr.aria-expanded]="popoverOpen()"
|
|
30
|
+
aria-label="Toggle filters"
|
|
31
|
+
class="gap-2"
|
|
32
|
+
>
|
|
33
|
+
<svg
|
|
34
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
35
|
+
class="h-4 w-4"
|
|
36
|
+
viewBox="0 0 24 24"
|
|
37
|
+
fill="none"
|
|
38
|
+
stroke="currentColor"
|
|
39
|
+
stroke-width="2"
|
|
40
|
+
stroke-linecap="round"
|
|
41
|
+
stroke-linejoin="round"
|
|
42
|
+
aria-hidden="true"
|
|
43
|
+
>
|
|
44
|
+
<path d="M3 6h18M7 12h10M10 18h4" />
|
|
45
|
+
</svg>
|
|
46
|
+
<span>Filters</span>
|
|
47
|
+
@if (nonSearchFilterCount() > 0) {
|
|
48
|
+
<span
|
|
49
|
+
class="inline-flex items-center justify-center h-5 min-w-5 rounded-full bg-primary text-primary-foreground text-xs px-1"
|
|
50
|
+
>
|
|
51
|
+
{{ nonSearchFilterCount() }}
|
|
52
|
+
</span>
|
|
53
|
+
}
|
|
54
|
+
</app-button>
|
|
55
|
+
<div class="p-3 min-w-72 max-w-96 space-y-3">
|
|
56
|
+
<ng-content select="[filters]" />
|
|
57
|
+
</div>
|
|
58
|
+
</app-popover>
|
|
59
|
+
</div>
|