From 74bb9cffa7db430831b9b4f0e7f0353bc73bf8f1 Mon Sep 17 00:00:00 2001 From: Susanne Schneider Date: Tue, 15 Nov 2022 16:30:12 +0100 Subject: [PATCH] feat: support for cXML punchout self service configuration (#1683) Co-authored-by: Stefan Hauke Co-authored-by: Silke --- .../punchout-management.b2b.e2e-spec.ts | 1 + src/app/core/icon.module.ts | 4 + .../icm-error-mapper.interceptor.spec.ts | 56 ++++++++ .../icm-error-mapper.interceptor.ts | 24 ++++ .../punchout/facades/punchout.facade.ts | 24 ++++ .../cxml-configuration.interface.ts | 16 +++ .../cxml-configuration.mapper.spec.ts | 43 ++++++ .../cxml-configuration.mapper.ts | 25 ++++ .../cxml-configuration.model.ts | 9 ++ .../oci-configuration-form.component.html | 33 +++-- ...out-cxml-configuration-page.component.html | 13 ++ ...-cxml-configuration-page.component.spec.ts | 64 +++++++++ ...chout-cxml-configuration-page.component.ts | 25 ++++ ...punchout-cxml-configuration-page.module.ts | 36 +++++ .../cxml-configuration-form.component.html | 40 ++++++ .../cxml-configuration-form.component.spec.ts | 55 ++++++++ .../cxml-configuration-form.component.ts | 103 +++++++++++++++ .../cxml-help-text-wrapper.component.html | 10 ++ .../cxml-help-text-wrapper.component.scss | 5 + .../cxml-help-text-wrapper.component.spec.ts | 66 ++++++++++ .../cxml-help-text-wrapper.component.ts | 21 +++ .../account-punchout-page.component.html | 9 ++ .../account-punchout-page.component.spec.ts | 4 +- .../pages/punchout-account-routing.module.ts | 7 + .../punchout/punchout.service.spec.ts | 21 +++ .../services/punchout/punchout.service.ts | 94 ++++++++++--- .../cxml-configuration.actions.ts | 24 ++++ .../cxml-configuration.effects.spec.ts | 123 ++++++++++++++++++ .../cxml-configuration.effects.ts | 64 +++++++++ .../cxml-configuration.reducer.ts | 42 ++++++ .../cxml-configuration.selectors.spec.ts | 67 ++++++++++ .../cxml-configuration.selectors.ts | 11 ++ .../store/cxml-configuration/index.ts | 3 + .../punchout/store/punchout-store.module.ts | 11 +- .../punchout/store/punchout-store.ts | 2 + .../punchout-users.effects.spec.ts | 2 + .../punchout-users/punchout-users.effects.ts | 15 ++- src/assets/i18n/de_DE.json | 7 + src/assets/i18n/en_US.json | 13 +- src/assets/i18n/fr_FR.json | 9 +- 40 files changed, 1161 insertions(+), 40 deletions(-) create mode 100644 src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.interface.ts create mode 100644 src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.mapper.spec.ts create mode 100644 src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.mapper.ts create mode 100644 src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.model.ts create mode 100644 src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.html create mode 100644 src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.spec.ts create mode 100644 src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.ts create mode 100644 src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.module.ts create mode 100644 src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.html create mode 100644 src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.spec.ts create mode 100644 src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.ts create mode 100644 src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.html create mode 100644 src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.scss create mode 100644 src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.spec.ts create mode 100644 src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.ts create mode 100644 src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.actions.ts create mode 100644 src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.effects.spec.ts create mode 100644 src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.effects.ts create mode 100644 src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.reducer.ts create mode 100644 src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.selectors.spec.ts create mode 100644 src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.selectors.ts create mode 100644 src/app/extensions/punchout/store/cxml-configuration/index.ts diff --git a/e2e/cypress/e2e/specs/extras/punchout-management.b2b.e2e-spec.ts b/e2e/cypress/e2e/specs/extras/punchout-management.b2b.e2e-spec.ts index afdc54bce9..323a0a0010 100644 --- a/e2e/cypress/e2e/specs/extras/punchout-management.b2b.e2e-spec.ts +++ b/e2e/cypress/e2e/specs/extras/punchout-management.b2b.e2e-spec.ts @@ -74,6 +74,7 @@ describe('Punchout MyAccount Functionality', () => { page.submit(); }); at(PunchoutOverviewPage, page => { + page.selectOciTab(); page.userList.should('contain', `${_.punchoutUser2.login}`); page.userList.should('not.contain', `Inactive`); page.successMessage.message.should('contain', 'created'); diff --git a/src/app/core/icon.module.ts b/src/app/core/icon.module.ts index 67aeab8676..ccbc96a2c5 100644 --- a/src/app/core/icon.module.ts +++ b/src/app/core/icon.module.ts @@ -17,6 +17,8 @@ import { faCalendarDay, faCheck, faCheckCircle, + faChevronDown, + faChevronUp, faCog, faCogs, faEnvelope, @@ -77,6 +79,8 @@ export class IconModule { faCalendarDay, faCheck, faCheckCircle, + faChevronDown, + faChevronUp, faCog, faCogs, faEnvelope, diff --git a/src/app/core/interceptors/icm-error-mapper.interceptor.spec.ts b/src/app/core/interceptors/icm-error-mapper.interceptor.spec.ts index 66f67a35d2..cbff9bee20 100644 --- a/src/app/core/interceptors/icm-error-mapper.interceptor.spec.ts +++ b/src/app/core/interceptors/icm-error-mapper.interceptor.spec.ts @@ -133,6 +133,62 @@ describe('Icm Error Mapper Interceptor', () => { ); }); + it('should convert ICM errors format with cause (new ADR) to simplified format concatenating all causes', done => { + http.get('some').subscribe({ + next: fail, + error: error => { + expect(error).toMatchInlineSnapshot(` + { + "errors": [ + { + "causes": [ + { + "code": "intershop.cxml.punchout.unitmapping.value.invalid", + "message": "The value must be a tab-separated list of 'value1;value2' pairs.", + }, + { + "code": "intershop.cxml.punchout.punchout.locale.value.invalid", + "message": "The value must be two lowercase letters for language and two uppercase letters for region.", + }, + ], + "code": "intershop.cxml.punchout.configuration.error", + "level": "ERROR", + "status": "400", + }, + ], + "message": "
The value must be a tab-separated list of 'value1;value2' pairs.
The value must be two lowercase letters for language and two uppercase letters for region.
", + "name": "HttpErrorResponse", + "status": 422, + } + `); + done(); + }, + }); + + httpController.expectOne('some').flush( + { + messages: [ + { + causes: [ + { + code: 'intershop.cxml.punchout.unitmapping.value.invalid', + message: "The value must be a tab-separated list of 'value1;value2' pairs.", + }, + { + code: 'intershop.cxml.punchout.punchout.locale.value.invalid', + message: 'The value must be two lowercase letters for language and two uppercase letters for region.', + }, + ], + code: 'intershop.cxml.punchout.configuration.error', + level: 'ERROR', + status: '400', + }, + ], + }, + { status: 422, statusText: 'Unprocessable Entity' } + ); + }); + it('should convert ICM errors format to simplified format', done => { http.get('some').subscribe({ next: fail, diff --git a/src/app/core/interceptors/icm-error-mapper.interceptor.ts b/src/app/core/interceptors/icm-error-mapper.interceptor.ts index 079d389d10..5b75102901 100644 --- a/src/app/core/interceptors/icm-error-mapper.interceptor.ts +++ b/src/app/core/interceptors/icm-error-mapper.interceptor.ts @@ -89,6 +89,30 @@ export class ICMErrorMapperInterceptor implements HttpInterceptor { errors: httpError.error?.errors, }; } + // new ADR - used for cXML Punchout configuration + else if (httpError.error?.messages?.length) { + const errors: { + code: string; + causes?: { + code: string; + message: string; + }[]; + }[] = httpError.error?.messages; + if (errors.length === 1) { + const error = errors[0]; + if (error.causes?.length) { + return { + ...responseError, + errors: httpError.error.messages, + message: error.causes.map(c => '
'.concat(c.message).concat('
')).join(''), + }; + } + } + return { + ...responseError, + errors: httpError.error?.errors, + }; + } // handle all other error responses with error object return { diff --git a/src/app/extensions/punchout/facades/punchout.facade.ts b/src/app/extensions/punchout/facades/punchout.facade.ts index 1020853c5b..710d97210e 100644 --- a/src/app/extensions/punchout/facades/punchout.facade.ts +++ b/src/app/extensions/punchout/facades/punchout.facade.ts @@ -8,8 +8,15 @@ import { selectQueryParam } from 'ish-core/store/core/router'; import { decamelizeString } from 'ish-core/utils/functions'; import { whenTruthy } from 'ish-core/utils/operators'; +import { CxmlConfiguration } from '../models/cxml-configuration/cxml-configuration.model'; import { OciConfigurationItem } from '../models/oci-configuration-item/oci-configuration-item.model'; import { PunchoutType, PunchoutUser } from '../models/punchout-user/punchout-user.model'; +import { + cxmlConfigurationActions, + getCxmlConfiguration, + getCxmlConfigurationError, + getCxmlConfigurationLoading, +} from '../store/cxml-configuration'; import { getOciConfiguration, getOciConfigurationError, @@ -101,4 +108,21 @@ export class PunchoutFacade { updateOciConfiguration(configuration: OciConfigurationItem[]) { this.store.dispatch(ociConfigurationActions.updateOCIConfiguration({ configuration })); } + + cxmlConfiguration$() { + this.store.dispatch(cxmlConfigurationActions.loadCXMLConfiguration()); + return this.store.pipe(select(getCxmlConfiguration)); + } + + cxmlConfigurationLoading$ = this.store.pipe(select(getCxmlConfigurationLoading)); + + cxmlConfigurationError$ = this.store.pipe(select(getCxmlConfigurationError)); + + updateCxmlConfiguration(configuration: CxmlConfiguration[]) { + this.store.dispatch(cxmlConfigurationActions.updateCXMLConfiguration({ configuration })); + } + + resetCxmlConfiguration() { + this.store.dispatch(cxmlConfigurationActions.resetCXMLConfiguration()); + } } diff --git a/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.interface.ts b/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.interface.ts new file mode 100644 index 0000000000..985ac6e605 --- /dev/null +++ b/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.interface.ts @@ -0,0 +1,16 @@ +import { CxmlConfigurationInputType } from './cxml-configuration.model'; + +export interface CxmlConfigurationData { + data: { + name: string; + value: string; + }[]; + info?: { + metaData: { + name: string; + defaultValue?: string; + description?: string; + inputType?: CxmlConfigurationInputType; + }[]; + }; +} diff --git a/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.mapper.spec.ts b/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.mapper.spec.ts new file mode 100644 index 0000000000..f3933f0842 --- /dev/null +++ b/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.mapper.spec.ts @@ -0,0 +1,43 @@ +import { CxmlConfigurationData } from './cxml-configuration.interface'; +import { CxmlConfigurationMapper } from './cxml-configuration.mapper'; + +describe('Cxml Configuration Mapper', () => { + describe('fromData', () => { + it('should return Cxml Configuration when getting CxmlConfigurationData', () => { + expect(() => CxmlConfigurationMapper.fromData(undefined)).toThrow(); + }); + + it('should map incoming data to model data', () => { + const data: CxmlConfigurationData = { + data: [ + { + name: 'classificationCatalogID', + value: '1', + }, + ], + info: { + metaData: [ + { + name: 'classificationCatalogID', + defaultValue: 'eCl@ass', + description: 'Enter the type of product classification catalog to be used, e.g. UNSPSC or eCl@ass.', + inputType: 'text-short', + }, + ], + }, + }; + const mapped = CxmlConfigurationMapper.fromData(data); + expect(mapped).toMatchInlineSnapshot(` + [ + { + "defaultValue": "eCl@ass", + "description": "Enter the type of product classification catalog to be used, e.g. UNSPSC or eCl@ass.", + "inputType": "text-short", + "name": "classificationCatalogID", + "value": "1", + }, + ] + `); + }); + }); +}); diff --git a/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.mapper.ts b/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.mapper.ts new file mode 100644 index 0000000000..458c0196b5 --- /dev/null +++ b/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.mapper.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; + +import { CxmlConfigurationData } from './cxml-configuration.interface'; +import { CxmlConfiguration } from './cxml-configuration.model'; + +@Injectable({ providedIn: 'root' }) +export class CxmlConfigurationMapper { + static fromData(cxmlConfigurationData: CxmlConfigurationData): CxmlConfiguration[] { + if (cxmlConfigurationData) { + const { data, info } = cxmlConfigurationData; + + return data?.map(cxmlConfiguration => { + const infoElement = info?.metaData.find(e => e.name === cxmlConfiguration.name); + return { + ...cxmlConfiguration, + defaultValue: infoElement?.defaultValue, + description: infoElement?.description, + inputType: infoElement?.inputType, + }; + }); + } else { + throw new Error(`CxmlConfigurationData is required`); + } + } +} diff --git a/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.model.ts b/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.model.ts new file mode 100644 index 0000000000..061c4f2eec --- /dev/null +++ b/src/app/extensions/punchout/models/cxml-configuration/cxml-configuration.model.ts @@ -0,0 +1,9 @@ +export interface CxmlConfiguration { + name: string; + value: string; + defaultValue?: string; + description?: string; + inputType?: CxmlConfigurationInputType; +} + +export type CxmlConfigurationInputType = 'text-short' | 'text-long'; diff --git a/src/app/extensions/punchout/pages/account-punchout-configuration/oci-configuration-form/oci-configuration-form.component.html b/src/app/extensions/punchout/pages/account-punchout-configuration/oci-configuration-form/oci-configuration-form.component.html index 99b6818211..19a28a4cd9 100644 --- a/src/app/extensions/punchout/pages/account-punchout-configuration/oci-configuration-form/oci-configuration-form.component.html +++ b/src/app/extensions/punchout/pages/account-punchout-configuration/oci-configuration-form/oci-configuration-form.component.html @@ -51,18 +51,27 @@
-
- - {{ - 'account.cancel.link' | translate - }} + diff --git a/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.html b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.html new file mode 100644 index 0000000000..956b798090 --- /dev/null +++ b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.html @@ -0,0 +1,13 @@ + +

{{ 'account.punchout.configuration.heading' | translate }} - {{ user.login }}

+

{{ 'account.punchout.cxml.configuration.helptext' | translate }}

+ + + + + +

{{ 'account.punchout.cxml.configuration.no_configuration' | translate }}

+
+
+ diff --git a/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.spec.ts b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.spec.ts new file mode 100644 index 0000000000..5978e9beeb --- /dev/null +++ b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.spec.ts @@ -0,0 +1,64 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { instance, mock, when } from 'ts-mockito'; + +import { LoadingComponent } from 'ish-shared/components/common/loading/loading.component'; + +import { PunchoutFacade } from '../../facades/punchout.facade'; +import { CxmlConfiguration } from '../../models/cxml-configuration/cxml-configuration.model'; +import { PunchoutUser } from '../../models/punchout-user/punchout-user.model'; + +import { AccountPunchoutCxmlConfigurationPageComponent } from './account-punchout-cxml-configuration-page.component'; +import { CxmlConfigurationFormComponent } from './cxml-configuration-form/cxml-configuration-form.component'; + +describe('Account Punchout Cxml Configuration Page Component', () => { + let component: AccountPunchoutCxmlConfigurationPageComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let punchoutFacade: PunchoutFacade; + + beforeEach(async () => { + punchoutFacade = mock(PunchoutFacade); + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [ + AccountPunchoutCxmlConfigurationPageComponent, + MockComponent(CxmlConfigurationFormComponent), + MockComponent(LoadingComponent), + ], + providers: [{ provide: PunchoutFacade, useFactory: () => instance(punchoutFacade) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AccountPunchoutCxmlConfigurationPageComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + + const user = { + login: '1', + } as PunchoutUser; + const cxmlConfiguration = [{ name: 'test', value: 'test value' }] as CxmlConfiguration[]; + when(punchoutFacade.selectedPunchoutUser$).thenReturn(of(user)); + when(punchoutFacade.cxmlConfiguration$()).thenReturn(of(cxmlConfiguration)); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + it('should display the configuration form after creation', () => { + fixture.detectChanges(); + expect(element.querySelector('ish-cxml-configuration-form')).toBeTruthy(); + }); + + it('should display a loading overlay if the configuration is loading', () => { + when(punchoutFacade.cxmlConfigurationLoading$).thenReturn(of(true)); + fixture.detectChanges(); + expect(element.querySelector('ish-loading')).toBeTruthy(); + }); +}); diff --git a/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.ts b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.ts new file mode 100644 index 0000000000..cd34475fac --- /dev/null +++ b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.component.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { PunchoutFacade } from '../../facades/punchout.facade'; +import { CxmlConfiguration } from '../../models/cxml-configuration/cxml-configuration.model'; +import { PunchoutUser } from '../../models/punchout-user/punchout-user.model'; + +@Component({ + selector: 'ish-account-punchout-cxml-configuration-page', + templateUrl: './account-punchout-cxml-configuration-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AccountPunchoutCxmlConfigurationPageComponent implements OnInit { + selectedUser$: Observable; + cxmlConfiguration$: Observable; + loading$: Observable; + + constructor(private punchoutFacade: PunchoutFacade) {} + + ngOnInit() { + this.selectedUser$ = this.punchoutFacade.selectedPunchoutUser$; + this.cxmlConfiguration$ = this.punchoutFacade.cxmlConfiguration$(); + this.loading$ = this.punchoutFacade.cxmlConfigurationLoading$; + } +} diff --git a/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.module.ts b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.module.ts new file mode 100644 index 0000000000..18819f333d --- /dev/null +++ b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.module.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { FormlyModule as FormlyBaseModule } from '@ngx-formly/core'; + +import { PunchoutModule } from '../../punchout.module'; + +import { AccountPunchoutCxmlConfigurationPageComponent } from './account-punchout-cxml-configuration-page.component'; +import { CxmlConfigurationFormComponent } from './cxml-configuration-form/cxml-configuration-form.component'; +import { CxmlHelpTextWrapperComponent } from './formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component'; + +const accountPunchoutCxmlConfigurationPageRoutes: Routes = [ + { + path: '', + data: { + breadcrumbData: [ + { key: 'account.punchout.link', link: '/account/punchout' }, + { key: 'account.punchout.cxml.configuration.link' }, + ], + }, + component: AccountPunchoutCxmlConfigurationPageComponent, + }, +]; + +const wrapperComponents = [CxmlHelpTextWrapperComponent]; + +@NgModule({ + imports: [ + RouterModule.forChild(accountPunchoutCxmlConfigurationPageRoutes), + PunchoutModule, + FormlyBaseModule.forChild({ + wrappers: [{ name: 'cxml-help-text', component: CxmlHelpTextWrapperComponent }], + }), + ], + declarations: [AccountPunchoutCxmlConfigurationPageComponent, CxmlConfigurationFormComponent, ...wrapperComponents], +}) +export class AccountPunchoutCxmlConfigurationPageModule {} diff --git a/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.html b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.html new file mode 100644 index 0000000000..d8c39d99a1 --- /dev/null +++ b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.html @@ -0,0 +1,40 @@ + +
+
+
+ {{ 'account.punchout.configuration.form.heading.attribute' | translate }} +
+
+ {{ 'account.punchout.configuration.form.heading.transformed-attribute' | translate }} +
+
+ +
+
+ +
+ + +
+
diff --git a/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.spec.ts b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.spec.ts new file mode 100644 index 0000000000..35a9bc287a --- /dev/null +++ b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { FormlyForm } from '@ngx-formly/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; + +import { ErrorMessageComponent } from 'ish-shared/components/common/error-message/error-message.component'; +import { FormlyTestingModule } from 'ish-shared/formly/dev/testing/formly-testing.module'; + +import { PunchoutFacade } from '../../../facades/punchout.facade'; +import { CxmlConfiguration } from '../../../models/cxml-configuration/cxml-configuration.model'; + +import { CxmlConfigurationFormComponent } from './cxml-configuration-form.component'; + +describe('Cxml Configuration Form Component', () => { + let component: CxmlConfigurationFormComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let punchoutFacade: PunchoutFacade; + + beforeEach(async () => { + punchoutFacade = mock(PunchoutFacade); + await TestBed.configureTestingModule({ + imports: [FormlyTestingModule, RouterTestingModule, TranslateModule.forRoot()], + declarations: [CxmlConfigurationFormComponent, MockComponent(ErrorMessageComponent), MockComponent(FormlyForm)], + providers: [{ provide: PunchoutFacade, useFactory: () => instance(punchoutFacade) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CxmlConfigurationFormComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + component.cxmlConfiguration = [{ name: 'test', value: 'test value' }] as CxmlConfiguration[]; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + it('should display the configuration values after creation', () => { + fixture.detectChanges(); + expect(element.querySelector('formly-form')).toBeTruthy(); + }); + + it('should submit a form when the user applies the changes', () => { + when(punchoutFacade.updateCxmlConfiguration(anything())); + + component.submitForm(); + verify(punchoutFacade.updateCxmlConfiguration(anything())).once(); + }); +}); diff --git a/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.ts b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.ts new file mode 100644 index 0000000000..09b7b903fd --- /dev/null +++ b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/cxml-configuration-form/cxml-configuration-form.component.ts @@ -0,0 +1,103 @@ +/* eslint-disable unicorn/no-null */ +import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { FormlyFieldConfig } from '@ngx-formly/core'; +import { Observable } from 'rxjs'; + +import { HttpError } from 'ish-core/models/http-error/http-error.model'; +import { markAsDirtyRecursive } from 'ish-shared/forms/utils/form-utils'; + +import { PunchoutFacade } from '../../../facades/punchout.facade'; +import { CxmlConfiguration } from '../../../models/cxml-configuration/cxml-configuration.model'; + +@Component({ + selector: 'ish-cxml-configuration-form', + templateUrl: './cxml-configuration-form.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CxmlConfigurationFormComponent implements OnDestroy, OnInit { + @Input({ required: true }) cxmlConfiguration: CxmlConfiguration[]; + + form: FormGroup = new FormGroup({}); + model: { [key: string]: string }; + fields: FormlyFieldConfig[]; + + private submitted = false; + cxmlConfigurationError$: Observable; + + constructor(private punchoutFacade: PunchoutFacade) {} + + ngOnInit() { + this.cxmlConfigurationError$ = this.punchoutFacade.cxmlConfigurationError$; + this.fields = this.getFields(); + this.model = this.getModel(); + } + + ngOnDestroy(): void { + this.resetConfiguration(); + } + + private getFields() { + return this.cxmlConfiguration.map(cxmlConfig => ({ + fieldGroupClassName: 'row list-item-row mb-0', + fieldGroup: [ + { + key: cxmlConfig.name.replaceAll('.', ':'), + type: cxmlConfig.inputType === 'text-long' ? 'ish-textarea-field' : 'ish-text-input-field', + wrappers: ['cxml-help-text', 'form-field-horizontal', 'description'], + className: 'list-item col-md-12', + props: { + rows: 3, + label: cxmlConfig.name, + labelNoTranslate: true, + placeholder: cxmlConfig.defaultValue, + helpText: cxmlConfig.description, + customDescription: { + key: 'account.punchout.cxml.configuration.default.description', + args: { defaultValue: cxmlConfig.defaultValue }, + }, + fieldClass: 'col-sm-7 pl-md-0', + labelClass: 'col-sm-5 mr-sm-0 pl-5', + }, + }, + ], + })); + } + + private getModel() { + return this.cxmlConfiguration.reduce( + (acc, config) => ({ + ...acc, + [config.name.replaceAll('.', ':')]: config.value === config.defaultValue ? null : config.value, + }), + {} + ); + } + + submitForm() { + if (this.form.invalid) { + this.submitted = true; + markAsDirtyRecursive(this.form); + return; + } + + const updateData = Object.entries(this.form.value).map(([name, value]) => ({ + name: name.replaceAll(':', '.'), + value: value ? value : null, + })); + + this.updateConfiguration(updateData as CxmlConfiguration[]); + } + + get formDisabled() { + return this.form.invalid && this.submitted; + } + + private updateConfiguration(cxmlConfig: CxmlConfiguration[]) { + this.punchoutFacade.updateCxmlConfiguration(cxmlConfig); + } + + private resetConfiguration() { + this.punchoutFacade.resetCxmlConfiguration(); + } +} diff --git a/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.html b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.html new file mode 100644 index 0000000000..b7d8055a95 --- /dev/null +++ b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.html @@ -0,0 +1,10 @@ + + + + +
diff --git a/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.scss b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.scss new file mode 100644 index 0000000000..4212f2f612 --- /dev/null +++ b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.scss @@ -0,0 +1,5 @@ +fa-icon { + position: absolute; + top: 20px; + z-index: 3; +} diff --git a/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.spec.ts b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.spec.ts new file mode 100644 index 0000000000..d25fade27a --- /dev/null +++ b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.spec.ts @@ -0,0 +1,66 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroup } from '@angular/forms'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'; +import { FormlyModule } from '@ngx-formly/core'; +import { MockComponent, MockDirective } from 'ng-mocks'; + +import { ServerHtmlDirective } from 'ish-core/directives/server-html.directive'; +import { FormlyTestingComponentsModule } from 'ish-shared/formly/dev/testing/formly-testing-components.module'; +import { FormlyTestingContainerComponent } from 'ish-shared/formly/dev/testing/formly-testing-container/formly-testing-container.component'; +import { FormlyTestingExampleComponent } from 'ish-shared/formly/dev/testing/formly-testing-example/formly-testing-example.component'; + +import { CxmlHelpTextWrapperComponent } from './cxml-help-text-wrapper.component'; + +const fieldBase = { + key: 'example', + type: 'example', + wrappers: ['description'], + props: { helpText: 'test' }, +}; + +describe('Cxml Help Text Wrapper Component', () => { + let component: FormlyTestingContainerComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + FormlyModule.forRoot({ + types: [{ name: 'example', component: FormlyTestingExampleComponent }], + wrappers: [{ name: 'description', component: CxmlHelpTextWrapperComponent }], + }), + FormlyTestingComponentsModule, + ], + declarations: [ + CxmlHelpTextWrapperComponent, + MockComponent(FaIconComponent), + MockDirective(NgbCollapse), + MockDirective(ServerHtmlDirective), + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FormlyTestingContainerComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + + component.form = new FormGroup({}); + component.model = {}; + component.fields = [fieldBase]; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + expect(element.querySelector('ish-cxml-help-text-wrapper')).toBeTruthy(); + }); + + it('should render the help text link', () => { + fixture.detectChanges(); + expect(element.querySelector('.btn-link')).toBeTruthy(); + }); +}); diff --git a/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.ts b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.ts new file mode 100644 index 0000000000..a560509fd7 --- /dev/null +++ b/src/app/extensions/punchout/pages/account-punchout-cxml-configuration/formly/cxml-help-text-wrapper/cxml-help-text-wrapper.component.ts @@ -0,0 +1,21 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { FieldWrapper } from '@ngx-formly/core'; + +/** + * Wrapper that adds a help text under an cxml configuration value. The help text can be collapsed. + * + * @props **helpText** - used to define the help text. + */ +@Component({ + selector: 'ish-cxml-help-text-wrapper', + templateUrl: './cxml-help-text-wrapper.component.html', + styleUrls: ['./cxml-help-text-wrapper.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CxmlHelpTextWrapperComponent extends FieldWrapper { + isCollapsed = false; + + get helpText() { + return this.props.helpText; + } +} diff --git a/src/app/extensions/punchout/pages/account-punchout/account-punchout-page.component.html b/src/app/extensions/punchout/pages/account-punchout/account-punchout-page.component.html index 5cd44955b8..be6702c23c 100644 --- a/src/app/extensions/punchout/pages/account-punchout/account-punchout-page.component.html +++ b/src/app/extensions/punchout/pages/account-punchout/account-punchout-page.component.html @@ -31,6 +31,15 @@

+ + + { MockComponent(FaIconComponent), MockComponent(LoadingComponent), MockComponent(ModalDialogComponent), + MockPipe(ServerSettingPipe, path => path === 'punchout.cxmlUserConfigurationEnabled'), ], providers: [ { provide: AccountFacade, useFactory: () => instance(accountFacade) }, diff --git a/src/app/extensions/punchout/pages/punchout-account-routing.module.ts b/src/app/extensions/punchout/pages/punchout-account-routing.module.ts index 1ccb6e4608..9ddd491ab1 100644 --- a/src/app/extensions/punchout/pages/punchout-account-routing.module.ts +++ b/src/app/extensions/punchout/pages/punchout-account-routing.module.ts @@ -14,6 +14,13 @@ const routes: Routes = [ m => m.AccountPunchoutConfigurationPageModule ), }, + { + path: 'cxmlConfiguration/:PunchoutLogin', + loadChildren: () => + import('./account-punchout-cxml-configuration/account-punchout-cxml-configuration-page.module').then( + m => m.AccountPunchoutCxmlConfigurationPageModule + ), + }, { path: 'create', loadChildren: () => diff --git a/src/app/extensions/punchout/services/punchout/punchout.service.spec.ts b/src/app/extensions/punchout/services/punchout/punchout.service.spec.ts index 68d7d81a2e..0e74b1d9e4 100644 --- a/src/app/extensions/punchout/services/punchout/punchout.service.spec.ts +++ b/src/app/extensions/punchout/services/punchout/punchout.service.spec.ts @@ -29,6 +29,7 @@ describe('Punchout Service', () => { when(apiServiceMock.resolveLink(anything())).thenReturn(() => of({})); when(apiServiceMock.put(anything(), anything(), anything())).thenReturn(of({})); when(apiServiceMock.delete(anything(), anything())).thenReturn(of({})); + when(apiServiceMock.patch(anything(), anything(), anything())).thenReturn(of({})); when(apiServiceMock.encodeResourceId(anything())).thenCall(id => id); TestBed.configureTestingModule({ @@ -120,4 +121,24 @@ describe('Punchout Service', () => { done(); }); }); + + it("should get punchout configuration items when 'getCxmlConfiguration' is called", done => { + punchoutService.getCxmlConfiguration('user').subscribe(() => { + verify(apiServiceMock.get(anything(), anything())).once(); + expect(capture(apiServiceMock.get).last()[0]).toMatchInlineSnapshot( + `"customers/4711/punchouts/cxml1.2/users/user/configurations"` + ); + done(); + }); + }); + + it("should update cxml configuration items when 'updateCxmlConfiguration' is called", done => { + punchoutService.updateCxmlConfiguration([], 'testuser').subscribe(() => { + verify(apiServiceMock.patch(anything(), anything(), anything())).once(); + expect(capture(apiServiceMock.patch).last()[0]).toMatchInlineSnapshot( + `"customers/4711/punchouts/cxml1.2/users/testuser/configurations"` + ); + done(); + }); + }); }); diff --git a/src/app/extensions/punchout/services/punchout/punchout.service.ts b/src/app/extensions/punchout/services/punchout/punchout.service.ts index 9a291c4db0..13b8254e20 100644 --- a/src/app/extensions/punchout/services/punchout/punchout.service.ts +++ b/src/app/extensions/punchout/services/punchout/punchout.service.ts @@ -15,6 +15,9 @@ import { CookiesService } from 'ish-core/utils/cookies/cookies.service'; import { DomService } from 'ish-core/utils/dom/dom.service'; import { whenTruthy } from 'ish-core/utils/operators'; +import { CxmlConfigurationData } from '../../models/cxml-configuration/cxml-configuration.interface'; +import { CxmlConfigurationMapper } from '../../models/cxml-configuration/cxml-configuration.mapper'; +import { CxmlConfiguration } from '../../models/cxml-configuration/cxml-configuration.model'; import { OciConfigurationItem } from '../../models/oci-configuration-item/oci-configuration-item.model'; import { OciOptionsData } from '../../models/oci-options/oci-options.interface'; import { OciOptionsMapper } from '../../models/oci-options/oci-options.mapper'; @@ -37,9 +40,12 @@ export class PunchoutService { /** * http header for Punchout API v2 */ - private punchoutHeaders = new HttpHeaders({ - Accept: 'application/vnd.intershop.punchout.v2+json', - }); + private punchoutHeaderV2 = new HttpHeaders({ Accept: 'application/vnd.intershop.punchout.v2+json' }); + + /** + * http header for cXML Punchout API v3 + */ + private punchoutHeaderV3 = new HttpHeaders({ Accept: 'application/vnd.intershop.punchout.cxml.v3+json' }); private getResourceType(punchoutType: PunchoutType): string { return punchoutType === 'oci' ? 'oci5' : punchoutType === 'cxml' ? 'cxml1.2' : punchoutType; @@ -55,12 +61,12 @@ export class PunchoutService { switchMap(customer => this.apiService .get(`customers/${this.apiService.encodeResourceId(customer.customerNo)}/punchouts`, { - headers: this.punchoutHeaders, + headers: this.punchoutHeaderV2, }) .pipe( unpackEnvelope(), this.apiService.resolveLinks<{ punchoutType: PunchoutType; version: string }>({ - headers: this.punchoutHeaders, + headers: this.punchoutHeaderV2, }), map(types => types?.map(type => type.punchoutType)) ) @@ -88,11 +94,11 @@ export class PunchoutService { `customers/${this.apiService.encodeResourceId(customer.customerNo)}/punchouts/${this.getResourceType( punchoutType )}/users`, - { headers: this.punchoutHeaders } + { headers: this.punchoutHeaderV2 } ) .pipe( unpackEnvelope(), - this.apiService.resolveLinks({ headers: this.punchoutHeaders }), + this.apiService.resolveLinks({ headers: this.punchoutHeaderV2 }), map(users => users.map(user => ({ ...user, punchoutType, password: undefined }))) ) ) @@ -118,10 +124,10 @@ export class PunchoutService { user.punchoutType )}/users`, user, - { headers: this.punchoutHeaders } + { headers: this.punchoutHeaderV2 } ) .pipe( - this.apiService.resolveLink({ headers: this.punchoutHeaders }), + this.apiService.resolveLink({ headers: this.punchoutHeaderV2 }), map(createdUser => ({ ...createdUser, punchoutType: user.punchoutType, password: undefined })) ) ) @@ -147,7 +153,7 @@ export class PunchoutService { user.punchoutType )}/users/${this.apiService.encodeResourceId(user.login)}`, user, - { headers: this.punchoutHeaders } + { headers: this.punchoutHeaderV2 } ) .pipe(map(updatedUser => ({ ...updatedUser, punchoutType: user.punchoutType, password: undefined }))) ) @@ -170,7 +176,7 @@ export class PunchoutService { `customers/${this.apiService.encodeResourceId(customer.customerNo)}/punchouts/${this.getResourceType( user.punchoutType )}/users/${this.apiService.encodeResourceId(user.login)}`, - { headers: this.punchoutHeaders } + { headers: this.punchoutHeaderV2 } ) ) ); @@ -226,7 +232,7 @@ export class PunchoutService { `customers/${this.apiService.encodeResourceId(customer.customerNo)}/punchouts/${this.getResourceType( 'cxml' )}/sessions/${this.apiService.encodeResourceId(sid)}`, - { headers: this.punchoutHeaders } + { headers: this.punchoutHeaderV2 } ) ) ); @@ -290,6 +296,58 @@ export class PunchoutService { return cXmlForm; } + /** + * Gets the cXML configuration for a given user. + * + * @param user The punchout user id. + * @returns An array of punchout cXML configurations. + */ + getCxmlConfiguration(userId: string): Observable { + return this.currentCustomer$.pipe( + switchMap(customer => + this.apiService + .get( + `customers/${this.apiService.encodeResourceId(customer.customerNo)}/punchouts/${this.getResourceType( + 'cxml' + )}/users/${this.apiService.encodeResourceId(userId)}/configurations`, + { headers: this.punchoutHeaderV3 } + ) + .pipe(map(CxmlConfigurationMapper.fromData)) + ) + ); + } + + /** + * Updates a punchout cxml configuration. + * + * @param cxmlConfiguration An array of cxml configuration items to update. + * @param user The selected punchout user id. + * @returns The updated cxml configuration. + */ + updateCxmlConfiguration(cxmlConfiguration: CxmlConfiguration[], userId: string): Observable { + if (!cxmlConfiguration) { + return throwError(() => new Error('updateCxmlConfiguration() called without required CxmlConfiguration')); + } + + if (!userId) { + return throwError(() => new Error('updateCxmlConfiguration() called without required userId')); + } + + return this.currentCustomer$.pipe( + switchMap(customer => + this.apiService + .patch( + `customers/${this.apiService.encodeResourceId(customer.customerNo)}/punchouts/${this.getResourceType( + 'cxml' + )}/users/${this.apiService.encodeResourceId(userId)}/configurations`, + cxmlConfiguration, + { headers: this.punchoutHeaderV3 } + ) + .pipe(map(CxmlConfigurationMapper.fromData)) + ) + ); + } + // OCI PUNCHOUT SHOPPING FUNCTIONALITY private transferOciPunchoutBasket() { @@ -319,7 +377,7 @@ export class PunchoutService { )}/transfer`, undefined, { - headers: this.punchoutHeaders, + headers: this.punchoutHeaderV2, params: new HttpParams().set('basketId', basketId), } ) @@ -349,7 +407,7 @@ export class PunchoutService { 'oci' )}/validate`, { - headers: this.punchoutHeaders, + headers: this.punchoutHeaderV2, params: new HttpParams().set('productId', productId).set('quantity', quantity), } ) @@ -378,7 +436,7 @@ export class PunchoutService { 'oci' )}/background-search`, { - headers: this.punchoutHeaders, + headers: this.punchoutHeaderV2, params: new HttpParams().set('searchString', searchString), } ) @@ -440,7 +498,7 @@ export class PunchoutService { `customers/${this.apiService.encodeResourceId(customer.customerNo)}/punchouts/${this.getResourceType( 'oci' )}/configurations`, - { headers: this.punchoutHeaders } + { headers: this.punchoutHeaderV2 } ) .pipe(map(data => data.items)) ) @@ -460,7 +518,7 @@ export class PunchoutService { `customers/${this.apiService.encodeResourceId(customer.customerNo)}/punchouts/${this.getResourceType( 'oci' )}`, - { headers: this.punchoutHeaders } + { headers: this.punchoutHeaderV2 } ) .pipe(map(OciOptionsMapper.fromData)) ) @@ -486,7 +544,7 @@ export class PunchoutService { 'oci' )}/configurations`, { items: ociConfiguration }, - { headers: this.punchoutHeaders } + { headers: this.punchoutHeaderV2 } ) .pipe(map(data => data.items)) ) diff --git a/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.actions.ts b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.actions.ts new file mode 100644 index 0000000000..07079ca367 --- /dev/null +++ b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.actions.ts @@ -0,0 +1,24 @@ +import { createActionGroup } from '@ngrx/store'; + +import { httpError, payload } from 'ish-core/utils/ngrx-creators'; + +import { CxmlConfiguration } from '../../models/cxml-configuration/cxml-configuration.model'; + +export const cxmlConfigurationActions = createActionGroup({ + source: 'Cxml Configuration', + events: { + 'Load CXML Configuration': payload(), + 'Update CXML Configuration': payload<{ configuration: CxmlConfiguration[] }>(), + 'Reset CXML Configuration': payload(), + }, +}); + +export const cxmlConfigurationApiActions = createActionGroup({ + source: 'CXML Configuration API', + events: { + 'Load CXML Configuration Success': payload<{ configuration: CxmlConfiguration[] }>(), + 'Load CXML Configuration Fail': httpError<{}>(), + 'Update CXML Configuration Success': payload<{ configuration: CxmlConfiguration[] }>(), + 'Update CXML Configuration Fail': httpError<{}>(), + }, +}); diff --git a/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.effects.spec.ts b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.effects.spec.ts new file mode 100644 index 0000000000..b9ba0acedc --- /dev/null +++ b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.effects.spec.ts @@ -0,0 +1,123 @@ +import { TestBed } from '@angular/core/testing'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { Action } from '@ngrx/store'; +import { provideMockStore } from '@ngrx/store/testing'; +import { cold, hot } from 'jasmine-marbles'; +import { Observable, of, throwError } from 'rxjs'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; + +import { displaySuccessMessage } from 'ish-core/store/core/messages'; +import { makeHttpError } from 'ish-core/utils/dev/api-service-utils'; + +import { PunchoutUser } from '../../models/punchout-user/punchout-user.model'; +import { PunchoutService } from '../../services/punchout/punchout.service'; +import { getSelectedPunchoutUser } from '../punchout-users'; + +import { cxmlConfigurationActions, cxmlConfigurationApiActions } from './cxml-configuration.actions'; +import { CxmlConfigurationEffects } from './cxml-configuration.effects'; + +describe('Cxml Configuration Effects', () => { + let actions$: Observable; + let effects: CxmlConfigurationEffects; + let punchoutService: PunchoutService; + + beforeEach(() => { + punchoutService = mock(PunchoutService); + when(punchoutService.getCxmlConfiguration(anything())).thenReturn(of(undefined)); + when(punchoutService.updateCxmlConfiguration(anything(), anything())).thenReturn(of(undefined)); + + TestBed.configureTestingModule({ + providers: [ + { provide: PunchoutService, useFactory: () => instance(punchoutService) }, + CxmlConfigurationEffects, + provideMockActions(() => actions$), + provideMockStore({ + selectors: [ + { + selector: getSelectedPunchoutUser, + value: { + active: true, + email: 'cxmluser@test.intershop.de', + id: 'cxmluser@test.intershop.de', + login: 'cxmluser@test.intershop.de', + punchoutType: 'cxml', + type: 'PunchoutUser', + } as PunchoutUser, + }, + ], + }), + ], + }); + + effects = TestBed.inject(CxmlConfigurationEffects); + }); + + describe('loadCxmlConfiguration$', () => { + it('should call the punchoutService for getCxmlConfiguration', done => { + const action = cxmlConfigurationActions.loadCXMLConfiguration; + actions$ = of(action); + + effects.loadCxmlConfiguration$.subscribe(() => { + verify(punchoutService.getCxmlConfiguration(anything())).once(); + done(); + }); + }); + + it('should map to action of type loadcxmlConfigurationSuccess', () => { + const action = cxmlConfigurationActions.loadCXMLConfiguration; + + const completion = cxmlConfigurationApiActions.loadCXMLConfigurationSuccess({ configuration: undefined }); + + actions$ = hot('-a-a-a', { a: action }); + const expected$ = cold('-b-b-b)', { b: completion }); + + expect(effects.loadCxmlConfiguration$).toBeObservable(expected$); + }); + + it('should map invalid request to action of type loadCxmlConfigurationFail', () => { + when(punchoutService.getCxmlConfiguration('')).thenReturn( + throwError(() => makeHttpError({ message: 'invalid' })) + ); + }); + }); + + describe('updateCxmlConfiguration$', () => { + it('should call the punchoutService for updateCxmlConfiguration', done => { + const action = cxmlConfigurationActions.updateCXMLConfiguration({ configuration: undefined }); + actions$ = of(action); + + effects.updateCxmlConfiguration$.subscribe(() => { + verify(punchoutService.updateCxmlConfiguration(anything(), anything())).once(); + done(); + }); + }); + + it('should map to actions of type updateCxmlConfigurationSuccess and displaySuccessMessage', () => { + const action = cxmlConfigurationActions.updateCXMLConfiguration({ configuration: [] }); + + const completion1 = cxmlConfigurationApiActions.updateCXMLConfigurationSuccess({ configuration: undefined }); + const completion2 = displaySuccessMessage({ + message: 'account.punchout.cxml.configuration.save_success.message', + }); + + actions$ = hot('-a----a----a----', { a: action }); + const expected$ = cold('-(bc)-(bc)-(bc)', { b: completion1, c: completion2 }); + + expect(effects.updateCxmlConfiguration$).toBeObservable(expected$); + }); + + it('should map invalid request to action of type updateCxmlConfigurationFail', () => { + when(punchoutService.updateCxmlConfiguration(anything(), anything())).thenReturn( + throwError(() => makeHttpError({ message: 'invalid' })) + ); + + const action = cxmlConfigurationActions.updateCXMLConfiguration({ configuration: [] }); + const error = makeHttpError({ message: 'invalid' }); + const completion = cxmlConfigurationApiActions.updateCXMLConfigurationFail({ error }); + actions$ = hot('-a-a-a', { a: action }); + const expected$ = cold('-c-c-c', { c: completion }); + + expect(effects.updateCxmlConfiguration$).toBeObservable(expected$); + }); + }); +}); diff --git a/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.effects.ts b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.effects.ts new file mode 100644 index 0000000000..41628975d3 --- /dev/null +++ b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.effects.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; +import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; +import { Store, select } from '@ngrx/store'; +import { concatMap, map, mergeMap } from 'rxjs/operators'; + +import { displayErrorMessage, displaySuccessMessage } from 'ish-core/store/core/messages'; +import { mapErrorToAction, mapToPayloadProperty } from 'ish-core/utils/operators'; + +import { PunchoutService } from '../../services/punchout/punchout.service'; +import { getSelectedPunchoutUser } from '../punchout-users'; + +import { cxmlConfigurationActions, cxmlConfigurationApiActions } from './cxml-configuration.actions'; + +@Injectable() +export class CxmlConfigurationEffects { + constructor(private actions$: Actions, private punchoutService: PunchoutService, private store: Store) {} + + loadCxmlConfiguration$ = createEffect(() => + this.actions$.pipe( + ofType(cxmlConfigurationActions.loadCXMLConfiguration), + concatLatestFrom(() => this.store.pipe(select(getSelectedPunchoutUser))), + concatMap(([_, user]) => + this.punchoutService.getCxmlConfiguration(user.id).pipe( + map(configuration => cxmlConfigurationApiActions.loadCXMLConfigurationSuccess({ configuration })), + mapErrorToAction(cxmlConfigurationApiActions.loadCXMLConfigurationFail) + ) + ) + ) + ); + + updateCxmlConfiguration$ = createEffect(() => + this.actions$.pipe( + ofType(cxmlConfigurationActions.updateCXMLConfiguration), + mapToPayloadProperty('configuration'), + concatLatestFrom(() => this.store.pipe(select(getSelectedPunchoutUser))), + concatMap(([configuration, user]) => + this.punchoutService.updateCxmlConfiguration(configuration, user.id).pipe( + mergeMap(configuration => [ + cxmlConfigurationApiActions.updateCXMLConfigurationSuccess({ configuration }), + displaySuccessMessage({ + message: 'account.punchout.cxml.configuration.save_success.message', + }), + ]), + mapErrorToAction(cxmlConfigurationApiActions.updateCXMLConfigurationFail) + ) + ) + ) + ); + + displayCxmlConfigurationErrorMessage$ = createEffect(() => + this.actions$.pipe( + ofType( + cxmlConfigurationApiActions.updateCXMLConfigurationFail, + cxmlConfigurationApiActions.loadCXMLConfigurationFail + ), + mapToPayloadProperty('error'), + map(error => + displayErrorMessage({ + message: error.message, + }) + ) + ) + ); +} diff --git a/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.reducer.ts b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.reducer.ts new file mode 100644 index 0000000000..c42d453ff2 --- /dev/null +++ b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.reducer.ts @@ -0,0 +1,42 @@ +import { createReducer, on } from '@ngrx/store'; + +import { HttpError } from 'ish-core/models/http-error/http-error.model'; +import { setErrorOn, setLoadingOn, unsetLoadingAndErrorOn } from 'ish-core/utils/ngrx-creators'; + +import { CxmlConfiguration } from '../../models/cxml-configuration/cxml-configuration.model'; + +import { cxmlConfigurationActions, cxmlConfigurationApiActions } from './cxml-configuration.actions'; + +export interface CxmlConfigurationState { + configuration: CxmlConfiguration[]; + loading: boolean; + error: HttpError; +} + +const initialState: CxmlConfigurationState = { + configuration: [], + loading: false, + error: undefined, +}; + +export const cxmlConfigurationReducer = createReducer( + initialState, + setLoadingOn(cxmlConfigurationActions.loadCXMLConfiguration, cxmlConfigurationActions.updateCXMLConfiguration), + unsetLoadingAndErrorOn( + cxmlConfigurationApiActions.loadCXMLConfigurationSuccess, + cxmlConfigurationApiActions.updateCXMLConfigurationSuccess + ), + setErrorOn( + cxmlConfigurationApiActions.loadCXMLConfigurationFail, + cxmlConfigurationApiActions.updateCXMLConfigurationFail + ), + on( + cxmlConfigurationApiActions.loadCXMLConfigurationSuccess, + cxmlConfigurationApiActions.updateCXMLConfigurationSuccess, + (state, { payload: { configuration } }): CxmlConfigurationState => ({ + ...state, + configuration, + }) + ), + on(cxmlConfigurationActions.resetCXMLConfiguration, (): CxmlConfigurationState => initialState) +); diff --git a/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.selectors.spec.ts b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.selectors.spec.ts new file mode 100644 index 0000000000..06926fb83e --- /dev/null +++ b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.selectors.spec.ts @@ -0,0 +1,67 @@ +import { TestBed } from '@angular/core/testing'; + +import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; +import { makeHttpError } from 'ish-core/utils/dev/api-service-utils'; +import { StoreWithSnapshots, provideStoreSnapshots } from 'ish-core/utils/dev/ngrx-testing'; + +import { PunchoutStoreModule } from '../punchout-store.module'; + +import { cxmlConfigurationActions, cxmlConfigurationApiActions } from './cxml-configuration.actions'; +import { getCxmlConfiguration } from './cxml-configuration.selectors'; + +describe('Cxml Configuration Selectors', () => { + let store$: StoreWithSnapshots; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CoreStoreModule.forTesting(), PunchoutStoreModule.forTesting('cxmlConfiguration')], + providers: [provideStoreSnapshots()], + }); + + store$ = TestBed.inject(StoreWithSnapshots); + }); + + describe('initial state', () => { + it('should not have entities when in initial state', () => { + expect(getCxmlConfiguration(store$.state)).toBeEmpty(); + }); + }); + + describe('loadCxmlConfiguration', () => { + const action = cxmlConfigurationActions.loadCXMLConfiguration(undefined); + + beforeEach(() => { + store$.dispatch(action); + }); + + describe('loadCXMLConfigurationSuccess', () => { + const successAction = cxmlConfigurationApiActions.loadCXMLConfigurationSuccess({ + configuration: [{ name: 'unitmapping', value: '' }], + }); + + beforeEach(() => { + store$.dispatch(successAction); + }); + + it('should have data when successfully loading', () => { + expect(getCxmlConfiguration(store$.state)).toMatchInlineSnapshot(` + [ + { + "name": "unitmapping", + "value": "", + }, + ] + `); + }); + }); + + describe('loadCXMLConfigurationFail', () => { + const error = makeHttpError({ message: 'ERROR' }); + const failAction = cxmlConfigurationApiActions.loadCXMLConfigurationFail({ error }); + + beforeEach(() => { + store$.dispatch(failAction); + }); + }); + }); +}); diff --git a/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.selectors.ts b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.selectors.ts new file mode 100644 index 0000000000..8aa8179df9 --- /dev/null +++ b/src/app/extensions/punchout/store/cxml-configuration/cxml-configuration.selectors.ts @@ -0,0 +1,11 @@ +import { createSelector } from '@ngrx/store'; + +import { getPunchoutState } from '../punchout-store'; + +const getCxmlConfigurationState = createSelector(getPunchoutState, state => state.cxmlConfiguration); + +export const getCxmlConfiguration = createSelector(getCxmlConfigurationState, state => state.configuration); + +export const getCxmlConfigurationLoading = createSelector(getCxmlConfigurationState, state => state.loading); + +export const getCxmlConfigurationError = createSelector(getCxmlConfigurationState, state => state.error); diff --git a/src/app/extensions/punchout/store/cxml-configuration/index.ts b/src/app/extensions/punchout/store/cxml-configuration/index.ts new file mode 100644 index 0000000000..a368785b12 --- /dev/null +++ b/src/app/extensions/punchout/store/cxml-configuration/index.ts @@ -0,0 +1,3 @@ +// API to access ngrx cxmlConfiguration state +export * from './cxml-configuration.actions'; +export * from './cxml-configuration.selectors'; diff --git a/src/app/extensions/punchout/store/punchout-store.module.ts b/src/app/extensions/punchout/store/punchout-store.module.ts index 06d3b0cb5b..b76fd51117 100644 --- a/src/app/extensions/punchout/store/punchout-store.module.ts +++ b/src/app/extensions/punchout/store/punchout-store.module.ts @@ -5,6 +5,8 @@ import { pick } from 'lodash-es'; import { resetOnLogoutMeta } from 'ish-core/utils/meta-reducers'; +import { CxmlConfigurationEffects } from './cxml-configuration/cxml-configuration.effects'; +import { cxmlConfigurationReducer } from './cxml-configuration/cxml-configuration.reducer'; import { OciConfigurationEffects } from './oci-configuration/oci-configuration.effects'; import { ociConfigurationReducer } from './oci-configuration/oci-configuration.reducer'; import { PunchoutFunctionsEffects } from './punchout-functions/punchout-functions.effects'; @@ -18,9 +20,16 @@ const punchoutReducers: ActionReducerMap = { ociConfiguration: ociConfigurationReducer, punchoutUsers: punchoutUsersReducer, punchoutTypes: punchoutTypesReducer, + cxmlConfiguration: cxmlConfigurationReducer, }; -const punchoutEffects = [OciConfigurationEffects, PunchoutUsersEffects, PunchoutFunctionsEffects, PunchoutTypesEffects]; +const punchoutEffects = [ + OciConfigurationEffects, + PunchoutUsersEffects, + PunchoutFunctionsEffects, + PunchoutTypesEffects, + CxmlConfigurationEffects, +]; @Injectable() export class PunchoutStoreConfig implements StoreConfig { diff --git a/src/app/extensions/punchout/store/punchout-store.ts b/src/app/extensions/punchout/store/punchout-store.ts index 4c7db73910..489588e3dd 100644 --- a/src/app/extensions/punchout/store/punchout-store.ts +++ b/src/app/extensions/punchout/store/punchout-store.ts @@ -1,5 +1,6 @@ import { createFeatureSelector } from '@ngrx/store'; +import { CxmlConfigurationState } from './cxml-configuration/cxml-configuration.reducer'; import { OciConfigurationState } from './oci-configuration/oci-configuration.reducer'; import { PunchoutTypesState } from './punchout-types/punchout-types.reducer'; import { PunchoutUsersState } from './punchout-users/punchout-users.reducer'; @@ -8,6 +9,7 @@ export interface PunchoutState { ociConfiguration: OciConfigurationState; punchoutUsers: PunchoutUsersState; punchoutTypes: PunchoutTypesState; + cxmlConfiguration: CxmlConfigurationState; } export const getPunchoutState = createFeatureSelector('punchout'); diff --git a/src/app/extensions/punchout/store/punchout-users/punchout-users.effects.spec.ts b/src/app/extensions/punchout/store/punchout-users/punchout-users.effects.spec.ts index efc43a941f..61b9ce71eb 100644 --- a/src/app/extensions/punchout/store/punchout-users/punchout-users.effects.spec.ts +++ b/src/app/extensions/punchout/store/punchout-users/punchout-users.effects.spec.ts @@ -15,6 +15,7 @@ import { makeHttpError } from 'ish-core/utils/dev/api-service-utils'; import { PunchoutUser } from '../../models/punchout-user/punchout-user.model'; import { PunchoutService } from '../../services/punchout/punchout.service'; +import { PunchoutStoreModule } from '../punchout-store.module'; import { addPunchoutUser, @@ -51,6 +52,7 @@ describe('Punchout Users Effects', () => { TestBed.configureTestingModule({ imports: [ CoreStoreModule.forTesting(['router']), + PunchoutStoreModule.forTesting('punchoutUsers'), RouterTestingModule.withRoutes([ { path: 'account/punchout', children: [] }, { path: 'account/punchout/:PunchoutLogin', children: [] }, diff --git a/src/app/extensions/punchout/store/punchout-users/punchout-users.effects.ts b/src/app/extensions/punchout/store/punchout-users/punchout-users.effects.ts index 368c7993e3..0a14fcb4ad 100644 --- a/src/app/extensions/punchout/store/punchout-users/punchout-users.effects.ts +++ b/src/app/extensions/punchout/store/punchout-users/punchout-users.effects.ts @@ -3,10 +3,10 @@ import { Router } from '@angular/router'; import { Actions, concatLatestFrom, createEffect, ofType } from '@ngrx/effects'; import { Store, select } from '@ngrx/store'; import { from } from 'rxjs'; -import { concatMap, exhaustMap, map, mergeMap, withLatestFrom } from 'rxjs/operators'; +import { concatMap, exhaustMap, filter, map, mergeMap, withLatestFrom } from 'rxjs/operators'; import { displaySuccessMessage } from 'ish-core/store/core/messages'; -import { selectQueryParam, selectRouteParam } from 'ish-core/store/core/router'; +import { selectQueryParam, selectRouteParam, selectUrl } from 'ish-core/store/core/router'; import { mapErrorToAction, mapToPayloadProperty, whenTruthy } from 'ish-core/utils/operators'; import { PunchoutType } from '../../models/punchout-user/punchout-user.model'; @@ -27,6 +27,7 @@ import { updatePunchoutUserFail, updatePunchoutUserSuccess, } from './punchout-users.actions'; +import { getSelectedPunchoutUser } from './punchout-users.selectors'; @Injectable() export class PunchoutUsersEffects { @@ -63,8 +64,14 @@ export class PunchoutUsersEffects { this.store.pipe( select(selectRouteParam('PunchoutLogin')), whenTruthy(), - withLatestFrom(this.store.pipe(select(selectQueryParam('format')))), - concatMap(([, format]) => + withLatestFrom( + this.store.pipe(select(getSelectedPunchoutUser)), + this.store.pipe(select(selectUrl)), + this.store.pipe(select(selectQueryParam('format'))) + ), + // don't load user on configuration page + filter(([, user, url]) => !user || !url.includes('cxmlConfiguration')), + concatMap(([, , , format]) => this.punchoutService.getUsers(format as PunchoutType).pipe( map(users => loadPunchoutUsersSuccess({ users })), mapErrorToAction(loadPunchoutUsersFail) diff --git a/src/assets/i18n/de_DE.json b/src/assets/i18n/de_DE.json index f8998da35f..402ca9adcf 100644 --- a/src/assets/i18n/de_DE.json +++ b/src/assets/i18n/de_DE.json @@ -346,6 +346,7 @@ "account.profile.update_email.message": "Ihre E-Mail-Adresse wurde aktualisiert. Wir haben zur Bestätigung eine E-Mail an {{0}} gesendet.", "account.profile.update_password.message": "Ihr Kennwort wurde aktualisiert.", "account.profile.update_profile.message": "Ihre Profilinformationen wurden aktualisiert.", + "account.punchout.configuration.back_to_list": "Zurück zur Benutzerliste", "account.punchout.configuration.button.label": "Konfigurieren", "account.punchout.configuration.description": "Legen Sie die Rückgabewerte des OCI-Punchout-Transferformats für Ihr Procurement-System fest.", "account.punchout.configuration.form.add_row.link": "Zeile hinzufügen", @@ -362,9 +363,15 @@ "account.punchout.configuration.link": "Konfiguration", "account.punchout.configuration.option.none.label": "Keine", "account.punchout.configuration.save_success.message": "Die OCI-Punchout-Konfiguration wurde gespeichert.", + "account.punchout.configure.link": "{{0}} konfigurieren", "account.punchout.create.description": "Bitte geben Sie die Anmeldedaten des {{0, translate, account.punchout.type.text}}-Punchout-Benutzers an.", "account.punchout.create.heading": "Einen neuen {{0, translate, account.punchout.type.text}}-Punchout-Benutzer anlegen", "account.punchout.create.link": "Einen neuen Punchout-Benutzer anlegen", + "account.punchout.cxml.configuration.default.description": "Standardwert: {{defaultValue}}", + "account.punchout.cxml.configuration.helptext": "Verwenden Sie die folgenden Einstellungen, um den Inhalt des cXML-Punchout-Dokuments festzulegen. Eingestellte Werte können auf den angegebenen Standardwert zurückgesetzt werden.", + "account.punchout.cxml.configuration.link": "Konfiguration", + "account.punchout.cxml.configuration.no_configuration": "Derzeit ist keine Konfiguration angelegt.", + "account.punchout.cxml.configuration.save_success.message": "Die cXML-Punchout-Konfiguration wurde gespeichert.", "account.punchout.cxml.info.url.helptext": "Bitte verwenden Sie die folgende URL für die Punchout-Konfiguration in Ihrem cXML-Procurement-System. Verwenden Sie für die Absender-Anmeldeinformationen den Benutzernamen als \"Identity\" und das Kennwort als \"SharedSecret\".", "account.punchout.heading": "Punchout", "account.punchout.link": "Punchout", diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index a4c0df3ed4..ef7674d3ef 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -346,6 +346,7 @@ "account.profile.update_email.message": "Your e-mail address has been updated. We have sent you a confirmation e-mail to {{0}}.", "account.profile.update_password.message": "Your password has been updated.", "account.profile.update_profile.message": "Your profile information has been updated.", + "account.punchout.configuration.back_to_list": "Back to user list", "account.punchout.configuration.button.label": "Configure", "account.punchout.configuration.description": "Specify the return values of the OCI punchout transfer format for your procurement system.", "account.punchout.configuration.form.add_row.link": "Add a row", @@ -362,10 +363,16 @@ "account.punchout.configuration.link": "Configuration", "account.punchout.configuration.option.none.label": "None", "account.punchout.configuration.save_success.message": "The OCI punchout configuration has been saved.", + "account.punchout.configure.link": "Configure {{0}}", "account.punchout.create.description": "Please specify your user with your {{0, translate, account.punchout.type.text}} punchout credentials.", - "account.punchout.create.heading": "Create new {{0, translate, account.punchout.type.text}} punchout user", - "account.punchout.create.link": "Create new punchout user", - "account.punchout.cxml.info.url.helptext": "Please use the following URL for the punchout configuration in your cXML procurement system. For the sender credentials use the username as identity and the password as SharedSecret.", + "account.punchout.create.heading": "Create a new {{0, translate, account.punchout.type.text}} punchout user", + "account.punchout.create.link": "Create a new punchout user", + "account.punchout.cxml.configuration.default.description": "Default value: {{defaultValue}}", + "account.punchout.cxml.configuration.helptext": "Use the following preferences to determine the content of the cXML Punchout document. Set values can be reset to return to the default value as specified.", + "account.punchout.cxml.configuration.link": "Configuration", + "account.punchout.cxml.configuration.no_configuration": "No configuration has been created at this time.", + "account.punchout.cxml.configuration.save_success.message": "The cXML Punchout configuration has been saved.", + "account.punchout.cxml.info.url.helptext": "Please use the following URL for the punchout configuration in your cXML procurement system. For the sender credentials use the username as \"Identity\" and the password as \"SharedSecret\".", "account.punchout.heading": "Punchout", "account.punchout.link": "Punchout", "account.punchout.no.protocols.info": "Punchout protocols are currently not supported. If the problem persists, please contact the customer support.", diff --git a/src/assets/i18n/fr_FR.json b/src/assets/i18n/fr_FR.json index 095547f036..d93118b4f3 100644 --- a/src/assets/i18n/fr_FR.json +++ b/src/assets/i18n/fr_FR.json @@ -346,6 +346,7 @@ "account.profile.update_email.message": "Votre adresse courriel a été mise à jour. Nous vous avons envoyé un courriel de confirmation à {{0}}.", "account.profile.update_password.message": "Votre mot de passe a été mis à jour.", "account.profile.update_profile.message": "Les données de votre profil ont été mises à jour.", + "account.punchout.configuration.back_to_list": "Retour à la liste des utilisateurs", "account.punchout.configuration.button.label": "Configurer", "account.punchout.configuration.description": "Spécifiez les valeurs de retour du format de transfert OCI Punchout pour votre système d’approvisionnement.", "account.punchout.configuration.form.add_row.link": "Ajouter une ligne", @@ -362,9 +363,15 @@ "account.punchout.configuration.link": "Configuration", "account.punchout.configuration.option.none.label": "Aucun", "account.punchout.configuration.save_success.message": "La configuration de l’OCI punchout a été sauvegardée.", + "account.punchout.configure.link": "Configurer {{0}}", "account.punchout.create.description": "Veuillez indiquer votre utilisateur avec vos données d’identification {{0, translate, account.punchout.type.text}} punchout.", "account.punchout.create.heading": "Créer un nouvel utilisateur {{0, translate, account.punchout.type.text}} punchout", - "account.punchout.create.link": "Créer un nouvel utilisateur \"Punchout\"", + "account.punchout.create.link": "Créer un nouvel utilisateur punchout", + "account.punchout.cxml.configuration.default.description": "Valeur par défaut : {{defaultValue}}", + "account.punchout.cxml.configuration.helptext": "Les préférences suivantes permettent de déterminer le contenu du document cXML punchout. Les valeurs définies peuvent être réinitialisées pour revenir à la valeur par défaut spécifiée.", + "account.punchout.cxml.configuration.link": "Configuration", + "account.punchout.cxml.configuration.no_configuration": "Aucune configuration n’est disponible pour le moment.", + "account.punchout.cxml.configuration.save_success.message": "La configuration de cXML punchout a été sauvegardée.", "account.punchout.cxml.info.url.helptext": "Veuillez utiliser l’URL suivante pour la configuration de Punchout dans votre système d’approvisionnement cXML. Pour les données d’identification de l’expéditeur, utilisez le nom d’utilisateur comme \"Identity\" et le mot de passe comme \"SharedSecret\".", "account.punchout.heading": "Punchout", "account.punchout.link": "Punchout",