Skip to content

Commit

Permalink
feat: Add exit button (#14043)
Browse files Browse the repository at this point in the history
Create a new component for 'EXIT CONFIGURATION' button to enable an opportunity to leave the configuration any time by navigation to a product detail page

Closes #13806
  • Loading branch information
Changsuwan authored Oct 20, 2021
1 parent 2579348 commit 0932e90
Show file tree
Hide file tree
Showing 16 changed files with 342 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export const configurator = {
select: 'Select',
add: 'Add',
remove: 'Remove',
exit: 'Exit Configuration',
exitMobile: 'Exit',
},
priceSummary: {
basePrice: 'Base Price',
Expand Down
2 changes: 1 addition & 1 deletion feature-libs/product-configurator/rulebased/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ $configurator-rulebased-components: cx-configurator-template,
cx-configurator-attribute-footer, cx-configurator-attribute-drop-down,
cx-configurator-group-menu, cx-configurator-price-summary,
cx-configurator-price, cx-configurator-add-to-cart-button,
cx-configurator-attribute-input-field,
cx-configurator-exit-button, cx-configurator-attribute-input-field,
cx-configurator-attribute-numeric-input-field, cx-configurator-tab-bar,
cx-configurator-textfield-add-to-cart-button, cx-configurator-group-title,
cx-configurator-attribute-checkbox, cx-configurator-attribute-checkbox-list,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<button
class="cx-config-exit-button"
tabindex="0"
(click)="exitConfiguration()"
>
<ng-container *ngIf="isDesktop() | async">
{{ 'configurator.button.exit' | cxTranslate }}
</ng-container>
<ng-container *ngIf="isMobile() | async">
{{ 'configurator.button.exitMobile' | cxTranslate }}
</ng-container>
</button>
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Type } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { NgSelectModule } from '@ng-select/ng-select';
import {
I18nTestingModule,
Product,
ProductService,
RoutingService,
} from '@spartacus/core';
import {
Configurator,
ConfiguratorCommonsService,
ConfiguratorExitButtonComponent,
} from '@spartacus/product-configurator/rulebased';
import { BreakpointService } from '@spartacus/storefront';
import {
CommonConfigurator,
ConfiguratorModelUtils,
ConfiguratorRouter,
ConfiguratorRouterExtractorService,
ConfiguratorType,
} from '@spartacus/product-configurator/common';
import { Observable, of } from 'rxjs';
import { ConfiguratorTestUtils } from '../../testing/configurator-test-utils';
import { CommonConfiguratorTestUtilsService } from '../../../common/testing/common-configurator-test-utils.service';

const PRODUCT_CODE = 'CONF_LAPTOP';
const PRODUCT: Product = {
code: PRODUCT_CODE,
};

const mockRouterData: any = {
pageType: ConfiguratorRouter.PageType.CONFIGURATION,
isOwnerCartEntry: false,
owner: {
type: CommonConfigurator.OwnerType.PRODUCT,
id: PRODUCT_CODE,
configuratorType: ConfiguratorType.CPQ,
},
displayOnly: false,
forceReload: false,
resolveIssues: false,
};

let configuration: Configurator.Configuration =
ConfiguratorTestUtils.createConfiguration(
'a',
ConfiguratorModelUtils.createOwner(
CommonConfigurator.OwnerType.PRODUCT,
PRODUCT_CODE
)
);

class MockConfiguratorRouterExtractorService {
extractRouterData(): Observable<ConfiguratorRouter.Data> {
return of(mockRouterData);
}
}

class MockConfiguratorCommonsService {
getConfiguration(): Observable<Configurator.Configuration> {
return of(configuration);
}
}

class MockRoutingService {
go() {}
}

class MockProductService {
get(): Observable<Product> {
return of(PRODUCT);
}
}

class MockBreakpointService {
isUp() {}
isDown() {}
}

describe('ConfiguratorExitButton', () => {
let component: ConfiguratorExitButtonComponent;
let fixture: ComponentFixture<ConfiguratorExitButtonComponent>;
let htmlElem: HTMLElement;
let routingService: RoutingService;
let breakpointService: BreakpointService;

beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [I18nTestingModule, ReactiveFormsModule, NgSelectModule],
declarations: [ConfiguratorExitButtonComponent],
providers: [
{
provide: ConfiguratorRouterExtractorService,
useClass: MockConfiguratorRouterExtractorService,
},
{
provide: ConfiguratorCommonsService,
useClass: MockConfiguratorCommonsService,
},
{
provide: ProductService,
useClass: MockProductService,
},
{
provide: RoutingService,
useClass: MockRoutingService,
},
{
provide: BreakpointService,
useClass: MockBreakpointService,
},
],
});
})
);

beforeEach(() => {
fixture = TestBed.createComponent(ConfiguratorExitButtonComponent);
component = fixture.componentInstance;
routingService = TestBed.inject(RoutingService as Type<RoutingService>);
breakpointService = TestBed.inject(
BreakpointService as Type<BreakpointService>
);
htmlElem = fixture.nativeElement;
fixture.detectChanges();
});

it('should create component', () => {
expect(component).toBeDefined();
});

describe('exit a configuration', () => {
it('should navigate to product detail page', () => {
spyOn(routingService, 'go').and.callThrough();
component.exitConfiguration();
expect(routingService.go).toHaveBeenCalledWith({
cxRoute: 'product',
params: PRODUCT,
});
});
});

describe('rendering tests', () => {
it('should render short text in mobile mode', () => {
spyOn(breakpointService, 'isDown').and.returnValue(of(true));
spyOn(breakpointService, 'isUp').and.returnValue(of(false));
fixture.detectChanges();
CommonConfiguratorTestUtilsService.expectElementToContainText(
expect,
htmlElem,
'.cx-config-exit-button',
'configurator.button.exitMobile'
);
});

it('should render long text in desktop mode', () => {
spyOn(breakpointService, 'isUp').and.returnValue(of(true));
spyOn(breakpointService, 'isDown').and.returnValue(of(false));
fixture.detectChanges();
CommonConfiguratorTestUtilsService.expectElementToContainText(
expect,
htmlElem,
'.cx-config-exit-button',
'configurator.button.exit'
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Component } from '@angular/core';
import {
Product,
ProductService,
RoutingService,
WindowRef,
} from '@spartacus/core';
import { ConfiguratorRouterExtractorService } from '@spartacus/product-configurator/common';
import { BREAKPOINT, BreakpointService } from '@spartacus/storefront';
import { Observable } from 'rxjs';
import { filter, switchMap, take } from 'rxjs/operators';
import { ConfiguratorCommonsService } from '../../core/facade/configurator-commons.service';
import { Configurator } from '../../core/model/configurator.model';

@Component({
selector: 'cx-configurator-exit-button',
templateUrl: './configurator-exit-button.component.html',
})
export class ConfiguratorExitButtonComponent {
product$: Observable<Product>;

constructor(
protected productService: ProductService,
protected routingService: RoutingService,
protected configRouterExtractorService: ConfiguratorRouterExtractorService,
protected configuratorCommonsService: ConfiguratorCommonsService,
protected breakpointService: BreakpointService,
protected windowRef: WindowRef
) {}
/**
* Navigates to the product detail page of the product that is being configured.
*/
exitConfiguration() {
this.configRouterExtractorService
.extractRouterData()
.pipe(
switchMap((routerData) =>
this.configuratorCommonsService.getConfiguration(routerData.owner)
),
switchMap((configuration: Configurator.Configuration) =>
this.productService.get(
configuration.productCode ? configuration.productCode : ''
)
),
filter((product) => product !== undefined),
take(1)
)
.subscribe((product) =>
this.routingService.go({ cxRoute: 'product', params: product })
);
}

/**
* Verifies whether the current screen size equals or is larger than breakpoint `BREAKPOINT.md`.
*
* @returns {Observable<boolean>} - If the given breakpoint equals or is larger than`BREAKPOINT.md` returns `true`, otherwise `false`.
*/
isDesktop(): Observable<boolean> {
return this.breakpointService?.isUp(BREAKPOINT.md);
}

/**
* Verifies whether the current screen size equals or is smaller than breakpoint `BREAKPOINT.sm`.
*
* @returns {Observable<boolean>} - If the given breakpoint equals or is smaller than`BREAKPOINT.sm` returns `true`, otherwise `false`.
*/
isMobile(): Observable<boolean> {
return this.breakpointService?.isDown(BREAKPOINT.sm);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import {
CmsConfig,
I18nModule,
provideDefaultConfig,
WindowRef,
} from '@spartacus/core';
import { ConfiguratorExitButtonComponent } from './configurator-exit-button.component';

@NgModule({
imports: [CommonModule, I18nModule],
providers: [
provideDefaultConfig(<CmsConfig>{
cmsComponents: {
ConfiguratorExitButton: {
component: ConfiguratorExitButtonComponent,
},
},
}),
WindowRef,
],
declarations: [ConfiguratorExitButtonComponent],
exports: [ConfiguratorExitButtonComponent],
})
export class ConfiguratorExitButtonModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './configurator-exit-button.component';
export * from './configurator-exit-button.module';
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from './show-more/index';
export * from './tab-bar/index';
export * from './update-message/index';
export * from './configurator-conflict-and-error-messages/index';
export * from './exit-button/index';
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ConfiguratorProductTitleModule } from './product-title/configurator-pro
import { ConfiguratorTabBarModule } from './tab-bar/configurator-tab-bar.module';
import { ConfiguratorUpdateMessageModule } from './update-message/configurator-update-message.module';
import { ConfiguratorConflictAndErrorMessagesModule } from './configurator-conflict-and-error-messages/configurator-conflict-and-error-messages.module';
import { ConfiguratorExitButtonModule } from './exit-button/configurator-exit-button.module';

@NgModule({
imports: [
Expand All @@ -28,6 +29,7 @@ import { ConfiguratorConflictAndErrorMessagesModule } from './configurator-confl
ConfiguratorOverviewFormModule,
ConfiguratorOverviewNotificationBannerModule,
ConfiguratorConflictAndErrorMessagesModule,
ConfiguratorExitButtonModule,
],
})
export class RulebasedConfiguratorComponentsModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { CpqConfiguratorPageLayoutHandler } from './cpq-configurator-page-layout
* Contains the layout configuration for the CPQ configurator pages. This configuration is
* optional as of version 4.2, and reduces the components that are rendered in the header section.
* It needs to be explicitly imported, otherwise the default configuration
* from VariantConfiguratorInteractiveModule is active
* from CpqConfiguratorInteractiveModule is active
*/
@NgModule({
providers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const cmsComponents: string[] = [
'ConfiguratorPriceSummary',
'ConfiguratorProductTitle',
'ConfiguratorTabBar',
'ConfiguratorExitButton',
'CpqConfiguratorConflictAndErrorMessagesComponent',
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,20 @@ import { LayoutConfig } from '@spartacus/storefront';
VariantConfigurationTemplate: {
header: {
md: {
slots: ['PreHeader', 'SiteLogo', 'MiniCart'],
slots: [
'PreHeader',
'SiteLogo',
'VariantConfigExitButton',
'MiniCart',
],
},
xs: {
slots: ['PreHeader', 'SiteLogo', 'MiniCart'],
slots: [
'PreHeader',
'SiteLogo',
'VariantConfigExitButton',
'MiniCart',
],
},
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@ import { LayoutConfig } from '@spartacus/storefront';
VariantConfigurationOverviewTemplate: {
header: {
md: {
slots: ['SiteLogo', 'MiniCart'],
slots: [
'SiteLogo',
'VariantConfigOverviewExitButton',
'MiniCart',
],
},
xs: {
slots: ['SiteLogo', 'MiniCart'],
slots: [
'SiteLogo',
'VariantConfigOverviewExitButton',
'MiniCart',
],
},
},
slots: [
Expand Down
Loading

0 comments on commit 0932e90

Please sign in to comment.