diff --git a/core-libs/schematics/src/shared/lib-configs/organization-schematics-config.ts b/core-libs/schematics/src/shared/lib-configs/organization-schematics-config.ts index f8a42a49719..f76ab1e7ec8 100644 --- a/core-libs/schematics/src/shared/lib-configs/organization-schematics-config.ts +++ b/core-libs/schematics/src/shared/lib-configs/organization-schematics-config.ts @@ -8,6 +8,7 @@ import { ORDER_FEATURE_NAME, ORGANIZATION_ACCOUNT_SUMMARY_FEATURE_NAME, ORGANIZATION_ADMINISTRATION_FEATURE_NAME, + ORGANIZATION_B2B_UNIT_SELECTION_FEATURE_NAME, ORGANIZATION_ORDER_APPROVAL_FEATURE_NAME, ORGANIZATION_USER_REGISTRATION_FEATURE_NAME, ORGANIZATION_UNIT_ORDER_FEATURE_NAME, @@ -21,6 +22,9 @@ import { SPARTACUS_ORGANIZATION_ACCOUNT_SUMMARY_ROOT, SPARTACUS_ORGANIZATION_ADMINISTRATION_ASSETS, SPARTACUS_ORGANIZATION_ADMINISTRATION_ROOT, + SPARTACUS_ORGANIZATION_B2B_UNIT_SELECTION, + SPARTACUS_ORGANIZATION_B2B_UNIT_SELECTION_ASSETS, + SPARTACUS_ORGANIZATION_B2B_UNIT_SELECTION_ROOT, SPARTACUS_ORGANIZATION_ORDER_APPROVAL, SPARTACUS_ORGANIZATION_ORDER_APPROVAL_ASSETS, SPARTACUS_ORGANIZATION_ORDER_APPROVAL_ROOT, @@ -279,3 +283,55 @@ export const ORGANIZATION_ACCOUNT_SUMMARY_SCHEMATICS_CONFIG: SchematicConfig = { }, dependencyFeatures: [ORGANIZATION_ADMINISTRATION_FEATURE_NAME], }; + +export const ORGANIZATION_B2B_UNIT_SELECTION_MODULE_NAME = + 'OrganizationB2bUnitSelection'; +export const B2B_UNIT_SELECTION_MODULE = 'B2bUnitSelectionModule'; +export const B2B_UNIT_SELECTION_ROOT_MODULE = 'B2bUnitSelectionRootModule'; +export const ORGANIZATION_B2B_UNIT_SELECTION_FEATURE_NAME_CONSTANT = + 'ORGANIZATION_B2B_UNIT_SELECTION_FEATURE'; +export const B2B_UNIT_SELECTION_TRANSLATIONS = 'b2bUnitSelectionTranslations'; +export const B2B_UNIT_SELECTION_TRANSLATION_CHUNKS_CONFIG = + 'b2bUnitSelectionTranslationChunksConfig'; + +export const ORGANIZATION_B2B_UNIT_SELECTION_SCHEMATICS_CONFIG: SchematicConfig = + { + library: { + featureName: ORGANIZATION_B2B_UNIT_SELECTION_FEATURE_NAME, + mainScope: SPARTACUS_ORGANIZATION, + featureScope: SPARTACUS_ORGANIZATION_B2B_UNIT_SELECTION, + b2b: true, + }, + folderName: ORGANIZATION_FOLDER_NAME, + moduleName: ORGANIZATION_B2B_UNIT_SELECTION_MODULE_NAME, + featureModule: { + name: B2B_UNIT_SELECTION_MODULE, + importPath: SPARTACUS_ORGANIZATION_B2B_UNIT_SELECTION, + }, + rootModule: { + name: B2B_UNIT_SELECTION_ROOT_MODULE, + importPath: SPARTACUS_ORGANIZATION_B2B_UNIT_SELECTION_ROOT, + }, + lazyLoadingChunk: { + moduleSpecifier: SPARTACUS_ORGANIZATION_B2B_UNIT_SELECTION_ROOT, + namedImports: [ORGANIZATION_B2B_UNIT_SELECTION_FEATURE_NAME_CONSTANT], + }, + i18n: { + resources: B2B_UNIT_SELECTION_TRANSLATIONS, + chunks: B2B_UNIT_SELECTION_TRANSLATION_CHUNKS_CONFIG, + importPath: SPARTACUS_ORGANIZATION_B2B_UNIT_SELECTION_ASSETS, + }, + styles: { + scssFileName: ORGANIZATION_SCSS_FILE_NAME, + importStyle: SPARTACUS_ORGANIZATION, + importStyles: [ + SPARTACUS_BOOTSTRAP_FUNCTIONS, + SPARTACUS_BOOTSTRAP_VARIABLES, + SPARTACUS_BOOTSTRAP_MIXINS, + ], + }, + // Requires B2bOrganization (Administration) to be installed first, + // as per the reviewer's guidance. The feature itself is opt-in at runtime + // via the b2bUnitSelection.enabled config flag. + dependencyFeatures: [ORGANIZATION_ADMINISTRATION_FEATURE_NAME], + }; diff --git a/core-libs/schematics/src/shared/libs-constants.ts b/core-libs/schematics/src/shared/libs-constants.ts index 092417277f9..3d6e7effb38 100644 --- a/core-libs/schematics/src/shared/libs-constants.ts +++ b/core-libs/schematics/src/shared/libs-constants.ts @@ -124,6 +124,10 @@ export const SPARTACUS_ORGANIZATION_ACCOUNT_SUMMARY_COMPONENTS = `@spartacus/org export const SPARTACUS_ORGANIZATION_ACCOUNT_SUMMARY_ASSETS = `@spartacus/organization/account-summary/assets`; export const SPARTACUS_ORGANIZATION_ACCOUNT_SUMMARY_OCC = `@spartacus/organization/account-summary/occ`; +export const SPARTACUS_ORGANIZATION_B2B_UNIT_SELECTION = `@spartacus/organization/b2b-unit-selection`; +export const SPARTACUS_ORGANIZATION_B2B_UNIT_SELECTION_ROOT = `@spartacus/organization/b2b-unit-selection/root`; +export const SPARTACUS_ORGANIZATION_B2B_UNIT_SELECTION_ASSETS = `@spartacus/organization/b2b-unit-selection/assets`; + export const SPARTACUS_PRODUCT = `@spartacus/product`; export const SPARTACUS_PRODUCT_VARIANTS_COMPONENTS = `@spartacus/product/variants/components`; export const SPARTACUS_PRODUCT_VARIANTS_ROOT = `@spartacus/product/variants/root`; @@ -336,6 +340,9 @@ export const ORGANIZATION_UNIT_ORDER_FEATURE_NAME = 'Unit-Order'; export const ORGANIZATION_ACCOUNT_SUMMARY_FEATURE_NAME = 'Account-Summary'; +export const ORGANIZATION_B2B_UNIT_SELECTION_FEATURE_NAME = + 'B2b-Unit-Selection'; + export const PRODUCT_BULK_PRICING_FEATURE_NAME = 'Bulk-Pricing'; export const PRODUCT_IMAGE_ZOOM_FEATURE_NAME = 'Image-Zoom'; export const PRODUCT_VARIANTS_FEATURE_NAME = 'Product-Variants'; diff --git a/core-libs/schematics/src/shared/schematics-config-mappings.ts b/core-libs/schematics/src/shared/schematics-config-mappings.ts index 30c695629f5..d9936546e23 100644 --- a/core-libs/schematics/src/shared/schematics-config-mappings.ts +++ b/core-libs/schematics/src/shared/schematics-config-mappings.ts @@ -50,6 +50,7 @@ import { import { ORGANIZATION_ACCOUNT_SUMMARY_SCHEMATICS_CONFIG, ORGANIZATION_ADMINISTRATION_SCHEMATICS_CONFIG, + ORGANIZATION_B2B_UNIT_SELECTION_SCHEMATICS_CONFIG, ORGANIZATION_ORDER_APPROVAL_SCHEMATICS_CONFIG, ORGANIZATION_UNIT_ORDER_SCHEMATICS_CONFIG, ORGANIZATION_USER_REGISTRATION_SCHEMATICS_CONFIG, @@ -126,6 +127,7 @@ export const SCHEMATICS_CONFIGS: SchematicConfig[] = [ ORGANIZATION_USER_REGISTRATION_SCHEMATICS_CONFIG, ORGANIZATION_UNIT_ORDER_SCHEMATICS_CONFIG, ORGANIZATION_ACCOUNT_SUMMARY_SCHEMATICS_CONFIG, + ORGANIZATION_B2B_UNIT_SELECTION_SCHEMATICS_CONFIG, PICKUP_IN_STORE_SCHEMATICS_CONFIG, PRODUCT_CONFIGURATOR_TEXTFIELD_SCHEMATICS_CONFIG, diff --git a/core-libs/schematics/src/shared/utils/graph-utils_spec.ts b/core-libs/schematics/src/shared/utils/graph-utils_spec.ts index b4587a1fc46..914cb652eb1 100644 --- a/core-libs/schematics/src/shared/utils/graph-utils_spec.ts +++ b/core-libs/schematics/src/shared/utils/graph-utils_spec.ts @@ -211,6 +211,7 @@ describe('Graph utils', () => { "Customer-Ticketing", "Organization-User-Registration", "Administration", + "B2b-Unit-Selection", "Account-Summary", "Unit-Order", "Order-Approval", diff --git a/core-libs/schematics/src/shared/utils/test-utils.ts b/core-libs/schematics/src/shared/utils/test-utils.ts index c4282ff654d..c53424c5dc5 100644 --- a/core-libs/schematics/src/shared/utils/test-utils.ts +++ b/core-libs/schematics/src/shared/utils/test-utils.ts @@ -66,6 +66,8 @@ export const organizationUnitOrderFeatureModulePath = 'src/app/spartacus/features/organization/organization-unit-order-feature.module.ts'; export const organizationAccountSummaryFeatureModulePath = 'src/app/spartacus/features/organization/organization-account-summary-feature.module.ts'; +export const organizationB2bUnitSelectionFeatureModulePath = + 'src/app/spartacus/features/organization/organization-b2b-unit-selection-feature.module.ts'; export const pdfInvoicesFeatureModulePath = 'src/app/spartacus/features/pdf-invoices/pdf-invoices-feature.module.ts'; export const productBulkPricingFeatureModulePath = diff --git a/core-libs/setup/tsconfig.spec.json b/core-libs/setup/tsconfig.spec.json index df2276d85d6..8b3549dc839 100644 --- a/core-libs/setup/tsconfig.spec.json +++ b/core-libs/setup/tsconfig.spec.json @@ -350,6 +350,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/asm/tsconfig.schematics.json b/feature-libs/asm/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/asm/tsconfig.schematics.json +++ b/feature-libs/asm/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/cart/tsconfig.schematics.json b/feature-libs/cart/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/cart/tsconfig.schematics.json +++ b/feature-libs/cart/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/checkout/tsconfig.schematics.json b/feature-libs/checkout/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/checkout/tsconfig.schematics.json +++ b/feature-libs/checkout/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/customer-ticketing/tsconfig.schematics.json b/feature-libs/customer-ticketing/tsconfig.schematics.json index 5d3997312e6..03e34d069f0 100644 --- a/feature-libs/customer-ticketing/tsconfig.schematics.json +++ b/feature-libs/customer-ticketing/tsconfig.schematics.json @@ -360,6 +360,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/estimated-delivery-date/tsconfig.schematics.json b/feature-libs/estimated-delivery-date/tsconfig.schematics.json index 5d3997312e6..03e34d069f0 100644 --- a/feature-libs/estimated-delivery-date/tsconfig.schematics.json +++ b/feature-libs/estimated-delivery-date/tsconfig.schematics.json @@ -360,6 +360,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/order/tsconfig.schematics.json b/feature-libs/order/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/order/tsconfig.schematics.json +++ b/feature-libs/order/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/organization/_index.scss b/feature-libs/organization/_index.scss index 6b2812e89c7..937cd2a9ad0 100644 --- a/feature-libs/organization/_index.scss +++ b/feature-libs/organization/_index.scss @@ -13,3 +13,4 @@ @import './user-registration/index'; @import './account-summary/index'; @import './unit-order/index'; +@import './b2b-unit-selection/styles/index'; diff --git a/feature-libs/organization/b2b-unit-selection/assets/ng-package.json b/feature-libs/organization/b2b-unit-selection/assets/ng-package.json new file mode 100644 index 00000000000..2a9c6221b0e --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/assets/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/feature-libs/organization/b2b-unit-selection/assets/public_api.ts b/feature-libs/organization/b2b-unit-selection/assets/public_api.ts new file mode 100644 index 00000000000..bae44a4d359 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/assets/public_api.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './translations/translations'; diff --git a/feature-libs/organization/b2b-unit-selection/assets/translations/en/b2b-unit-selection.json b/feature-libs/organization/b2b-unit-selection/assets/translations/en/b2b-unit-selection.json new file mode 100644 index 00000000000..29fb8d9b5ff --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/assets/translations/en/b2b-unit-selection.json @@ -0,0 +1,13 @@ +{ + "b2bUnitSelection": { + "dialog": { + "title": "Confirm B2B Unit", + "description": "Confirm the B2B unit you want to act on behalf of.", + "unitLabel": "B2B Unit", + "confirm": "Confirm" + }, + "selector": { + "label": "B2B Unit:" + } + } +} diff --git a/feature-libs/organization/b2b-unit-selection/assets/translations/en/index.ts b/feature-libs/organization/b2b-unit-selection/assets/translations/en/index.ts new file mode 100644 index 00000000000..3bf9d093d05 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/assets/translations/en/index.ts @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import b2bUnitSelection from './b2b-unit-selection.json'; + +export const en = { b2bUnitSelection }; diff --git a/feature-libs/organization/b2b-unit-selection/assets/translations/translations.ts b/feature-libs/organization/b2b-unit-selection/assets/translations/translations.ts new file mode 100644 index 00000000000..026cb2a5dbc --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/assets/translations/translations.ts @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TranslationChunksConfig } from '@spartacus/core'; + +export const b2bUnitSelectionTranslationChunksConfig: TranslationChunksConfig = + { + b2bUnitSelection: ['b2bUnitSelection'], + }; + +export { en as b2bUnitSelectionTranslationsEn } from './en/index'; diff --git a/feature-libs/organization/b2b-unit-selection/b2b-unit-selection.module.ts b/feature-libs/organization/b2b-unit-selection/b2b-unit-selection.module.ts new file mode 100644 index 00000000000..3f22dcaabe1 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/b2b-unit-selection.module.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { B2bUnitSelectionCoreModule } from './core/b2b-unit-selection-core.module'; +import { B2bUnitSelectionOccModule } from './occ/b2b-unit-selection-occ.module'; +import { B2bUnitSelectionComponentsModule } from './components/b2b-unit-selection-components.module'; + +@NgModule({ + imports: [ + B2bUnitSelectionCoreModule, + B2bUnitSelectionOccModule, + B2bUnitSelectionComponentsModule, + ], +}) +export class B2bUnitSelectionModule {} diff --git a/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-components.module.ts b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-components.module.ts new file mode 100644 index 00000000000..0921d386fb8 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-components.module.ts @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { CmsConfig, provideDefaultConfig } from '@spartacus/core'; +import { B2bUnitSelectionDialogComponent } from './b2b-unit-selection-dialog/b2b-unit-selection-dialog.component'; +import { B2bUnitSelectorComponent } from './b2b-unit-selector/b2b-unit-selector.component'; +import { defaultB2bUnitSelectionLayoutConfig } from './default-b2b-unit-selection-layout.config'; + +@NgModule({ + imports: [B2bUnitSelectionDialogComponent, B2bUnitSelectorComponent], + providers: [ + provideDefaultConfig(defaultB2bUnitSelectionLayoutConfig), + provideDefaultConfig({ + cmsComponents: { + B2bUnitSelectorComponent: { + component: B2bUnitSelectorComponent, + }, + }, + }), + ], +}) +export class B2bUnitSelectionComponentsModule {} diff --git a/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-dialog/b2b-unit-selection-dialog.component.html b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-dialog/b2b-unit-selection-dialog.component.html new file mode 100644 index 00000000000..db967786ce1 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-dialog/b2b-unit-selection-dialog.component.html @@ -0,0 +1,58 @@ + + + diff --git a/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-dialog/b2b-unit-selection-dialog.component.spec.ts b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-dialog/b2b-unit-selection-dialog.component.spec.ts new file mode 100644 index 00000000000..9390c795182 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-dialog/b2b-unit-selection-dialog.component.spec.ts @@ -0,0 +1,180 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ChangeDetectionStrategy, Directive, Input } from '@angular/core'; +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NgSelectModule } from '@ng-select/ng-select'; +import { B2BUnit, I18nTestingModule } from '@spartacus/core'; +import { + FocusConfig, + FocusDirective, + FormErrorsModule, + LaunchDialogService, + NgSelectA11yDirective, +} from '@spartacus/storefront'; +import { BehaviorSubject } from 'rxjs'; +import { B2bUnitSelectionDialogComponent } from './b2b-unit-selection-dialog.component'; +import createSpy = jasmine.createSpy; + +const mockUnits: B2BUnit[] = [ + { uid: 'unit-1', name: 'Rustic' }, + { uid: 'unit-2', name: 'Rustic Services' }, +]; + +@Directive({ selector: '[cxFocus]' }) +class MockFocusDirective { + @Input('cxFocus') config: FocusConfig = {}; +} + +@Directive({ selector: '[cxNgSelectA11y]' }) +class MockNgSelectA11yDirective { + @Input() cxNgSelectA11y: { ariaLabel?: string } = {}; +} + +describe('B2bUnitSelectionDialogComponent', () => { + let component: B2bUnitSelectionDialogComponent; + let fixture: ComponentFixture; + let data$: BehaviorSubject; + + function createComponent(dialogData: any = {}): void { + data$ = new BehaviorSubject(dialogData); + + TestBed.configureTestingModule({ + imports: [ + B2bUnitSelectionDialogComponent, + ReactiveFormsModule, + NgSelectModule, + I18nTestingModule, + FormErrorsModule, + ], + providers: [{ provide: LaunchDialogService, useValue: { data$ } }], + }) + .overrideComponent(B2bUnitSelectionDialogComponent, { + remove: { imports: [FocusDirective, NgSelectA11yDirective] }, + add: { + imports: [MockFocusDirective, MockNgSelectA11yDirective], + changeDetection: ChangeDetectionStrategy.Default, + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(B2bUnitSelectionDialogComponent); + component = fixture.componentInstance; + } + + // ── ngOnInit ────────────────────────────────────────────────────────────── + + describe('ngOnInit()', () => { + it('should populate orgUnits from dialog data', waitForAsync(() => { + createComponent({ orgUnits: mockUnits, defaultUnitName: 'unit-1' }); + fixture.detectChanges(); + + expect(component.orgUnits).toEqual(mockUnits); + })); + + it('should pre-select the unit matching defaultUnitName', waitForAsync(() => { + createComponent({ + orgUnits: mockUnits, + defaultUnitName: 'Rustic Services', + }); + fixture.detectChanges(); + + expect(component.form.value.selectedUnit).toEqual(mockUnits[1]); + })); + + it('should fall back to the first unit when defaultUnitName does not match', waitForAsync(() => { + createComponent({ orgUnits: mockUnits, defaultUnitName: 'unknown-name' }); + fixture.detectChanges(); + + expect(component.form.value.selectedUnit).toEqual(mockUnits[0]); + })); + + it('should set selectedUnit to null when orgUnits is empty', waitForAsync(() => { + createComponent({ orgUnits: [], defaultUnitName: undefined }); + fixture.detectChanges(); + + expect(component.form.value.selectedUnit).toBeNull(); + })); + + it('should handle missing dialog data gracefully', waitForAsync(() => { + createComponent(null); + fixture.detectChanges(); + + expect(component.orgUnits).toEqual([]); + expect(component.form.value.selectedUnit).toBeNull(); + })); + + it('should store the onConfirm callback from dialog data', waitForAsync(() => { + const onConfirm = createSpy('onConfirm'); + createComponent({ + orgUnits: mockUnits, + defaultUnitName: 'Rustic', + onConfirm, + }); + fixture.detectChanges(); + + expect((component as any).onConfirm).toBe(onConfirm); + })); + }); + + // ── confirm() ───────────────────────────────────────────────────────────── + + describe('confirm()', () => { + let onConfirm: jasmine.Spy; + + beforeEach(waitForAsync(() => { + onConfirm = createSpy('onConfirm'); + createComponent({ + orgUnits: mockUnits, + defaultUnitName: 'Rustic', + onConfirm, + }); + fixture.detectChanges(); + })); + + it('should invoke the onConfirm callback with the selected unit name', () => { + component.confirm(); + + expect(onConfirm).toHaveBeenCalledWith('Rustic'); + }); + + it('should mark form as touched and NOT invoke onConfirm when selectedUnit is null', () => { + component.form.get('selectedUnit')?.setValue(null); + + component.confirm(); + + expect(onConfirm).not.toHaveBeenCalled(); + expect(component.form.get('selectedUnit')?.touched).toBeTrue(); + }); + + it('should NOT throw when onConfirm is not provided', () => { + // Re-create without onConfirm in data payload. + TestBed.resetTestingModule(); + createComponent({ orgUnits: mockUnits, defaultUnitName: 'Rustic' }); + fixture.detectChanges(); + + expect(() => component.confirm()).not.toThrow(); + }); + }); + + // ── ngOnDestroy ─────────────────────────────────────────────────────────── + + describe('ngOnDestroy()', () => { + it('should unsubscribe from all subscriptions', waitForAsync(() => { + createComponent({ orgUnits: mockUnits }); + fixture.detectChanges(); + + const unsubscribeSpy = spyOn( + (component as any).subscriptions, + 'unsubscribe' + ); + component.ngOnDestroy(); + + expect(unsubscribeSpy).toHaveBeenCalled(); + })); + }); +}); diff --git a/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-dialog/b2b-unit-selection-dialog.component.ts b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-dialog/b2b-unit-selection-dialog.component.ts new file mode 100644 index 00000000000..fada24c081d --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selection-dialog/b2b-unit-selection-dialog.component.ts @@ -0,0 +1,120 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ChangeDetectionStrategy, + Component, + inject, + OnDestroy, + OnInit, +} from '@angular/core'; +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { B2BUnit, TranslatePipe } from '@spartacus/core'; +import { + FocusConfig, + FocusDirective, + FormErrorsModule, + LaunchDialogService, + NgSelectA11yDirective, +} from '@spartacus/storefront'; +import { NgSelectComponent } from '@ng-select/ng-select'; +import { Subscription } from 'rxjs'; +import { take } from 'rxjs/operators'; + +/** + * Dialog component for B2B unit selection. + * + * This component is standalone and rendered in the root outlet via + * `inlineRoot: true`. It does NOT inject `B2bUnitSelectionService` because + * that service is module-scoped (lives in the lazy feature module's child + * injector) and is therefore not reachable from the root outlet context. + * + * Instead, `B2bUnitSelectionService.openDialogWhenReady` passes an `onConfirm` + * callback via the dialog data payload. On confirmation this component calls + * that callback with the selected unit name, which triggers `setDefaultUnit` + * in the service. The service then calls `LaunchDialogService.closeDialog` on + * HTTP success — preserving the original close-after-persist behaviour. + */ +@Component({ + selector: 'cx-b2b-unit-selection-dialog', + templateUrl: './b2b-unit-selection-dialog.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + ReactiveFormsModule, + FocusDirective, + TranslatePipe, + FormErrorsModule, + NgSelectComponent, + NgSelectA11yDirective, + ], +}) +export class B2bUnitSelectionDialogComponent implements OnInit, OnDestroy { + protected launchDialogService = inject(LaunchDialogService); + + orgUnits: B2BUnit[] = []; + protected subscriptions = new Subscription(); + + private onConfirm?: (unitName: string) => void; + + focusConfig: FocusConfig = { + trap: true, + block: true, + autofocus: 'ng-select', + focusOnEscape: false, + }; + + form = new FormGroup({ + selectedUnit: new FormControl(null, [Validators.required]), + }); + + ngOnInit(): void { + this.subscriptions.add( + this.launchDialogService.data$ + .pipe(take(1)) + .subscribe( + (data: { + orgUnits: B2BUnit[]; + defaultUnitName?: string; + onConfirm?: (unitName: string) => void; + }) => { + this.orgUnits = data?.orgUnits ?? []; + this.onConfirm = data?.onConfirm; + const matched = + this.orgUnits.find((u) => u.name === data?.defaultUnitName) ?? + this.orgUnits[0] ?? + null; + this.form.get('selectedUnit')?.setValue(matched); + } + ) + ); + } + + /** + * Invokes the `onConfirm` callback provided by `B2bUnitSelectionService` + * via the dialog data payload. The service handles the API call and closes + * the dialog on success via `LaunchDialogService.closeDialog`. + */ + confirm(): void { + if (this.form.invalid) { + this.form.markAllAsTouched(); + return; + } + const unit = this.form.value.selectedUnit; + if (!unit) { + return; + } + this.onConfirm?.(unit.name ?? ''); + } + + ngOnDestroy(): void { + this.subscriptions.unsubscribe(); + } +} diff --git a/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/abstract-b2b-unit-selector.component.spec.ts b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/abstract-b2b-unit-selector.component.spec.ts new file mode 100644 index 00000000000..a48137b4138 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/abstract-b2b-unit-selector.component.spec.ts @@ -0,0 +1,204 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { B2BUnit, OCC_USER_ID_ANONYMOUS, UserIdService } from '@spartacus/core'; +import { of, throwError } from 'rxjs'; +import { B2bUnitSelectionConnector } from '../../core/connectors/b2b-unit-selection.connector'; +import { B2bUnitSelectionService } from '../../core/services/b2b-unit-selection.service'; +import { B2bUnitSelectorStateService } from '../../core/services/b2b-unit-selector-state.service'; +import { B2bUnitSelectionConfig } from '../../root/config/b2b-unit-selection.config'; +import { AbstractB2bUnitSelectorComponent } from './abstract-b2b-unit-selector.component'; +import createSpy = jasmine.createSpy; + +const mockUnits: B2BUnit[] = [ + { uid: 'unit-1', name: 'Rustic' }, + { uid: 'unit-2', name: 'Rustic Services' }, +]; +const mockUserId = 'current'; +const mockDefaultUid = 'Rustic'; + +/** Minimal concrete subclass for testing. */ +@Component({ + standalone: true, + selector: 'cx-test-selector', + template: '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class TestB2bUnitSelectorComponent extends AbstractB2bUnitSelectorComponent {} + +class MockB2bUnitSelectorStateService { + private _orgUnitsValue: B2BUnit[] = []; + private _activeUnitNameValue: string | null = null; + + orgUnits$ = of(this._orgUnitsValue); + activeUnitName$ = of(this._activeUnitNameValue); + + setOrgUnits = createSpy('setOrgUnits').and.callFake((units: B2BUnit[]) => { + this._orgUnitsValue = units; + this.orgUnits$ = of(units); + }); + setActiveUnit = createSpy('setActiveUnit'); +} + +class MockB2bUnitSelectionConnector { + loadOrgUnits = createSpy('loadOrgUnits').and.returnValue(of(mockUnits)); + loadDefaultOrgUnitName = createSpy('loadDefaultOrgUnitName').and.returnValue( + of(mockDefaultUid) + ); +} + +class MockUserIdService { + getUserId = createSpy('getUserId').and.returnValue(of(mockUserId)); + takeUserId = createSpy('takeUserId').and.returnValue(of(mockUserId)); +} + +class MockB2bUnitSelectionService { + setDefaultUnit = createSpy('setDefaultUnit'); +} + +describe('AbstractB2bUnitSelectorComponent', () => { + let component: TestB2bUnitSelectorComponent; + let fixture: ComponentFixture; + let stateService: MockB2bUnitSelectorStateService; + let connector: MockB2bUnitSelectionConnector; + let unitSelectionService: MockB2bUnitSelectionService; + + function createComponent(enabled = true, initialUnits: B2BUnit[] = []): void { + stateService = new MockB2bUnitSelectorStateService(); + stateService.orgUnits$ = of(initialUnits); + + TestBed.configureTestingModule({ + imports: [TestB2bUnitSelectorComponent], + providers: [ + { provide: B2bUnitSelectorStateService, useValue: stateService }, + { + provide: B2bUnitSelectionConnector, + useClass: MockB2bUnitSelectionConnector, + }, + { provide: UserIdService, useClass: MockUserIdService }, + { + provide: B2bUnitSelectionService, + useClass: MockB2bUnitSelectionService, + }, + { + provide: B2bUnitSelectionConfig, + useValue: { b2bUnitSelection: { enabled } }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(TestB2bUnitSelectorComponent); + component = fixture.componentInstance; + connector = TestBed.inject(B2bUnitSelectionConnector) as any; + unitSelectionService = TestBed.inject(B2bUnitSelectionService) as any; + } + + describe('computed signals', () => { + beforeEach(() => createComponent()); + + it('hasMultipleUnits should be false when items is empty', () => { + expect(component.hasMultipleUnits()).toBeFalse(); + }); + + it('hasAnyUnit should be false when items is empty', () => { + expect(component.hasAnyUnit()).toBeFalse(); + }); + }); + + describe('signal initialisation when disabled', () => { + it('should expose static empty signal for items when feature is disabled', () => { + createComponent(false, []); + fixture.detectChanges(); + + // items() must return an empty array without subscribing to stateService.orgUnits$ + expect(component.items()).toEqual([]); + }); + + it('should expose static null signal for activeUnitName when feature is disabled', () => { + createComponent(false, []); + fixture.detectChanges(); + + expect(component.activeUnitName()).toBeNull(); + }); + + it('hasAnyUnit and hasMultipleUnits should both be false when disabled', () => { + createComponent(false, mockUnits); + fixture.detectChanges(); + + // Even though stateService holds units, the static signal stays empty + expect(component.hasAnyUnit()).toBeFalse(); + expect(component.hasMultipleUnits()).toBeFalse(); + }); + }); + + describe('ngOnInit()', () => { + it('should NOT load units when feature is disabled', () => { + createComponent(false, []); + fixture.detectChanges(); + expect(connector.loadOrgUnits).not.toHaveBeenCalled(); + }); + + it('should NOT load units when state already has data', () => { + createComponent(true, mockUnits); + fixture.detectChanges(); + expect(connector.loadOrgUnits).not.toHaveBeenCalled(); + }); + + it('should load units and populate state when state is empty', () => { + createComponent(true, []); + fixture.detectChanges(); + + expect(connector.loadOrgUnits).toHaveBeenCalledWith(mockUserId); + expect(stateService.setOrgUnits).toHaveBeenCalledWith(mockUnits); + expect(stateService.setActiveUnit).toHaveBeenCalledWith(mockDefaultUid); + }); + + it('should NOT throw when user is anonymous on init (page-refresh race condition)', () => { + // Simulate page refresh: component initialises before OAuth token is restored. + // getUserId() first emits OCC_USER_ID_ANONYMOUS, then the real userId. + createComponent(true, []); + const userIdService = TestBed.inject(UserIdService) as any; + // Emit anonymous first, then the real userId — mirrors token-restore sequence. + userIdService.getUserId.and.returnValue( + of(OCC_USER_ID_ANONYMOUS, mockUserId) + ); + + expect(() => fixture.detectChanges()).not.toThrow(); + // The filter skips anonymous; the real userId triggers the load. + expect(connector.loadOrgUnits).toHaveBeenCalledWith(mockUserId); + }); + + it('should set active unit to null when loadDefaultOrgUnitName fails', () => { + createComponent(true, []); + connector = TestBed.inject(B2bUnitSelectionConnector) as any; + connector.loadDefaultOrgUnitName.and.returnValue( + throwError(() => new Error('not found')) + ); + fixture.detectChanges(); + + expect(stateService.setActiveUnit).toHaveBeenCalledWith(null); + }); + }); + + describe('onSelect()', () => { + beforeEach(() => { + createComponent(true, mockUnits); + fixture.detectChanges(); + }); + + it('should call setDefaultUnit with correct payload', () => { + component.onSelect('Rustic Services'); + + expect(unitSelectionService.setDefaultUnit).toHaveBeenCalledWith( + mockUserId, + 'Rustic Services', + true + ); + }); + }); +}); diff --git a/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/abstract-b2b-unit-selector.component.ts b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/abstract-b2b-unit-selector.component.ts new file mode 100644 index 00000000000..b0b86e7f744 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/abstract-b2b-unit-selector.component.ts @@ -0,0 +1,137 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + computed, + Directive, + inject, + NgZone, + OnInit, + signal, +} from '@angular/core'; +import { toSignal } from '@angular/core/rxjs-interop'; +import { B2BUnit, OCC_USER_ID_ANONYMOUS, UserIdService } from '@spartacus/core'; +import { forkJoin, of } from 'rxjs'; +import { catchError, filter, switchMap, take } from 'rxjs/operators'; +import { + B2bUnitSelectionConnector, + B2bUnitSelectionService, + B2bUnitSelectorStateService, +} from '@spartacus/organization/b2b-unit-selection/core'; +import { B2bUnitSelectionConfig } from '@spartacus/organization/b2b-unit-selection/root'; + +/** + * Abstract base class encapsulating state and interaction logic for the Company selector. + * Subclasses only need to provide the selector and template. + * + * On init, if the stateService holds no data (e.g. after a page refresh before effects + * have written), it proactively calls the connector to ensure the selector renders correctly. + */ +@Directive() +export abstract class AbstractB2bUnitSelectorComponent implements OnInit { + protected stateService = inject(B2bUnitSelectorStateService); + protected unitSelectionService = inject(B2bUnitSelectionService); + protected userIdService = inject(UserIdService); + protected connector = inject(B2bUnitSelectionConnector); + protected config = inject(B2bUnitSelectionConfig); + protected ngZone = inject(NgZone); + + /** + * Whether the B2B unit selection feature is enabled. + * Evaluated once at construction time so signal creation can be gated on it. + */ + protected readonly enabled = this.config.b2bUnitSelection?.enabled ?? false; + + /** + * All assignable B2B units for the current user. + * + * When the feature is disabled we use a static `signal([])` rather than + * `toSignal(observable)` to avoid creating any RxJS subscription. In + * Angular 21 the signal-reactive scheduler (backed by `queueMicrotask`) + * is patched by Zone.js; a live `toSignal` subscription can therefore + * prevent `ApplicationRef.isStable` from emitting `true`, which in turn + * blocks Cypress' `cy.wait()` from resolving — causing test timeouts even + * when `enabled = false` and no HTTP requests are issued. + */ + readonly items = this.enabled + ? toSignal(this.stateService.orgUnits$, { initialValue: [] as B2BUnit[] }) + : signal([] as B2BUnit[]); + + /** Currently active (default) unit name. */ + readonly activeUnitName = this.enabled + ? toSignal(this.stateService.activeUnitName$, { + initialValue: null as string | null, + }) + : signal(null as string | null); + + /** True when the user has more than one unit — selector is interactive. */ + readonly hasMultipleUnits = computed(() => this.items().length > 1); + + /** True when the user has at least one unit — selector is rendered (disabled for single unit). */ + readonly hasAnyUnit = computed(() => this.items().length > 0); + + ngOnInit(): void { + if (!this.enabled) { + return; + } + // After a page refresh the BehaviorSubject is empty (the service subscribes + // to LoginEvent, but on refresh there is no login event — only token restore). + // Proactively load once to ensure the selector is populated. + this.stateService.orgUnits$.pipe(take(1)).subscribe((units) => { + if (units.length === 0) { + this.loadAndPopulateState(); + } + }); + } + + /** Calls the service to persist the unit change when the user switches via the header selector. */ + onSelect(unitName: string): void { + this.userIdService + .takeUserId(true) + .pipe(take(1)) + .subscribe((userId) => { + this.unitSelectionService.setDefaultUnit(userId, unitName, true); + }); + } + + private loadAndPopulateState(): void { + // Run the HTTP requests outside Angular's zone so the pending subscription + // does not prevent ApplicationRef.isStable from emitting true. + // This mirrors the pattern used in B2bUnitSelectionService.openDialogWhenReady(). + // Without this, Cypress cy.wait() calls time out because the zone never + // stabilises while these requests are in-flight. + this.ngZone.runOutsideAngular(() => { + // Use getUserId() + filter instead of takeUserId(true) to avoid throwing + // "Requested user id for logged user while user is not logged in" during + // page refresh: on refresh the component initialises before the OAuth token + // is restored from localStorage, so the user appears anonymous at first. + // getUserId() + filter waits silently until the session is fully restored. + this.userIdService + .getUserId() + .pipe( + filter((userId) => userId !== OCC_USER_ID_ANONYMOUS), + take(1), + switchMap((userId) => + forkJoin({ + // Gracefully degrade on error so the component never blocks page rendering. + orgUnits: this.connector + .loadOrgUnits(userId) + .pipe(catchError(() => of([] as B2BUnit[]))), + defaultUnitName: this.connector + .loadDefaultOrgUnitName(userId) + .pipe(catchError(() => of(undefined))), + }) + ) + ) + .subscribe(({ orgUnits, defaultUnitName }) => { + this.ngZone.run(() => { + this.stateService.setOrgUnits(orgUnits); + this.stateService.setActiveUnit(defaultUnitName ?? null); + }); + }); + }); + } +} diff --git a/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/b2b-unit-selector.component.html b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/b2b-unit-selector.component.html new file mode 100644 index 00000000000..a2415dcf10b --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/b2b-unit-selector.component.html @@ -0,0 +1,33 @@ + + + + + diff --git a/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/b2b-unit-selector.component.ts b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/b2b-unit-selector.component.ts new file mode 100644 index 00000000000..584ef9daf15 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/components/b2b-unit-selector/b2b-unit-selector.component.ts @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgIf } from '@angular/common'; +import { NgSelectComponent } from '@ng-select/ng-select'; +import { TranslatePipe } from '@spartacus/core'; +import { + IconComponent, + ICON_TYPE, + NgSelectA11yDirective, +} from '@spartacus/storefront'; +import { AbstractB2bUnitSelectorComponent } from './abstract-b2b-unit-selector.component'; + +/** + * Company (B2B Unit) selector component. + * Registered as a CMS Flex component (flexType: B2bUnitSelectorComponent) + * and placed in SiteContextSlot via ImpEx. + */ +@Component({ + selector: 'cx-b2b-unit-selector', + templateUrl: './b2b-unit-selector.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + NgIf, + FormsModule, + NgSelectComponent, + NgSelectA11yDirective, + IconComponent, + TranslatePipe, + ], +}) +export class B2bUnitSelectorComponent extends AbstractB2bUnitSelectorComponent { + readonly iconTypes = ICON_TYPE; +} diff --git a/feature-libs/organization/b2b-unit-selection/components/default-b2b-unit-selection-layout.config.ts b/feature-libs/organization/b2b-unit-selection/components/default-b2b-unit-selection-layout.config.ts new file mode 100644 index 00000000000..5318ba0e5e3 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/components/default-b2b-unit-selection-layout.config.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DIALOG_TYPE, LayoutConfig } from '@spartacus/storefront'; +import { B2bUnitSelectionDialogComponent } from './b2b-unit-selection-dialog/b2b-unit-selection-dialog.component'; + +export const defaultB2bUnitSelectionLayoutConfig: LayoutConfig = { + launch: { + B2B_UNIT_SELECTION: { + inlineRoot: true, + component: B2bUnitSelectionDialogComponent, + dialogType: DIALOG_TYPE.DIALOG, + }, + }, +}; diff --git a/feature-libs/organization/b2b-unit-selection/components/ng-package.json b/feature-libs/organization/b2b-unit-selection/components/ng-package.json new file mode 100644 index 00000000000..2a9c6221b0e --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/components/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/feature-libs/organization/b2b-unit-selection/components/public_api.ts b/feature-libs/organization/b2b-unit-selection/components/public_api.ts new file mode 100644 index 00000000000..34411684e3a --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/components/public_api.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './b2b-unit-selection-components.module'; +export * from './b2b-unit-selection-dialog/b2b-unit-selection-dialog.component'; +export * from './b2b-unit-selector/abstract-b2b-unit-selector.component'; +export * from './b2b-unit-selector/b2b-unit-selector.component'; +export * from './default-b2b-unit-selection-layout.config'; diff --git a/feature-libs/organization/b2b-unit-selection/core/b2b-unit-selection-core.module.ts b/feature-libs/organization/b2b-unit-selection/core/b2b-unit-selection-core.module.ts new file mode 100644 index 00000000000..1c0cd3a16ca --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/core/b2b-unit-selection-core.module.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { B2bUnitSelectionConnector } from './connectors/b2b-unit-selection.connector'; +import { B2bUnitSelectionService } from './services/b2b-unit-selection.service'; + +@NgModule({ + providers: [ + B2bUnitSelectionConnector, + // Eagerly initialise the service so it subscribes to user ID changes + // as soon as the feature module is loaded. + B2bUnitSelectionService, + ], +}) +export class B2bUnitSelectionCoreModule { + // Inject here to trigger instantiation eagerly when the feature module loads. + constructor(_service: B2bUnitSelectionService) {} +} diff --git a/feature-libs/organization/b2b-unit-selection/core/connectors/b2b-unit-selection.adapter.ts b/feature-libs/organization/b2b-unit-selection/core/connectors/b2b-unit-selection.adapter.ts new file mode 100644 index 00000000000..3d40e4ffcd0 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/core/connectors/b2b-unit-selection.adapter.ts @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { B2BUnit } from '@spartacus/core'; +import { Observable } from 'rxjs'; + +export abstract class B2bUnitSelectionAdapter { + /** + * Fetches the current B2B user profile and returns the default org unit name. + * Corresponds to GET /{baseSiteId}/orgUsers/{userId} + */ + abstract loadDefaultOrgUnitName( + userId: string + ): Observable; + + /** + * Fetches all org units assigned to the current B2B user. + */ + abstract loadOrgUnits(userId: string): Observable; + + /** + * Sets the default org unit for the current B2B user. + */ + abstract setDefaultOrgUnit( + userId: string, + unitName: string + ): Observable; +} diff --git a/feature-libs/organization/b2b-unit-selection/core/connectors/b2b-unit-selection.connector.ts b/feature-libs/organization/b2b-unit-selection/core/connectors/b2b-unit-selection.connector.ts new file mode 100644 index 00000000000..7e1ca79b08c --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/core/connectors/b2b-unit-selection.connector.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { B2BUnit } from '@spartacus/core'; +import { Observable } from 'rxjs'; +import { B2bUnitSelectionAdapter } from './b2b-unit-selection.adapter'; + +@Injectable() +export class B2bUnitSelectionConnector { + constructor(protected adapter: B2bUnitSelectionAdapter) {} + + loadDefaultOrgUnitName(userId: string): Observable { + return this.adapter.loadDefaultOrgUnitName(userId); + } + + loadOrgUnits(userId: string): Observable { + return this.adapter.loadOrgUnits(userId); + } + + setDefaultOrgUnit(userId: string, unitName: string): Observable { + return this.adapter.setDefaultOrgUnit(userId, unitName); + } +} diff --git a/feature-libs/organization/b2b-unit-selection/core/ng-package.json b/feature-libs/organization/b2b-unit-selection/core/ng-package.json new file mode 100644 index 00000000000..2a9c6221b0e --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/core/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/feature-libs/organization/b2b-unit-selection/core/public_api.ts b/feature-libs/organization/b2b-unit-selection/core/public_api.ts new file mode 100644 index 00000000000..dcca2ff11f1 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/core/public_api.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './b2b-unit-selection-core.module'; +export * from './connectors/b2b-unit-selection.adapter'; +export * from './connectors/b2b-unit-selection.connector'; +export * from './services/b2b-unit-selector-state.service'; +export * from './services/b2b-unit-selection.service'; diff --git a/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selection.service.spec.ts b/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selection.service.spec.ts new file mode 100644 index 00000000000..cfddf604aa3 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selection.service.spec.ts @@ -0,0 +1,439 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ApplicationRef, ComponentRef } from '@angular/core'; +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { HttpErrorResponse } from '@angular/common/http'; +import { + B2BUnit, + EventService, + LoggerService, + OCC_USER_ID_ANONYMOUS, + UserIdService, + WindowRef, +} from '@spartacus/core'; +import { + B2bUnitSelectionConfig, + B2bUnitSwitchedEvent, + B2bUnitSwitchFailedEvent, +} from '@spartacus/organization/b2b-unit-selection/root'; +import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; +import { BehaviorSubject, of, throwError } from 'rxjs'; +import { B2bUnitSelectionConnector } from '../connectors/b2b-unit-selection.connector'; +import { B2bUnitSelectorStateService } from './b2b-unit-selector-state.service'; +import { B2bUnitSelectionService } from './b2b-unit-selection.service'; +import createSpy = jasmine.createSpy; + +const mockUnits: B2BUnit[] = [ + { uid: 'unit-1', name: 'Rustic' }, + { uid: 'unit-2', name: 'Rustic Services' }, +]; +const mockUserId = 'current'; +const mockDefaultUnitName = 'Rustic'; + +class MockUserIdService { + private userId$ = new BehaviorSubject(OCC_USER_ID_ANONYMOUS); + getUserId = createSpy('getUserId').and.callFake(() => this.userId$); + setUserId(id: string): void { + this.userId$.next(id); + } +} + +class MockB2bUnitSelectionConnector { + loadOrgUnits = createSpy('loadOrgUnits').and.returnValue(of(mockUnits)); + loadDefaultOrgUnitName = createSpy('loadDefaultOrgUnitName').and.returnValue( + of(mockDefaultUnitName) + ); + setDefaultOrgUnit = createSpy('setDefaultOrgUnit').and.returnValue( + of(void 0) + ); +} + +class MockB2bUnitSelectorStateService { + setOrgUnits = createSpy('setOrgUnits'); + setActiveUnit = createSpy('setActiveUnit'); +} + +class MockLaunchDialogService { + openDialogAndSubscribe = createSpy('openDialogAndSubscribe'); + closeDialog = createSpy('closeDialog'); +} + +class MockEventService { + dispatch = createSpy('dispatch'); +} + +class MockLoggerService { + error = createSpy('error'); + warn = createSpy('warn'); +} + +/** + * Mock WindowRef: proxies sessionStorage to the real window.sessionStorage + * so session-flag tests work, but stubs location.reload/assign to prevent + * real browser page reloads during Karma tests. + */ +class MockWindowRef { + nativeWindow = { + location: { + reload: createSpy('reload'), + assign: createSpy('assign'), + }, + }; + + get sessionStorage(): Storage { + return window.sessionStorage; + } +} + +describe('B2bUnitSelectionService', () => { + let service: B2bUnitSelectionService; + let userIdService: MockUserIdService; + let connector: MockB2bUnitSelectionConnector; + let stateService: MockB2bUnitSelectorStateService; + let launchDialogService: MockLaunchDialogService; + let eventService: MockEventService; + + function createService( + enabled = true, + mockComponents: ComponentRef[] = [{}] as any + ): void { + TestBed.configureTestingModule({ + providers: [ + B2bUnitSelectionService, + { provide: UserIdService, useClass: MockUserIdService }, + { + provide: B2bUnitSelectionConnector, + useClass: MockB2bUnitSelectionConnector, + }, + { + provide: B2bUnitSelectorStateService, + useClass: MockB2bUnitSelectorStateService, + }, + { provide: LaunchDialogService, useClass: MockLaunchDialogService }, + { provide: EventService, useClass: MockEventService }, + { provide: LoggerService, useClass: MockLoggerService }, + { + provide: B2bUnitSelectionConfig, + useValue: { b2bUnitSelection: { enabled } }, + }, + { provide: WindowRef, useClass: MockWindowRef }, + ], + }); + + service = TestBed.inject(B2bUnitSelectionService); + userIdService = TestBed.inject(UserIdService) as any; + connector = TestBed.inject(B2bUnitSelectionConnector) as any; + stateService = TestBed.inject(B2bUnitSelectorStateService) as any; + launchDialogService = TestBed.inject(LaunchDialogService) as any; + eventService = TestBed.inject(EventService) as any; + + // Override `components` on the real ApplicationRef instead of replacing + // the whole service. Replacing ApplicationRef with a bare object breaks + // Angular internals (ChangeDetectionSchedulerImpl) that access properties + // our mock does not provide. + (TestBed.inject(ApplicationRef) as any).components = mockComponents; + } + + afterEach(() => { + TestBed.resetTestingModule(); + }); + + // ── constructor / feature flag ────────────────────────────────────────── + + describe('constructor', () => { + it('should be created', () => { + createService(); + expect(service).toBeTruthy(); + }); + + it('should NOT subscribe to userId changes when feature is disabled', () => { + createService(false); + // Force a login-like emission + (userIdService as any).setUserId(mockUserId); + + expect(connector.loadOrgUnits).not.toHaveBeenCalled(); + }); + + it('should subscribe to userId changes when feature is enabled', () => { + createService(true); + (userIdService as any).setUserId(mockUserId); + + expect(connector.loadOrgUnits).toHaveBeenCalledWith(mockUserId); + }); + }); + + // ── subscribeToUserIdChanges – logout ─────────────────────────────────── + + describe('subscribeToUserIdChanges() – logout path', () => { + beforeEach(() => createService()); + + it('should clear state when userId is anonymous', () => { + // Already anonymous from initial BehaviorSubject value. + expect(stateService.setOrgUnits).toHaveBeenCalledWith([]); + expect(stateService.setActiveUnit).toHaveBeenCalledWith(null); + }); + + it('should clear state when userId becomes anonymous after login', () => { + (userIdService as any).setUserId(mockUserId); + stateService.setOrgUnits.calls.reset(); + stateService.setActiveUnit.calls.reset(); + + (userIdService as any).setUserId(OCC_USER_ID_ANONYMOUS); + + expect(stateService.setOrgUnits).toHaveBeenCalledWith([]); + expect(stateService.setActiveUnit).toHaveBeenCalledWith(null); + }); + + it('should clear state when userId becomes empty string', () => { + (userIdService as any).setUserId(mockUserId); + stateService.setOrgUnits.calls.reset(); + stateService.setActiveUnit.calls.reset(); + + (userIdService as any).setUserId(''); + + expect(stateService.setOrgUnits).toHaveBeenCalledWith([]); + expect(stateService.setActiveUnit).toHaveBeenCalledWith(null); + }); + + it('should NOT load org units for anonymous userId', () => { + expect(connector.loadOrgUnits).not.toHaveBeenCalled(); + }); + }); + + // ── subscribeToUserIdChanges – login ──────────────────────────────────── + + describe('subscribeToUserIdChanges() – login path', () => { + beforeEach(() => createService()); + + it('should call loadOrgUnits and loadDefaultOrgUnitName with userId', () => { + (userIdService as any).setUserId(mockUserId); + + expect(connector.loadOrgUnits).toHaveBeenCalledWith(mockUserId); + expect(connector.loadDefaultOrgUnitName).toHaveBeenCalledWith(mockUserId); + }); + + it('should populate state with loaded org units and default unit name', () => { + (userIdService as any).setUserId(mockUserId); + + expect(stateService.setOrgUnits).toHaveBeenCalledWith(mockUnits); + expect(stateService.setActiveUnit).toHaveBeenCalledWith( + mockDefaultUnitName + ); + }); + + it('should fall back to null activeUnit when loadDefaultOrgUnitName errors', () => { + connector.loadDefaultOrgUnitName.and.returnValue( + throwError(() => new HttpErrorResponse({ status: 500 })) + ); + + (userIdService as any).setUserId(mockUserId); + + expect(stateService.setActiveUnit).toHaveBeenCalledWith(null); + }); + + it('should NOT duplicate reactions when userId emits same value twice (distinctUntilChanged)', () => { + (userIdService as any).setUserId(mockUserId); + connector.loadOrgUnits.calls.reset(); + + // Same value again — should be swallowed by distinctUntilChanged. + (userIdService as any).setUserId(mockUserId); + + expect(connector.loadOrgUnits).not.toHaveBeenCalled(); + }); + + it('should log error and continue when loadOrgUnits errors', () => { + const loggerService = TestBed.inject(LoggerService) as any; + // Provide a proper error body so tryNormalizeHttpError does not throw + // before logger.error is reached. + connector.loadOrgUnits.and.returnValue( + throwError( + () => + new HttpErrorResponse({ + status: 500, + error: { message: 'Server Error' }, + }) + ) + ); + + (userIdService as any).setUserId(mockUserId); + + expect(loggerService.error).toHaveBeenCalled(); + // Stream should still be alive after error (handled via catchError → of(null)). + }); + + it('should NOT open dialog when orgUnits is empty', fakeAsync(() => { + connector.loadOrgUnits.and.returnValue(of([])); + + (userIdService as any).setUserId(mockUserId); + tick(100); + + expect(launchDialogService.openDialogAndSubscribe).not.toHaveBeenCalled(); + })); + + it('should NOT open dialog when orgUnits has only one unit', fakeAsync(() => { + connector.loadOrgUnits.and.returnValue(of([mockUnits[0]])); + + (userIdService as any).setUserId(mockUserId); + tick(100); + + expect(launchDialogService.openDialogAndSubscribe).not.toHaveBeenCalled(); + })); + }); + + // ── openDialogWhenReady ───────────────────────────────────────────────── + + describe('openDialogWhenReady()', () => { + it('should open dialog when ApplicationRef has components', fakeAsync(() => { + // AppComponent already present (mockComponents = [{}]). + createService(true, [{}] as any); + (userIdService as any).setUserId(mockUserId); + + // Advance past the 50 ms polling interval. + tick(100); + + expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalledWith( + (LAUNCH_CALLER as any)['B2B_UNIT_SELECTION'], + undefined, + jasmine.objectContaining({ + orgUnits: mockUnits, + defaultUnitName: mockDefaultUnitName, + onConfirm: jasmine.any(Function), + }) + ); + })); + + it('should open dialog after timeout when AppComponent never mounts', fakeAsync(() => { + // No components mounted. + createService(true, [] as any); + (userIdService as any).setUserId(mockUserId); + + // Advance past STABLE_TIMEOUT_MS (10 000 ms). + tick(10_001); + + expect(launchDialogService.openDialogAndSubscribe).toHaveBeenCalled(); + })); + + it('should NOT open dialog when sessionStorage flag is set', fakeAsync(() => { + createService(true, [{}] as any); + const windowRef = TestBed.inject(WindowRef); + windowRef.sessionStorage?.setItem('cx-b2b-unit-selected', 'true'); + + (userIdService as any).setUserId(mockUserId); + tick(100); + + expect(launchDialogService.openDialogAndSubscribe).not.toHaveBeenCalled(); + windowRef.sessionStorage?.removeItem('cx-b2b-unit-selected'); + })); + }); + + // ── setDefaultUnit ────────────────────────────────────────────────────── + + describe('setDefaultUnit()', () => { + beforeEach(() => createService()); + + it('should call connector.setDefaultOrgUnit with userId and unitName', () => { + service.setDefaultUnit(mockUserId, mockDefaultUnitName); + + expect(connector.setDefaultOrgUnit).toHaveBeenCalledWith( + mockUserId, + mockDefaultUnitName + ); + }); + + it('should close dialog on success', () => { + service.setDefaultUnit(mockUserId, mockDefaultUnitName); + + expect(launchDialogService.closeDialog).toHaveBeenCalledWith('CONFIRMED'); + }); + + it('should set sessionStorage confirmed flag on success', () => { + const windowRef = TestBed.inject(WindowRef); + service.setDefaultUnit(mockUserId, mockDefaultUnitName); + + expect(windowRef.sessionStorage?.getItem('cx-b2b-unit-selected')).toBe( + 'true' + ); + windowRef.sessionStorage?.removeItem('cx-b2b-unit-selected'); + }); + + it('should update active unit in state on success', () => { + stateService.setActiveUnit.calls.reset(); + service.setDefaultUnit(mockUserId, mockDefaultUnitName); + + expect(stateService.setActiveUnit).toHaveBeenCalledWith( + mockDefaultUnitName + ); + }); + + it('should dispatch B2bUnitSwitchedEvent on success (redirectToHome=false by default)', () => { + service.setDefaultUnit(mockUserId, mockDefaultUnitName); + + expect(eventService.dispatch).toHaveBeenCalledWith( + { + userId: mockUserId, + unitName: mockDefaultUnitName, + redirectedToHome: false, + }, + B2bUnitSwitchedEvent + ); + }); + + it('should dispatch B2bUnitSwitchedEvent with redirectedToHome=true when redirectToHome=true', () => { + service.setDefaultUnit(mockUserId, mockDefaultUnitName, true); + + expect(eventService.dispatch).toHaveBeenCalledWith( + { + userId: mockUserId, + unitName: mockDefaultUnitName, + redirectedToHome: true, + }, + B2bUnitSwitchedEvent + ); + }); + + it('should dispatch B2bUnitSwitchFailedEvent on error', () => { + connector.setDefaultOrgUnit.and.returnValue( + throwError( + () => + new HttpErrorResponse({ + status: 400, + error: { message: 'Bad Request' }, + }) + ) + ); + + service.setDefaultUnit(mockUserId, mockDefaultUnitName); + + expect(eventService.dispatch).toHaveBeenCalledWith( + jasmine.objectContaining({ + userId: mockUserId, + unitName: mockDefaultUnitName, + }), + B2bUnitSwitchFailedEvent + ); + }); + + it('should NOT dispatch B2bUnitSwitchedEvent on error', () => { + connector.setDefaultOrgUnit.and.returnValue( + throwError( + () => + new HttpErrorResponse({ + status: 400, + error: { message: 'Bad Request' }, + }) + ) + ); + + service.setDefaultUnit(mockUserId, mockDefaultUnitName); + + expect(eventService.dispatch).not.toHaveBeenCalledWith( + jasmine.anything(), + B2bUnitSwitchedEvent + ); + }); + }); +}); diff --git a/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selection.service.ts b/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selection.service.ts new file mode 100644 index 00000000000..46b719f8db8 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selection.service.ts @@ -0,0 +1,266 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ApplicationRef, inject, Injectable, NgZone } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { + B2BUnit, + EventService, + LoggerService, + OCC_USER_ID_ANONYMOUS, + tryNormalizeHttpError, + UserIdService, + WindowRef, +} from '@spartacus/core'; +import { + B2bUnitSelectionConfig, + B2bUnitSwitchedEvent, + B2bUnitSwitchFailedEvent, +} from '@spartacus/organization/b2b-unit-selection/root'; +import { LAUNCH_CALLER, LaunchDialogService } from '@spartacus/storefront'; +import { forkJoin, interval, of, race, Subscription, timer } from 'rxjs'; +import { + catchError, + distinctUntilChanged, + filter, + startWith, + switchMap, + take, + tap, +} from 'rxjs/operators'; +import { B2bUnitSelectionConnector } from '../connectors/b2b-unit-selection.connector'; +import { B2bUnitSelectorStateService } from './b2b-unit-selector-state.service'; + +/** + * Core service orchestrating B2B unit selection. + * + * Provided in the lazy `B2bUnitSelectionCoreModule` (not `providedIn: 'root'`) + * so that it lives in the same injector as `B2bUnitSelectionConnector` and + * `B2bUnitSelectionAdapter`, avoiding cross-injector dependency errors. + * + * Eagerly initialised by `B2bUnitSelectionCoreModule` via constructor injection. + * By using `UserIdService.getUserId()` instead of `LoginEvent` / `LogoutEvent`, + * the service reacts correctly whether it is instantiated before or after the + * auth token is restored from storage. + * + * **Dialog communication:** The dialog component (`B2bUnitSelectionDialogComponent`) + * does not inject this service directly (it is module-scoped and lives in a lazy + * child injector, while the dialog is rendered in the root outlet). Instead, the + * dialog calls `LaunchDialogService.closeDialog(unitName)` with the selected unit + * name, and this service subscribes to `LaunchDialogService.dialogClose` after + * opening the dialog to receive the result. + */ +@Injectable() +export class B2bUnitSelectionService { + protected logger = inject(LoggerService); + protected config = inject(B2bUnitSelectionConfig); + protected applicationRef = inject(ApplicationRef); + protected ngZone = inject(NgZone); + protected stateService = inject(B2bUnitSelectorStateService); + protected windowRef = inject(WindowRef); + protected eventService = inject(EventService); + protected userIdService = inject(UserIdService); + protected connector = inject(B2bUnitSelectionConnector); + protected launchDialogService = inject(LaunchDialogService); + + /** Maximum time (ms) to wait for AppComponent to mount before opening the dialog anyway. */ + protected readonly STABLE_TIMEOUT_MS = 10_000; + + /** + * sessionStorage key used to suppress the unit-selection dialog after the + * user has already confirmed a unit in the current browser session. + * Cleared on logout so the dialog reappears on the next login. + */ + protected readonly UNIT_CONFIRMED_SESSION_KEY = 'cx-b2b-unit-selected'; + + private readonly subscriptions = new Subscription(); + + constructor() { + if (!this.config.b2bUnitSelection?.enabled) { + return; + } + this.subscribeToUserIdChanges(); + } + + /** + * Reacts to every distinct change of the active user ID. + * + * - **Login** (userId transitions from anonymous → real ID): loads the + * user's org units and opens the unit-selection dialog when the user + * belongs to more than one unit. + * - **Logout** (userId transitions to anonymous / empty): clears the state + * so the Company header selector is hidden. + * + * Using `getUserId()` (a `BehaviorSubject`) instead of `LoginEvent` / + * `LogoutEvent` (plain `Subject`s) makes the service robust to lazy-loading + * timing: if the feature module loads after the auth token has already been + * restored from storage, `getUserId()` immediately emits the current user ID + * and the dialog still opens correctly. + */ + protected subscribeToUserIdChanges(): void { + this.subscriptions.add( + this.userIdService + .getUserId() + .pipe( + distinctUntilChanged(), + tap((userId) => { + if (!userId || userId === OCC_USER_ID_ANONYMOUS) { + this.stateService.setOrgUnits([]); + this.stateService.setActiveUnit(null); + // Clear the session flag so the dialog appears again on next login. + this.windowRef.sessionStorage?.removeItem( + this.UNIT_CONFIRMED_SESSION_KEY + ); + } + }), + filter((userId) => !!userId && userId !== OCC_USER_ID_ANONYMOUS), + switchMap((userId) => + forkJoin({ + orgUnits: this.connector.loadOrgUnits(userId), + // Gracefully degrade if loading the default unit fails. + defaultUnitName: this.connector + .loadDefaultOrgUnitName(userId) + .pipe(catchError(() => of(undefined))), + }).pipe( + tap(({ orgUnits, defaultUnitName }) => { + // Always write to the state service so the Company header + // selector is populated. + this.stateService.setOrgUnits(orgUnits); + this.stateService.setActiveUnit(defaultUnitName ?? null); + // Open the dialog when the user has multiple org units to + // choose from and has NOT already confirmed a unit in this + // browser session (flag is set in setDefaultUnit and cleared + // on logout, preventing the dialog from reopening after the + // post-confirm page reload). + const alreadyConfirmed = + this.windowRef.sessionStorage?.getItem( + this.UNIT_CONFIRMED_SESSION_KEY + ) === 'true'; + if (orgUnits.length > 1 && !alreadyConfirmed) { + this.openDialogWhenReady(orgUnits, defaultUnitName, userId); + } + }), + catchError((error: HttpErrorResponse) => { + this.logger.error(tryNormalizeHttpError(error, this.logger)); + return of(null); + }) + ) + ) + ) + .subscribe() + ); + } + + /** + * Calls the connector to persist the newly selected org unit, then + * closes the dialog and reloads the page so all org-context-dependent data + * (prices, catalog, CMS) is refreshed. + * + * Called either from the dialog flow (via the `onConfirm` callback passed in + * dialog data) or directly from the header selector component. + * + * @param userId The OCC user identifier. + * @param unitName The `name` of the selected B2B unit. + * @param redirectToHome When `true` (header selector), navigates to the + * home root; when `false` (dialog, default), reloads + * the current page in place. + */ + setDefaultUnit( + userId: string, + unitName: string, + redirectToHome = false + ): void { + this.connector + .setDefaultOrgUnit(userId, unitName) + .pipe(take(1)) + .subscribe({ + next: () => { + this.launchDialogService.closeDialog('CONFIRMED'); + this.stateService.setActiveUnit(unitName); + this.eventService.dispatch( + { userId, unitName, redirectedToHome: redirectToHome }, + B2bUnitSwitchedEvent + ); + // Persist the session flag before reloading so the dialog does not + // reopen when the page reinitialises with the same user ID. + this.windowRef.sessionStorage?.setItem( + this.UNIT_CONFIRMED_SESSION_KEY, + 'true' + ); + // Reload page so all org-context data is refreshed for the new unit. + // Header selector: navigate to home root via location.assign('/'). + // Dialog: reload the current page in-place via location.reload(). + if (redirectToHome) { + this.windowRef.nativeWindow?.location?.assign('/'); + } else { + this.windowRef.nativeWindow?.location?.reload(); + } + }, + error: (error: HttpErrorResponse) => { + this.eventService.dispatch( + { + userId, + unitName, + error: tryNormalizeHttpError(error, this.logger), + }, + B2bUnitSwitchFailedEvent + ); + }, + }); + } + + /** + * Opens the unit-selection dialog once the AppComponent is mounted. + * + * The dialog component (`B2bUnitSelectionDialogComponent`) is standalone and + * rendered in the root outlet via `inlineRoot: true`. Because this service + * lives in the lazy feature module's child injector, the dialog cannot inject + * it directly. Instead, an `onConfirm` callback is passed via the dialog data + * payload. The dialog calls this callback with the selected unit name, which + * in turn calls `setDefaultUnit` — which closes the dialog via + * `LaunchDialogService.closeDialog` on HTTP success, preserving the original + * close-after-persist behaviour. + * + * The polling runs OUTSIDE Angular's zone via `NgZone.runOutsideAngular` so + * the `setInterval` does not keep the zone perpetually unstable, which would + * otherwise prevent `ApplicationRef.isStable` from emitting and block CMS + * page rendering. + * + * A `timer(STABLE_TIMEOUT_MS)` fallback ensures the subscription always + * completes even if AppComponent never mounts, preventing a memory leak. + */ + protected openDialogWhenReady( + orgUnits: B2BUnit[], + defaultUnitName: string | undefined, + userId: string + ): void { + const open = () => { + this.launchDialogService.openDialogAndSubscribe( + (LAUNCH_CALLER as any)['B2B_UNIT_SELECTION'], + undefined, + { + orgUnits, + defaultUnitName, + // Pass a bound callback so the dialog can trigger setDefaultUnit + // without injecting this module-scoped service directly. + onConfirm: (unitName: string) => + this.setDefaultUnit(userId, unitName), + } + ); + }; + + this.ngZone.runOutsideAngular(() => { + race( + interval(50).pipe( + startWith(0), + filter(() => this.applicationRef.components.length > 0), + take(1) + ), + timer(this.STABLE_TIMEOUT_MS) + ).subscribe(() => this.ngZone.run(() => open())); + }); + } +} diff --git a/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selector-state.service.spec.ts b/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selector-state.service.spec.ts new file mode 100644 index 00000000000..41cb40de48e --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selector-state.service.spec.ts @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TestBed } from '@angular/core/testing'; +import { B2BUnit } from '@spartacus/core'; +import { B2bUnitSelectorStateService } from './b2b-unit-selector-state.service'; + +const mockUnits: B2BUnit[] = [ + { uid: 'unit-1', name: 'Rustic' }, + { uid: 'unit-2', name: 'Rustic Services' }, +]; + +describe('B2bUnitSelectorStateService', () => { + let service: B2bUnitSelectorStateService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(B2bUnitSelectorStateService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('orgUnits$', () => { + it('should emit an empty array as initial value', (done) => { + service.orgUnits$.subscribe((units) => { + expect(units).toEqual([]); + done(); + }); + }); + + it('should emit the updated list after setOrgUnits()', (done) => { + service.setOrgUnits(mockUnits); + service.orgUnits$.subscribe((units) => { + expect(units).toEqual(mockUnits); + done(); + }); + }); + + it('should emit an empty array when reset via setOrgUnits([])', (done) => { + service.setOrgUnits(mockUnits); + service.setOrgUnits([]); + service.orgUnits$.subscribe((units) => { + expect(units).toEqual([]); + done(); + }); + }); + }); + + describe('activeUnitName$', () => { + it('should emit null as initial value', (done) => { + service.activeUnitName$.subscribe((name) => { + expect(name).toBeNull(); + done(); + }); + }); + + it('should emit the updated name after setActiveUnit()', (done) => { + service.setActiveUnit('Rustic'); + service.activeUnitName$.subscribe((name) => { + expect(name).toBe('Rustic'); + done(); + }); + }); + + it('should emit null when reset via setActiveUnit(null)', (done) => { + service.setActiveUnit('Rustic'); + service.setActiveUnit(null); + service.activeUnitName$.subscribe((name) => { + expect(name).toBeNull(); + done(); + }); + }); + }); +}); diff --git a/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selector-state.service.ts b/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selector-state.service.ts new file mode 100644 index 00000000000..3f7ce6e43e9 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/core/services/b2b-unit-selector-state.service.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { B2BUnit } from '@spartacus/core'; +import { BehaviorSubject, Observable } from 'rxjs'; + +/** + * Lightweight state service that shares B2B unit data between the unit-selection + * dialog and the persistent B2B Unit selector in the header. + * + * Data is written by B2bUnitSelectionService after login; components subscribe + * directly without issuing additional API calls. + */ +@Injectable({ providedIn: 'root' }) +export class B2bUnitSelectorStateService { + private _orgUnits$ = new BehaviorSubject([]); + private _activeUnitName$ = new BehaviorSubject(null); + + /** All org units assigned to the current user. */ + readonly orgUnits$: Observable = this._orgUnits$.asObservable(); + + /** The name of the currently active (default) org unit. */ + readonly activeUnitName$: Observable = + this._activeUnitName$.asObservable(); + + setOrgUnits(units: B2BUnit[]): void { + this._orgUnits$.next(units); + } + + setActiveUnit(name: string | null): void { + this._activeUnitName$.next(name); + } +} diff --git a/feature-libs/organization/b2b-unit-selection/ng-package.json b/feature-libs/organization/b2b-unit-selection/ng-package.json new file mode 100644 index 00000000000..38e01ac17de --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/feature-libs/organization/b2b-unit-selection/occ/adapters/occ-b2b-unit-selection.adapter.spec.ts b/feature-libs/organization/b2b-unit-selection/occ/adapters/occ-b2b-unit-selection.adapter.spec.ts new file mode 100644 index 00000000000..a18de2ad826 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/occ/adapters/occ-b2b-unit-selection.adapter.spec.ts @@ -0,0 +1,202 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + HttpRequest, + provideHttpClient, + withInterceptorsFromDi, +} from '@angular/common/http'; +import { + HttpTestingController, + provideHttpClientTesting, +} from '@angular/common/http/testing'; +import { fakeAsync, TestBed } from '@angular/core/testing'; +import { LoggerService, OccEndpointsService } from '@spartacus/core'; +import { MockOccEndpointsService } from 'core-libs/core/src/occ/adapters/user/unit-test.helper'; +import { OccB2bUnitSelectionAdapter } from './occ-b2b-unit-selection.adapter'; + +const userId = 'current'; +const unitUid = 'Rustic'; + +class MockLoggerService { + log(): void {} + warn(): void {} + error(): void {} + info(): void {} + debug(): void {} +} + +describe('OccB2bUnitSelectionAdapter', () => { + let adapter: OccB2bUnitSelectionAdapter; + let httpMock: HttpTestingController; + let occEndpointsService: OccEndpointsService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + OccB2bUnitSelectionAdapter, + { provide: OccEndpointsService, useClass: MockOccEndpointsService }, + { provide: LoggerService, useClass: MockLoggerService }, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }); + + adapter = TestBed.inject(OccB2bUnitSelectionAdapter); + httpMock = TestBed.inject(HttpTestingController); + occEndpointsService = TestBed.inject(OccEndpointsService); + spyOn(occEndpointsService, 'buildUrl').and.callThrough(); + }); + + afterEach(() => { + httpMock.verify(); + }); + + // ── loadDefaultOrgUnitName ─────────────────────────────────────────────── + + describe('loadDefaultOrgUnitName()', () => { + it('should call the orgUser endpoint', fakeAsync(() => { + adapter.loadDefaultOrgUnitName(userId).subscribe(); + + httpMock.expectOne((req: HttpRequest) => req.method === 'GET'); + + expect(occEndpointsService.buildUrl).toHaveBeenCalledWith('orgUser', { + urlParams: { userId }, + }); + })); + + it('should return the orgUnit name from the response', (done) => { + adapter.loadDefaultOrgUnitName(userId).subscribe((name) => { + expect(name).toBe('Rustic'); + done(); + }); + + httpMock + .expectOne((req: HttpRequest) => req.method === 'GET') + .flush({ orgUnit: { name: 'Rustic' } }); + }); + + it('should return undefined when orgUnit is missing from response', (done) => { + adapter.loadDefaultOrgUnitName(userId).subscribe((name) => { + expect(name).toBeUndefined(); + done(); + }); + + httpMock + .expectOne((req: HttpRequest) => req.method === 'GET') + .flush({}); + }); + + it('should rethrow a normalised error on HTTP failure', (done) => { + adapter.loadDefaultOrgUnitName(userId).subscribe({ + error: (err) => { + expect(err).toBeDefined(); + done(); + }, + }); + + httpMock + .expectOne((req: HttpRequest) => req.method === 'GET') + .flush('error', { status: 500, statusText: 'Server Error' }); + }); + }); + + // ── loadOrgUnits ───────────────────────────────────────────────────────── + + describe('loadOrgUnits()', () => { + it('should call the orgUserUnits endpoint', fakeAsync(() => { + adapter.loadOrgUnits(userId).subscribe(); + + httpMock.expectOne((req: HttpRequest) => req.method === 'GET'); + + expect(occEndpointsService.buildUrl).toHaveBeenCalledWith( + 'orgUserUnits', + { + urlParams: { userId }, + } + ); + })); + + it('should return the units array from the response', (done) => { + const mockUnits = [{ uid: 'unit-1', name: 'Rustic' }]; + + adapter.loadOrgUnits(userId).subscribe((units) => { + expect(units).toEqual(mockUnits); + done(); + }); + + httpMock + .expectOne((req: HttpRequest) => req.method === 'GET') + .flush({ orgUnits: mockUnits }); + }); + + it('should return an empty array when orgUnits is missing', (done) => { + adapter.loadOrgUnits(userId).subscribe((units) => { + expect(units).toEqual([]); + done(); + }); + + httpMock + .expectOne((req: HttpRequest) => req.method === 'GET') + .flush({}); + }); + + it('should rethrow a normalised error on HTTP failure', (done) => { + adapter.loadOrgUnits(userId).subscribe({ + error: (err) => { + expect(err).toBeDefined(); + done(); + }, + }); + + httpMock + .expectOne((req: HttpRequest) => req.method === 'GET') + .flush('error', { status: 404, statusText: 'Not Found' }); + }); + }); + + // ── setDefaultOrgUnit ──────────────────────────────────────────────────── + + describe('setDefaultOrgUnit()', () => { + it('should call the orgUserDefaultUnit endpoint with PUT', fakeAsync(() => { + adapter.setDefaultOrgUnit(userId, unitUid).subscribe(); + + const req = httpMock.expectOne( + (r: HttpRequest) => r.method === 'PUT' + ); + expect(req.request.body).toEqual({ uid: unitUid }); + + expect(occEndpointsService.buildUrl).toHaveBeenCalledWith( + 'orgUserDefaultUnit', + { urlParams: { userId } } + ); + })); + + it('should complete (and not error) on success', (done) => { + adapter.setDefaultOrgUnit(userId, unitUid).subscribe({ + error: () => fail('should not error'), + complete: done, + }); + + httpMock + .expectOne((req: HttpRequest) => req.method === 'PUT') + .flush(null); + }); + + it('should rethrow a normalised error on HTTP failure', (done) => { + adapter.setDefaultOrgUnit(userId, unitUid).subscribe({ + error: (err) => { + expect(err).toBeDefined(); + done(); + }, + }); + + httpMock + .expectOne((req: HttpRequest) => req.method === 'PUT') + .flush('error', { status: 400, statusText: 'Bad Request' }); + }); + }); +}); diff --git a/feature-libs/organization/b2b-unit-selection/occ/adapters/occ-b2b-unit-selection.adapter.ts b/feature-libs/organization/b2b-unit-selection/occ/adapters/occ-b2b-unit-selection.adapter.ts new file mode 100644 index 00000000000..cdc9f8408e0 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/occ/adapters/occ-b2b-unit-selection.adapter.ts @@ -0,0 +1,72 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { inject, Injectable } from '@angular/core'; +import { + B2BUnit, + LoggerService, + OccEndpointsService, + tryNormalizeHttpError, +} from '@spartacus/core'; +import { B2bUnitSelectionAdapter } from '@spartacus/organization/b2b-unit-selection/core'; +import { Observable } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +// Side-effect import: registers the OccEndpoints module augmentation. +import '../model/occ-b2b-unit-selection-endpoints.model'; + +@Injectable() +export class OccB2bUnitSelectionAdapter implements B2bUnitSelectionAdapter { + protected logger = inject(LoggerService); + + constructor( + protected http: HttpClient, + protected occEndpoints: OccEndpointsService + ) {} + + loadDefaultOrgUnitName(userId: string): Observable { + return this.http + .get<{ + orgUnit?: { name?: string }; + }>(this.occEndpoints.buildUrl('orgUser', { urlParams: { userId } })) + .pipe( + map((response) => response?.orgUnit?.name), + catchError((error: HttpErrorResponse) => { + throw tryNormalizeHttpError(error, this.logger); + }) + ); + } + + loadOrgUnits(userId: string): Observable { + return this.http + .get<{ orgUnits: B2BUnit[] }>( + this.occEndpoints.buildUrl('orgUserUnits', { + urlParams: { userId }, + }) + ) + .pipe( + map((response) => response?.orgUnits ?? []), + catchError((error: HttpErrorResponse) => { + throw tryNormalizeHttpError(error, this.logger); + }) + ); + } + + setDefaultOrgUnit(userId: string, unitName: string): Observable { + return this.http + .put( + this.occEndpoints.buildUrl('orgUserDefaultUnit', { + urlParams: { userId }, + }), + { uid: unitName } + ) + .pipe( + catchError((error: HttpErrorResponse) => { + throw tryNormalizeHttpError(error, this.logger); + }) + ); + } +} diff --git a/feature-libs/organization/b2b-unit-selection/occ/b2b-unit-selection-occ.module.ts b/feature-libs/organization/b2b-unit-selection/occ/b2b-unit-selection-occ.module.ts new file mode 100644 index 00000000000..532baad1e1e --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/occ/b2b-unit-selection-occ.module.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { B2bUnitSelectionAdapter } from '@spartacus/organization/b2b-unit-selection/core'; +import { OccB2bUnitSelectionAdapter } from './adapters/occ-b2b-unit-selection.adapter'; + +@NgModule({ + providers: [ + { + provide: B2bUnitSelectionAdapter, + useClass: OccB2bUnitSelectionAdapter, + }, + ], +}) +export class B2bUnitSelectionOccModule {} diff --git a/feature-libs/organization/b2b-unit-selection/occ/config/default-occ-b2b-unit-selection-config.ts b/feature-libs/organization/b2b-unit-selection/occ/config/default-occ-b2b-unit-selection-config.ts new file mode 100644 index 00000000000..41cb61986c3 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/occ/config/default-occ-b2b-unit-selection-config.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OccConfig } from '@spartacus/core'; + +export const defaultOccB2bUnitSelectionConfig: OccConfig = { + backend: { + occ: { + endpoints: { + orgUser: 'orgUsers/${userId}', + orgUserUnits: 'orgUsers/${userId}/orgUnits', + orgUserDefaultUnit: 'orgUsers/${userId}/defaultOrgUnit', + }, + }, + }, +}; diff --git a/feature-libs/organization/b2b-unit-selection/occ/model/index.ts b/feature-libs/organization/b2b-unit-selection/occ/model/index.ts new file mode 100644 index 00000000000..2db5d68ad45 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/occ/model/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './occ-b2b-unit-selection-endpoints.model'; diff --git a/feature-libs/organization/b2b-unit-selection/occ/model/occ-b2b-unit-selection-endpoints.model.ts b/feature-libs/organization/b2b-unit-selection/occ/model/occ-b2b-unit-selection-endpoints.model.ts new file mode 100644 index 00000000000..f99a29e438c --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/occ/model/occ-b2b-unit-selection-endpoints.model.ts @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OccEndpoint } from '@spartacus/core'; + +export interface B2bUnitSelectionOccEndpoints { + /** + * Fetches current B2B user profile (includes the default orgUnit). + * GET /{baseSiteId}/orgUsers/{userId} + */ + orgUser?: string | OccEndpoint; + + /** + * Fetches all org units assigned to the current B2B user. + * GET /{baseSiteId}/orgUsers/{userId}/orgUnits + */ + orgUserUnits?: string | OccEndpoint; + + /** + * Sets the default org unit for the current B2B user. + * PUT /{baseSiteId}/orgUsers/{userId}/defaultOrgUnit + */ + orgUserDefaultUnit?: string | OccEndpoint; +} + +declare module '@spartacus/core' { + interface OccEndpoints extends B2bUnitSelectionOccEndpoints {} +} diff --git a/feature-libs/organization/b2b-unit-selection/occ/ng-package.json b/feature-libs/organization/b2b-unit-selection/occ/ng-package.json new file mode 100644 index 00000000000..2a9c6221b0e --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/occ/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/feature-libs/organization/b2b-unit-selection/occ/public_api.ts b/feature-libs/organization/b2b-unit-selection/occ/public_api.ts new file mode 100644 index 00000000000..74d10b615d2 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/occ/public_api.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './adapters/occ-b2b-unit-selection.adapter'; +export * from './b2b-unit-selection-occ.module'; diff --git a/feature-libs/organization/b2b-unit-selection/public_api.ts b/feature-libs/organization/b2b-unit-selection/public_api.ts new file mode 100644 index 00000000000..bba9db72d21 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/public_api.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './b2b-unit-selection.module'; +export * from './components/public_api'; +export * from './core/public_api'; +export * from './occ/public_api'; +export * from './root/public_api'; diff --git a/feature-libs/organization/b2b-unit-selection/root/b2b-unit-selection-root.module.ts b/feature-libs/organization/b2b-unit-selection/root/b2b-unit-selection-root.module.ts new file mode 100644 index 00000000000..3dc75e4769a --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/root/b2b-unit-selection-root.module.ts @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { + CmsConfig, + provideDefaultConfig, + provideDefaultConfigFactory, +} from '@spartacus/core'; +import { defaultB2bUnitSelectionConfig } from './config/default-b2b-unit-selection-config'; +import { defaultOccB2bUnitSelectionConfig } from './config/default-occ-b2b-unit-selection-config'; +import { ORGANIZATION_B2B_UNIT_SELECTION_FEATURE } from './feature-name'; +// Side-effect imports: register module augmentations at application startup. +import './model/augmented-core.model'; +import './model/occ-b2b-unit-selection-endpoints.model'; + +// TODO: Inline this factory when we start releasing Ivy compiled libraries +export function defaultB2bUnitSelectionComponentsConfig(): CmsConfig { + const config: CmsConfig = { + featureModules: { + [ORGANIZATION_B2B_UNIT_SELECTION_FEATURE]: { + cmsComponents: ['B2bUnitSelectorComponent'], + }, + }, + }; + return config; +} + +@NgModule({ + providers: [ + provideDefaultConfig(defaultB2bUnitSelectionConfig), + // OCC endpoint config must be registered eagerly (in the root module) so that + // OccEndpointsService — a root singleton built once at app startup — can + // resolve our custom endpoint keys before the lazy feature module loads. + provideDefaultConfig(defaultOccB2bUnitSelectionConfig), + provideDefaultConfigFactory(defaultB2bUnitSelectionComponentsConfig), + ], +}) +export class B2bUnitSelectionRootModule {} diff --git a/feature-libs/organization/b2b-unit-selection/root/config/b2b-unit-selection.config.ts b/feature-libs/organization/b2b-unit-selection/root/config/b2b-unit-selection.config.ts new file mode 100644 index 00000000000..4b375f96005 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/root/config/b2b-unit-selection.config.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { Config } from '@spartacus/core'; + +@Injectable({ + providedIn: 'root', + useExisting: Config, +}) +export abstract class B2bUnitSelectionConfig { + b2bUnitSelection?: { + /** + * Whether to enable the B2B Unit selection feature + * (post-login dialog + header Company selector). + * Defaults to false; consumers opt in via provideConfig(). + */ + enabled?: boolean; + }; +} + +declare module '@spartacus/core' { + interface Config extends B2bUnitSelectionConfig {} +} diff --git a/feature-libs/organization/b2b-unit-selection/root/config/default-b2b-unit-selection-config.ts b/feature-libs/organization/b2b-unit-selection/root/config/default-b2b-unit-selection-config.ts new file mode 100644 index 00000000000..664bf0d9e5f --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/root/config/default-b2b-unit-selection-config.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { B2bUnitSelectionConfig } from './b2b-unit-selection.config'; + +export const defaultB2bUnitSelectionConfig: B2bUnitSelectionConfig = { + b2bUnitSelection: { + enabled: false, + }, +}; diff --git a/feature-libs/organization/b2b-unit-selection/root/config/default-occ-b2b-unit-selection-config.ts b/feature-libs/organization/b2b-unit-selection/root/config/default-occ-b2b-unit-selection-config.ts new file mode 100644 index 00000000000..6d3d270cd65 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/root/config/default-occ-b2b-unit-selection-config.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OccConfig } from '@spartacus/core'; + +/** + * Default OCC endpoint configuration for the B2B unit-selection feature. + * + * Placed in the **root** entry-point (eagerly loaded) so that + * `OccEndpointsService` — a root singleton whose config is built once at + * app startup — can resolve these endpoint keys before the lazy feature + * module is bootstrapped. + */ +export const defaultOccB2bUnitSelectionConfig: OccConfig = { + backend: { + occ: { + endpoints: { + orgUser: 'orgUsers/${userId}', + orgUserUnits: 'orgUsers/${userId}/orgUnits', + orgUserDefaultUnit: 'orgUsers/${userId}/defaultOrgUnit', + }, + }, + }, +}; diff --git a/feature-libs/organization/b2b-unit-selection/root/events/b2b-unit-selection.events.ts b/feature-libs/organization/b2b-unit-selection/root/events/b2b-unit-selection.events.ts new file mode 100644 index 00000000000..5fdbe9de2b7 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/root/events/b2b-unit-selection.events.ts @@ -0,0 +1,52 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CxEvent } from '@spartacus/core'; + +/** + * Base event for B2B unit selection actions. + */ +export abstract class B2bUnitSelectionEvent extends CxEvent { + /** + * User ID of the logged-in B2B customer. + */ + userId: string; + /** + * The unit name (uid) that was selected. + */ + unitName: string; +} + +/** + * Fired when the user has successfully switched to a new organisation unit. + * Other domains can listen to this event via `EventService.get(B2bUnitSwitchedEvent)` + * to react to an org-unit change (e.g. clear caches, reload quotes, etc.). + */ +export class B2bUnitSwitchedEvent extends B2bUnitSelectionEvent { + /** + * Event's type + */ + static readonly type = 'B2bUnitSwitchedEvent'; + /** + * `true` when the app navigated to the home page after the switch; + * `false` when the current page was reloaded in-place. + */ + redirectedToHome: boolean; +} + +/** + * Fired when an attempt to switch organisation unit has failed. + */ +export class B2bUnitSwitchFailedEvent extends B2bUnitSelectionEvent { + /** + * Event's type + */ + static readonly type = 'B2bUnitSwitchFailedEvent'; + /** + * Normalised error payload. + */ + error: unknown; +} diff --git a/feature-libs/organization/b2b-unit-selection/root/feature-name.ts b/feature-libs/organization/b2b-unit-selection/root/feature-name.ts new file mode 100644 index 00000000000..8d502669eb0 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/root/feature-name.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export const ORGANIZATION_B2B_UNIT_SELECTION_FEATURE = + 'organizationB2BUnitSelection'; diff --git a/feature-libs/organization/b2b-unit-selection/root/model/augmented-core.model.ts b/feature-libs/organization/b2b-unit-selection/root/model/augmented-core.model.ts new file mode 100644 index 00000000000..a921bc63321 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/root/model/augmented-core.model.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { LAUNCH_CALLER } from '@spartacus/storefront'; + +// Runtime registration: extends the LAUNCH_CALLER enum at runtime. +(LAUNCH_CALLER as any)['B2B_UNIT_SELECTION'] = 'B2B_UNIT_SELECTION'; diff --git a/feature-libs/organization/b2b-unit-selection/root/model/index.ts b/feature-libs/organization/b2b-unit-selection/root/model/index.ts new file mode 100644 index 00000000000..20ab831a93c --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/root/model/index.ts @@ -0,0 +1,7 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './augmented-core.model'; diff --git a/feature-libs/organization/b2b-unit-selection/root/model/occ-b2b-unit-selection-endpoints.model.ts b/feature-libs/organization/b2b-unit-selection/root/model/occ-b2b-unit-selection-endpoints.model.ts new file mode 100644 index 00000000000..f99a29e438c --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/root/model/occ-b2b-unit-selection-endpoints.model.ts @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { OccEndpoint } from '@spartacus/core'; + +export interface B2bUnitSelectionOccEndpoints { + /** + * Fetches current B2B user profile (includes the default orgUnit). + * GET /{baseSiteId}/orgUsers/{userId} + */ + orgUser?: string | OccEndpoint; + + /** + * Fetches all org units assigned to the current B2B user. + * GET /{baseSiteId}/orgUsers/{userId}/orgUnits + */ + orgUserUnits?: string | OccEndpoint; + + /** + * Sets the default org unit for the current B2B user. + * PUT /{baseSiteId}/orgUsers/{userId}/defaultOrgUnit + */ + orgUserDefaultUnit?: string | OccEndpoint; +} + +declare module '@spartacus/core' { + interface OccEndpoints extends B2bUnitSelectionOccEndpoints {} +} diff --git a/feature-libs/organization/b2b-unit-selection/root/ng-package.json b/feature-libs/organization/b2b-unit-selection/root/ng-package.json new file mode 100644 index 00000000000..2a9c6221b0e --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/root/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "./public_api.ts" + } +} diff --git a/feature-libs/organization/b2b-unit-selection/root/public_api.ts b/feature-libs/organization/b2b-unit-selection/root/public_api.ts new file mode 100644 index 00000000000..c4cc6e36cc4 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/root/public_api.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './b2b-unit-selection-root.module'; +export * from './config/b2b-unit-selection.config'; +export * from './config/default-b2b-unit-selection-config'; +export * from './config/default-occ-b2b-unit-selection-config'; +export * from './events/b2b-unit-selection.events'; +export * from './feature-name'; +export * from './model/occ-b2b-unit-selection-endpoints.model'; diff --git a/feature-libs/organization/b2b-unit-selection/styles/_b2b-unit-selection-dialog.scss b/feature-libs/organization/b2b-unit-selection/styles/_b2b-unit-selection-dialog.scss new file mode 100644 index 00000000000..e5704872103 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/styles/_b2b-unit-selection-dialog.scss @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2026 SAP Spartacus team +// +// SPDX-License-Identifier: Apache-2.0 + +// Dialog width: narrower than the default modal width (768px) to suit a simple selection form. +$b2b-unit-selection-dialog-width: 400px; + +.cx-b2b-unit-selection-dialog { + // Replaces .cx-modal-container layout so the base 768px !important rules don't apply. + display: flex; + align-items: center; + margin: auto; + height: 100%; + overflow-y: auto; + max-width: $b2b-unit-selection-dialog-width; + min-width: $b2b-unit-selection-dialog-width; + + @include media-breakpoint-down(sm) { + max-width: 100%; + min-width: 100%; + height: 100%; + } +} + +.cx-required-asterisk { + color: var(--cx-color-danger); + margin-inline-start: 2px; +} + +// SiteContext: cx-page-slot defaults to flex-wrap: wrap (_common-template.scss), which causes +// Company (cx-b2b-unit-selector) to wrap to a second line when Language + Theme + +// Company together exceed the 50% slot width. +// +// flex-wrap: nowrap – keeps all three items on a single row. +// min-width: 0 – overrides the flex item's default min-width: auto. Without this, +// the browser widens SiteContext to fit its non-wrappable content, +// pushing SiteLinks (Quick Order …) to a second row. +// align-items: center – Language / Theme use a native + // in cx-site-context-selector (padding: 2px 4px → height = line-height + 4px). + // height: auto leaves the door open for ng-select library styles (height: 36px, + // min-height: 36px) to affect layout; an explicit calc() value closes that gap + // so SiteContext height = Language/Theme height and align-items: center on + // SiteContext does not shift Language / Theme downward ("too low" effect). + height: calc( + var(--cx-font-size, 0.875rem) * var(--cx-line-height, 1.2222) + 4px + ) !important; + min-height: unset !important; + padding: 2px 0 2px 0; + + .ng-value-container { + // ng-select default theme specificity (3 classes) is higher than our rules; + // !important is required to override padding-left: 10px. + padding: 0 !important; + padding-inline-end: #{$b2b-unit-selector-caret-size * 2} !important; + + .ng-value { + color: var(--cx-color-text); + font-weight: var(--cx-font-weight-semi); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + } + + // ng-select sets top:5px on .ng-input in single-select mode; reset to zero. + .ng-input { + top: 0 !important; + padding: 0 !important; + + > input { + cursor: pointer; + } + } + } + + .ng-arrow-wrapper { + display: none; + } + } + + &.ng-select-focused .ng-select-container, + &.ng-select-opened .ng-select-container { + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + } + + // Disabled state for single-unit users: non-interactive, restore default cursor. + &.ng-select-disabled .ng-select-container { + cursor: default; + } + } +} diff --git a/feature-libs/organization/b2b-unit-selection/styles/_index.scss b/feature-libs/organization/b2b-unit-selection/styles/_index.scss new file mode 100644 index 00000000000..07273f8fc47 --- /dev/null +++ b/feature-libs/organization/b2b-unit-selection/styles/_index.scss @@ -0,0 +1,2 @@ +@import './b2b-unit-selector'; +@import './b2b-unit-selection-dialog'; diff --git a/feature-libs/organization/schematics/add-organization/__snapshots__/index_spec.ts.snap b/feature-libs/organization/schematics/add-organization/__snapshots__/index_spec.ts.snap index c5cb94d43d8..0f85118fa5d 100644 --- a/feature-libs/organization/schematics/add-organization/__snapshots__/index_spec.ts.snap +++ b/feature-libs/organization/schematics/add-organization/__snapshots__/index_spec.ts.snap @@ -452,6 +452,232 @@ exports[`Spartacus Organization schematics: ng-add Administration feature genera }" `; +exports[`Spartacus Organization schematics: ng-add B2B unit selection feature eager loading should import appropriate modules 1`] = ` +"import { NgModule } from '@angular/core'; +import { I18nConfig, provideConfig } from "@spartacus/core"; +import { B2bUnitSelectionModule } from "@spartacus/organization/b2b-unit-selection"; +import { b2bUnitSelectionTranslationChunksConfig, b2bUnitSelectionTranslationsEn } from "@spartacus/organization/b2b-unit-selection/assets"; +import { B2bUnitSelectionRootModule } from "@spartacus/organization/b2b-unit-selection/root"; + +@NgModule({ + declarations: [], + imports: [ + B2bUnitSelectionRootModule, + B2bUnitSelectionModule + ], + providers: [provideConfig({ + i18n: { + resources: { en: b2bUnitSelectionTranslationsEn }, + chunks: b2bUnitSelectionTranslationChunksConfig, + }, + })] +}) +export class OrganizationB2bUnitSelectionFeatureModule { } +" +`; + +exports[`Spartacus Organization schematics: ng-add B2B unit selection feature general setup b2b features configuration should be added 1`] = ` +"import { NgModule } from '@angular/core'; +import { translationChunksConfig, translationsEn } from "@spartacus/assets"; +import { FeaturesConfig, I18nConfig, OccConfig, provideConfig, provideConfigFactory, SiteContextConfig } from "@spartacus/core"; +import { defaultB2bOccConfig } from "@spartacus/setup"; +import { defaultCmsContentProviders, layoutConfigFactory, mediaConfig } from "@spartacus/storefront"; + +@NgModule({ + declarations: [], + imports: [ + ], + providers: [provideConfigFactory(layoutConfigFactory), provideConfig(mediaConfig), ...defaultCmsContentProviders, provideConfig({ + backend: { + occ: { + baseUrl: 'https://localhost:9002', + } + }, + }), provideConfig({ + context: {}, + }), provideConfig({ + i18n: { + resources: { en: translationsEn }, + chunks: translationChunksConfig, + fallbackLang: 'en' + }, + }), provideConfig({ + features: { + level: '221121.17' + } + }), provideConfig(defaultB2bOccConfig)] +}) +export class SpartacusConfigurationModule { } +" +`; + +exports[`Spartacus Organization schematics: ng-add B2B unit selection feature general setup should add the feature using the lazy loading syntax 1`] = ` +"import { NgModule } from '@angular/core'; +import { CmsConfig, I18nConfig, provideConfig } from "@spartacus/core"; +import { b2bUnitSelectionTranslationChunksConfig, b2bUnitSelectionTranslationsEn } from "@spartacus/organization/b2b-unit-selection/assets"; +import { B2bUnitSelectionRootModule, ORGANIZATION_B2B_UNIT_SELECTION_FEATURE } from "@spartacus/organization/b2b-unit-selection/root"; + +@NgModule({ + declarations: [], + imports: [ + B2bUnitSelectionRootModule + ], + providers: [provideConfig({ + featureModules: { + [ORGANIZATION_B2B_UNIT_SELECTION_FEATURE]: { + module: () => + import('@spartacus/organization/b2b-unit-selection').then((m) => m.B2bUnitSelectionModule), + }, + } + }), + provideConfig({ + i18n: { + resources: { en: b2bUnitSelectionTranslationsEn }, + chunks: b2bUnitSelectionTranslationChunksConfig, + }, + }) + ] +}) +export class OrganizationB2bUnitSelectionFeatureModule { } +" +`; + +exports[`Spartacus Organization schematics: ng-add B2B unit selection feature general setup styling should create a proper scss file 1`] = ` +"@import "../../styles-config"; +@import "@spartacus/organization"; +@import "@spartacus/styles/vendor/bootstrap/scss/functions"; +@import "@spartacus/styles/vendor/bootstrap/scss/variables"; +@import "@spartacus/styles/vendor/bootstrap/scss/_mixins"; +" +`; + +exports[`Spartacus Organization schematics: ng-add B2B unit selection feature general setup styling should update angular.json 1`] = ` +"{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "", + "projects": { + "schematics-test": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss", + "type": "component", + "addTypeToClassName": false + }, + "@schematics/angular:directive": { + "type": "directive", + "addTypeToClassName": false + }, + "@schematics/angular:service": { + "type": "service", + "addTypeToClassName": false + }, + "@schematics/angular:guard": { + "typeSeparator": "." + }, + "@schematics/angular:interceptor": { + "typeSeparator": "." + }, + "@schematics/angular:module": { + "typeSeparator": "." + }, + "@schematics/angular:pipe": { + "typeSeparator": "." + }, + "@schematics/angular:resolver": { + "typeSeparator": "." + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular/build:application", + "options": { + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + { + "glob": "**/*", + "input": "public" + } + ], + "styles": [ + "src/styles.scss", + "src/styles/spartacus/organization.scss" + ], + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/" + ], + "sass": { + "silenceDeprecations": [ + "import" + ] + } + } + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kB", + "maximumError": "3.5mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "4kB", + "maximumError": "8kB" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "schematics-test:build:production" + }, + "development": { + "buildTarget": "schematics-test:build:development" + } + }, + "defaultConfiguration": "development" + }, + "test": { + "builder": "@angular/build:unit-test", + "options": { + "stylePreprocessorOptions": { + "includePaths": [ + "node_modules/" + ] + }, + "styles": [ + "src/styles/spartacus/organization.scss" + ] + } + } + } + } + } +}" +`; + exports[`Spartacus Organization schematics: ng-add Order approval feature eager loading should import appropriate modules 1`] = ` "import { NgModule } from '@angular/core'; import { I18nConfig, provideConfig } from "@spartacus/core"; diff --git a/feature-libs/organization/schematics/add-organization/index_spec.ts b/feature-libs/organization/schematics/add-organization/index_spec.ts index dfc176ff54d..dfaad14b64e 100644 --- a/feature-libs/organization/schematics/add-organization/index_spec.ts +++ b/feature-libs/organization/schematics/add-organization/index_spec.ts @@ -10,11 +10,13 @@ import { orderFeatureModulePath, ORGANIZATION_ACCOUNT_SUMMARY_FEATURE_NAME, ORGANIZATION_ADMINISTRATION_FEATURE_NAME, + ORGANIZATION_B2B_UNIT_SELECTION_FEATURE_NAME, ORGANIZATION_ORDER_APPROVAL_FEATURE_NAME, ORGANIZATION_UNIT_ORDER_FEATURE_NAME, ORGANIZATION_USER_REGISTRATION_FEATURE_NAME, organizationAccountSummaryFeatureModulePath, organizationAdministrationFeatureModulePath, + organizationB2bUnitSelectionFeatureModulePath, organizationOrderApprovalFeatureModulePath, organizationUnitOrderFeatureModulePath, organizationUserRegistrationFeatureModulePath, @@ -68,6 +70,11 @@ describe('Spartacus Organization schematics: ng-add', () => { features: [ORGANIZATION_UNIT_ORDER_FEATURE_NAME], }; + const b2bUnitSelectionFeatureOptions: SpartacusOrganizationOptions = { + ...libraryNoFeaturesOptions, + features: [ORGANIZATION_B2B_UNIT_SELECTION_FEATURE_NAME], + }; + describe('Without features', () => { beforeAll(async () => { appTree = await generateDefaultWorkspace(schematicRunner, appTree); @@ -95,6 +102,9 @@ describe('Spartacus Organization schematics: ng-add', () => { expect( appTree.exists(organizationUnitOrderFeatureModulePath) ).toBeFalsy(); + expect( + appTree.exists(organizationB2bUnitSelectionFeatureModulePath) + ).toBeFalsy(); }); it('should install necessary Spartacus libraries', () => { @@ -457,4 +467,70 @@ describe('Spartacus Organization schematics: ng-add', () => { }); }); }); + + describe('B2B unit selection feature', () => { + describe('general setup', () => { + beforeAll(async () => { + appTree = await generateDefaultWorkspace(schematicRunner, appTree); + appTree = await schematicRunner.runSchematic( + 'ng-add', + b2bUnitSelectionFeatureOptions, + appTree + ); + }); + + it('should add the feature using the lazy loading syntax', async () => { + const module = appTree.readContent( + organizationB2bUnitSelectionFeatureModulePath + ); + expect(module).toMatchSnapshot(); + }); + + it('should NOT install the required feature dependencies', async () => { + const administrationFeatureModule = appTree.readContent( + organizationAdministrationFeatureModulePath + ); + expect(administrationFeatureModule).toBeFalsy(); + }); + + describe('styling', () => { + it('should create a proper scss file', () => { + const scssContent = appTree.readContent(scssFilePath); + expect(scssContent).toMatchSnapshot(); + }); + + it('should update angular.json', async () => { + const content = appTree.readContent('/angular.json'); + expect(content).toMatchSnapshot(); + }); + }); + + describe('b2b features', () => { + it('configuration should be added', () => { + const configurationModule = appTree.readContent( + `src/app/spartacus/${SPARTACUS_CONFIGURATION_MODULE}.module.ts` + ); + expect(configurationModule).toMatchSnapshot(); + }); + }); + }); + + describe('eager loading', () => { + beforeAll(async () => { + appTree = await generateDefaultWorkspace(schematicRunner, appTree); + appTree = await schematicRunner.runSchematic( + 'ng-add', + { ...b2bUnitSelectionFeatureOptions, lazy: false }, + appTree + ); + }); + + it('should import appropriate modules', async () => { + const module = appTree.readContent( + organizationB2bUnitSelectionFeatureModulePath + ); + expect(module).toMatchSnapshot(); + }); + }); + }); }); diff --git a/feature-libs/organization/schematics/add-organization/schema.json b/feature-libs/organization/schematics/add-organization/schema.json index 3fed48166d5..644608bbfd3 100644 --- a/feature-libs/organization/schematics/add-organization/schema.json +++ b/feature-libs/organization/schematics/add-organization/schema.json @@ -37,7 +37,8 @@ "Order-Approval", "Account-Summary", "Organization-User-Registration", - "Unit-Order" + "Unit-Order", + "B2b-Unit-Selection" ], "type": "string" }, @@ -70,6 +71,11 @@ "value": "Unit-Order", "label": "Unit Order (b2b feature)", "checked": true + }, + { + "value": "B2b-Unit-Selection", + "label": "B2B Unit Selection (b2b feature, requires Organization - Administration)", + "checked": false } ] } diff --git a/feature-libs/organization/tsconfig.schematics.json b/feature-libs/organization/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/organization/tsconfig.schematics.json +++ b/feature-libs/organization/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/pdf-invoices/tsconfig.schematics.json b/feature-libs/pdf-invoices/tsconfig.schematics.json index 5d3997312e6..03e34d069f0 100644 --- a/feature-libs/pdf-invoices/tsconfig.schematics.json +++ b/feature-libs/pdf-invoices/tsconfig.schematics.json @@ -360,6 +360,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/pickup-in-store/tsconfig.schematics.json b/feature-libs/pickup-in-store/tsconfig.schematics.json index f467a252ddd..e45c6220dee 100644 --- a/feature-libs/pickup-in-store/tsconfig.schematics.json +++ b/feature-libs/pickup-in-store/tsconfig.schematics.json @@ -362,6 +362,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/product-configurator/tsconfig.schematics.json b/feature-libs/product-configurator/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/product-configurator/tsconfig.schematics.json +++ b/feature-libs/product-configurator/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/product-multi-dimensional/tsconfig.schematics.json b/feature-libs/product-multi-dimensional/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/product-multi-dimensional/tsconfig.schematics.json +++ b/feature-libs/product-multi-dimensional/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/product/tsconfig.schematics.json b/feature-libs/product/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/product/tsconfig.schematics.json +++ b/feature-libs/product/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/qualtrics/tsconfig.schematics.json b/feature-libs/qualtrics/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/qualtrics/tsconfig.schematics.json +++ b/feature-libs/qualtrics/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/quote/tsconfig.schematics.json b/feature-libs/quote/tsconfig.schematics.json index f467a252ddd..e45c6220dee 100644 --- a/feature-libs/quote/tsconfig.schematics.json +++ b/feature-libs/quote/tsconfig.schematics.json @@ -362,6 +362,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/requested-delivery-date/tsconfig.schematics.json b/feature-libs/requested-delivery-date/tsconfig.schematics.json index 5d3997312e6..03e34d069f0 100644 --- a/feature-libs/requested-delivery-date/tsconfig.schematics.json +++ b/feature-libs/requested-delivery-date/tsconfig.schematics.json @@ -360,6 +360,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/smartedit/tsconfig.schematics.json b/feature-libs/smartedit/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/smartedit/tsconfig.schematics.json +++ b/feature-libs/smartedit/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/storefinder/tsconfig.schematics.json b/feature-libs/storefinder/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/storefinder/tsconfig.schematics.json +++ b/feature-libs/storefinder/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/subscription-billing/tsconfig.schematics.json b/feature-libs/subscription-billing/tsconfig.schematics.json index 5d3997312e6..03e34d069f0 100644 --- a/feature-libs/subscription-billing/tsconfig.schematics.json +++ b/feature-libs/subscription-billing/tsconfig.schematics.json @@ -360,6 +360,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/tracking/tsconfig.schematics.json b/feature-libs/tracking/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/feature-libs/tracking/tsconfig.schematics.json +++ b/feature-libs/tracking/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/feature-libs/user/tsconfig.schematics.json b/feature-libs/user/tsconfig.schematics.json index 27f8244d477..2a24eb5b319 100644 --- a/feature-libs/user/tsconfig.schematics.json +++ b/feature-libs/user/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/cdc/tsconfig.lib.json b/integration-libs/cdc/tsconfig.lib.json index cb9e0e05d86..68b11c56743 100644 --- a/integration-libs/cdc/tsconfig.lib.json +++ b/integration-libs/cdc/tsconfig.lib.json @@ -74,6 +74,24 @@ "@spartacus/organization/order-approval/assets": [ "dist/organization/order-approval/assets" ], + "@spartacus/organization/b2b-unit-selection": [ + "dist/organization/b2b-unit-selection" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "dist/organization/b2b-unit-selection/root" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "dist/organization/b2b-unit-selection/occ" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "dist/organization/b2b-unit-selection/core" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "dist/organization/b2b-unit-selection/components" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "dist/organization/b2b-unit-selection/assets" + ], "@spartacus/organization/administration": [ "dist/organization/administration" ], diff --git a/integration-libs/cdc/tsconfig.schematics.json b/integration-libs/cdc/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/integration-libs/cdc/tsconfig.schematics.json +++ b/integration-libs/cdc/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/cdp/tsconfig.schematics.json b/integration-libs/cdp/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/integration-libs/cdp/tsconfig.schematics.json +++ b/integration-libs/cdp/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/cds/tsconfig.schematics.json b/integration-libs/cds/tsconfig.schematics.json index 2feb80ee2cf..148143cbef4 100644 --- a/integration-libs/cds/tsconfig.schematics.json +++ b/integration-libs/cds/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/cpq-quote/tsconfig.schematics.json b/integration-libs/cpq-quote/tsconfig.schematics.json index 5d3997312e6..03e34d069f0 100644 --- a/integration-libs/cpq-quote/tsconfig.schematics.json +++ b/integration-libs/cpq-quote/tsconfig.schematics.json @@ -360,6 +360,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/digital-payments/tsconfig.schematics.json b/integration-libs/digital-payments/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/integration-libs/digital-payments/tsconfig.schematics.json +++ b/integration-libs/digital-payments/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/epd-visualization/tsconfig.schematics.json b/integration-libs/epd-visualization/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/integration-libs/epd-visualization/tsconfig.schematics.json +++ b/integration-libs/epd-visualization/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/omf/tsconfig.schematics.json b/integration-libs/omf/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/integration-libs/omf/tsconfig.schematics.json +++ b/integration-libs/omf/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/opf/tsconfig.schematics.json b/integration-libs/opf/tsconfig.schematics.json index b77671f4d07..97b2cee1efa 100644 --- a/integration-libs/opf/tsconfig.schematics.json +++ b/integration-libs/opf/tsconfig.schematics.json @@ -21,8 +21,11 @@ "esModuleInterop": true, "paths": { "@spartacus/schematics": ["../../core-libs/schematics/index"], + "@spartacus/storefront": ["../../core-libs/storefront/public_api"], "@spartacus/setup": ["../../core-libs/setup/public_api"], "@spartacus/setup/ssr": ["../../core-libs/setup/ssr/public_api"], + "@spartacus/core": ["../../core-libs/core/public_api"], + "@spartacus/assets": ["../../core-libs/assets/src/public_api"], "@spartacus/user": ["../../feature-libs/user/public_api"], "@spartacus/user/wishlist": [ "../../feature-libs/user/wishlist/public_api" @@ -360,6 +363,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], @@ -847,10 +868,7 @@ "@spartacus/cdc/components": [ "../../integration-libs/cdc/components/public_api" ], - "@spartacus/cdc/assets": ["../../integration-libs/cdc/assets/public_api"], - "@spartacus/storefront": ["../../core-libs/storefront/public_api"], - "@spartacus/core": ["../../core-libs/core/public_api"], - "@spartacus/assets": ["../../core-libs/assets/src/public_api"] + "@spartacus/cdc/assets": ["../../integration-libs/cdc/assets/public_api"] } }, "include": ["schematics/**/*.ts"], diff --git a/integration-libs/opps/tsconfig.schematics.json b/integration-libs/opps/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/integration-libs/opps/tsconfig.schematics.json +++ b/integration-libs/opps/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/punchout/tsconfig.schematics.json b/integration-libs/punchout/tsconfig.schematics.json index 5d3997312e6..03e34d069f0 100644 --- a/integration-libs/punchout/tsconfig.schematics.json +++ b/integration-libs/punchout/tsconfig.schematics.json @@ -360,6 +360,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/s4-service/tsconfig.schematics.json b/integration-libs/s4-service/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/integration-libs/s4-service/tsconfig.schematics.json +++ b/integration-libs/s4-service/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/s4om/tsconfig.schematics.json b/integration-libs/s4om/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/integration-libs/s4om/tsconfig.schematics.json +++ b/integration-libs/s4om/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/integration-libs/segment-refs/tsconfig.schematics.json b/integration-libs/segment-refs/tsconfig.schematics.json index 4747bcbf104..c283d304116 100644 --- a/integration-libs/segment-refs/tsconfig.schematics.json +++ b/integration-libs/segment-refs/tsconfig.schematics.json @@ -347,6 +347,24 @@ "@spartacus/organization/order-approval/assets": [ "../../feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "../../feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "../../feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "../../feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "../../feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "../../feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "../../feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "../../feature-libs/organization/administration/public_api" ], diff --git a/projects/storefrontapp/src/app/spartacus/features/organization/organization-b2b-unit-selection-feature.module.ts b/projects/storefrontapp/src/app/spartacus/features/organization/organization-b2b-unit-selection-feature.module.ts new file mode 100644 index 00000000000..d0d1511b311 --- /dev/null +++ b/projects/storefrontapp/src/app/spartacus/features/organization/organization-b2b-unit-selection-feature.module.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2026 SAP Spartacus team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { CmsConfig, I18nConfig, provideConfig } from '@spartacus/core'; +import { + b2bUnitSelectionTranslationChunksConfig, + b2bUnitSelectionTranslationsEn, +} from '@spartacus/organization/b2b-unit-selection/assets'; +import { + B2bUnitSelectionRootModule, + ORGANIZATION_B2B_UNIT_SELECTION_FEATURE, +} from '@spartacus/organization/b2b-unit-selection/root'; + +@NgModule({ + imports: [B2bUnitSelectionRootModule], + providers: [ + provideConfig({ + featureModules: { + [ORGANIZATION_B2B_UNIT_SELECTION_FEATURE]: { + module: () => + import('@spartacus/organization/b2b-unit-selection').then( + (m) => m.B2bUnitSelectionModule + ), + }, + }, + }), + provideConfig({ + i18n: { + resources: { + en: b2bUnitSelectionTranslationsEn, + }, + chunks: b2bUnitSelectionTranslationChunksConfig, + }, + }), + ], +}) +export class OrganizationB2bUnitSelectionFeatureModule {} diff --git a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts index 0ad8c158672..aa3ca704d6e 100644 --- a/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts +++ b/projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts @@ -86,6 +86,7 @@ import { OrderDocumentFlowFeatureModule } from './features/order/order-document- import { OrderFeatureModule } from './features/order/order-feature.module'; import { AccountSummaryFeatureModule } from './features/organization/organization-account-summary-feature.module'; import { AdministrationFeatureModule } from './features/organization/organization-administration-feature.module'; +import { OrganizationB2bUnitSelectionFeatureModule } from './features/organization/organization-b2b-unit-selection-feature.module'; import { OrderApprovalFeatureModule } from './features/organization/organization-order-approval-feature.module'; import { UnitOrderFeatureModule } from './features/organization/organization-unit-order-feature.module'; import { PDFInvoicesFeatureModule } from './features/pdf-invoices/pdf-invoices-feature.module'; @@ -120,6 +121,7 @@ if (environment.b2b) { AccountSummaryFeatureModule, BulkPricingFeatureModule, OrderApprovalFeatureModule, + OrganizationB2bUnitSelectionFeatureModule, OrganizationUserRegistrationFeatureModule, UnitOrderFeatureModule, FutureStockFeatureModule diff --git a/projects/storefrontapp/tsconfig.app.prod.json b/projects/storefrontapp/tsconfig.app.prod.json index 425f94a3261..4c4d2fcd5b4 100644 --- a/projects/storefrontapp/tsconfig.app.prod.json +++ b/projects/storefrontapp/tsconfig.app.prod.json @@ -2,8 +2,11 @@ "extends": "./tsconfig.app.json", "compilerOptions": { "paths": { + "@spartacus/storefront": ["dist/storefront"], "@spartacus/setup": ["dist/setup"], "@spartacus/setup/ssr": ["dist/setup/ssr"], + "@spartacus/core": ["dist/core"], + "@spartacus/assets": ["dist/assets"], "@spartacus/user": ["dist/user"], "@spartacus/user/wishlist": ["dist/user/wishlist"], "@spartacus/user/wishlist/root": ["dist/user/wishlist/root"], @@ -235,6 +238,24 @@ "@spartacus/organization/order-approval/assets": [ "dist/organization/order-approval/assets" ], + "@spartacus/organization/b2b-unit-selection": [ + "dist/organization/b2b-unit-selection" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "dist/organization/b2b-unit-selection/root" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "dist/organization/b2b-unit-selection/occ" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "dist/organization/b2b-unit-selection/core" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "dist/organization/b2b-unit-selection/components" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "dist/organization/b2b-unit-selection/assets" + ], "@spartacus/organization/administration": [ "dist/organization/administration" ], @@ -504,10 +525,7 @@ ], "@spartacus/cdc/core": ["dist/cdc/core"], "@spartacus/cdc/components": ["dist/cdc/components"], - "@spartacus/cdc/assets": ["dist/cdc/assets"], - "@spartacus/storefront": ["dist/storefront"], - "@spartacus/core": ["dist/core"], - "@spartacus/assets": ["dist/assets"] + "@spartacus/cdc/assets": ["dist/cdc/assets"] } } } diff --git a/tsconfig.compodoc.json b/tsconfig.compodoc.json index 3342c99907b..040273ff500 100644 --- a/tsconfig.compodoc.json +++ b/tsconfig.compodoc.json @@ -407,6 +407,24 @@ "@spartacus/organization/order-approval/assets": [ "feature-libs/organization/order-approval/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/organization/administration": [ "feature-libs/organization/administration/public_api" ], diff --git a/tsconfig.json b/tsconfig.json index a8697c3e89e..6e353034f96 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,12 +21,21 @@ "@spartacus/schematics": [ "core-libs/schematics/index" ], + "@spartacus/storefront": [ + "core-libs/storefront/public_api" + ], "@spartacus/setup": [ "core-libs/setup/public_api" ], "@spartacus/setup/ssr": [ "core-libs/setup/ssr/public_api" ], + "@spartacus/core": [ + "core-libs/core/public_api" + ], + "@spartacus/assets": [ + "core-libs/assets/src/public_api" + ], "@spartacus/user": [ "feature-libs/user/public_api" ], @@ -243,29 +252,6 @@ "@spartacus/product-configurator/common": [ "feature-libs/product-configurator/common/public_api" ], - "@spartacus/opf/gift-card/components": [ - "integration-libs/opf/gift-card/components/public_api" - ], - "@spartacus/opf/gift-card/core": [ - "integration-libs/opf/gift-card/core/public_api" - ], - "@spartacus/opf/gift-card": [ - "integration-libs/opf/gift-card/public_api" - ], - "@spartacus/opf/gift-card/occ": [ - "integration-libs/opf/gift-card/occ/public_api" - ], - "@spartacus/opf/gift-card/root": [ - "integration-libs/opf/gift-card/root/public_api" - ], - "@spartacus/opf/gift-card/assets": [ - "integration-libs/opf/gift-card/assets/public_api" - ], - "@spartacus/opf": ["integration-libs/opf/public_api"], - "@spartacus/opf/order": ["integration-libs/opf/order/public_api"], - "@spartacus/opf/payment/assets": [ - "integration-libs/opf/payment/assets/public_api" - ], "@spartacus/product-configurator/common/assets": [ "feature-libs/product-configurator/common/assets/public_api" ], @@ -455,6 +441,24 @@ "@spartacus/organization/account-summary/assets": [ "feature-libs/organization/account-summary/assets/public_api" ], + "@spartacus/organization/b2b-unit-selection": [ + "feature-libs/organization/b2b-unit-selection/public_api" + ], + "@spartacus/organization/b2b-unit-selection/root": [ + "feature-libs/organization/b2b-unit-selection/root/public_api" + ], + "@spartacus/organization/b2b-unit-selection/occ": [ + "feature-libs/organization/b2b-unit-selection/occ/public_api" + ], + "@spartacus/organization/b2b-unit-selection/core": [ + "feature-libs/organization/b2b-unit-selection/core/public_api" + ], + "@spartacus/organization/b2b-unit-selection/components": [ + "feature-libs/organization/b2b-unit-selection/components/public_api" + ], + "@spartacus/organization/b2b-unit-selection/assets": [ + "feature-libs/organization/b2b-unit-selection/assets/public_api" + ], "@spartacus/order": [ "feature-libs/order/public_api" ], @@ -755,6 +759,9 @@ "@spartacus/opps/root": [ "integration-libs/opps/root/public_api" ], + "@spartacus/opf": [ + "integration-libs/opf/public_api" + ], "@spartacus/opf/tokenisation": [ "integration-libs/opf/tokenisation/public_api" ], @@ -803,6 +810,12 @@ "@spartacus/opf/payment/core": [ "integration-libs/opf/payment/core/public_api" ], + "@spartacus/opf/payment/assets": [ + "integration-libs/opf/payment/assets/public_api" + ], + "@spartacus/opf/order": [ + "integration-libs/opf/order/public_api" + ], "@spartacus/opf/global-functions": [ "integration-libs/opf/global-functions/public_api" ], @@ -812,6 +825,24 @@ "@spartacus/opf/global-functions/core": [ "integration-libs/opf/global-functions/core/public_api" ], + "@spartacus/opf/gift-card": [ + "integration-libs/opf/gift-card/public_api" + ], + "@spartacus/opf/gift-card/root": [ + "integration-libs/opf/gift-card/root/public_api" + ], + "@spartacus/opf/gift-card/occ": [ + "integration-libs/opf/gift-card/occ/public_api" + ], + "@spartacus/opf/gift-card/core": [ + "integration-libs/opf/gift-card/core/public_api" + ], + "@spartacus/opf/gift-card/components": [ + "integration-libs/opf/gift-card/components/public_api" + ], + "@spartacus/opf/gift-card/assets": [ + "integration-libs/opf/gift-card/assets/public_api" + ], "@spartacus/opf/cta": [ "integration-libs/opf/cta/public_api" ], @@ -949,15 +980,6 @@ ], "@spartacus/cdc/assets": [ "integration-libs/cdc/assets/public_api" - ], - "@spartacus/storefront": [ - "core-libs/storefront/public_api" - ], - "@spartacus/core": [ - "core-libs/core/public_api" - ], - "@spartacus/assets": [ - "core-libs/assets/src/public_api" ] }, "useDefineForClassFields": false