@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,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026, Salesforce, Inc.,
|
|
3
|
+
* All rights reserved.
|
|
4
|
+
* For full license text, see the LICENSE.txt file
|
|
5
|
+
*/
|
|
6
|
+
import { Injectable } from "@angular/core";
|
|
7
|
+
import {
|
|
8
|
+
embedAgentforceClient,
|
|
9
|
+
type AgentforceClientConfig,
|
|
10
|
+
} from "@salesforce/agentforce-conversation-client";
|
|
11
|
+
import type {
|
|
12
|
+
AgentforceErrorHandler,
|
|
13
|
+
AgentforceReadyHandler,
|
|
14
|
+
ResolvedEmbedOptions,
|
|
15
|
+
} from "../../types/conversation";
|
|
16
|
+
|
|
17
|
+
/** Optional ready/error handlers forwarded to the embed client. */
|
|
18
|
+
export interface AgentforceEmbedHandlers {
|
|
19
|
+
onReady?: AgentforceReadyHandler;
|
|
20
|
+
onError?: AgentforceErrorHandler;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const GLOBAL_HOST_ID = "agentforce-conversation-client-global-host";
|
|
24
|
+
const EXISTING_EMBED_SELECTOR = 'lightning-out-application[data-lo="acc"]';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* App-wide singleton that owns the one-time Lightning Out embed of the
|
|
28
|
+
* Agentforce Conversation Client. The Angular analogue of the React
|
|
29
|
+
* `window.__agentforceConversationClientSingleton`: because the service is
|
|
30
|
+
* `providedIn: 'root'`, a single instance is shared across the app, so its
|
|
31
|
+
* `initialized` / `initPromise` fields guarantee exactly one embed per app —
|
|
32
|
+
* regardless of how many client components mount.
|
|
33
|
+
*
|
|
34
|
+
* A valid Salesforce session is required for the given org; auth is resolved at
|
|
35
|
+
* embed time (frontdoor URL in dev via `/__lo/frontdoor`, `SFDC_ENV.orgUrl` in
|
|
36
|
+
* prod). Config is passed through to the embed client as-is.
|
|
37
|
+
*/
|
|
38
|
+
@Injectable({ providedIn: "root" })
|
|
39
|
+
export class AgentforceEmbedService {
|
|
40
|
+
private initialized = false;
|
|
41
|
+
private initPromise?: Promise<void>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Embed the client once. Subsequent calls are no-ops while an embed is in
|
|
45
|
+
* flight or already complete. `host` is the inline container element (when
|
|
46
|
+
* rendering inline); when omitted the global floating host is used.
|
|
47
|
+
*/
|
|
48
|
+
embed(
|
|
49
|
+
config: AgentforceClientConfig,
|
|
50
|
+
overrides: ResolvedEmbedOptions,
|
|
51
|
+
host?: HTMLElement | null,
|
|
52
|
+
handlers?: AgentforceEmbedHandlers,
|
|
53
|
+
): void {
|
|
54
|
+
if (!config.agentId) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"AgentforceConversationClient requires agentId. " +
|
|
57
|
+
"Pass flat inputs only (agentId, agentLabel, inline, headerEnabled, showHeaderIcon, width, height, styleTokens).",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const inline = config.renderingConfig?.mode === "inline";
|
|
62
|
+
if (inline && !host) {
|
|
63
|
+
// Inline host not yet in the DOM — the component retries after render.
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (this.initialized || this.initPromise) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const shouldFetchFrontdoor = window.location.hostname === "localhost";
|
|
72
|
+
|
|
73
|
+
if (shouldFetchFrontdoor) {
|
|
74
|
+
this.initPromise = fetch("/__lo/frontdoor")
|
|
75
|
+
.then(async (res) => {
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
console.error("frontdoor fetch failed");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const { frontdoorUrl: resolvedFrontdoorUrl } = await res.json();
|
|
81
|
+
this.safeInitialize(
|
|
82
|
+
config,
|
|
83
|
+
{ ...overrides, frontdoorUrl: resolvedFrontdoorUrl },
|
|
84
|
+
host,
|
|
85
|
+
handlers,
|
|
86
|
+
);
|
|
87
|
+
})
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
console.error("AgentforceConversationClient: failed to fetch frontdoor URL", err);
|
|
90
|
+
})
|
|
91
|
+
.finally(() => {
|
|
92
|
+
this.initPromise = undefined;
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
this.initPromise = Promise.resolve()
|
|
96
|
+
.then(() => {
|
|
97
|
+
this.safeInitialize(
|
|
98
|
+
config,
|
|
99
|
+
{ ...this.getDefaultEmbedOptions(), ...overrides },
|
|
100
|
+
host,
|
|
101
|
+
handlers,
|
|
102
|
+
);
|
|
103
|
+
})
|
|
104
|
+
.catch((err) => {
|
|
105
|
+
console.error("AgentforceConversationClient: failed to embed Agentforce client", err);
|
|
106
|
+
})
|
|
107
|
+
.finally(() => {
|
|
108
|
+
this.initPromise = undefined;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private safeInitialize(
|
|
114
|
+
config: AgentforceClientConfig,
|
|
115
|
+
options: ResolvedEmbedOptions,
|
|
116
|
+
host?: HTMLElement | null,
|
|
117
|
+
handlers?: AgentforceEmbedHandlers,
|
|
118
|
+
): void {
|
|
119
|
+
try {
|
|
120
|
+
this.initialize(config, options, host, handlers);
|
|
121
|
+
} catch (initErr) {
|
|
122
|
+
console.error("AgentforceConversationClient: initialization failed", initErr);
|
|
123
|
+
this.initialized = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private initialize(
|
|
128
|
+
config: AgentforceClientConfig,
|
|
129
|
+
options: ResolvedEmbedOptions,
|
|
130
|
+
host?: HTMLElement | null,
|
|
131
|
+
handlers?: AgentforceEmbedHandlers,
|
|
132
|
+
): void {
|
|
133
|
+
if (this.initialized) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const existingEmbed = document.querySelector(EXISTING_EMBED_SELECTOR);
|
|
138
|
+
if (existingEmbed) {
|
|
139
|
+
this.initialized = true;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const inline = config.renderingConfig?.mode === "inline";
|
|
144
|
+
const container = inline ? host! : this.getOrCreateGlobalHost();
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
embedAgentforceClient({
|
|
148
|
+
container,
|
|
149
|
+
salesforceOrigin: options.salesforceOrigin,
|
|
150
|
+
frontdoorUrl: options.frontdoorUrl,
|
|
151
|
+
agentforceClientConfig: config,
|
|
152
|
+
onReady: handlers?.onReady,
|
|
153
|
+
onError: handlers?.onError,
|
|
154
|
+
});
|
|
155
|
+
this.initialized = true;
|
|
156
|
+
} catch (err) {
|
|
157
|
+
// Strip a partially-created LO element so the next mount can retry.
|
|
158
|
+
const partialEmbed = document.querySelector(EXISTING_EMBED_SELECTOR);
|
|
159
|
+
partialEmbed?.remove();
|
|
160
|
+
console.error("AgentforceConversationClient: initialization failed", err);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private getOrCreateGlobalHost(): HTMLDivElement {
|
|
165
|
+
let host = document.getElementById(GLOBAL_HOST_ID) as HTMLDivElement | null;
|
|
166
|
+
if (!host) {
|
|
167
|
+
host = document.createElement("div");
|
|
168
|
+
host.id = GLOBAL_HOST_ID;
|
|
169
|
+
document.body.appendChild(host);
|
|
170
|
+
}
|
|
171
|
+
return host;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private getDefaultEmbedOptions(): ResolvedEmbedOptions {
|
|
175
|
+
return { salesforceOrigin: SFDC_ENV?.orgUrl };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@if (chips().length > 0) {
|
|
2
|
+
<div class="flex flex-wrap gap-2">
|
|
3
|
+
@for (chip of chips(); track chip.field) {
|
|
4
|
+
<app-button
|
|
5
|
+
appearance="outlined"
|
|
6
|
+
size="sm"
|
|
7
|
+
iconEnd="close"
|
|
8
|
+
[ariaLabel]="'Remove filter ' + chip.label"
|
|
9
|
+
(clicked)="onRemove(chip.field)"
|
|
10
|
+
>
|
|
11
|
+
{{ chip.label }}
|
|
12
|
+
</app-button>
|
|
13
|
+
}
|
|
14
|
+
<app-button
|
|
15
|
+
appearance="destructive"
|
|
16
|
+
size="sm"
|
|
17
|
+
ariaLabel="Clear all filters"
|
|
18
|
+
(clicked)="onClearAll()"
|
|
19
|
+
>
|
|
20
|
+
Clear all
|
|
21
|
+
</app-button>
|
|
22
|
+
</div>
|
|
23
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 } from "@angular/core";
|
|
7
|
+
import { ButtonComponent } from "../../../../components/ui/button/button.component";
|
|
8
|
+
import { ObjectSearchStateService } from "../../object-search-state.service";
|
|
9
|
+
import type { ActiveFilterValue } from "../../utils/filter-utils";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format the display label for one active filter chip. Mirrors the React
|
|
13
|
+
* `formatFilterLabel`; a switch over the filter's declared `type`.
|
|
14
|
+
*/
|
|
15
|
+
function formatFilterLabel(filter: ActiveFilterValue): string {
|
|
16
|
+
const { label, type, value, min, max } = filter;
|
|
17
|
+
switch (type) {
|
|
18
|
+
case "search":
|
|
19
|
+
return `Search: ${value}`;
|
|
20
|
+
case "text":
|
|
21
|
+
case "picklist":
|
|
22
|
+
return `${label}: ${value}`;
|
|
23
|
+
case "multipicklist": {
|
|
24
|
+
const values = value ? value.split(",") : [];
|
|
25
|
+
if (values.length <= 2) return `${label}: ${values.join(", ")}`;
|
|
26
|
+
return `${label}: ${values.length} selected`;
|
|
27
|
+
}
|
|
28
|
+
case "boolean":
|
|
29
|
+
return `${label}: ${value === "true" ? "Yes" : "No"}`;
|
|
30
|
+
case "numeric": {
|
|
31
|
+
if (min && max) return `${label}: ${min} - ${max}`;
|
|
32
|
+
if (min) return `${label}: >= ${min}`;
|
|
33
|
+
return `${label}: <= ${max}`;
|
|
34
|
+
}
|
|
35
|
+
case "date":
|
|
36
|
+
case "datetime":
|
|
37
|
+
case "daterange":
|
|
38
|
+
case "datetimerange": {
|
|
39
|
+
if (min && max) return `${label}: ${min} to ${max}`;
|
|
40
|
+
if (min) return `${label}: from ${min}`;
|
|
41
|
+
return `${label}: until ${max}`;
|
|
42
|
+
}
|
|
43
|
+
default:
|
|
44
|
+
return label;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Removable-chip strip for the currently active filters.
|
|
50
|
+
*
|
|
51
|
+
* Reads `filters()` from {@link ObjectSearchStateService}; clicking a chip
|
|
52
|
+
* removes that filter, and the `Clear all` chip clears everything via
|
|
53
|
+
* `resetAll()`. Mirrors the React `ActiveFilters` component.
|
|
54
|
+
*/
|
|
55
|
+
@Component({
|
|
56
|
+
selector: "app-active-filters",
|
|
57
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
58
|
+
imports: [ButtonComponent],
|
|
59
|
+
templateUrl: "./active-filters.component.html",
|
|
60
|
+
})
|
|
61
|
+
export class ActiveFiltersComponent {
|
|
62
|
+
private readonly state = inject(ObjectSearchStateService);
|
|
63
|
+
|
|
64
|
+
/** Active filters signal from the state service. */
|
|
65
|
+
protected readonly filters = this.state.filters;
|
|
66
|
+
/** Display strings paired with the underlying field for chip actions. */
|
|
67
|
+
protected readonly chips = computed(() =>
|
|
68
|
+
this.filters().map((f) => ({ field: f.field, label: formatFilterLabel(f) })),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
protected onRemove(field: string): void {
|
|
72
|
+
this.state.removeFilter(field);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected onClearAll(): void {
|
|
76
|
+
this.state.resetAll();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<app-filter-field-wrapper [label]="config().label" [helpText]="config().helpText">
|
|
2
|
+
<div class="flex gap-2">
|
|
3
|
+
<app-select
|
|
4
|
+
class="flex-1"
|
|
5
|
+
[options]="operatorOptions"
|
|
6
|
+
[value]="operator()"
|
|
7
|
+
(valueChange)="onOperatorChange($event)"
|
|
8
|
+
/>
|
|
9
|
+
<app-date-picker
|
|
10
|
+
class="flex-1"
|
|
11
|
+
placeholder="Pick a date"
|
|
12
|
+
[readonly]="true"
|
|
13
|
+
[max]="maxDate()"
|
|
14
|
+
[value]="currentDate()"
|
|
15
|
+
(valueChange)="onDateChange($event)"
|
|
16
|
+
/>
|
|
17
|
+
</div>
|
|
18
|
+
</app-filter-field-wrapper>
|
|
@@ -0,0 +1,137 @@
|
|
|
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 wrappers, inherited (not recreated) by this feature. Post-compose
|
|
8
|
+
// they land at uiBundles/<target>/src/app/components/ui/{date-picker,select}/..., four
|
|
9
|
+
// levels up.
|
|
10
|
+
import { DatePickerComponent } from "../../../../../components/ui/date-picker/date-picker.component";
|
|
11
|
+
import {
|
|
12
|
+
SelectComponent,
|
|
13
|
+
type AppSelectOption,
|
|
14
|
+
} from "../../../../../components/ui/select/select.component";
|
|
15
|
+
import { ObjectSearchStateService } from "../../../object-search-state.service";
|
|
16
|
+
import type { FilterFieldConfig, FilterFieldType } from "../../../utils/filter-utils";
|
|
17
|
+
import { FilterFieldWrapperComponent } from "../filter-field-wrapper/filter-field-wrapper.component";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Single-date filter — operator select ("After" / "Before") plus a date
|
|
21
|
+
* picker. Emits a filter whose `value` is the operator (`'gt'` or `'lt'`) and
|
|
22
|
+
* whose date is placed in `min` (for `gt`) or `max` (for `lt`), matching the
|
|
23
|
+
* React reference and the `date` / `datetime` branch of `buildSingleFilter`.
|
|
24
|
+
*
|
|
25
|
+
* The `filterType` input toggles the emitted `type` between `'date'` and
|
|
26
|
+
* `'datetime'` (the latter adds start/end-of-day promotion in the filter
|
|
27
|
+
* builder).
|
|
28
|
+
*/
|
|
29
|
+
type DateOperator = "gt" | "lt";
|
|
30
|
+
|
|
31
|
+
const OPERATOR_OPTIONS: readonly AppSelectOption[] = [
|
|
32
|
+
{ value: "gt", label: "After" },
|
|
33
|
+
{ value: "lt", label: "Before" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
@Component({
|
|
37
|
+
selector: "app-date-filter",
|
|
38
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
39
|
+
imports: [DatePickerComponent, SelectComponent, FilterFieldWrapperComponent],
|
|
40
|
+
templateUrl: "./date-filter.component.html",
|
|
41
|
+
})
|
|
42
|
+
export class DateFilterComponent {
|
|
43
|
+
private readonly state = inject(ObjectSearchStateService);
|
|
44
|
+
|
|
45
|
+
/** Field config: `field`, `label`, `helpText`, ... */
|
|
46
|
+
readonly config = input.required<FilterFieldConfig>();
|
|
47
|
+
/** Underlying filter type — `'date'` (default) or `'datetime'`. */
|
|
48
|
+
readonly filterType = input<FilterFieldType>("date");
|
|
49
|
+
|
|
50
|
+
protected readonly operatorOptions = OPERATOR_OPTIONS;
|
|
51
|
+
|
|
52
|
+
/** Active filter (if any) — the single source of truth for both fields. */
|
|
53
|
+
private readonly active = computed(() => this.state.filterFor(this.config().field));
|
|
54
|
+
|
|
55
|
+
/** Cap the picker at today when the field can't hold a future date. */
|
|
56
|
+
protected readonly maxDate = computed<Date | null>(() =>
|
|
57
|
+
this.config().disableFuture ? new Date() : null,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Selected operator. Defaults to `'gt'` when no filter is active, mirroring
|
|
62
|
+
* the React initial state (`min` present -> `gt`, else `lt`, but with no
|
|
63
|
+
* active filter React ended up on `lt`; we prefer `'gt'` as the safer
|
|
64
|
+
* "After a date" default when nothing is selected).
|
|
65
|
+
*/
|
|
66
|
+
protected readonly operator = computed<DateOperator>(() => {
|
|
67
|
+
const a = this.active();
|
|
68
|
+
if (a?.value === "gt" || a?.value === "lt") return a.value;
|
|
69
|
+
if (a?.min) return "gt";
|
|
70
|
+
if (a?.max) return "lt";
|
|
71
|
+
return "gt";
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Date picker value — parsed from either `min` (gt) or `max` (lt). */
|
|
75
|
+
protected readonly currentDate = computed<Date | null>(() => {
|
|
76
|
+
const a = this.active();
|
|
77
|
+
return toDate(a?.min ?? a?.max);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
protected onOperatorChange(v: string): void {
|
|
81
|
+
const op = v === "lt" ? "lt" : "gt";
|
|
82
|
+
const date = this.currentDate();
|
|
83
|
+
// Only re-emit if we already had a date; otherwise wait until the user
|
|
84
|
+
// picks one (matches React `emitChange` gate on `currentDate`).
|
|
85
|
+
if (date) this.emit(op, date);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected onDateChange(date: Date | null): void {
|
|
89
|
+
const cfg = this.config();
|
|
90
|
+
if (!date) {
|
|
91
|
+
this.state.removeFilter(cfg.field);
|
|
92
|
+
} else {
|
|
93
|
+
this.emit(this.operator(), date);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private emit(op: DateOperator, date: Date): void {
|
|
98
|
+
const cfg = this.config();
|
|
99
|
+
const dateStr = toDateString(date);
|
|
100
|
+
const isMin = op === "gt";
|
|
101
|
+
this.state.setFilter(cfg.field, {
|
|
102
|
+
field: cfg.field,
|
|
103
|
+
label: cfg.label,
|
|
104
|
+
type: this.filterType(),
|
|
105
|
+
value: op,
|
|
106
|
+
min: isMin ? dateStr : undefined,
|
|
107
|
+
max: isMin ? undefined : dateStr,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Parses `YYYY-MM-DD` (or any ISO date) into a `Date`, or `null` on failure. */
|
|
113
|
+
export function toDate(value: string | undefined): Date | null {
|
|
114
|
+
if (!value) return null;
|
|
115
|
+
// Interpret bare YYYY-MM-DD as a local-time date so the picker doesn't
|
|
116
|
+
// display "yesterday" in negative UTC offsets.
|
|
117
|
+
const parts = value.split("T")[0].split("-");
|
|
118
|
+
if (parts.length === 3) {
|
|
119
|
+
const [y, m, d] = parts.map((p) => Number(p));
|
|
120
|
+
const dt = new Date(y, m - 1, d);
|
|
121
|
+
return isNaN(dt.getTime()) ? null : dt;
|
|
122
|
+
}
|
|
123
|
+
const dt = new Date(value);
|
|
124
|
+
return isNaN(dt.getTime()) ? null : dt;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Formats a `Date` as `YYYY-MM-DD` in local time. */
|
|
128
|
+
export function toDateString(date: Date | null | undefined): string {
|
|
129
|
+
if (!date || isNaN(date.getTime())) return "";
|
|
130
|
+
const y = date.getFullYear();
|
|
131
|
+
// Guard against partially-typed years (e.g. `202` instead of `2026`), which
|
|
132
|
+
// would serialize to an invalid `202-07-09` and be rejected by the API.
|
|
133
|
+
if (y < 1000 || y > 9999) return "";
|
|
134
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
135
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
136
|
+
return `${y}-${m}-${d}`;
|
|
137
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<app-filter-field-wrapper [label]="config().label" [helpText]="config().helpText">
|
|
2
|
+
<app-date-range-picker
|
|
3
|
+
class="w-full"
|
|
4
|
+
startPlaceholder="Start"
|
|
5
|
+
endPlaceholder="End"
|
|
6
|
+
[readonly]="true"
|
|
7
|
+
[max]="maxDate()"
|
|
8
|
+
[value]="range()"
|
|
9
|
+
(valueChange)="onRangeChange($event)"
|
|
10
|
+
/>
|
|
11
|
+
</app-filter-field-wrapper>
|
|
@@ -0,0 +1,69 @@
|
|
|
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/date-range-picker/..., three levels up.
|
|
9
|
+
import {
|
|
10
|
+
DateRange,
|
|
11
|
+
DateRangePickerComponent,
|
|
12
|
+
} from "../../../../../components/ui/date-range-picker/date-range-picker.component";
|
|
13
|
+
import { ObjectSearchStateService } from "../../../object-search-state.service";
|
|
14
|
+
import type { FilterFieldConfig, FilterFieldType } from "../../../utils/filter-utils";
|
|
15
|
+
import { toDate, toDateString } from "../date-filter/date-filter.component";
|
|
16
|
+
import { FilterFieldWrapperComponent } from "../filter-field-wrapper/filter-field-wrapper.component";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Date range filter — a single two-ended range picker. Emits a `daterange`
|
|
20
|
+
* (or `datetimerange`) filter with `min` / `max` as `YYYY-MM-DD` strings; the
|
|
21
|
+
* filter builder wires each end into a `gte` / `lte` clause.
|
|
22
|
+
*
|
|
23
|
+
* Thin bridge over the base `app-date-range-picker` (Material's
|
|
24
|
+
* `mat-date-range-picker`): it maps the picker's `DateRange<Date>` value to and
|
|
25
|
+
* from the `min` / `max` string split the filter state stores.
|
|
26
|
+
*/
|
|
27
|
+
@Component({
|
|
28
|
+
selector: "app-date-range-filter",
|
|
29
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
30
|
+
imports: [DateRangePickerComponent, FilterFieldWrapperComponent],
|
|
31
|
+
templateUrl: "./date-range-filter.component.html",
|
|
32
|
+
})
|
|
33
|
+
export class DateRangeFilterComponent {
|
|
34
|
+
private readonly state = inject(ObjectSearchStateService);
|
|
35
|
+
|
|
36
|
+
/** Field config: `field`, `label`, `helpText`, ... */
|
|
37
|
+
readonly config = input.required<FilterFieldConfig>();
|
|
38
|
+
/** Underlying filter type — `'daterange'` (default) or `'datetimerange'`. */
|
|
39
|
+
readonly filterType = input<FilterFieldType>("daterange");
|
|
40
|
+
|
|
41
|
+
private readonly active = computed(() => this.state.filterFor(this.config().field));
|
|
42
|
+
|
|
43
|
+
/** The active filter's `min` / `max` strings surfaced as a picker range. */
|
|
44
|
+
protected readonly range = computed<DateRange<Date>>(
|
|
45
|
+
() => new DateRange(toDate(this.active()?.min), toDate(this.active()?.max)),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/** Cap the picker at today when the field can't hold a future date. */
|
|
49
|
+
protected readonly maxDate = computed<Date | null>(() =>
|
|
50
|
+
this.config().disableFuture ? new Date() : null,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
protected onRangeChange(range: DateRange<Date> | null): void {
|
|
54
|
+
const cfg = this.config();
|
|
55
|
+
const from = range?.start ?? null;
|
|
56
|
+
const to = range?.end ?? null;
|
|
57
|
+
if (!from && !to) {
|
|
58
|
+
this.state.removeFilter(cfg.field);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.state.setFilter(cfg.field, {
|
|
62
|
+
field: cfg.field,
|
|
63
|
+
label: cfg.label,
|
|
64
|
+
type: this.filterType(),
|
|
65
|
+
min: toDateString(from),
|
|
66
|
+
max: toDateString(to),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div class="space-y-1">
|
|
2
|
+
<app-label [for]="htmlFor()">{{ label() }}</app-label>
|
|
3
|
+
<ng-content />
|
|
4
|
+
<div class="min-h-4">
|
|
5
|
+
@if (error(); as err) {
|
|
6
|
+
<p class="text-xs text-red-600" role="alert">{{ err }}</p>
|
|
7
|
+
} @else if (helpText(); as help) {
|
|
8
|
+
<p class="text-xs text-gray-500">{{ help }}</p>
|
|
9
|
+
}
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,36 @@
|
|
|
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, 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/label/..., three levels up from this
|
|
9
|
+
// filter folder (app/object-search/components/filters/ -> app/components/ui/).
|
|
10
|
+
import { LabelComponent } from "../../../../../components/ui/label/label.component";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Standard label + control + help/error scaffold shared by every filter
|
|
14
|
+
* component (the Angular analogue of the React `FilterFieldWrapper`).
|
|
15
|
+
*
|
|
16
|
+
* The control itself is content-projected so each filter can supply the
|
|
17
|
+
* primitive that fits its data type (input, select, popover, date picker, ...)
|
|
18
|
+
* while the wrapper keeps the layout, label, and error/help affordances
|
|
19
|
+
* consistent across the search page.
|
|
20
|
+
*/
|
|
21
|
+
@Component({
|
|
22
|
+
selector: "app-filter-field-wrapper",
|
|
23
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
24
|
+
imports: [LabelComponent],
|
|
25
|
+
templateUrl: "./filter-field-wrapper.component.html",
|
|
26
|
+
})
|
|
27
|
+
export class FilterFieldWrapperComponent {
|
|
28
|
+
/** Visible field label rendered above the control. */
|
|
29
|
+
readonly label = input.required<string>();
|
|
30
|
+
/** Optional `for` attribute forwarded to the underlying `<label>`. */
|
|
31
|
+
readonly htmlFor = input<string | null>(null);
|
|
32
|
+
/** Advisory copy rendered below the control when there is no error. */
|
|
33
|
+
readonly helpText = input<string | undefined>(undefined);
|
|
34
|
+
/** Validation message; when set, replaces the help text. */
|
|
35
|
+
readonly error = input<string | undefined>(undefined);
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<app-filter-field-wrapper [label]="config().label" [helpText]="config().helpText">
|
|
2
|
+
<app-select
|
|
3
|
+
multiple
|
|
4
|
+
[size]="size()"
|
|
5
|
+
[options]="config().options ?? []"
|
|
6
|
+
[placeholder]="placeholder()"
|
|
7
|
+
[values]="selected()"
|
|
8
|
+
(valuesChange)="onSelectionChange($event)"
|
|
9
|
+
/>
|
|
10
|
+
</app-filter-field-wrapper>
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
import type { AppFieldSize } from "../../../../../components/ui/field-size";
|
|
8
|
+
import { SelectComponent } from "../../../../../components/ui/select/select.component";
|
|
9
|
+
import { ObjectSearchStateService } from "../../../object-search-state.service";
|
|
10
|
+
import type { FilterFieldConfig } from "../../../utils/filter-utils";
|
|
11
|
+
import { FilterFieldWrapperComponent } from "../filter-field-wrapper/filter-field-wrapper.component";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Multi-select picklist filter. All presentation (checkbox options, trigger
|
|
15
|
+
* summary, height) lives in `app-select multiple`; this component only bridges
|
|
16
|
+
* the UI's `string[]` selection to the object-search state, serializing it into
|
|
17
|
+
* the comma-joined string the `type: 'multipicklist'` contract expects
|
|
18
|
+
* (`buildSingleFilter` maps that to `{ eq }` for one value, `{ in: [...] }` for
|
|
19
|
+
* many).
|
|
20
|
+
*/
|
|
21
|
+
@Component({
|
|
22
|
+
selector: "app-multi-select-filter",
|
|
23
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
24
|
+
imports: [SelectComponent, FilterFieldWrapperComponent],
|
|
25
|
+
templateUrl: "./multi-select-filter.component.html",
|
|
26
|
+
})
|
|
27
|
+
export class MultiSelectFilterComponent {
|
|
28
|
+
private readonly state = inject(ObjectSearchStateService);
|
|
29
|
+
|
|
30
|
+
/** Field config: `field`, `label`, `options`, `helpText`, ... */
|
|
31
|
+
readonly config = input.required<FilterFieldConfig>();
|
|
32
|
+
|
|
33
|
+
/** Field height: `sm` = 24px, `default` = 32px, `lg` = 40px. */
|
|
34
|
+
readonly size = input<AppFieldSize>("default");
|
|
35
|
+
|
|
36
|
+
/** Currently selected option values (parsed back from the comma-joined string). */
|
|
37
|
+
protected readonly selected = computed<string[]>(() => {
|
|
38
|
+
const raw = this.state.filterFor(this.config().field)?.value;
|
|
39
|
+
return raw ? raw.split(",") : [];
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Placeholder shown on the trigger when nothing is selected. */
|
|
43
|
+
protected readonly placeholder = computed(() => `Select ${this.config().label.toLowerCase()}`);
|
|
44
|
+
|
|
45
|
+
protected onSelectionChange(next: readonly string[]): void {
|
|
46
|
+
const cfg = this.config();
|
|
47
|
+
if (next.length === 0) {
|
|
48
|
+
this.state.removeFilter(cfg.field);
|
|
49
|
+
} else {
|
|
50
|
+
this.state.setFilter(cfg.field, {
|
|
51
|
+
field: cfg.field,
|
|
52
|
+
label: cfg.label,
|
|
53
|
+
type: "multipicklist",
|
|
54
|
+
value: next.join(","),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<app-filter-field-wrapper
|
|
2
|
+
[label]="config().label"
|
|
3
|
+
[helpText]="config().helpText"
|
|
4
|
+
[error]="errorMessage()"
|
|
5
|
+
>
|
|
6
|
+
<div class="flex gap-2">
|
|
7
|
+
<app-input
|
|
8
|
+
class="flex-1"
|
|
9
|
+
type="number"
|
|
10
|
+
placeholder="Min"
|
|
11
|
+
[value]="localMin()"
|
|
12
|
+
(valueChange)="onMinChange($event)"
|
|
13
|
+
/>
|
|
14
|
+
<app-input
|
|
15
|
+
class="flex-1"
|
|
16
|
+
type="number"
|
|
17
|
+
placeholder="Max"
|
|
18
|
+
[value]="localMax()"
|
|
19
|
+
(valueChange)="onMaxChange($event)"
|
|
20
|
+
/>
|
|
21
|
+
</div>
|
|
22
|
+
</app-filter-field-wrapper>
|