Skip to content

Commit

Permalink
feat: OCI punchout configuration (#1431)
Browse files Browse the repository at this point in the history
- add a new page for the oci punchout configuration
- configuration items, placeholders and available formatters are provided by the ICM server
- the punchout administrator can navigate to the configuration page from the punchout oci tab

---------

Co-authored-by: Dilara Gueler <D.Gueler@intershop.de>
Co-authored-by: Susanne Schneider <s.schneider@intershop.de>
Co-authored-by: Marcel Eisentraut <meisentraut@intershop.de>
Co-authored-by: Silke Grüber <SGrueber@intershop.com>
  • Loading branch information
5 people committed Jun 2, 2023
1 parent a8f0817 commit dfc19ac
Show file tree
Hide file tree
Showing 48 changed files with 1,580 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Expand Up @@ -219,7 +219,7 @@
"message": "use star imports only for aggregation of deeper lying imports"
},
{
"importNamePattern": "^(?!(range|uniq|memoize|once|groupBy|countBy|flatten|isEqual|intersection|pick|differenceBy|unionBy|mergeWith)$).*",
"importNamePattern": "^(?!(range|uniq|memoize|once|groupBy|countBy|flatten|isEqual|intersection|pick|differenceBy|unionBy|mergeWith|snakeCase|capitalize)$).*",
"name": "lodash.*",
"filePattern": "^.*/src/app/(?!.*\\.spec\\.ts$).*\\.ts$",
"message": "importing this operator from lodash is forbidden"
Expand Down
1 change: 1 addition & 0 deletions .vscode/intershop.txt
Expand Up @@ -28,6 +28,7 @@ crosssells
cxml
cybersource
datepicker
decamelize
directdebit
dockerignore
dockerized
Expand Down
4 changes: 3 additions & 1 deletion src/app/core/configuration.module.ts
Expand Up @@ -6,16 +6,18 @@ import { dataRequestErrorHandler } from './utils/http-error/data-request.error-h
import { editPasswordErrorHandler } from './utils/http-error/edit-password.error-handler';
import { LoginUserErrorHandler } from './utils/http-error/login-user.error-handler';
import { requestReminderErrorHandler } from './utils/http-error/request-reminder.error-handler';
import { updateOciConfigurationErrorHandler } from './utils/http-error/update-oci-configuration.error-handler';
import { updatePasswordErrorHandler } from './utils/http-error/update-password.error-handler';

@NgModule({
providers: [
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: updatePasswordErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useClass: LoginUserErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: requestReminderErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: dataRequestErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: editPasswordErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: createPaymentErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: updatePasswordErrorHandler, multi: true },
{ provide: SPECIAL_HTTP_ERROR_HANDLER, useValue: updateOciConfigurationErrorHandler, multi: true },
],
})
export class ConfigurationModule {}
2 changes: 2 additions & 0 deletions src/app/core/icon.module.ts
Expand Up @@ -8,6 +8,7 @@ import {
faAngleRight,
faAngleUp,
faArrowAltCircleRight,
faArrowRight,
faArrowsAlt,
faBalanceScale,
faBan,
Expand Down Expand Up @@ -67,6 +68,7 @@ export class IconModule {
faAngleRight,
faAngleUp,
faArrowAltCircleRight,
faArrowRight,
faArrowsAlt,
faBalanceScale,
faBan,
Expand Down
14 changes: 13 additions & 1 deletion src/app/core/utils/functions.spec.ts
@@ -1,4 +1,4 @@
import { arraySlices, isArrayEqual, mergeDeep, omit, parseTimeToSeconds } from './functions';
import { arraySlices, decamelizeString, isArrayEqual, mergeDeep, omit, parseTimeToSeconds } from './functions';

describe('Functions', () => {
describe('arraySlices', () => {
Expand Down Expand Up @@ -228,4 +228,16 @@ describe('Functions', () => {
expect(() => parseTimeToSeconds('asdf')).toThrowErrorMatchingInlineSnapshot(`"Cannot parse "asdf" as time."`);
});
});

describe('decamelizeString', () => {
it.each([
[undefined, undefined],
['', ''],
['A', 'A'],
['A', 'a'],
['Aaa Bbb', 'aaaBbb'],
])(`should return '%s' when decamelize '%p'`, (result, str) => {
expect(decamelizeString(str)).toEqual(result);
});
});
});
14 changes: 13 additions & 1 deletion src/app/core/utils/functions.ts
@@ -1,4 +1,4 @@
import { range } from 'lodash-es';
import { capitalize, range, snakeCase } from 'lodash-es';
import { Observable, isObservable, of } from 'rxjs';

/**
Expand Down Expand Up @@ -70,3 +70,15 @@ export function parseTimeToSeconds(timeString: string): number {
const [, time, unit] = match;
return +time * (unit === 'd' ? 24 * 60 * 60 : unit === 'h' ? 60 * 60 : unit === 'm' ? 60 : 1);
}

/**
* If a camelized string is given the string is returned as separate capitalized words, e.g. 'lowerCase' will be 'Lower Case'
*/
export function decamelizeString(str: string): string {
return str
? snakeCase(str)
.split('_')
.map(part => capitalize(part))
.join(' ')
: str;
}
@@ -0,0 +1,10 @@
import { SpecialHttpErrorHandler } from 'ish-core/interceptors/icm-error-mapper.interceptor';

export const updateOciConfigurationErrorHandler: SpecialHttpErrorHandler = {
test: (error, request) => error.url.endsWith('/oci5/configurations') && request.method === 'PUT',
map: error => {
if (error.status === 400) {
return { message: error.error };
}
},
};
35 changes: 34 additions & 1 deletion src/app/extensions/punchout/facades/punchout.facade.ts
Expand Up @@ -5,9 +5,19 @@ import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';

import { HttpError } from 'ish-core/models/http-error/http-error.model';
import { selectRouteParam } from 'ish-core/store/core/router';
import { decamelizeString } from 'ish-core/utils/functions';
import { whenTruthy } from 'ish-core/utils/operators';

import { OciConfigurationItem } from '../models/oci-configuration-item/oci-configuration-item.model';
import { PunchoutType, PunchoutUser } from '../models/punchout-user/punchout-user.model';
import {
getOciConfiguration,
getOciConfigurationError,
getOciConfigurationLoading,
getOciFormatters,
getOciPlaceholders,
ociConfigurationActions,
} from '../store/oci-configuration';
import { transferPunchoutBasket } from '../store/punchout-functions';
import { getPunchoutTypes, getPunchoutTypesError, getPunchoutTypesLoading } from '../store/punchout-types';
import {
Expand All @@ -32,7 +42,6 @@ export class PunchoutFacade {

punchoutError$ = this.store.pipe(select(getPunchoutError));
punchoutTypesError$: Observable<HttpError> = this.store.pipe(select(getPunchoutTypesError));

supportedPunchoutTypes$: Observable<PunchoutType[]> = this.store.pipe(select(getPunchoutTypes));

selectedPunchoutType$ = combineLatest([
Expand Down Expand Up @@ -68,4 +77,28 @@ export class PunchoutFacade {
transferBasket() {
this.store.dispatch(transferPunchoutBasket());
}

ociConfiguration$() {
this.store.dispatch(ociConfigurationActions.loadOciOptionsAndConfiguration());
return this.store.pipe(select(getOciConfiguration));
}
ociConfigurationLoading$ = this.store.pipe(select(getOciConfigurationLoading));
ociConfigurationError$ = this.store.pipe(select(getOciConfigurationError));
ociFormatterSelectOptions$ = this.store.pipe(
select(getOciFormatters),
whenTruthy(),
map(formatters => formatters.map(f => ({ label: decamelizeString(f), value: f }))),
map(options => {
options.push({
value: '',
label: 'account.punchout.configuration.option.none.label',
});
return options;
})
);
ociPlaceholders$ = this.store.pipe(select(getOciPlaceholders));

updateOciConfiguration(configuration: OciConfigurationItem[]) {
this.store.dispatch(ociConfigurationActions.updateOciConfiguration({ configuration }));
}
}
@@ -0,0 +1,9 @@
export interface OciConfigurationItem {
field: string;
transform: string;
formatter: string;
mappings?: {
mapFromValue: string;
mapToValue: string;
}[];
}
@@ -0,0 +1,4 @@
export interface OciOptionsData {
availableFormatters: { id: string }[];
availablePlaceholders: { id: string }[];
}
@@ -0,0 +1,27 @@
import { OciOptionsData } from './oci-options.interface';
import { OciOptionsMapper } from './oci-options.mapper';

describe('Oci Options Mapper', () => {
describe('fromData', () => {
it(`should return Oci Options when getting OciOptionsData`, () => {
const optionsData = {
availableFormatters: [{ id: 'LowerCase' }, { id: 'UpperCase' }],
availablePlaceholders: [{ id: 'Currency' }, { id: 'Price' }],
} as OciOptionsData;
const options = OciOptionsMapper.fromData(optionsData);

expect(options).toMatchInlineSnapshot(`
{
"availableFormatters": [
"LowerCase",
"UpperCase",
],
"availablePlaceholders": [
"Currency",
"Price",
],
}
`);
});
});
});
@@ -0,0 +1,13 @@
import { OciOptionsData } from './oci-options.interface';
import { OciOptions } from './oci-options.model';

export class OciOptionsMapper {
static fromData(payload: OciOptionsData): OciOptions {
const { availableFormatters, availablePlaceholders } = payload;

return {
availableFormatters: availableFormatters?.map(formatter => formatter.id),
availablePlaceholders: availablePlaceholders?.map(placeholder => placeholder.id),
};
}
}
@@ -0,0 +1,4 @@
export interface OciOptions {
availableFormatters: string[];
availablePlaceholders: string[];
}
@@ -0,0 +1,5 @@
<h1>{{ 'account.punchout.configuration.heading' | translate }}</h1>

<p>{{ 'account.punchout.configuration.description' | translate }}</p>

<ish-oci-configuration-form></ish-oci-configuration-form>
@@ -0,0 +1,31 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { MockComponent } from 'ng-mocks';

import { AccountPunchoutConfigurationPageComponent } from './account-punchout-configuration-page.component';
import { OciConfigurationFormComponent } from './oci-configuration-form/oci-configuration-form.component';

describe('Account Punchout Configuration Page Component', () => {
let component: AccountPunchoutConfigurationPageComponent;
let fixture: ComponentFixture<AccountPunchoutConfigurationPageComponent>;
let element: HTMLElement;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [AccountPunchoutConfigurationPageComponent, MockComponent(OciConfigurationFormComponent)],
}).compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(AccountPunchoutConfigurationPageComponent);
component = fixture.componentInstance;
element = fixture.nativeElement;
});

it('should be created', () => {
expect(component).toBeTruthy();
expect(element).toBeTruthy();
expect(() => fixture.detectChanges()).not.toThrow();
});
});
@@ -0,0 +1,8 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';

@Component({
selector: 'ish-account-punchout-configuration-page',
templateUrl: './account-punchout-configuration-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccountPunchoutConfigurationPageComponent {}
@@ -0,0 +1,60 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ConfigOption, FormlyModule } from '@ngx-formly/core';

import { PunchoutModule } from '../../punchout.module';

import { AccountPunchoutConfigurationPageComponent } from './account-punchout-configuration-page.component';
import { OciConfigurationMappingRepeatFieldComponent } from './formly/oci-configuration-mapping-repeat-field/oci-configuration-mapping-repeat-field.component';
import { OciConfigurationMappingWrapperComponent } from './formly/oci-configuration-mapping-wrapper/oci-configuration-mapping-wrapper.component';
import { OciConfigurationRepeatFieldComponent } from './formly/oci-configuration-repeat-field/oci-configuration-repeat-field.component';
import { OciConfigurationFormComponent } from './oci-configuration-form/oci-configuration-form.component';

const accountPunchoutConfigurationPageRoutes: Routes = [
{
path: '',
data: {
breadcrumbData: [
{ key: 'account.punchout.link', link: '/account/punchout' },
{ key: 'account.punchout.configuration.link' },
],
},
component: AccountPunchoutConfigurationPageComponent,
},
];

const ociConfigurationFormlyConfig: ConfigOption = {
types: [
{
name: 'repeat-oci-config',
component: OciConfigurationRepeatFieldComponent,
},
{
name: 'repeat-oci-configuration-mapping',
component: OciConfigurationMappingRepeatFieldComponent,
},
],
wrappers: [
{
name: 'oci-configuration-mapping-wrapper',
component: OciConfigurationMappingWrapperComponent,
},
],
};

@NgModule({
imports: [
FormlyModule.forChild(ociConfigurationFormlyConfig),
RouterModule.forChild(accountPunchoutConfigurationPageRoutes),
PunchoutModule,
],

declarations: [
AccountPunchoutConfigurationPageComponent,
OciConfigurationFormComponent,
OciConfigurationMappingRepeatFieldComponent,
OciConfigurationMappingWrapperComponent,
OciConfigurationRepeatFieldComponent,
],
})
export class AccountPunchoutConfigurationPageModule {}
@@ -0,0 +1,6 @@
<span *ngFor="let field of field?.fieldGroup; let i = index" [attr.data-testing-id]="'oci-config-mapping-line-' + i">
<formly-field [field]="field"></formly-field>
</span>
<a [routerLink]="[]" (click)="addRow()" data-testing-id="add-config-line">{{
'account.punchout.configuration.form.add_row.link' | translate
}}</a>

0 comments on commit dfc19ac

Please sign in to comment.