@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,199 @@
|
|
|
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, signal } from "@angular/core";
|
|
7
|
+
import { ActivatedRoute, Router } from "@angular/router";
|
|
8
|
+
import { toSignal } from "@angular/core/rxjs-interop";
|
|
9
|
+
import { map } from "rxjs/operators";
|
|
10
|
+
// Base-app UI wrappers, inherited (not recreated) by this feature. Post-compose
|
|
11
|
+
// they land at uiBundles/<target>/src/app/components/ui/..., three levels up from this
|
|
12
|
+
// page folder (app/features/account-search/ -> app/components/ui/).
|
|
13
|
+
import { AlertComponent } from "../../components/ui/alert/alert.component";
|
|
14
|
+
import { ButtonComponent } from "../../components/ui/button/button.component";
|
|
15
|
+
import {
|
|
16
|
+
CardComponent,
|
|
17
|
+
CardContentComponent,
|
|
18
|
+
} from "../../components/ui/card/card.component";
|
|
19
|
+
import { CollapsibleComponent } from "../../components/ui/collapsible/collapsible.component";
|
|
20
|
+
import { SeparatorComponent } from "../../components/ui/separator/separator.component";
|
|
21
|
+
import { SkeletonComponent } from "../../components/ui/skeleton/skeleton.component";
|
|
22
|
+
// Object-search feature building blocks (Waves 1-2), two levels up.
|
|
23
|
+
import { ObjectBreadcrumbComponent } from "../../features/object-search/components/object-breadcrumb/object-breadcrumb.component";
|
|
24
|
+
import { createAsyncData } from "../../utils/async-data";
|
|
25
|
+
import {
|
|
26
|
+
fieldValue,
|
|
27
|
+
getAddressFieldLines,
|
|
28
|
+
formatDateTimeField,
|
|
29
|
+
} from "../../features/object-search/utils/field-utils";
|
|
30
|
+
import { AccountSearchService } from "../../api/account/account-search.service";
|
|
31
|
+
|
|
32
|
+
/** `Intl` options for the created / last-modified timestamps (matches React). */
|
|
33
|
+
const DATE_TIME_OPTIONS: Intl.DateTimeFormatOptions = {
|
|
34
|
+
dateStyle: "medium",
|
|
35
|
+
timeStyle: "short",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** A `{ label, value }` pair rendered in a field grid. */
|
|
39
|
+
interface Field {
|
|
40
|
+
label: string;
|
|
41
|
+
value: string | null;
|
|
42
|
+
/** `true` renders the value as a `tel:` link (Phone / Fax). */
|
|
43
|
+
telephone?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Account detail page. The Angular analogue of the React
|
|
48
|
+
* `AccountObjectDetailPage`: a breadcrumb plus a card of collapsible sections
|
|
49
|
+
* (top fields, Additional / Address / System information), with loading,
|
|
50
|
+
* error, and not-found states.
|
|
51
|
+
*
|
|
52
|
+
* The record id comes from the `:recordId` route param (reactively, so a
|
|
53
|
+
* client-side nav between two detail routes refetches). Data flows through
|
|
54
|
+
* {@link createAsyncData} for latest-wins fetching.
|
|
55
|
+
*/
|
|
56
|
+
@Component({
|
|
57
|
+
selector: "app-account-detail-page",
|
|
58
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
59
|
+
imports: [
|
|
60
|
+
AlertComponent,
|
|
61
|
+
ButtonComponent,
|
|
62
|
+
CardComponent,
|
|
63
|
+
CardContentComponent,
|
|
64
|
+
CollapsibleComponent,
|
|
65
|
+
SeparatorComponent,
|
|
66
|
+
SkeletonComponent,
|
|
67
|
+
ObjectBreadcrumbComponent,
|
|
68
|
+
],
|
|
69
|
+
templateUrl: "./account-detail-page.component.html",
|
|
70
|
+
styleUrl: "./account-detail-page.component.scss",
|
|
71
|
+
})
|
|
72
|
+
export class AccountDetailPageComponent {
|
|
73
|
+
private readonly service = inject(AccountSearchService);
|
|
74
|
+
private readonly route = inject(ActivatedRoute);
|
|
75
|
+
private readonly router = inject(Router);
|
|
76
|
+
|
|
77
|
+
/** Reactive `:recordId` param — drives (and re-drives) the fetch. */
|
|
78
|
+
private readonly recordId = toSignal(
|
|
79
|
+
this.route.paramMap.pipe(map((p) => p.get("recordId") ?? "")),
|
|
80
|
+
{ initialValue: this.route.snapshot.paramMap.get("recordId") ?? "" },
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
private readonly result = createAsyncData(this.recordId, (id) =>
|
|
84
|
+
this.service.getAccountDetail(id),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
protected readonly account = this.result.data;
|
|
88
|
+
protected readonly loading = this.result.loading;
|
|
89
|
+
protected readonly error = this.result.error;
|
|
90
|
+
/** True once a resolved fetch returned no record (distinct from loading). */
|
|
91
|
+
protected readonly notFound = computed(() => !this.loading() && !this.error() && !this.account());
|
|
92
|
+
|
|
93
|
+
/** Per-section collapsible open state (all default open, matching React). */
|
|
94
|
+
protected readonly additionalOpen = signal<boolean>(true);
|
|
95
|
+
protected readonly addressOpen = signal<boolean>(true);
|
|
96
|
+
protected readonly systemOpen = signal<boolean>(true);
|
|
97
|
+
|
|
98
|
+
/** Placeholder rows for skeleton field grids. */
|
|
99
|
+
protected readonly skeletonRows = [0, 1, 2];
|
|
100
|
+
|
|
101
|
+
// -- Breadcrumb leaf label -------------------------------------------------
|
|
102
|
+
protected readonly recordName = computed<string | undefined>(() => {
|
|
103
|
+
const a = this.account();
|
|
104
|
+
if (a) return fieldValue(a.Name) ?? "";
|
|
105
|
+
if (this.error()) return "Error";
|
|
106
|
+
if (this.loading()) return undefined;
|
|
107
|
+
return "Not Found";
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// -- Header ----------------------------------------------------------------
|
|
111
|
+
protected readonly title = computed(() => fieldValue(this.account()?.Name ?? null) ?? "");
|
|
112
|
+
|
|
113
|
+
// -- Top field rows --------------------------------------------------------
|
|
114
|
+
protected readonly topRows = computed<Field[][]>(() => {
|
|
115
|
+
const a = this.account();
|
|
116
|
+
if (!a) return [];
|
|
117
|
+
return [
|
|
118
|
+
[
|
|
119
|
+
{ label: "Account Owner", value: fieldValue(a.Owner?.Name) },
|
|
120
|
+
{ label: "Phone", value: fieldValue(a.Phone), telephone: true },
|
|
121
|
+
],
|
|
122
|
+
[
|
|
123
|
+
{ label: "Account Name", value: fieldValue(a.Name) },
|
|
124
|
+
{ label: "Fax", value: fieldValue(a.Fax), telephone: true },
|
|
125
|
+
],
|
|
126
|
+
[
|
|
127
|
+
{ label: "Parent Account", value: fieldValue(a.Parent?.Name) },
|
|
128
|
+
{ label: "Website", value: fieldValue(a.Website) },
|
|
129
|
+
],
|
|
130
|
+
];
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// -- Additional Information ------------------------------------------------
|
|
134
|
+
protected readonly additionalRows = computed<Field[][]>(() => {
|
|
135
|
+
const a = this.account();
|
|
136
|
+
if (!a) return [];
|
|
137
|
+
return [
|
|
138
|
+
[
|
|
139
|
+
{ label: "Type", value: fieldValue(a.Type) },
|
|
140
|
+
{ label: "Employees", value: fieldValue(a.NumberOfEmployees) },
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
{ label: "Industry", value: fieldValue(a.Industry) },
|
|
144
|
+
{ label: "Annual Revenue", value: fieldValue(a.AnnualRevenue) },
|
|
145
|
+
],
|
|
146
|
+
];
|
|
147
|
+
});
|
|
148
|
+
protected readonly description = computed(() => fieldValue(this.account()?.Description ?? null));
|
|
149
|
+
|
|
150
|
+
// -- Address Information ---------------------------------------------------
|
|
151
|
+
protected readonly billingAddress = computed<string[] | null>(() => {
|
|
152
|
+
const a = this.account();
|
|
153
|
+
if (!a) return null;
|
|
154
|
+
return getAddressFieldLines({
|
|
155
|
+
street: fieldValue(a.BillingStreet),
|
|
156
|
+
city: fieldValue(a.BillingCity),
|
|
157
|
+
state: fieldValue(a.BillingState),
|
|
158
|
+
postalCode: fieldValue(a.BillingPostalCode),
|
|
159
|
+
country: fieldValue(a.BillingCountry),
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
protected readonly shippingAddress = computed<string[] | null>(() => {
|
|
163
|
+
const a = this.account();
|
|
164
|
+
if (!a) return null;
|
|
165
|
+
return getAddressFieldLines({
|
|
166
|
+
street: fieldValue(a.ShippingStreet),
|
|
167
|
+
city: fieldValue(a.ShippingCity),
|
|
168
|
+
state: fieldValue(a.ShippingState),
|
|
169
|
+
postalCode: fieldValue(a.ShippingPostalCode),
|
|
170
|
+
country: fieldValue(a.ShippingCountry),
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// -- System Information ----------------------------------------------------
|
|
175
|
+
protected readonly createdBy = computed(() =>
|
|
176
|
+
this.joinNameAndDate(this.account()?.CreatedBy?.Name, this.account()?.CreatedDate),
|
|
177
|
+
);
|
|
178
|
+
protected readonly lastModifiedBy = computed(() =>
|
|
179
|
+
this.joinNameAndDate(this.account()?.LastModifiedBy?.Name, this.account()?.LastModifiedDate),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
private joinNameAndDate(
|
|
183
|
+
name: { value?: unknown; displayValue?: string | null } | null | undefined,
|
|
184
|
+
date: { value?: unknown; displayValue?: string | null } | null | undefined,
|
|
185
|
+
): string | null {
|
|
186
|
+
const formatted = formatDateTimeField(fieldValue(date ?? null), undefined, DATE_TIME_OPTIONS);
|
|
187
|
+
return [fieldValue(name ?? null), formatted].filter(Boolean).join(" ") || null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Navigate back to the previous view (mirrors the React `navigate(-1)`). */
|
|
191
|
+
protected goBack(): void {
|
|
192
|
+
void this.router.navigate(["/accounts"]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Full page reload retry, matching the React "Retry" affordance. */
|
|
196
|
+
protected reload(): void {
|
|
197
|
+
window.location.reload();
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<div class="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
2
|
+
<app-object-breadcrumb listPath="/accounts" listLabel="Accounts" />
|
|
3
|
+
|
|
4
|
+
<h1 class="text-2xl font-bold mb-4">Search Accounts</h1>
|
|
5
|
+
|
|
6
|
+
<div class="flex flex-col lg:flex-row gap-6">
|
|
7
|
+
<!-- Sidebar — Filter Panel -->
|
|
8
|
+
<aside class="w-full lg:w-80 shrink-0">
|
|
9
|
+
<app-collapsible title="Filters" [(open)]="filtersOpen">
|
|
10
|
+
<div class="space-y-1">
|
|
11
|
+
<app-search-filter [config]="searchConfig" />
|
|
12
|
+
<app-text-filter [config]="nameConfig" />
|
|
13
|
+
<app-select-filter [config]="industryConfig()" />
|
|
14
|
+
<app-multi-select-filter [config]="typeConfig()" />
|
|
15
|
+
<app-numeric-range-filter [config]="revenueConfig" />
|
|
16
|
+
<app-date-filter [config]="createdConfig" filterType="datetime" />
|
|
17
|
+
<app-date-range-filter [config]="lastModifiedConfig" filterType="datetimerange" />
|
|
18
|
+
</div>
|
|
19
|
+
</app-collapsible>
|
|
20
|
+
</aside>
|
|
21
|
+
|
|
22
|
+
<!-- Main area — Sort + Results -->
|
|
23
|
+
<div class="flex-1 min-w-0">
|
|
24
|
+
<div class="flex flex-wrap items-center gap-2 mb-4">
|
|
25
|
+
<app-sort-control [sortFields]="sortConfigs" />
|
|
26
|
+
<app-active-filters />
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="min-h-132" [attr.aria-busy]="loading()">
|
|
30
|
+
@if (loading()) {
|
|
31
|
+
<app-skeleton class="h-5 w-30 mb-3" />
|
|
32
|
+
<div class="divide-y">
|
|
33
|
+
@for (row of skeletonRows(); track row) {
|
|
34
|
+
<div class="flex items-center justify-between py-3">
|
|
35
|
+
<div class="space-y-2">
|
|
36
|
+
<app-skeleton class="h-5 w-40" />
|
|
37
|
+
<app-skeleton class="h-4 w-28" />
|
|
38
|
+
</div>
|
|
39
|
+
<div class="space-y-2 flex flex-col items-end">
|
|
40
|
+
<app-skeleton class="h-4 w-24" />
|
|
41
|
+
<app-skeleton class="h-4 w-20" />
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
}
|
|
45
|
+
</div>
|
|
46
|
+
} @else if (error()) {
|
|
47
|
+
<p class="text-sm text-muted-foreground mb-3">0 accounts found</p>
|
|
48
|
+
<app-alert
|
|
49
|
+
variant="destructive"
|
|
50
|
+
icon="error"
|
|
51
|
+
title="Failed to load accounts"
|
|
52
|
+
description="Something went wrong while loading accounts. Please try again later."
|
|
53
|
+
/>
|
|
54
|
+
} @else if (nodes().length > 0) {
|
|
55
|
+
<p class="text-sm text-muted-foreground mb-3">{{ resultCountLabel() }}</p>
|
|
56
|
+
<ul class="divide-y">
|
|
57
|
+
@for (node of nodes(); track node.Id) {
|
|
58
|
+
<li>
|
|
59
|
+
<a
|
|
60
|
+
[routerLink]="['/accounts', node.Id]"
|
|
61
|
+
class="flex items-center justify-between py-3 px-3 -mx-3 rounded-md transition-colors hover:bg-accent"
|
|
62
|
+
>
|
|
63
|
+
<div>
|
|
64
|
+
<span class="font-medium">{{ name(node) }}</span>
|
|
65
|
+
<p class="text-sm text-muted-foreground">{{ secondaryLine(node) }}</p>
|
|
66
|
+
</div>
|
|
67
|
+
<div class="text-right text-sm">
|
|
68
|
+
<p>{{ phone(node) }}</p>
|
|
69
|
+
<p class="text-muted-foreground">{{ ownerName(node) }}</p>
|
|
70
|
+
</div>
|
|
71
|
+
</a>
|
|
72
|
+
</li>
|
|
73
|
+
}
|
|
74
|
+
</ul>
|
|
75
|
+
} @else {
|
|
76
|
+
<div class="flex flex-col items-center justify-center py-16 text-center">
|
|
77
|
+
<svg
|
|
78
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
79
|
+
class="size-12 text-muted-foreground mb-4"
|
|
80
|
+
viewBox="0 0 24 24"
|
|
81
|
+
fill="none"
|
|
82
|
+
stroke="currentColor"
|
|
83
|
+
stroke-width="2"
|
|
84
|
+
stroke-linecap="round"
|
|
85
|
+
stroke-linejoin="round"
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
>
|
|
88
|
+
<path d="m13.5 8.5-5 5" />
|
|
89
|
+
<path d="m8.5 8.5 5 5" />
|
|
90
|
+
<circle cx="11" cy="11" r="8" />
|
|
91
|
+
<path d="m21 21-4.3-4.3" />
|
|
92
|
+
</svg>
|
|
93
|
+
<h2 class="text-lg font-semibold mb-1">No accounts found</h2>
|
|
94
|
+
<p class="text-sm text-muted-foreground">
|
|
95
|
+
Try adjusting your filters or search criteria.
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<!-- Pagination — always visible, disabled while loading or on error -->
|
|
102
|
+
<app-pagination-controls
|
|
103
|
+
[totalCount]="totalCount()"
|
|
104
|
+
[hasNextPage]="hasNextPage()"
|
|
105
|
+
[endCursor]="endCursor()"
|
|
106
|
+
[pageSizeOptions]="pageSizeOptions"
|
|
107
|
+
[disabled]="controlsDisabled()"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
@@ -0,0 +1,217 @@
|
|
|
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, signal } from "@angular/core";
|
|
7
|
+
import { RouterLink } from "@angular/router";
|
|
8
|
+
// Base-app UI wrappers, inherited (not recreated) by this feature. Post-compose
|
|
9
|
+
// they land at uiBundles/<target>/src/app/components/ui/..., three levels up from this
|
|
10
|
+
// page folder (app/features/account-search/ -> app/components/ui/).
|
|
11
|
+
import { AlertComponent } from "../../components/ui/alert/alert.component";
|
|
12
|
+
import { CollapsibleComponent } from "../../components/ui/collapsible/collapsible.component";
|
|
13
|
+
import { SkeletonComponent } from "../../components/ui/skeleton/skeleton.component";
|
|
14
|
+
// Object-search feature building blocks (Waves 1-2), two levels up.
|
|
15
|
+
import { ObjectBreadcrumbComponent } from "../../features/object-search/components/object-breadcrumb/object-breadcrumb.component";
|
|
16
|
+
import { SortControlComponent } from "../../features/object-search/components/sort-control/sort-control.component";
|
|
17
|
+
import { ActiveFiltersComponent } from "../../features/object-search/components/active-filters/active-filters.component";
|
|
18
|
+
import { PaginationControlsComponent } from "../../features/object-search/components/pagination-controls/pagination-controls.component";
|
|
19
|
+
import { SearchFilterComponent } from "../../features/object-search/components/filters/search-filter/search-filter.component";
|
|
20
|
+
import { TextFilterComponent } from "../../features/object-search/components/filters/text-filter/text-filter.component";
|
|
21
|
+
import { SelectFilterComponent } from "../../features/object-search/components/filters/select-filter/select-filter.component";
|
|
22
|
+
import { MultiSelectFilterComponent } from "../../features/object-search/components/filters/multi-select-filter/multi-select-filter.component";
|
|
23
|
+
import { NumericRangeFilterComponent } from "../../features/object-search/components/filters/numeric-range-filter/numeric-range-filter.component";
|
|
24
|
+
import { DateFilterComponent } from "../../features/object-search/components/filters/date-filter/date-filter.component";
|
|
25
|
+
import { DateRangeFilterComponent } from "../../features/object-search/components/filters/date-range-filter/date-range-filter.component";
|
|
26
|
+
import {
|
|
27
|
+
ObjectSearchStateService,
|
|
28
|
+
type PaginationConfig,
|
|
29
|
+
} from "../../features/object-search/object-search-state.service";
|
|
30
|
+
import { createAsyncData } from "../../utils/async-data";
|
|
31
|
+
import { fieldValue } from "../../features/object-search/utils/field-utils";
|
|
32
|
+
import type { FilterFieldConfig } from "../../features/object-search/utils/filter-utils";
|
|
33
|
+
import type { SortFieldConfig } from "../../features/object-search/utils/sort-utils";
|
|
34
|
+
import { AccountSearchService, type AccountNode } from "../../api/account/account-search.service";
|
|
35
|
+
|
|
36
|
+
/** Cursor-pagination config for the account search (mirrors the React reference). */
|
|
37
|
+
const PAGINATION_CONFIG: PaginationConfig = {
|
|
38
|
+
defaultPageSize: 7,
|
|
39
|
+
validPageSizes: [7, 14, 28, 42],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Filter field configs — the single source of truth for URL (de)serialization
|
|
44
|
+
* and GraphQL `where` building. The picklist option lists for Industry / Type
|
|
45
|
+
* are filled in at runtime (see the component's computed configs) from the
|
|
46
|
+
* distinct-value aggregate queries.
|
|
47
|
+
*/
|
|
48
|
+
const FILTER_CONFIGS: FilterFieldConfig[] = [
|
|
49
|
+
{
|
|
50
|
+
field: "search",
|
|
51
|
+
label: "Search",
|
|
52
|
+
type: "search",
|
|
53
|
+
searchFields: ["Name", "Phone", "Industry"],
|
|
54
|
+
placeholder: "Search by name, phone, or industry...",
|
|
55
|
+
},
|
|
56
|
+
{ field: "Name", label: "Account Name", type: "text", placeholder: "Search by name..." },
|
|
57
|
+
{ field: "Industry", label: "Industry", type: "picklist" },
|
|
58
|
+
{ field: "Type", label: "Type", type: "multipicklist" },
|
|
59
|
+
{ field: "AnnualRevenue", label: "Annual Revenue", type: "numeric" },
|
|
60
|
+
{ field: "CreatedDate", label: "Created Date", type: "datetime", disableFuture: true },
|
|
61
|
+
{
|
|
62
|
+
field: "LastModifiedDate",
|
|
63
|
+
label: "Last Modified Date",
|
|
64
|
+
type: "datetimerange",
|
|
65
|
+
disableFuture: true,
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/** Sortable fields exposed by the sort control (mirrors the React reference). */
|
|
70
|
+
const ACCOUNT_SORT_CONFIGS: SortFieldConfig[] = [
|
|
71
|
+
{ field: "Name", label: "Name" },
|
|
72
|
+
{ field: "AnnualRevenue", label: "Annual Revenue" },
|
|
73
|
+
{ field: "Industry", label: "Industry" },
|
|
74
|
+
{ field: "CreatedDate", label: "Created Date" },
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Account search / list page. The Angular analogue of the React `AccountSearch`
|
|
79
|
+
* example page: a sidebar filter panel (Card + Collapsible) plus a main results
|
|
80
|
+
* area with sort control, active-filter chips, a results list, and cursor
|
|
81
|
+
* pagination.
|
|
82
|
+
*
|
|
83
|
+
* State lives in a page-scoped {@link ObjectSearchStateService} (provided here,
|
|
84
|
+
* NOT in root) so each search page owns its own filters / sort / cursor stack.
|
|
85
|
+
* The filter components projected into the sidebar inject that same instance.
|
|
86
|
+
*
|
|
87
|
+
* Data flows through {@link createAsyncData}: whenever the derived query args
|
|
88
|
+
* (where / orderBy / pageSize / afterCursor) change, the search re-runs with
|
|
89
|
+
* latest-wins semantics.
|
|
90
|
+
*/
|
|
91
|
+
@Component({
|
|
92
|
+
selector: "app-account-search-page",
|
|
93
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
94
|
+
providers: [ObjectSearchStateService],
|
|
95
|
+
imports: [
|
|
96
|
+
RouterLink,
|
|
97
|
+
AlertComponent,
|
|
98
|
+
CollapsibleComponent,
|
|
99
|
+
SkeletonComponent,
|
|
100
|
+
ObjectBreadcrumbComponent,
|
|
101
|
+
SortControlComponent,
|
|
102
|
+
ActiveFiltersComponent,
|
|
103
|
+
PaginationControlsComponent,
|
|
104
|
+
SearchFilterComponent,
|
|
105
|
+
TextFilterComponent,
|
|
106
|
+
SelectFilterComponent,
|
|
107
|
+
MultiSelectFilterComponent,
|
|
108
|
+
NumericRangeFilterComponent,
|
|
109
|
+
DateFilterComponent,
|
|
110
|
+
DateRangeFilterComponent,
|
|
111
|
+
],
|
|
112
|
+
templateUrl: "./account-search-page.component.html",
|
|
113
|
+
})
|
|
114
|
+
export class AccountSearchPageComponent {
|
|
115
|
+
private readonly service = inject(AccountSearchService);
|
|
116
|
+
private readonly state = inject(ObjectSearchStateService);
|
|
117
|
+
|
|
118
|
+
/** Sortable field configs surfaced to the sort control. */
|
|
119
|
+
protected readonly sortConfigs = ACCOUNT_SORT_CONFIGS;
|
|
120
|
+
/** Allowed page sizes forwarded to the pagination controls. */
|
|
121
|
+
protected readonly pageSizeOptions = PAGINATION_CONFIG.validPageSizes;
|
|
122
|
+
|
|
123
|
+
/** Collapsible open state for the sidebar filter panel. */
|
|
124
|
+
protected readonly filtersOpen = signal<boolean>(true);
|
|
125
|
+
|
|
126
|
+
/** Derived, API-ready `where` / `orderBy` signals from the shared state. */
|
|
127
|
+
private readonly query = this.state.init<unknown, unknown>(FILTER_CONFIGS, PAGINATION_CONFIG);
|
|
128
|
+
|
|
129
|
+
/** Live page-size / cursor signals from the shared state. */
|
|
130
|
+
protected readonly pageSize = this.state.pageSize;
|
|
131
|
+
protected readonly hasPreviousPage = this.state.hasPreviousPage;
|
|
132
|
+
|
|
133
|
+
// -- Distinct-value option loads (Industry / Type filters) -----------------
|
|
134
|
+
private readonly industries = createAsyncData(signal(0), () =>
|
|
135
|
+
this.service.fetchDistinctIndustries(),
|
|
136
|
+
);
|
|
137
|
+
private readonly types = createAsyncData(signal(0), () => this.service.fetchDistinctTypes());
|
|
138
|
+
|
|
139
|
+
// -- Per-filter configs (options merged in for the picklist filters) -------
|
|
140
|
+
protected readonly searchConfig = FILTER_CONFIGS[0];
|
|
141
|
+
protected readonly nameConfig = FILTER_CONFIGS[1];
|
|
142
|
+
protected readonly revenueConfig = FILTER_CONFIGS[4];
|
|
143
|
+
protected readonly createdConfig = FILTER_CONFIGS[5];
|
|
144
|
+
protected readonly lastModifiedConfig = FILTER_CONFIGS[6];
|
|
145
|
+
protected readonly industryConfig = computed<FilterFieldConfig>(() => ({
|
|
146
|
+
...FILTER_CONFIGS[2],
|
|
147
|
+
options: this.industries.data() ?? [],
|
|
148
|
+
}));
|
|
149
|
+
protected readonly typeConfig = computed<FilterFieldConfig>(() => ({
|
|
150
|
+
...FILTER_CONFIGS[3],
|
|
151
|
+
options: this.types.data() ?? [],
|
|
152
|
+
}));
|
|
153
|
+
|
|
154
|
+
// -- Search results --------------------------------------------------------
|
|
155
|
+
private readonly searchArgs = computed(() => ({
|
|
156
|
+
where: this.query.where(),
|
|
157
|
+
orderBy: this.query.orderBy(),
|
|
158
|
+
first: this.state.pageSize(),
|
|
159
|
+
after: this.state.afterCursor(),
|
|
160
|
+
}));
|
|
161
|
+
private readonly result = createAsyncData(this.searchArgs, (args) =>
|
|
162
|
+
this.service.searchAccounts(args),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
protected readonly loading = this.result.loading;
|
|
166
|
+
protected readonly error = this.result.error;
|
|
167
|
+
|
|
168
|
+
private readonly pageInfo = computed(() => this.result.data()?.pageInfo);
|
|
169
|
+
protected readonly totalCount = computed(() => this.result.data()?.totalCount);
|
|
170
|
+
protected readonly hasNextPage = computed(() => this.pageInfo()?.hasNextPage ?? false);
|
|
171
|
+
protected readonly endCursor = computed(() => this.pageInfo()?.endCursor ?? undefined);
|
|
172
|
+
|
|
173
|
+
/** Non-null account nodes from the current page's edges. */
|
|
174
|
+
protected readonly nodes = computed<AccountNode[]>(() => {
|
|
175
|
+
const edges = this.result.data()?.edges ?? [];
|
|
176
|
+
const out: AccountNode[] = [];
|
|
177
|
+
for (const edge of edges) {
|
|
178
|
+
if (edge?.node) out.push(edge.node);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
/** Pagination controls (and the results list) are inert while fetching/errored. */
|
|
184
|
+
protected readonly controlsDisabled = computed(() => this.loading() || !!this.error());
|
|
185
|
+
|
|
186
|
+
/** Placeholder rows for the loading skeleton — one per requested page size. */
|
|
187
|
+
protected readonly skeletonRows = computed(() =>
|
|
188
|
+
Array.from({ length: this.pageSize() }, (_, i) => i),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
/** Result-count copy above the list, matching the React reference wording. */
|
|
192
|
+
protected readonly resultCountLabel = computed(() => {
|
|
193
|
+
const total = this.totalCount();
|
|
194
|
+
const shown = this.nodes().length;
|
|
195
|
+
if (total != null && (this.hasNextPage() || this.hasPreviousPage())) {
|
|
196
|
+
return `${total} account${total !== 1 ? "s" : ""} found`;
|
|
197
|
+
}
|
|
198
|
+
return `Showing ${shown} account${shown !== 1 ? "s" : ""}`;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/** Display helpers for the results list (mirrors `fieldValue` in the template). */
|
|
202
|
+
protected name(node: AccountNode): string {
|
|
203
|
+
return fieldValue(node.Name) ?? "—";
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
protected secondaryLine(node: AccountNode): string {
|
|
207
|
+
return [fieldValue(node.Industry), fieldValue(node.Type)].filter(Boolean).join(" · ") || "—";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
protected phone(node: AccountNode): string {
|
|
211
|
+
return fieldValue(node.Phone) ?? "";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
protected ownerName(node: AccountNode): string {
|
|
215
|
+
return fieldValue(node.Owner?.Name) ?? "";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
2
|
+
<div class="flex items-center gap-6 mb-6">
|
|
3
|
+
<h1 class="text-2xl font-bold">Account Search</h1>
|
|
4
|
+
<app-button appearance="outlined" size="sm" (clicked)="browseAll()"
|
|
5
|
+
>Browse All Accounts</app-button
|
|
6
|
+
>
|
|
7
|
+
</div>
|
|
8
|
+
<form class="flex gap-2 items-center" (submit)="onSubmit($event)">
|
|
9
|
+
<app-input
|
|
10
|
+
class="flex-1"
|
|
11
|
+
type="text"
|
|
12
|
+
placeholder="Search by name, phone, or industry..."
|
|
13
|
+
label="Search"
|
|
14
|
+
[(value)]="term"
|
|
15
|
+
/>
|
|
16
|
+
<app-button type="submit">Search</app-button>
|
|
17
|
+
</form>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,47 @@
|
|
|
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, signal } from "@angular/core";
|
|
7
|
+
import { Router } from "@angular/router";
|
|
8
|
+
// Base-app UI wrappers, inherited (not recreated) by this feature. Post-compose
|
|
9
|
+
// they land at uiBundles/<target>/src/app/components/ui/{button,input}/..., three levels
|
|
10
|
+
// up from this page folder (app/features/account-search/ -> app/components/ui/).
|
|
11
|
+
import { ButtonComponent } from "../../components/ui/button/button.component";
|
|
12
|
+
import { InputComponent } from "../../components/ui/input/input.component";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Account-search landing page. Mirrors the React `Home` page: a single search
|
|
16
|
+
* box plus a "Browse All Accounts" shortcut. Submitting navigates to the
|
|
17
|
+
* `accounts` list, carrying the typed term as the `q` query param so the search
|
|
18
|
+
* page's {@link ObjectSearchStateService} seeds the multi-field search filter.
|
|
19
|
+
*
|
|
20
|
+
* This is intentionally NOT the state-service-driven `app-search-bar` — Home
|
|
21
|
+
* has no search state of its own; it just routes into the search page.
|
|
22
|
+
*/
|
|
23
|
+
@Component({
|
|
24
|
+
selector: "app-home-page",
|
|
25
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
26
|
+
imports: [ButtonComponent, InputComponent],
|
|
27
|
+
templateUrl: "./home-page.component.html",
|
|
28
|
+
})
|
|
29
|
+
export class HomePageComponent {
|
|
30
|
+
private readonly router = inject(Router);
|
|
31
|
+
|
|
32
|
+
/** Bound to the search input; forwarded as `?q=` on submit. */
|
|
33
|
+
protected readonly term = signal<string>("");
|
|
34
|
+
|
|
35
|
+
/** Navigate to the search page, seeding the search term when present. */
|
|
36
|
+
protected onSubmit(event: Event): void {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
this.browseAll(this.term() || undefined);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Go to the accounts list, optionally with a seeded search term. */
|
|
42
|
+
protected browseAll(q?: string): void {
|
|
43
|
+
void this.router.navigate(["/accounts"], {
|
|
44
|
+
queryParams: q ? { q } : {},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { HomeComponent } from './home.component';
|
|
3
|
+
|
|
4
|
+
describe('HomeComponent', () => {
|
|
5
|
+
beforeEach(async () => {
|
|
6
|
+
await TestBed.configureTestingModule({
|
|
7
|
+
imports: [HomeComponent],
|
|
8
|
+
}).compileComponents();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should create the component', () => {
|
|
12
|
+
const fixture = TestBed.createComponent(HomeComponent);
|
|
13
|
+
const component = fixture.componentInstance;
|
|
14
|
+
expect(component).toBeTruthy();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should render main landmark', async () => {
|
|
18
|
+
const fixture = TestBed.createComponent(HomeComponent);
|
|
19
|
+
await fixture.whenStable();
|
|
20
|
+
const compiled = fixture.nativeElement as HTMLElement;
|
|
21
|
+
expect(compiled.querySelector('main')).toBeTruthy();
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
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 } from '@angular/core';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'app-home',
|
|
10
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
11
|
+
templateUrl: './home.component.html',
|
|
12
|
+
})
|
|
13
|
+
export class HomeComponent {}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
|
2
|
+
<div class="text-center">
|
|
3
|
+
<h1 class="text-4xl font-bold text-gray-900 mb-4">404</h1>
|
|
4
|
+
<p class="text-lg text-gray-600 mb-8">Page not found</p>
|
|
5
|
+
<a
|
|
6
|
+
routerLink="/"
|
|
7
|
+
class="inline-block px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
|
8
|
+
>
|
|
9
|
+
Go to Home
|
|
10
|
+
</a>
|
|
11
|
+
</div>
|
|
12
|
+
</main>
|