Skip to content

Commit

Permalink
feat: read-only view for textfield configurator (#13963)
Browse files Browse the repository at this point in the history
Introduces a read-only mode for the textfield (template) configurator page. Includes the routes, OCC, state, facade and component layer. Allows to view a configuration attached to an order entry in order history.


Closes #13939
  • Loading branch information
ChristophHi authored Oct 8, 2021
1 parent b8af7ca commit 19848ed
Show file tree
Hide file tree
Showing 29 changed files with 543 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ export class CommonConfiguratorUtilsService {
decomposeOwnerId(ownerId: string): any {
const parts: string[] = ownerId.split('+');
if (parts.length !== 2) {
throw new Error('We only expect 2 parts in ownerId, separated by +');
throw new Error(
'We only expect 2 parts in ownerId, separated by +, but was: ' + ownerId
);
}
const result = { documentId: parts[0], entryNumber: parts[1] };
return result;
Expand Down
3 changes: 2 additions & 1 deletion feature-libs/product-configurator/textfield/_index.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
@import 'styles/index';

$configurator-textfield-components: cx-configurator-textfield-input-field,
cx-configurator-textfield-form, cx-configurator-textfield-add-to-cart-button !default;
cx-configurator-textfield-input-field-readonly, cx-configurator-textfield-form,
cx-configurator-textfield-add-to-cart-button !default;

$configurator-textfield-pages: TextfieldConfigurationTemplate !default;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
<ng-container *ngIf="configuration$ | async as configuration">
<div
class="cx-attribute"
*ngFor="let attribute of configuration.configurationInfos"
>
<cx-configurator-textfield-input-field
[attribute]="attribute"
(inputChange)="updateConfiguration($event)"
></cx-configurator-textfield-input-field>
</div>
<cx-configurator-textfield-add-to-cart-button
[configuration]="configuration"
></cx-configurator-textfield-add-to-cart-button>
<ng-container *ngIf="isEditable$ | async as isEditable; else readonly">
<div
class="cx-attribute"
*ngFor="let attribute of configuration.configurationInfos"
>
<cx-configurator-textfield-input-field
[attribute]="attribute"
(inputChange)="updateConfiguration($event)"
></cx-configurator-textfield-input-field>
</div>

<cx-configurator-textfield-add-to-cart-button
[configuration]="configuration"
></cx-configurator-textfield-add-to-cart-button>
</ng-container>
<ng-template #readonly>
<div
class="cx-attribute"
*ngFor="let attribute of configuration.configurationInfos"
>
<cx-configurator-textfield-input-field-readonly
[attribute]="attribute"
></cx-configurator-textfield-input-field-readonly>
</div>
</ng-template>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ import { ConfiguratorTextfieldFormComponent } from './configurator-textfield-for

const PRODUCT_CODE = 'CONF_LAPTOP';
const CART_ENTRY_KEY = '3';
const ORDER_ENTRY_KEY = '00100/3';
const ATTRIBUTE_NAME = 'AttributeName';
const ROUTE_CONFIGURATION = 'configureTEXTFIELD';
const ROUTE_CONFIGURATION_OVERVIEW = 'configureOverviewTEXTFIELD';
const mockRouterState: any = {
state: {
params: {
Expand Down Expand Up @@ -57,6 +59,11 @@ class MockConfiguratorTextfieldService {
p: productConfig,
});
}
readConfigurationForOrderEntry(): Observable<ConfiguratorTextfield.Configuration> {
return cold('-p', {
p: productConfig,
});
}
}
describe('TextfieldFormComponent', () => {
let component: ConfiguratorTextfieldFormComponent;
Expand Down Expand Up @@ -133,9 +140,55 @@ describe('TextfieldFormComponent', () => {
);
});

it('should know textfield configuration after init when starting from order entry', () => {
mockRouterState.state = {
params: {
ownerType: CommonConfigurator.OwnerType.ORDER_ENTRY,
entityKey: ORDER_ENTRY_KEY,
},
semanticRoute: ROUTE_CONFIGURATION,
};

expect(component.configuration$).toBeObservable(
cold('--p', {
p: productConfig,
})
);
});

it('should call update configuration on facade in case it was triggered on component', () => {
spyOn(textfieldService, 'updateConfiguration').and.callThrough();
component.updateConfiguration(productConfig.configurationInfos[0]);
expect(textfieldService.updateConfiguration).toHaveBeenCalledTimes(1);
});

it('should detect that content is editable in case route refers to configuration', () => {
mockRouterState.state = {
params: {
ownerType: CommonConfigurator.OwnerType.PRODUCT,
entityKey: PRODUCT_CODE,
},
semanticRoute: ROUTE_CONFIGURATION,
};
expect(component.isEditable$).toBeObservable(
cold('-b', {
b: true,
})
);
});

it('should detect that content is read-only in case route refers to configuration overview', () => {
mockRouterState.state = {
params: {
ownerType: CommonConfigurator.OwnerType.PRODUCT,
entityKey: PRODUCT_CODE,
},
semanticRoute: ROUTE_CONFIGURATION_OVERVIEW,
};
expect(component.isEditable$).toBeObservable(
cold('-b', {
b: false,
})
);
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Component } from '@angular/core';
import {
CommonConfigurator,
ConfiguratorRouter,
ConfiguratorRouterExtractorService,
} from '@spartacus/product-configurator/common';
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { map, switchMap } from 'rxjs/operators';
import { ConfiguratorTextfieldService } from '../../core/facade/configurator-textfield.service';
import { ConfiguratorTextfield } from '../../core/model/configurator-textfield.model';

Expand All @@ -26,11 +27,22 @@ export class ConfiguratorTextfieldFormComponent {
routerData.owner
);
case CommonConfigurator.OwnerType.ORDER_ENTRY:
throw new Error('Order history integration not yet implemented');
return this.configuratorTextfieldService.readConfigurationForOrderEntry(
routerData.owner
);
}
})
);

isEditable$: Observable<boolean> = this.configRouterExtractorService
.extractRouterData()
.pipe(
map(
(routerData) =>
routerData.pageType === ConfiguratorRouter.PageType.CONFIGURATION
)
);

constructor(
protected configuratorTextfieldService: ConfiguratorTextfieldService,
protected configRouterExtractorService: ConfiguratorRouterExtractorService
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './add-to-cart-button/configurator-textfield-add-to-cart-button.component';
export * from './form/configurator-textfield-form.component';
export * from './input-field/configurator-textfield-input-field.component';
export * from './input-field-readonly/configurator-textfield-input-field-readonly.component';
export * from './textfield-configurator-components.module';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<label
id="{{ getIdLabel(attribute) }}"
[attr.aria-label]="attribute.configurationLabel"
>{{ attribute.configurationLabel }}</label
>
<div attr.aria-labelledby="{{ getIdLabel(attribute) }}">
{{ attribute.configurationValue }}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { ChangeDetectionStrategy } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';

import { ConfiguratorTextfieldInputFieldReadonlyComponent } from './configurator-textfield-input-field-readonly.component';

describe('TextfieldInputFieldReadonlyComponent', () => {
let component: ConfiguratorTextfieldInputFieldReadonlyComponent;
let htmlElem: HTMLElement;
let fixture: ComponentFixture<ConfiguratorTextfieldInputFieldReadonlyComponent>;

beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ConfiguratorTextfieldInputFieldReadonlyComponent],
})
.overrideComponent(ConfiguratorTextfieldInputFieldReadonlyComponent, {
set: {
changeDetection: ChangeDetectionStrategy.Default,
},
})
.compileComponents();
})
);

beforeEach(() => {
fixture = TestBed.createComponent(
ConfiguratorTextfieldInputFieldReadonlyComponent
);
component = fixture.componentInstance;
component.attribute = {
configurationLabel: 'attributeName',
configurationValue: 'input123',
};
fixture.detectChanges();
htmlElem = fixture.nativeElement;
});

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

it('should render label', () => {
const idLabel = component.getIdLabel(component.attribute);
const elementsLabel = htmlElem.querySelectorAll('#' + idLabel);
expect(elementsLabel.length).toBe(1);
const elementLabel = elementsLabel[0];
expect(elementLabel.innerHTML).toBe(component.attribute.configurationLabel);
});

it('should render value', () => {
const elementsDiv = htmlElem.querySelectorAll('div');
expect(elementsDiv.length).toBe(1);
const elementDiv = elementsDiv[0];
expect(elementDiv.innerHTML).toContain(
component.attribute.configurationValue
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

import { ConfiguratorTextfield } from '../../core/model/configurator-textfield.model';

@Component({
selector: 'cx-configurator-textfield-input-field-readonly',
templateUrl: './configurator-textfield-input-field-readonly.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfiguratorTextfieldInputFieldReadonlyComponent {
PREFIX_TEXTFIELD = 'cx-configurator-textfield';

@Input() attribute: ConfiguratorTextfield.ConfigurationInfo;

/**
* Compiles an ID for the attribute label by using the label from the backend and a prefix 'label'
* @param {ConfiguratorTextfield.ConfigurationInfo} attribute Textfield configurator attribute. Carries the attribute label information from the backend
* @returns {string} ID
*/
getIdLabel(attribute: ConfiguratorTextfield.ConfigurationInfo): string {
return (
this.PREFIX_TEXTFIELD + 'label' + this.getLabelForIdGeneration(attribute)
);
}

protected getLabelForIdGeneration(
attribute: ConfiguratorTextfield.ConfigurationInfo
): string {
//replace white spaces with an empty string
return attribute.configurationLabel.replace(/\s/g, '');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { ConfiguratorTextfieldAddToCartButtonComponent } from './add-to-cart-button/configurator-textfield-add-to-cart-button.component';
import { ConfiguratorTextfieldFormComponent } from './form/configurator-textfield-form.component';
import { ConfiguratorTextfieldInputFieldComponent } from './input-field/configurator-textfield-input-field.component';
import { ConfiguratorTextfieldInputFieldReadonlyComponent } from './input-field-readonly/configurator-textfield-input-field-readonly.component';

@NgModule({
imports: [
Expand All @@ -35,11 +36,13 @@ import { ConfiguratorTextfieldInputFieldComponent } from './input-field/configur
declarations: [
ConfiguratorTextfieldFormComponent,
ConfiguratorTextfieldInputFieldComponent,
ConfiguratorTextfieldInputFieldReadonlyComponent,
ConfiguratorTextfieldAddToCartButtonComponent,
],
exports: [
ConfiguratorTextfieldFormComponent,
ConfiguratorTextfieldInputFieldComponent,
ConfiguratorTextfieldInputFieldReadonlyComponent,
ConfiguratorTextfieldAddToCartButtonComponent,
],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ export abstract class ConfiguratorTextfieldAdapter {
parameters: CommonConfigurator.ReadConfigurationFromCartEntryParameters
): Observable<ConfiguratorTextfield.Configuration>;

/**
* Abstract method to read a configuration for an order entry
*
* @param {CommonConfigurator.ReadConfigurationFromOrderEntryParameters} parameters read from order entry parameters object
* @returns {Observable<ConfiguratorTextfield.Configuration>} Observable of configurations
*/
abstract readConfigurationForOrderEntry(
parameters: CommonConfigurator.ReadConfigurationFromOrderEntryParameters
): Observable<ConfiguratorTextfield.Configuration>;

/**
* Abstract method to update a configuration attached to a cart entry
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ class MockConfiguratorTextfieldAdapter implements ConfiguratorTextfieldAdapter {
(params: CommonConfigurator.ReadConfigurationFromCartEntryParameters) =>
of('readConfigurationForCartEntry' + params)
);
readConfigurationForOrderEntry = createSpy().and.callFake(
(params: CommonConfigurator.ReadConfigurationFromOrderEntryParameters) =>
of('readConfigurationForOrderEntry' + params)
);
}

describe('ConfiguratorTextfieldConnector', () => {
Expand Down Expand Up @@ -100,6 +104,23 @@ describe('ConfiguratorTextfieldConnector', () => {
expect(adapter.readConfigurationForCartEntry).toHaveBeenCalledWith(params);
});

it('should call adapter on readConfigurationForOrderEntry', () => {
const adapter = TestBed.inject(
ConfiguratorTextfieldAdapter as Type<ConfiguratorTextfieldAdapter>
);

const params: CommonConfigurator.ReadConfigurationFromOrderEntryParameters =
{
owner: ConfiguratorModelUtils.createInitialOwner(),
};
let result;
service
.readConfigurationForOrderEntry(params)
.subscribe((res) => (result = res));
expect(result).toBe('readConfigurationForOrderEntry' + params);
expect(adapter.readConfigurationForOrderEntry).toHaveBeenCalledWith(params);
});

it('should call adapter on addToCart', () => {
const adapter = TestBed.inject(
ConfiguratorTextfieldAdapter as Type<ConfiguratorTextfieldAdapter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ export class ConfiguratorTextfieldConnector {
): Observable<ConfiguratorTextfield.Configuration> {
return this.adapter.readConfigurationForCartEntry(parameters);
}
/**
* Reads an existing configuration for an order entry
* @param {CommonConfigurator.ReadConfigurationFromOrderEntryParameters} parameters Attributes needed to read a product configuration for an order entry
* @returns {Observable<ConfiguratorTextfield.Configuration>} Observable of product configurations
*/
readConfigurationForOrderEntry(
parameters: CommonConfigurator.ReadConfigurationFromOrderEntryParameters
): Observable<ConfiguratorTextfield.Configuration> {
return this.adapter.readConfigurationForOrderEntry(parameters);
}
/**
* Updates a configuration that is attached to a cart entry
* @param parameters Attributes needed to update a cart entries' configuration
Expand Down
Loading

0 comments on commit 19848ed

Please sign in to comment.