diff --git a/alfa-client/Jenkinsfile.e2e b/alfa-client/Jenkinsfile.e2e index da6f80f30f5cb7e07babc7fd53e3195f86d3afc4..ebde17972347d5b54e42ab4607728a852ac91b0e 100644 --- a/alfa-client/Jenkinsfile.e2e +++ b/alfa-client/Jenkinsfile.e2e @@ -462,7 +462,7 @@ Void initEnvAdminDefaultVersions() { env.ADMINISTRATION_HELM_CHART_VERSION = getHelmChartVersion(values) env.ADMINISTRATION_HELM_REPO_URL = getHelmRepoUrl() - values = getApplicationValues('admin-client') + values = getApplicationValues('administration-client') env.ADMIN_CLIENT_IMAGE_TAG = getImageTag(values) env.ADMIN_CLIENT_HELM_CHART_VERSION = getHelmChartVersion(values) env.ADMIN_CLIENT_HELM_REPO_URL = getHelmRepoUrl() @@ -565,8 +565,8 @@ Void generateAdminNamespaceYaml() { envValues.administration.put("helm", ['version': env.ADMINISTRATION_HELM_CHART_VERSION, 'repoUrl': env.ADMINISTRATION_HELM_REPO_URL]) envValues.administration.put("ozgcloud", ['feature': ['organisationsEinheiten': "true"], 'organisationEinheit': ['zufiSearchUri': generateZufiSearchUri(bezeichner)]]) - envValues.admin_client.put("image", ['tag': env.ADMIN_CLIENT_IMAGE_TAG]) - envValues.admin_client.put("helm", ['version': env.ADMIN_CLIENT_HELM_CHART_VERSION, 'repoUrl': env.ADMIN_CLIENT_HELM_REPO_URL]) + envValues.administration_client.put("image", ['tag': env.ADMIN_CLIENT_IMAGE_TAG]) + envValues.administration_client.put("helm", ['version': env.ADMIN_CLIENT_HELM_CHART_VERSION, 'repoUrl': env.ADMIN_CLIENT_HELM_REPO_URL]) } return writeYamlToGitOps(bezeichner, envValues); } @@ -657,7 +657,7 @@ Void waitForAlfaRollout(ozgCloudBezeichner) { Void waitForAdminRollout(String bezeichner) { waitForAlfaRollout([bezeichner]) waitForHealthyApplication(bezeichner, 'administration') - waitForHealthyApplication(bezeichner, 'admin-client') + waitForHealthyApplication(bezeichner, 'administration-client') } Void waitForAlfaRollout(String bezeichner) { @@ -798,7 +798,7 @@ String generateCypressConfig(String bezeichner, String appName, String appVarian String generateUrlBezeichner(String bezeichner, String appName) { if (appName == 'admin-e2e') { - return "${bezeichner}-admin"; + return "${bezeichner}-administration"; } return bezeichner; } diff --git a/alfa-client/apps/admin-e2e/src/components/statistik/statistik-fields-form.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/aggregation-mapping/aggregation-mapping-form.e2e.component.ts similarity index 53% rename from alfa-client/apps/admin-e2e/src/components/statistik/statistik-fields-form.e2e.component.ts rename to alfa-client/apps/admin-e2e/src/components/aggregation-mapping/aggregation-mapping-form.e2e.component.ts index 8936e02edbe69c765364304bc497d2be61a970da..e140ef04867eeb97212c840b44e44535696422e4 100644 --- a/alfa-client/apps/admin-e2e/src/components/statistik/statistik-fields-form.e2e.component.ts +++ b/alfa-client/apps/admin-e2e/src/components/aggregation-mapping/aggregation-mapping-form.e2e.component.ts @@ -1,67 +1,58 @@ -import { enterWith } from '../../support/cypress.util'; +export class AggregationMappingFormE2EComponent { + private readonly root: string = 'aggregation-mapping-form'; -export class StatistikFieldsFormE2EComponent { + private readonly nameInput: string = 'aggregation-mapping-name-text-input'; private readonly formEngineInput: string = 'form-engine-name-text-input'; private readonly formIdInput: string = 'form-id-text-input'; - private readonly formDataFieldInput: string = 'mapping-field-'; + private readonly dataFieldInputPrefix: string = 'aggregation-mapping-field-mapping-form-'; + private readonly sourceMappingFieldInputPrefix: string = 'source-mapping-field-'; + private readonly targetMappingFieldInputPrefix: string = 'target-mapping-field-'; private readonly addDataFieldButton: string = 'add-mapping-button'; private readonly deleteDataFieldButtonPrefix: string = 'remove-mapping-button-'; private readonly saveButton: string = 'save-button'; private readonly cancelButton: string = 'cancel-button'; - public getFormEngineInput(): Cypress.Chainable<Element> { - return cy.getTestElement(this.formEngineInput); + public getRoot(): Cypress.Chainable<Element> { + return cy.getTestElement(this.root); + } + + public getNameInput(): Cypress.Chainable<Element> { + return cy.getTestElement(this.nameInput); } - public enterFormEngine(text: string): void { - enterWith(this.getFormEngineInput(), text); + public getFormEngineInput(): Cypress.Chainable<Element> { + return cy.getTestElement(this.formEngineInput); } public getFormIdInput(): Cypress.Chainable<Element> { return cy.getTestElement(this.formIdInput); } - public enterFormId(text: string): void { - enterWith(this.getFormIdInput(), text); + public getDataFieldInput(index: number): Cypress.Chainable<Element> { + return cy.getTestElement(`${this.dataFieldInputPrefix}${index}`); } public getAddFieldButton(): Cypress.Chainable<Element> { return cy.getTestElement(this.addDataFieldButton); } - public addField(): void { - this.getAddFieldButton().click(); - } - - public getDataFieldInput(index: number): Cypress.Chainable<Element> { - return cy.getTestElement(this.formDataFieldInput + index + '-text-input'); + public getSourceMappingFieldInput(index: number): Cypress.Chainable<Element> { + return cy.getTestElement(`${this.sourceMappingFieldInputPrefix}${index}-text-input`); } - public enterDataFieldPath(text: string, index: number): void { - enterWith(this.getDataFieldInput(index), text); + public getTargetMappingFieldInput(index: number): Cypress.Chainable<Element> { + return cy.getTestElement(`${this.targetMappingFieldInputPrefix}${index}-text-input`); } public getDataFieldDeleteButton(index: number): Cypress.Chainable<Element> { return cy.getTestElement(this.deleteDataFieldButtonPrefix + index); } - public deleteDataField(index: number): void { - this.getDataFieldDeleteButton(index).click(); - } - public getSaveButton(): Cypress.Chainable<Element> { return cy.getTestElement(this.saveButton); } - public save(): void { - this.getSaveButton().click(); - } - public getCancelButton(): Cypress.Chainable<Element> { return cy.getTestElement(this.cancelButton); } - - public cancel(): void { - this.getCancelButton().click(); - } } diff --git a/alfa-client/apps/admin-e2e/src/components/aggregation-mapping/aggregation-mapping.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/aggregation-mapping/aggregation-mapping.e2e.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..69df7bcedef6c8927a65f0e24873afc3d6c54bbb --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/components/aggregation-mapping/aggregation-mapping.e2e.component.ts @@ -0,0 +1,47 @@ +import { getTestElement } from '../../support/cypress-helper'; +import { convertToDataTestId } from '../../support/tech-util'; + +export class AggregationMappingE2EComponent { + private readonly headerText: string = 'aggregation-mapping-header-text'; + private readonly weitereFelderAuswertenButton = 'weitere-felder-auswerten-button'; + + public getHeaderText(): Cypress.Chainable<Element> { + return cy.getTestElement(this.headerText); + } + + public getWeitereFelderAuswertenButton(): Cypress.Chainable<Element> { + return cy.getTestElement(this.weitereFelderAuswertenButton); + } + + public getListItem(name: string): AggregationMappingListItemE2EComponent { + return new AggregationMappingListItemE2EComponent(name); + } +} + +export class AggregationMappingListItemE2EComponent { + private root: string; + + private readonly listItemName: string = 'list-item-name'; + private readonly listItemFormEngineName: string = 'list-item-form-engine-name'; + private readonly listItemFormId: string = 'list-item-form-id'; + + constructor(root: string) { + this.root = convertToDataTestId(root); + } + + public getRoot(): Cypress.Chainable<Element> { + return getTestElement(this.root); + } + + public getName(): Cypress.Chainable<Element> { + return this.getRoot().findTestElementWithClass(this.listItemName); + } + + public getFormEngineName(): Cypress.Chainable<Element> { + return this.getRoot().findTestElementWithClass(this.listItemFormEngineName); + } + + public getFormId(): Cypress.Chainable<Element> { + return this.getRoot().findTestElementWithClass(this.listItemFormId); + } +} diff --git a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts index 673ea2c47571873cc69795547f6dc5359609e99a..c80f8d4b5056e16a6de0d772cfd6df4e8e02b45e 100644 --- a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts +++ b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts @@ -23,6 +23,7 @@ */ import 'cypress-real-events'; import { convertToDataTestId } from '../../support/tech-util'; +import { TextEditorE2EComponent } from '../ods/text-editor-e2-e.component'; //TODO BenutzerListPage erstellen welche den Button und die Liste enthaelt. export class BenutzerListE2EComponent { @@ -95,11 +96,6 @@ export class BenutzerListItemE2EComponent { export class BenutzerE2EComponent { private readonly headline: string = 'benutzer-form-headline'; - private readonly userVorname: string = 'Vorname-text-input'; - private readonly userNachname: string = 'Nachname-text-input'; - private readonly userBenutzername: string = 'Benutzername-text-input'; - private readonly userMail: string = 'E-Mail-text-input'; - private readonly adminCheckboxLabel: string = 'Admin'; private readonly loeschenCheckboxLabel: string = 'Löschen'; private readonly userCheckboxLabel: string = 'User'; @@ -111,6 +107,13 @@ export class BenutzerE2EComponent { private readonly saveButton: string = 'save-button'; private readonly deleteButton: string = 'delete-button'; + private readonly rollenValidationError: string = 'rollen-error'; + + private readonly vornameTextFeld: TextEditorE2EComponent = new TextEditorE2EComponent('firstName'); + private readonly nachnameTextFeld: TextEditorE2EComponent = new TextEditorE2EComponent('lastName'); + private readonly benutzernameTextFeld: TextEditorE2EComponent = new TextEditorE2EComponent('username'); + private readonly emailTextFeld: TextEditorE2EComponent = new TextEditorE2EComponent('email'); + public getHeadline(): Cypress.Chainable<Element> { return cy.getTestElement(this.headline); } @@ -120,19 +123,19 @@ export class BenutzerE2EComponent { } public getVornameInput(): Cypress.Chainable<Element> { - return cy.getTestElement(this.userVorname); + return this.vornameTextFeld.getInput(); } public getNachnameInput(): Cypress.Chainable<Element> { - return cy.getTestElement(this.userNachname); + return this.nachnameTextFeld.getInput(); } public getBenutzernameInput(): Cypress.Chainable<Element> { - return cy.getTestElement(this.userBenutzername); + return this.benutzernameTextFeld.getInput(); } public getMailInput(): Cypress.Chainable<Element> { - return cy.getTestElement(this.userMail); + return this.emailTextFeld.getInput(); } public getAdminCheckbox(): BenutzerCheckboxE2EComponent { @@ -166,6 +169,26 @@ export class BenutzerE2EComponent { public getDeleteButton(): Cypress.Chainable<Element> { return cy.getTestElement(this.deleteButton); } + + public getVornameTextFeld(): TextEditorE2EComponent { + return this.vornameTextFeld; + } + + public getNachnameTextFeld(): TextEditorE2EComponent { + return this.nachnameTextFeld; + } + + public getBenutzernameTextFeld(): TextEditorE2EComponent { + return this.benutzernameTextFeld; + } + + public getEmailTextFeld(): TextEditorE2EComponent { + return this.emailTextFeld; + } + + public getRollenValidationError(): Cypress.Chainable<Element> { + return cy.getTestElement(this.rollenValidationError); + } } export class BenutzerDeleteDialogE2EComponent { diff --git a/alfa-client/apps/admin-e2e/src/components/ods/text-editor-e2-e.component.ts b/alfa-client/apps/admin-e2e/src/components/ods/text-editor-e2-e.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..0605779543307d829c2fc65dc3380643cc519bba --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/components/ods/text-editor-e2-e.component.ts @@ -0,0 +1,19 @@ +export class TextEditorE2EComponent { + private readonly root: string; + private readonly input: string; + private readonly validationError: string; + + constructor(root: string) { + this.root = `${root}-text-editor`; + this.input = `${root}-text-input`; + this.validationError = `${root}-text-editor-error`; + } + + public getInput(): Cypress.Chainable<Element> { + return cy.getTestElement(this.input); + } + + public getErrorMessage(): Cypress.Chainable<Element> { + return cy.getTestElement(this.validationError); + } +} diff --git a/alfa-client/apps/admin-e2e/src/components/statistik/statistik.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/statistik/statistik.e2e.component.ts deleted file mode 100644 index b7467232f5d596a2984bc109e84180810c5e8dc3..0000000000000000000000000000000000000000 --- a/alfa-client/apps/admin-e2e/src/components/statistik/statistik.e2e.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class StatistikE2EComponent { - private readonly locatorHeaderText: string = 'statistik-header-text'; - private readonly locatorWeitereFelderAuswertenButton = 'weitere-felder-auswerten-button'; - - public getHeaderText(): Cypress.Chainable<Element> { - return cy.getTestElement(this.locatorHeaderText); - } - - public getWeitereFelderAuswertenButton(): Cypress.Chainable<Element> { - return cy.getTestElement(this.locatorWeitereFelderAuswertenButton); - } -} diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/aggregation-mapping/aggregation-mapping.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/aggregation-mapping/aggregation-mapping.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..ffabc87021d990125ddb5402cac4244d4dc2fa6b --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/aggregation-mapping/aggregation-mapping.cy.ts @@ -0,0 +1,86 @@ +import { AggregationMapping, FieldMapping } from '@admin-client/reporting-shared'; +import { E2EAggregationMappingVerifier } from 'apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.verifier'; +import { AggregationMappingFormE2EComponent } from '../../../components/aggregation-mapping/aggregation-mapping-form.e2e.component'; +import { AggregationMappingE2EComponent } from '../../../components/aggregation-mapping/aggregation-mapping.e2e.component'; +import { E2EAggregationMappingHelper } from '../../../helper/aggregation-mapping/aggregation-mapping.helper'; +import { dropCollections } from '../../../support/cypress-helper'; +import { exist, notExist } from '../../../support/cypress.util'; +import { loginAsDaria } from '../../../support/user-util'; + +describe('Aggregation Mapping hinzufügen', () => { + const page: AggregationMappingE2EComponent = new AggregationMappingE2EComponent(); + const form: AggregationMappingFormE2EComponent = new AggregationMappingFormE2EComponent(); + + const helper: E2EAggregationMappingHelper = new E2EAggregationMappingHelper(); + const verifier: E2EAggregationMappingVerifier = new E2EAggregationMappingVerifier(); + + const fieldMapping0: FieldMapping = { + sourcePath: '/path/to/source/a', + targetPath: '/path/to/target/a', + }; + + const fieldMapping1: FieldMapping = { + sourcePath: '/path/to/source/b', + targetPath: '/path/to/target/b', + }; + + const aggregationMapping: AggregationMapping = { + name: 'Aggregation Mapping', + formIdentifier: { + formEngineName: 'formEngineName', + formId: 'formId', + }, + mappings: [fieldMapping0, fieldMapping1], + }; + + before(() => { + loginAsDaria(); + }); + + after(() => { + dropCollections(); + }); + + it('should show "Weitere Felder auswerten" button', () => { + helper.openStatistik(); + + exist(page.getWeitereFelderAuswertenButton()); + }); + + it('should show form after button click', () => { + page.getWeitereFelderAuswertenButton().click(); + + exist(form.getRoot()); + }); + + it('should navigate to aggregation mapping on cancel', () => { + form.getCancelButton().click(); + + exist(page.getWeitereFelderAuswertenButton()); + }); + + it('should add data field in form', () => { + page.getWeitereFelderAuswertenButton().click(); + form.getAddFieldButton().click(); + + exist(form.getDataFieldInput(1)); + }); + + it('should fill out form with two data fields', () => { + helper.fillFormular(aggregationMapping); + + verifier.verifyForm(aggregationMapping); + }); + + it('should delete data fields', () => { + form.getDataFieldDeleteButton(0).click(); + + notExist(form.getDataFieldInput(1)); + }); + + it('should show aggregation mapping in list after save', () => { + form.getSaveButton().click(); + + verifier.verifyAggregationMappingInList(aggregationMapping); + }); +}); diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts index 6869eafd381b1d9df0dde4b213466d4c8db8b4db..e659bf8dde0d751c2f2af41860eb90944aadf1fb 100644 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts @@ -122,4 +122,96 @@ describe('Benutzer anlegen', () => { benutzerHelper.deleteBenutzer(newAdminUser.username); }); }); + + describe('when filling formular', () => { + it('should show validation errors on empty formular', () => { + benutzerHelper.openNewBenutzerPage(); + benutzerHelper.saveBenutzer(); + + benutzerVerifier.verifyEmptyVornameError(); + benutzerVerifier.verifyEmptyNameError(); + benutzerVerifier.verifyUsernameLengthError(); + benutzerVerifier.verifyInvalidEmailError(); + benutzerVerifier.verifyEmptyRollenError(); + }); + + it('should hide Vorname validation error on filled', () => { + benutzerHelper.editVorname('Max'); + benutzerVerifier.verifyVornameHasNoError(); + benutzerVerifier.verifyEmptyNameError(); + benutzerVerifier.verifyUsernameLengthError(); + benutzerVerifier.verifyInvalidEmailError(); + benutzerVerifier.verifyEmptyRollenError(); + }); + + it('should hide Nachname validation error on filled', () => { + benutzerHelper.editNachname('Mustermann'); + benutzerVerifier.verifyVornameHasNoError(); + benutzerVerifier.verifyLastNameHasNoError(); + benutzerVerifier.verifyUsernameLengthError(); + benutzerVerifier.verifyInvalidEmailError(); + benutzerVerifier.verifyEmptyRollenError(); + }); + + it('should hide Benutzername validation error on filled', () => { + benutzerHelper.editBenutzername('Max'); + benutzerVerifier.verifyVornameHasNoError(); + benutzerVerifier.verifyLastNameHasNoError(); + benutzerVerifier.verifyUsernameHasNoError(); + benutzerVerifier.verifyInvalidEmailError(); + benutzerVerifier.verifyEmptyRollenError(); + }); + + it('should hide Email validation error on filled', () => { + benutzerHelper.editEmail('max@max.local'); + benutzerVerifier.verifyVornameHasNoError(); + benutzerVerifier.verifyLastNameHasNoError(); + benutzerVerifier.verifyUsernameHasNoError(); + benutzerVerifier.verifyEmailHasNoError(); + benutzerVerifier.verifyEmptyRollenError(); + }); + + it('should hide Rollen validation error on filled', () => { + benutzerHelper.addUserRole(); + benutzerVerifier.verifyVornameHasNoError(); + benutzerVerifier.verifyLastNameHasNoError(); + benutzerVerifier.verifyUsernameHasNoError(); + benutzerVerifier.verifyEmailHasNoError(); + benutzerVerifier.verifyRollenHasNoError(); + }); + }); + + describe('after formular submission', () => { + it('should have user created', () => { + benutzerHelper.openNewBenutzerPage(); + + benutzerHelper.addBenutzer(newAdminUser); + benutzerHelper.saveBenutzer(); + + benutzerVerifier.verifyUserInList(newAdminUser); + }); + + it('should show validation error on existing email', () => { + benutzerHelper.openNewBenutzerPage(); + benutzerHelper.addBenutzer(newAdminUser); + benutzerHelper.saveBenutzer(); + + benutzerVerifier.verifyEmailExistsError(); + }); + + it('should show validation error on existing username', () => { + benutzerHelper.openNewBenutzerPage(); + benutzerHelper.addBenutzer({ ...newAdminUser, email: faker.internet.email() + '.local' }); + benutzerHelper.saveBenutzer(); + + benutzerVerifier.verifyUsernameExistsError(); + }); + + it('should remove benutzer', () => { + benutzerHelper.openBenutzerPage(newAdminUser.username); + benutzerHelper.deleteBenutzer(newAdminUser.username); + + benutzerVerifier.verifyUserNotInList(newAdminUser.username); + }); + }); }); diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/statistik/statistik-fields.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/statistik/statistik-fields.cy.ts deleted file mode 100644 index d877ae4b900417af37328322176ce8070c6d0844..0000000000000000000000000000000000000000 --- a/alfa-client/apps/admin-e2e/src/e2e/main-tests/statistik/statistik-fields.cy.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { StatistikE2EComponent } from 'apps/admin-e2e/src/components/statistik/statistik.e2e.component'; -import { StatistikFieldsFormE2EComponent } from '../../../components/statistik/statistik-fields-form.e2e.component'; -import { exist, haveText, haveValue } from '../../../support/cypress.util'; -import { loginAsDaria } from '../../../support/user-util'; - -describe('Felder in Statistik hinzufügen', () => { - const component: StatistikE2EComponent = new StatistikE2EComponent(); - const fieldsFormComponent: StatistikFieldsFormE2EComponent = new StatistikFieldsFormE2EComponent(); - - const dataText1: string = 'Eingabe A'; - const dataText2: string = 'Eingabe B'; - - before(() => { - loginAsDaria(); - }); - - it('should show "Weitere Felder auswerten" button', () => { - exist(component.getWeitereFelderAuswertenButton()); - }); - - it('should have all form elements after button click', () => { - component.getWeitereFelderAuswertenButton().click(); - - exist(fieldsFormComponent.getFormEngineInput()); - exist(fieldsFormComponent.getFormIdInput()); - exist(fieldsFormComponent.getDataFieldInput(0)); - exist(fieldsFormComponent.getDataFieldDeleteButton(0)); - exist(fieldsFormComponent.getAddFieldButton()); - exist(fieldsFormComponent.getSaveButton()); - exist(fieldsFormComponent.getCancelButton()); - }); - - it('should add data field', () => { - fieldsFormComponent.addField(); - - exist(fieldsFormComponent.getDataFieldInput(1)); - }); - - it('should enter text in both data fields', () => { - fieldsFormComponent.enterDataFieldPath(dataText1, 0); - fieldsFormComponent.enterDataFieldPath(dataText2, 1); - - haveValue(fieldsFormComponent.getDataFieldInput(0), dataText1); - haveValue(fieldsFormComponent.getDataFieldInput(1), dataText2); - }); - - it('should delete data fields', () => { - fieldsFormComponent.deleteDataField(0); - haveValue(fieldsFormComponent.getDataFieldInput(0), dataText2); - }); - - it('should navigate to statistik on cancel', () => { - fieldsFormComponent.cancel(); - - exist(component.getWeitereFelderAuswertenButton()); - }); -}); diff --git a/alfa-client/apps/admin-e2e/src/fixtures/argocd/by-admin-dev.yaml b/alfa-client/apps/admin-e2e/src/fixtures/argocd/by-admin-dev.yaml index 1a25b8d740968a62ad08eb5ca4947866650f04ac..f7781946a0ca5c123c2dea8a408caab17f871793 100644 --- a/alfa-client/apps/admin-e2e/src/fixtures/argocd/by-admin-dev.yaml +++ b/alfa-client/apps/admin-e2e/src/fixtures/argocd/by-admin-dev.yaml @@ -87,7 +87,7 @@ alfa_client: ingress: use_staging_cert: true -admin_client: +administration_client: enabled: true ingress: use_staging_cert: true diff --git a/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.executor.ts b/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.executor.ts new file mode 100644 index 0000000000000000000000000000000000000000..7e9b392696e1839765a8d8c6668722db0bffd262 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.executor.ts @@ -0,0 +1,22 @@ +import { AggregationMapping, FieldMapping } from '@admin-client/reporting-shared'; +import { AggregationMappingFormE2EComponent } from '../../components/aggregation-mapping/aggregation-mapping-form.e2e.component'; +import { enterWith } from '../../support/cypress.util'; + +export class E2EAggregationMappingExecutor { + private formComponent: AggregationMappingFormE2EComponent = new AggregationMappingFormE2EComponent(); + + public fillFormular(aggregationMapping: AggregationMapping): void { + enterWith(this.formComponent.getNameInput(), aggregationMapping.name); + enterWith(this.formComponent.getFormEngineInput(), aggregationMapping.formIdentifier.formEngineName); + enterWith(this.formComponent.getFormIdInput(), aggregationMapping.formIdentifier.formId); + + aggregationMapping.mappings.forEach((fieldMapping, index) => { + this.enterFieldMapping(fieldMapping, index); + }); + } + + private enterFieldMapping(fieldMapping: FieldMapping, index: number): void { + enterWith(this.formComponent.getSourceMappingFieldInput(index), fieldMapping.sourcePath); + enterWith(this.formComponent.getTargetMappingFieldInput(index), fieldMapping.targetPath); + } +} diff --git a/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.helper.ts b/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.helper.ts new file mode 100644 index 0000000000000000000000000000000000000000..b39a391240e6a139c1f9df4e673e4e3ccda6e3e8 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.helper.ts @@ -0,0 +1,16 @@ +import { AggregationMapping } from '@admin-client/reporting-shared'; +import { E2EAggregationMappingExecutor } from './aggregation-mapping.executor'; +import { E2EAggregationMapperNavigator } from './aggregation-mapping.navigator'; + +export class E2EAggregationMappingHelper { + private readonly navigator: E2EAggregationMapperNavigator = new E2EAggregationMapperNavigator(); + private readonly executor: E2EAggregationMappingExecutor = new E2EAggregationMappingExecutor(); + + public openStatistik(): void { + this.navigator.openStatistik(); + } + + public fillFormular(aggregationMapping: AggregationMapping): void { + this.executor.fillFormular(aggregationMapping); + } +} diff --git a/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.navigator.ts b/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.navigator.ts new file mode 100644 index 0000000000000000000000000000000000000000..f82cc547926244caca1d78afeb30be3a843b665e --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.navigator.ts @@ -0,0 +1,18 @@ +import { AggregationMappingE2EComponent } from '../../components/aggregation-mapping/aggregation-mapping.e2e.component'; +import { MainPage } from '../../page-objects/main.po'; +import { exist } from '../../support/cypress.util'; +import { E2EAppHelper } from '../app.helper'; + +export class E2EAggregationMapperNavigator { + private readonly appHelper: E2EAppHelper = new E2EAppHelper(); + + private readonly mainPage: MainPage = new MainPage(); + + private readonly aggregationMappingPage: AggregationMappingE2EComponent = new AggregationMappingE2EComponent(); + + public openStatistik(): void { + this.appHelper.navigateToDomain(); + this.mainPage.getStatistikNavigationItem().click(); + exist(this.aggregationMappingPage.getHeaderText()); + } +} diff --git a/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.verifier.ts b/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.verifier.ts new file mode 100644 index 0000000000000000000000000000000000000000..df93defea446b90102747c7af9ddd41edb254e59 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/helper/aggregation-mapping/aggregation-mapping.verifier.ts @@ -0,0 +1,34 @@ +import { AggregationMapping, FieldMapping } from '@admin-client/reporting-shared'; +import { AggregationMappingFormE2EComponent } from '../../components/aggregation-mapping/aggregation-mapping-form.e2e.component'; +import { + AggregationMappingE2EComponent, + AggregationMappingListItemE2EComponent, +} from '../../components/aggregation-mapping/aggregation-mapping.e2e.component'; +import { haveText, haveValue } from '../../support/cypress.util'; + +export class E2EAggregationMappingVerifier { + private component: AggregationMappingE2EComponent = new AggregationMappingE2EComponent(); + private formComponent: AggregationMappingFormE2EComponent = new AggregationMappingFormE2EComponent(); + + public verifyFieldMapping(fieldMapping: FieldMapping, index: number): void { + haveValue(this.formComponent.getSourceMappingFieldInput(index), fieldMapping.sourcePath); + haveValue(this.formComponent.getTargetMappingFieldInput(index), fieldMapping.targetPath); + } + + public verifyForm(aggregationMapping: AggregationMapping): void { + haveValue(this.formComponent.getNameInput(), aggregationMapping.name); + haveValue(this.formComponent.getFormEngineInput(), aggregationMapping.formIdentifier.formEngineName); + haveValue(this.formComponent.getFormIdInput(), aggregationMapping.formIdentifier.formId); + + aggregationMapping.mappings.forEach((fieldMapping, index) => { + this.verifyFieldMapping(fieldMapping, index); + }); + } + + public verifyAggregationMappingInList(aggregationMapping: AggregationMapping): void { + const listItem: AggregationMappingListItemE2EComponent = this.component.getListItem(aggregationMapping.name); + haveText(listItem.getName(), aggregationMapping.name); + haveText(listItem.getFormEngineName(), aggregationMapping.formIdentifier.formEngineName); + haveText(listItem.getFormId(), aggregationMapping.formIdentifier.formId); + } +} diff --git a/alfa-client/apps/admin-e2e/src/helper/app.helper.ts b/alfa-client/apps/admin-e2e/src/helper/app.helper.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee3170e548ffc5b7cad695a0f28d0b1e7e993a11 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/helper/app.helper.ts @@ -0,0 +1,9 @@ +import { MainPage } from '../page-objects/main.po'; + +export class E2EAppHelper { + private readonly mainPage: MainPage = new MainPage(); + + public navigateToDomain(): void { + this.mainPage.getHeader().getLogo().click(); + } +} diff --git a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.executor.ts b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.executor.ts index ec1bbb05e560d3c45de062711da3da9ed8cf39c1..d6a839a2c755d0e43ab6608f1a8dcb60b57855b0 100644 --- a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.executor.ts +++ b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.executor.ts @@ -34,6 +34,26 @@ export class E2EBenutzerExecutor { this.modifyOrganisationsEinheiten(user.organisationseinheiten); } + public modifyVorname(vorname: string): void { + this.benutzerPage.getVornameInput().type(vorname); + } + + public modifyNachname(nachname: string): void { + this.benutzerPage.getNachnameInput().type(nachname); + } + + public modifyBenutzername(benutzername: string): void { + this.benutzerPage.getBenutzernameInput().type(benutzername); + } + + public modifyEmail(email: string): void { + this.benutzerPage.getMailInput().type(email); + } + + public checkUserRole(): void { + this.benutzerPage.getUserCheckbox().getRoot().click(); + } + public modifyOrganisationsEinheiten(organisationseinheiten: OrganisationsEinheitE2E[]): void { organisationseinheiten.forEach((name: string) => this.benutzerPage.getOrganisationsEinheitCheckbox(name).click()); } diff --git a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.helper.ts b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.helper.ts index 0c0ad0c26492851788cb31352522ac0244f4217f..daf08ce5a6fb0869a60531b9a0179e44d4ce4b72 100644 --- a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.helper.ts +++ b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.helper.ts @@ -1,6 +1,7 @@ import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; import { OrganisationsEinheitE2E } from '../../model/organisations-einheit'; import { AdminUserE2E } from '../../model/util'; +import { waitForSpinnerToDisappear } from '../../page-objects/main.po'; import { E2EBenutzerExecutor } from './benutzer.executor'; import { E2EBenutzerNavigator } from './benutzer.navigator'; @@ -34,6 +35,26 @@ export class E2EBenutzerHelper { this.modifyBenutzer(user); } + public editVorname(vorname: string): void { + this.executor.modifyVorname(vorname); + } + + public editNachname(nachname: string): void { + this.executor.modifyNachname(nachname); + } + + public editBenutzername(username: string): void { + this.executor.modifyBenutzername(username); + } + + public editEmail(email: string): void { + this.executor.modifyEmail(email); + } + + public addUserRole(): void { + this.executor.checkUserRole(); + } + private modifyBenutzer(user: AdminUserE2E): void { this.executor.modifyBenutzer(user); } @@ -53,11 +74,13 @@ export class E2EBenutzerHelper { public saveBenutzer(): void { this.executor.saveBenutzer(); + waitForSpinnerToDisappear(); } public deleteBenutzer(userName: string): void { this.openBenutzerPage(userName); this.executor.deleteBenutzer(); + waitForSpinnerToDisappear(); } public openBenutzerPage(userName: string): void { diff --git a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.verifier.ts b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.verifier.ts index 277f735a6038200c0c21c6efaa8b06d71d6d9e9c..22f42fb09f6d560491cc9230cb705a3729059dec 100644 --- a/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.verifier.ts +++ b/alfa-client/apps/admin-e2e/src/helper/benutzer/benutzer.verifier.ts @@ -1,10 +1,12 @@ +import { EMPTY_STRING } from '@alfa-client/tech-shared'; import { BenutzerE2EComponent, BenutzerListE2EComponent, BenutzerListItemE2EComponent, } from '../../components/benutzer/benutzer.e2e.component'; +import { E2EBenutzerValidationMessages } from '../../model/benutzer.messages'; import { AdminUserE2E } from '../../model/util'; -import { contains, exist, notBeEnabled, notExist } from '../../support/cypress.util'; +import { contains, exist, haveText, notBeEnabled, notContains, notExist } from '../../support/cypress.util'; import { AlfaRollen } from '../../support/user-util'; export class E2EBenutzerVerifier { @@ -67,4 +69,62 @@ export class E2EBenutzerVerifier { private getBenutzerItem(userName: string): BenutzerListItemE2EComponent { return this.benutzerListPage.getItem(userName); } + + public verifyEmptyVornameError(): void { + exist(this.benutzerPage.getVornameTextFeld().getErrorMessage()); + haveText(this.benutzerPage.getVornameTextFeld().getErrorMessage(), E2EBenutzerValidationMessages.VORNAME_EMPTY_ERROR); + } + + public verifyEmptyNameError(): void { + exist(this.benutzerPage.getNachnameTextFeld().getErrorMessage()); + haveText(this.benutzerPage.getNachnameTextFeld().getErrorMessage(), E2EBenutzerValidationMessages.NACHNAME_EMPTY_ERROR); + } + + public verifyUsernameLengthError(): void { + exist(this.benutzerPage.getBenutzernameTextFeld().getErrorMessage()); + haveText( + this.benutzerPage.getBenutzernameTextFeld().getErrorMessage(), + E2EBenutzerValidationMessages.BENUTZERNAME_SIZE_ERROR, + ); + } + + public verifyUsernameExistsError(): void { + exist(this.benutzerPage.getBenutzernameTextFeld().getErrorMessage()); + haveText(this.benutzerPage.getBenutzernameTextFeld().getErrorMessage(), E2EBenutzerValidationMessages.BENUTZER_NAME_EXISTS); + } + + public verifyInvalidEmailError(): void { + exist(this.benutzerPage.getEmailTextFeld().getErrorMessage()); + haveText(this.benutzerPage.getEmailTextFeld().getErrorMessage(), E2EBenutzerValidationMessages.EMAIL_INVALID_ERROR); + } + + public verifyEmailExistsError(): void { + exist(this.benutzerPage.getEmailTextFeld().getErrorMessage()); + haveText(this.benutzerPage.getEmailTextFeld().getErrorMessage(), E2EBenutzerValidationMessages.EMAIL_EXISTS); + } + + public verifyEmptyRollenError(): void { + exist(this.benutzerPage.getRollenValidationError()); + contains(this.benutzerPage.getRollenValidationError(), E2EBenutzerValidationMessages.ROLLEN_EMPTY_ERROR); + } + + public verifyVornameHasNoError(): void { + haveText(this.benutzerPage.getVornameTextFeld().getErrorMessage(), EMPTY_STRING); + } + + public verifyLastNameHasNoError(): void { + haveText(this.benutzerPage.getNachnameTextFeld().getErrorMessage(), EMPTY_STRING); + } + + public verifyUsernameHasNoError(): void { + haveText(this.benutzerPage.getBenutzernameTextFeld().getErrorMessage(), EMPTY_STRING); + } + + public verifyEmailHasNoError(): void { + haveText(this.benutzerPage.getEmailTextFeld().getErrorMessage(), EMPTY_STRING); + } + + public verifyRollenHasNoError(): void { + notContains(this.benutzerPage.getRollenValidationError(), E2EBenutzerValidationMessages.ROLLEN_EMPTY_ERROR); + } } diff --git a/alfa-client/apps/admin-e2e/src/model/benutzer.messages.ts b/alfa-client/apps/admin-e2e/src/model/benutzer.messages.ts new file mode 100644 index 0000000000000000000000000000000000000000..db6aa9f09f5f2115a1de2e6295b564ea845c4aa8 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/model/benutzer.messages.ts @@ -0,0 +1,9 @@ +export enum E2EBenutzerValidationMessages { + VORNAME_EMPTY_ERROR = 'Bitte Vorname ausfüllen', + NACHNAME_EMPTY_ERROR = 'Bitte Nachname ausfüllen', + BENUTZERNAME_SIZE_ERROR = 'Benutzername muss mindestens 3 und darf höchstens 255 Zeichen enthalten', + EMAIL_INVALID_ERROR = 'Bitte E-Mail korrekt ausfüllen', + ROLLEN_EMPTY_ERROR = 'Bitte Rollen ausfüllen', + BENUTZER_NAME_EXISTS = 'Benutzername bereits verwendet', + EMAIL_EXISTS = 'Email-Adresse bereits verwendet', +} diff --git a/alfa-client/apps/admin-e2e/src/page-objects/main.po.ts b/alfa-client/apps/admin-e2e/src/page-objects/main.po.ts index 75ecbdaefa5d692ca637af325afb58ec05f80abe..b07542fc24c5b83a7d9b623842d7a89ee4e9d9ce 100644 --- a/alfa-client/apps/admin-e2e/src/page-objects/main.po.ts +++ b/alfa-client/apps/admin-e2e/src/page-objects/main.po.ts @@ -31,7 +31,7 @@ export class MainPage { private readonly benutzerNavigationItem: string = 'link-path-benutzer'; private readonly organisationEinheitNavigationItem: string = 'link-path-organisationseinheiten'; private readonly postfachNavigationItem: string = 'link-path-postfach'; - private readonly statistikNavigationItem: string = 'link-path-statistik'; + private readonly statistikNavigationItem: string = 'link-path-auswertungen'; public getBuildInfo(): BuildInfoE2EComponent { return this.buildInfo; diff --git a/alfa-client/apps/admin-e2e/src/support/cypress-helper.ts b/alfa-client/apps/admin-e2e/src/support/cypress-helper.ts index a48d664eb799ccad45bf162f74c804d524fbf8d4..5d457ea558240cbd2d5e415ffc8136f8be4452ad 100644 --- a/alfa-client/apps/admin-e2e/src/support/cypress-helper.ts +++ b/alfa-client/apps/admin-e2e/src/support/cypress-helper.ts @@ -21,6 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { HttpMethod } from '@alfa-client/tech-shared'; import { Interception, RouteHandler, RouteMatcher } from 'cypress/types/net-stubbing'; import { OrganisationsEinheitE2E } from './organisationseinheit'; @@ -31,6 +32,7 @@ enum CypressTasks { enum MongoCollections { ORGANISATIONS_EINHEIT = 'organisationsEinheit', + AGGREGATION_MAPPING = 'aggregationMapping', } const DOWNLOAD_FOLDER: string = 'cypress/downloads'; @@ -47,7 +49,7 @@ export function intercept(method: string, url: string): Cypress.Chainable<null> return cy.intercept(method, url); } -export function interceptWithResponse(method, url: RouteMatcher, response: RouteHandler): Cypress.Chainable<null> { +export function interceptWithResponse(method: HttpMethod, url: RouteMatcher, response: RouteHandler): Cypress.Chainable<null> { return cy.intercept(method, url, response); } @@ -109,5 +111,5 @@ export function initOrganisationsEinheitenData(data: OrganisationsEinheitE2E[]): } export function dropCollections() { - cy.task(CypressTasks.DROP_COLLECTIONS, [MongoCollections.ORGANISATIONS_EINHEIT]); + cy.task(CypressTasks.DROP_COLLECTIONS, [MongoCollections.ORGANISATIONS_EINHEIT, MongoCollections.AGGREGATION_MAPPING]); } diff --git a/alfa-client/apps/admin/Jenkinsfile b/alfa-client/apps/admin/Jenkinsfile index 8ae0ccfcdb74541493558353c758eb9896742caa..2253bfcdd61fcf724892c3f0efef1b21970e4f80 100644 --- a/alfa-client/apps/admin/Jenkinsfile +++ b/alfa-client/apps/admin/Jenkinsfile @@ -50,7 +50,7 @@ pipeline { steps { script { FAILED_STAGE = env.STAGE_NAME - VERSION = getAdminPackageJsonVersion() + VERSION = getPackageJsonVersion() if(isReleaseBranch()){ if ( !isReleaseVersion([VERSION]) ) { @@ -65,7 +65,7 @@ pipeline { } } - stage('Build admin client and its docker image') { + stage('Build administration client and its docker image') { steps { script { FAILED_STAGE=env.STAGE_NAME @@ -75,9 +75,9 @@ pipeline { sh 'pnpm install --frozen-lockfile' if (isReleaseBranch()) { - sh 'pnpm run ci-prodBuild-admin' + sh 'pnpm run ci-prodBuild-administration' } else { - sh 'pnpm run ci-build-admin' + sh 'pnpm run ci-build-administration' } if (isMainBranch()) { withSonarQubeEnv('sonarqube-ozg-sh'){ @@ -148,7 +148,7 @@ pipeline { stage('Trigger Test rollout') { when { - branch 'release-admin' + branch 'release-administration' } steps { script { @@ -176,7 +176,7 @@ pipeline { Boolean isReleaseBranch() { - return env.BRANCH_NAME == 'release-admin' + return env.BRANCH_NAME == 'release-administration' } def validateBranchName(branchName) { @@ -224,24 +224,24 @@ Void setNewTestVersion() { Void setNewGitopsVersion(String environment) { dir("gitops") { - def envFile = "${environment}/application/values/admin-client-values.yaml" + def envFile = "${environment}/application/values/administration-client-values.yaml" def envVersions = readYaml file: envFile - envVersions.admin_client.image.tag = IMAGE_TAG - envVersions.admin_client.helm.version = HELM_CHART_VERSION + envVersions.administration_client.image.tag = IMAGE_TAG + envVersions.administration_client.helm.version = HELM_CHART_VERSION writeYaml file: envFile, data: envVersions, overwrite: true if (hasValuesFileChanged(environment)) { sh "git add ${envFile}" - sh "git commit -m 'jenkins rollout ${environment} admin_client version ${IMAGE_TAG}'" + sh "git commit -m 'jenkins rollout ${environment} administration_client version ${IMAGE_TAG}'" } } } Boolean hasValuesFileChanged(String environment) { - return sh (script: "git status | grep '${environment}/application/values/admin-client-values.yaml'", returnStatus: true) == env.SH_SUCCESS_STATUS_CODE as Integer + return sh (script: "git status | grep '${environment}/application/values/administration-client-values.yaml'", returnStatus: true) == env.SH_SUCCESS_STATUS_CODE as Integer } @@ -263,11 +263,11 @@ Void tagAndPushDockerImage(String newTag){ withCredentials([usernamePassword(credentialsId: 'jenkins-nexus-login', usernameVariable: 'USER', passwordVariable: 'PASSWORD')]) { sh 'docker login docker.ozg-sh.de -u ${USER} -p ${PASSWORD}' - sh "docker tag docker.ozg-sh.de/admin-client:build-latest docker.ozg-sh.de/admin-client:${newTag}" - sh "docker push docker.ozg-sh.de/admin-client:${newTag}" + sh "docker tag docker.ozg-sh.de/administration-client:build-latest docker.ozg-sh.de/administration-client:${newTag}" + sh "docker push docker.ozg-sh.de/administration-client:${newTag}" } } -String getAdminPackageJsonVersion() { +String getPackageJsonVersion() { def packageJSON = readJSON file: 'alfa-client/apps/admin/package.json' def packageJSONVersion = packageJSON.version echo packageJSONVersion @@ -277,10 +277,10 @@ String getAdminPackageJsonVersion() { Void deployHelmChart(String helmChartVersion) { withCredentials([usernamePassword(credentialsId: 'jenkins-nexus-login', usernameVariable: 'USERNAME', passwordVariable: 'PASSWORD')]){ if (isReleaseBranch()) { - result = sh script: '''curl -u $USERNAME:$PASSWORD https://nexus.ozg-sh.de/service/rest/v1/components?repository=ozg-base-apps -F file=@admin-client-'''+helmChartVersion+'''.tgz''', returnStdout: true + result = sh script: '''curl -u $USERNAME:$PASSWORD https://nexus.ozg-sh.de/service/rest/v1/components?repository=ozg-base-apps -F file=@administration-client-'''+helmChartVersion+'''.tgz''', returnStdout: true } else { - result = sh script: '''curl -u $USERNAME:$PASSWORD https://nexus.ozg-sh.de/service/rest/v1/components?repository=ozg-base-apps-snapshot -F file=@admin-client-'''+helmChartVersion+'''.tgz''', returnStdout: true + result = sh script: '''curl -u $USERNAME:$PASSWORD https://nexus.ozg-sh.de/service/rest/v1/components?repository=ozg-base-apps-snapshot -F file=@administration-client-'''+helmChartVersion+'''.tgz''', returnStdout: true } if (result != '') { @@ -296,9 +296,9 @@ Boolean isMainBranch() { Void sendFailureMessage() { def room = '' def data = """{"msgtype":"m.text", \ - "body":"Admin-Client: Build Failed. Stage: ${FAILED_STAGE} Build-ID: ${env.BUILD_NUMBER} Link: ${JENKINS_URL}", \ + "body":"Administration-Client: Build Failed. Stage: ${FAILED_STAGE} Build-ID: ${env.BUILD_NUMBER} Link: ${JENKINS_URL}", \ "format": "org.matrix.custom.html", \ - "formatted_body":"Admin-Client: Build Failed. Stage: ${FAILED_STAGE} Build-ID: <a href='${JENKINS_URL}'>${env.BUILD_NUMBER}</a>"}""" + "formatted_body":"Administration-Client: Build Failed. Stage: ${FAILED_STAGE} Build-ID: <a href='${JENKINS_URL}'>${env.BUILD_NUMBER}</a>"}""" if (isMainBranch()) { room = "!iQPAvQIiRwRpNOszjw:matrix.ozg-sh.de" diff --git a/alfa-client/apps/admin/package.json b/alfa-client/apps/admin/package.json index d124f972122f91564b8d04044ab8541beb8b4ae6..65a6ef5178180d831904cfd25ca58ae832164385 100644 --- a/alfa-client/apps/admin/package.json +++ b/alfa-client/apps/admin/package.json @@ -1,4 +1,4 @@ { "name": "admin", - "version": "1.7.0-SNAPSHOT" + "version": "1.8.0-SNAPSHOT" } diff --git a/alfa-client/apps/admin/project.json b/alfa-client/apps/admin/project.json index 9876b834e6f340fb8d71da344f75a75a9386f41b..8bffa965bb8057ebe41383397a5bf311660a5f24 100644 --- a/alfa-client/apps/admin/project.json +++ b/alfa-client/apps/admin/project.json @@ -15,10 +15,7 @@ "main": "apps/admin/src/main.ts", "polyfills": ["zone.js"], "tsConfig": "apps/admin/tsconfig.app.json", - "allowedCommonJsDependencies": [ - "sanitize-filename-ts", - "jsrsasign" - ], + "allowedCommonJsDependencies": ["sanitize-filename-ts", "jsrsasign"], "assets": [ "apps/admin/src/assets", { @@ -112,7 +109,7 @@ "engine": "docker", "push": false, "metadata": { - "images": ["docker.ozg-sh.de/admin-client"], + "images": ["docker.ozg-sh.de/administration-client"], "load": true, "tags": ["build-latest"] } diff --git a/alfa-client/apps/admin/src/app/app.component.spec.ts b/alfa-client/apps/admin/src/app/app.component.spec.ts index baa8c7fcc532a0e859fe596782a94bf7b2257803..fb01cab1df3733fa40d66591f3562cf452661d65 100644 --- a/alfa-client/apps/admin/src/app/app.component.spec.ts +++ b/alfa-client/apps/admin/src/app/app.component.spec.ts @@ -27,11 +27,24 @@ import { KeycloakTokenService } from '@admin/keycloak-shared'; import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; import { BuildInfoComponent } from '@alfa-client/common'; import { createEmptyStateResource, createStateResource, HasLinkPipe } from '@alfa-client/tech-shared'; -import { existsAsHtmlElement, getElementComponentFromFixtureByCss, Mock, mock, notExistsAsHtmlElement, } from '@alfa-client/test-utils'; +import { + existsAsHtmlElement, + getElementComponentFromFixtureByCss, + Mock, + mock, + notExistsAsHtmlElement, +} from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router, RouterOutlet } from '@angular/router'; import { AuthenticationService } from '@authentication'; -import { AdminLogoIconComponent, MailboxIconComponent, NavbarComponent, NavItemComponent, OrgaUnitIconComponent, UsersIconComponent, } from '@ods/system'; +import { + AdminLogoIconComponent, + MailboxIconComponent, + NavbarComponent, + NavItemComponent, + OrgaUnitIconComponent, + UsersIconComponent, +} from '@ods/system'; import { createConfigurationResource } from 'libs/admin/configuration-shared/test/configuration'; import { MenuContainerComponent } from 'libs/admin/configuration/src/lib/menu-container/menu-container.component'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; @@ -297,7 +310,7 @@ describe('AppComponent', () => { it('should navigate to statistik if aggregation mapping link exists', () => { component._navigateByConfiguration(createConfigurationResource([ConfigurationLinkRel.AGGREGATION_MAPPINGS])); - expect(router.navigate).toHaveBeenCalledWith(['/statistik']); + expect(router.navigate).toHaveBeenCalledWith(['/auswertungen']); }); it('should navigate to unavailable page if no link exists', () => { diff --git a/alfa-client/apps/admin/src/app/app.component.ts b/alfa-client/apps/admin/src/app/app.component.ts index 909b6cd198bd3ccb91dd61e922abb6a1ed79210c..989a4ad4b3fd75d2767750eb535a8db27a9d6c29 100644 --- a/alfa-client/apps/admin/src/app/app.component.ts +++ b/alfa-client/apps/admin/src/app/app.component.ts @@ -123,7 +123,7 @@ export class AppComponent implements OnInit { if (hasLink(configurationResource, ConfigurationLinkRel.SETTING)) { this.navigate(ROUTES.POSTFACH); } else if (hasLink(configurationResource, ConfigurationLinkRel.AGGREGATION_MAPPINGS)) { - this.navigate(ROUTES.STATISTIK); + this.navigate(ROUTES.AGGREGATION_MAPPING); } else { this.navigate(ROUTES.UNAVAILABLE); } diff --git a/alfa-client/apps/admin/src/app/app.routes.ts b/alfa-client/apps/admin/src/app/app.routes.ts index c5974993c237ec15254220a64157b63ae0f46136..dcdc28a4fdd107ad204ac6caf3b94775462e953e 100644 --- a/alfa-client/apps/admin/src/app/app.routes.ts +++ b/alfa-client/apps/admin/src/app/app.routes.ts @@ -26,10 +26,10 @@ import { ROUTES } from '@admin-client/shared'; import { UserFormComponent } from '@admin-client/user'; import { ApiRootLinkRel } from '@alfa-client/api-root-shared'; import { Route } from '@angular/router'; +import { AggregationMappingFormPageComponent } from '../pages/aggregation-mapping/aggregation-mapping-form-page/aggregation-mapping-form-page.component'; +import { AggregationMappingListPageComponent } from '../pages/aggregation-mapping/aggregation-mapping-list-page/aggregation-mapping-list-page.component'; import { OrganisationsEinheitPageComponent } from '../pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component'; import { PostfachPageComponent } from '../pages/postfach/postfach-page/postfach-page.component'; -import { StatistikFieldsFormPageComponent } from '../pages/statistik/statistik-fields-form-page/statistik-fields-form-page.component'; -import { StatistikPageComponent } from '../pages/statistik/statistik-page/statistik-page.component'; import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component'; import { UserFormPageComponent } from '../pages/user/user-form-page/user-form-page.component'; import { UserListPageComponent } from '../pages/user/user-list-page/user-list-page.component'; @@ -83,17 +83,25 @@ export const appRoutes: Route[] = [ title: 'Unavailable', }, { - path: ROUTES.STATISTIK, - component: StatistikPageComponent, + path: ROUTES.AGGREGATION_MAPPING, + component: AggregationMappingListPageComponent, title: 'Admin | Statistik', runGuardsAndResolvers: 'always', canActivate: [configurationGuard], data: <GuardData>{ linkRelName: ConfigurationLinkRel.AGGREGATION_MAPPINGS }, }, { - path: ROUTES.STATISTIK_NEU, - component: StatistikFieldsFormPageComponent, - title: 'Admin | Statistik weitere Felder auswerten', + path: ROUTES.AGGREGATION_MAPPING_NEU, + component: AggregationMappingFormPageComponent, + title: 'Admin | Auswertung anlegen', + runGuardsAndResolvers: 'always', + canActivate: [configurationGuard], + data: <GuardData>{ linkRelName: ConfigurationLinkRel.AGGREGATION_MAPPINGS }, + }, + { + path: ROUTES.AGGREGATION_MAPPING_ID, + component: AggregationMappingFormPageComponent, + title: 'Admin | Auswertung bearbeiten', runGuardsAndResolvers: 'always', canActivate: [configurationGuard], data: <GuardData>{ linkRelName: ConfigurationLinkRel.AGGREGATION_MAPPINGS }, diff --git a/alfa-client/apps/admin/src/main/helm/Chart.yaml b/alfa-client/apps/admin/src/main/helm/Chart.yaml index 7f1c7ec2c239956bf3b275ede276c5520603a2b1..98e2c2f72c110125d1ecf1e333e1b696841e9834 100644 --- a/alfa-client/apps/admin/src/main/helm/Chart.yaml +++ b/alfa-client/apps/admin/src/main/helm/Chart.yaml @@ -24,7 +24,7 @@ apiVersion: v1 appVersion: '1.0' -description: A Helm chart for Admin Client -name: admin-client +description: A Helm chart for Administration Client +name: administration-client version: 0.0.0-MANAGED-BY-JENKINS icon: https://simpleicons.org/icons/helm.svg diff --git a/alfa-client/apps/admin/src/main/helm/templates/_helpers.tpl b/alfa-client/apps/admin/src/main/helm/templates/_helpers.tpl index 3e25f32b0a413172ebebbcd61487f45237d1e420..fd312ca2b186f6b8f711f5af52270e35a6c8905a 100644 --- a/alfa-client/apps/admin/src/main/helm/templates/_helpers.tpl +++ b/alfa-client/apps/admin/src/main/helm/templates/_helpers.tpl @@ -51,7 +51,7 @@ {{/* Default Labels: Helm recommended best-practice labels https://helm.sh/docs/chart_best_practices/labels/ */}} {{- define "app.defaultLabels" }} -app.kubernetes.io/instance: admin-client +app.kubernetes.io/instance: administration-client app.kubernetes.io/managed-by: {{ include "app.managedBy" . }} app.kubernetes.io/name: {{ .Release.Name }} app.kubernetes.io/namespace: {{ include "app.namespace" . }} @@ -80,12 +80,12 @@ app.kubernetes.io/namespace: {{ include "app.namespace" . }} {{- define "app.serviceAccountName" -}} -{{ printf "%s" ( (.Values.serviceAccount).name | default "admin-client-service-account" ) }} +{{ printf "%s" ( (.Values.serviceAccount).name | default "administration-client-service-account" ) }} {{- end -}} {{- define "app.baseDomain" -}} -{{- printf "%s-%s.%s" (include "app.ozgcloudBezeichner" . ) (.Values.ozgcloud).adminDomainSuffix (include "app.baseUrl" . ) }} +{{- printf "%s-%s.%s" (include "app.ozgcloudBezeichner" . ) (.Values.ozgcloud).administrationDomainSuffix (include "app.baseUrl" . ) }} {{- end -}} {{- define "app.ozgcloudBezeichner" -}} diff --git a/alfa-client/apps/admin/src/main/helm/templates/deployment.yaml b/alfa-client/apps/admin/src/main/helm/templates/deployment.yaml index 313812a726ff3f48bdab917e1b7ecfe8812cc3c0..d42402bb33022a66ed4f19427f3f9072b0330e2f 100644 --- a/alfa-client/apps/admin/src/main/helm/templates/deployment.yaml +++ b/alfa-client/apps/admin/src/main/helm/templates/deployment.yaml @@ -45,7 +45,7 @@ spec: metadata: labels: {{- include "app.defaultLabels" . | indent 8 }} - component: admin-client + component: administration-client spec: {{- if (.Values.serviceAccount).create }} serviceAccountName: {{ include "app.serviceAccountName" . }} @@ -69,7 +69,7 @@ spec: image: "{{ .Values.image.repo }}/{{ .Values.image.name }}:{{ coalesce (.Values.image).tag "latest" }}" imagePullPolicy: Always - name: admin-client + name: administration-client startupProbe: httpGet: diff --git a/alfa-client/apps/admin/src/main/helm/templates/ingress.yaml b/alfa-client/apps/admin/src/main/helm/templates/ingress.yaml index eb9b7523e636cd573782b12cd4851ef087afa782..3a98e2e4a60e9dab74d960f6b51860dbc22d7fe3 100644 --- a/alfa-client/apps/admin/src/main/helm/templates/ingress.yaml +++ b/alfa-client/apps/admin/src/main/helm/templates/ingress.yaml @@ -49,14 +49,14 @@ spec: backend: service: name: administration - port: + port: number: 8080 - path: / pathType: Prefix backend: service: - name: admin-client - port: + name: administration-client + port: number: 8080 host: {{ include "app.baseDomain" . }} diff --git a/alfa-client/apps/admin/src/main/helm/templates/network_policy.yaml b/alfa-client/apps/admin/src/main/helm/templates/network_policy.yaml index 27b2d9d14a038a1dc5c96d66ca4c464ea4a93679..9a5db61e93b95cb856adc1a337e863d1cf28fc83 100644 --- a/alfa-client/apps/admin/src/main/helm/templates/network_policy.yaml +++ b/alfa-client/apps/admin/src/main/helm/templates/network_policy.yaml @@ -26,7 +26,7 @@ apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: - name: network-policy-admin-client + name: network-policy-administration-client namespace: {{ .Release.Namespace }} spec: podSelector: @@ -46,7 +46,7 @@ spec: {{- end }} egress: - to: - - namespaceSelector: + - namespaceSelector: matchLabels: kubernetes.io/metadata.name: administration - to: diff --git a/alfa-client/apps/admin/src/main/helm/templates/service.yaml b/alfa-client/apps/admin/src/main/helm/templates/service.yaml index 53c0aa703d4563cc7322a8f672fa78c6bda526eb..4c3ba099a334cb01d4bcd07516d8426124e6d5a6 100644 --- a/alfa-client/apps/admin/src/main/helm/templates/service.yaml +++ b/alfa-client/apps/admin/src/main/helm/templates/service.yaml @@ -29,7 +29,7 @@ metadata: namespace: {{ include "app.namespace" . }} labels: {{- include "app.defaultLabels" . | indent 4 }} - component: admin-client-service + component: administration-client-service spec: type: ClusterIP ports: @@ -40,4 +40,4 @@ spec: selector: {{- include "app.matchLabels" . | indent 4 }} - component: admin-client \ No newline at end of file + component: administration-client \ No newline at end of file diff --git a/alfa-client/apps/admin/src/main/helm/values.yaml b/alfa-client/apps/admin/src/main/helm/values.yaml index c1b1df2cdae5978d1c6dbe828a4a36f973f70ede..3bd9e23beaf72777caffab31fcfa85bfc25bf327 100644 --- a/alfa-client/apps/admin/src/main/helm/values.yaml +++ b/alfa-client/apps/admin/src/main/helm/values.yaml @@ -24,9 +24,9 @@ image: repo: docker.ozg-sh.de - name: admin-client + name: administration-client tag: 0.1.0 # [default: latest] replicaCount: 1 ozgcloud: - adminDomainSuffix: admin + administrationDomainSuffix: administration diff --git a/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-form-page/aggregation-mapping-form-page.component.html b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-form-page/aggregation-mapping-form-page.component.html new file mode 100644 index 0000000000000000000000000000000000000000..d51d31da0551989004ab7559e67800e689e0bb1f --- /dev/null +++ b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-form-page/aggregation-mapping-form-page.component.html @@ -0,0 +1 @@ +<admin-aggregation-mapping-form-container></admin-aggregation-mapping-form-container> \ No newline at end of file diff --git a/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-form-page/aggregation-mapping-form-page.component.spec.ts b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-form-page/aggregation-mapping-form-page.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f8d31cfc83f60711e8639ea6e8f3f281eaf8ee5b --- /dev/null +++ b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-form-page/aggregation-mapping-form-page.component.spec.ts @@ -0,0 +1,32 @@ +import { AggregationMappingFormContainerComponent } from '@admin-client/aggregation-mapping'; +import { expectComponentExistsInTemplate } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; +import { AggregationMappingFormPageComponent } from './aggregation-mapping-form-page.component'; + +describe('AggregationMappingFormPageComponent', () => { + let component: AggregationMappingFormPageComponent; + let fixture: ComponentFixture<AggregationMappingFormPageComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AggregationMappingFormPageComponent, MockComponent(AggregationMappingFormContainerComponent)], + }).compileComponents(); + + fixture = TestBed.createComponent(AggregationMappingFormPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('template', () => { + describe('aggregation mapping form', () => { + it('should exists', () => { + expectComponentExistsInTemplate(fixture, AggregationMappingFormContainerComponent); + }); + }); + }); +}); diff --git a/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-form-page/aggregation-mapping-form-page.component.ts b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-form-page/aggregation-mapping-form-page.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a85688c8b243b0bd400b30d7db4b088514227554 --- /dev/null +++ b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-form-page/aggregation-mapping-form-page.component.ts @@ -0,0 +1,10 @@ +import { AggregationMappingFormContainerComponent } from '@admin-client/aggregation-mapping'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'admin-aggregation-mapping-form-page', + standalone: true, + imports: [AggregationMappingFormContainerComponent], + templateUrl: './aggregation-mapping-form-page.component.html', +}) +export class AggregationMappingFormPageComponent {} diff --git a/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.html b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-list-page/aggregation-mapping-list-page.component.html similarity index 91% rename from alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.html rename to alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-list-page/aggregation-mapping-list-page.component.html index 5213f512591359ed5e619f95d17e27b36dc4d3a7..ac183ce76a05631f1880ccf6d982135071806adf 100644 --- a/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.html +++ b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-list-page/aggregation-mapping-list-page.component.html @@ -23,4 +23,4 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<admin-statistik-container data-test-id="statistik-container" /> +<admin-aggregation-mapping-list-container data-test-id="aggregation-mapping-container" /> diff --git a/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.spec.ts b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-list-page/aggregation-mapping-list-page.component.spec.ts similarity index 70% rename from alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.spec.ts rename to alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-list-page/aggregation-mapping-list-page.component.spec.ts index 2166339888793c63b712e4b4d67cd286002ac6b0..b036d9be9e55aea5415fdc7d172b401789b9d3c0 100644 --- a/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.spec.ts +++ b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-list-page/aggregation-mapping-list-page.component.spec.ts @@ -21,24 +21,23 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { StatistikContainerComponent } from '@admin-client/statistik'; +import { AggregationMappingListContainerComponent } from '@admin-client/aggregation-mapping'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MockComponent } from 'ng-mocks'; -import { StatistikPageComponent } from './statistik-page.component'; +import { AggregationMappingListPageComponent } from './aggregation-mapping-list-page.component'; -describe('StatistikPageComponent', () => { - let component: StatistikPageComponent; - let fixture: ComponentFixture<StatistikPageComponent>; +describe('AggregationMappingListPageComponent', () => { + let component: AggregationMappingListPageComponent; + let fixture: ComponentFixture<AggregationMappingListPageComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [], - declarations: [StatistikPageComponent, MockComponent(StatistikContainerComponent)], + imports: [AggregationMappingListPageComponent, MockComponent(AggregationMappingListContainerComponent)], }).compileComponents(); }); beforeEach(() => { - fixture = TestBed.createComponent(StatistikPageComponent); + fixture = TestBed.createComponent(AggregationMappingListPageComponent); component = fixture.componentInstance; fixture.detectChanges(); diff --git a/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.ts b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-list-page/aggregation-mapping-list-page.component.ts similarity index 76% rename from alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.ts rename to alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-list-page/aggregation-mapping-list-page.component.ts index 53fd00fc7c8677f8b78216c013f6f191bd24cd76..47858d2978fa3ec8ac8231539f4b1a9fba3c5c85 100644 --- a/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.ts +++ b/alfa-client/apps/admin/src/pages/aggregation-mapping/aggregation-mapping-list-page/aggregation-mapping-list-page.component.ts @@ -21,13 +21,13 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { StatistikContainerComponent } from '@admin-client/statistik'; +import { AggregationMappingListContainerComponent } from '@admin-client/aggregation-mapping'; import { Component } from '@angular/core'; @Component({ - selector: 'statistik-page', + selector: 'admin-aggregation-mapping-list-page', standalone: true, - imports: [StatistikContainerComponent], - templateUrl: './statistik-page.component.html', + imports: [AggregationMappingListContainerComponent], + templateUrl: './aggregation-mapping-list-page.component.html', }) -export class StatistikPageComponent {} +export class AggregationMappingListPageComponent {} diff --git a/alfa-client/apps/admin/src/pages/statistik/statistik-fields-form-page/statistik-fields-form-page.component.html b/alfa-client/apps/admin/src/pages/statistik/statistik-fields-form-page/statistik-fields-form-page.component.html deleted file mode 100644 index a2e1b29bb93d508d58ca98beaf365e1999e301ab..0000000000000000000000000000000000000000 --- a/alfa-client/apps/admin/src/pages/statistik/statistik-fields-form-page/statistik-fields-form-page.component.html +++ /dev/null @@ -1 +0,0 @@ -<admin-statistik-fields-form data-test-id="evaluate-fields-form"></admin-statistik-fields-form> \ No newline at end of file diff --git a/alfa-client/apps/admin/src/pages/statistik/statistik-fields-form-page/statistik-fields-form-page.component.spec.ts b/alfa-client/apps/admin/src/pages/statistik/statistik-fields-form-page/statistik-fields-form-page.component.spec.ts deleted file mode 100644 index 938cc1b0061d2194105629ca70c1e5f8619d881c..0000000000000000000000000000000000000000 --- a/alfa-client/apps/admin/src/pages/statistik/statistik-fields-form-page/statistik-fields-form-page.component.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { AdminStatistikFieldsFormComponent } from '@admin-client/statistik'; -import { existsAsHtmlElement } from '@alfa-client/test-utils'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockComponent } from 'ng-mocks'; -import { getDataTestIdOf } from '../../../../../../libs/tech-shared/test/data-test'; -import { StatistikFieldsFormPageComponent } from './statistik-fields-form-page.component'; - -describe('StatistikFieldsFormPageComponent', () => { - let component: StatistikFieldsFormPageComponent; - let fixture: ComponentFixture<StatistikFieldsFormPageComponent>; - - const evaluateFieldsForm: string = getDataTestIdOf('evaluate-fields-form'); - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [StatistikFieldsFormPageComponent, MockComponent(AdminStatistikFieldsFormComponent)], - }).compileComponents(); - - fixture = TestBed.createComponent(StatistikFieldsFormPageComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('template', () => { - describe('weiter felder auswerten form', () => { - it('should exists', () => { - fixture.detectChanges(); - - existsAsHtmlElement(fixture, evaluateFieldsForm); - }); - }); - }); -}); diff --git a/alfa-client/apps/admin/src/pages/statistik/statistik-fields-form-page/statistik-fields-form-page.component.ts b/alfa-client/apps/admin/src/pages/statistik/statistik-fields-form-page/statistik-fields-form-page.component.ts deleted file mode 100644 index c4122b72d148865b4219d5446a3c2060574b23e7..0000000000000000000000000000000000000000 --- a/alfa-client/apps/admin/src/pages/statistik/statistik-fields-form-page/statistik-fields-form-page.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AdminStatistikFieldsFormComponent } from '@admin-client/statistik'; -import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; - -@Component({ - selector: 'statistik-fields-form-page', - standalone: true, - imports: [CommonModule, AdminStatistikFieldsFormComponent], - templateUrl: './statistik-fields-form-page.component.html', -}) -export class StatistikFieldsFormPageComponent {} diff --git a/alfa-client/apps/admin/src/test/helm/deployment_63_char_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_63_char_test.yaml index aefd7f81b2c70e7b750b09f5545507345b5bf4ab..db1cc9f92904d10299725421e4c7206b7d2e5798 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_63_char_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_63_char_test.yaml @@ -24,7 +24,7 @@ suite: test deyploment less than 63 chars release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: @@ -49,7 +49,7 @@ tests: version: 1.0-test1234567890123123456789012345678901234567890123456789012345678901234567890123456789012345678904567890 asserts: - failedTemplate: - errorMessage: .Chart.Name-.Chart.Version admin-client-1.0-test1234567890123123456789012345678901234567890123456789012345678901234567890123456789012345678904567890 ist zu lang (max. 63 Zeichen) + errorMessage: .Chart.Name-.Chart.Version administration-client-1.0-test1234567890123123456789012345678901234567890123456789012345678901234567890123456789012345678904567890 ist zu lang (max. 63 Zeichen) - it: should not fail on .Chart.Name-.Chart.Version length less than 63 characters asserts: - - notFailedTemplate: {} \ No newline at end of file + - notFailedTemplate: {} diff --git a/alfa-client/apps/admin/src/test/helm/deployment_container_basic_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_container_basic_test.yaml index fd783faf0597f003256bfc3b6cdb3a4b48fda537..301b06eb9bd252846c9cc2e908902380ecf6739d 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_container_basic_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_container_basic_test.yaml @@ -24,7 +24,7 @@ suite: test deployment container basics release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/deployment.yaml @@ -38,20 +38,10 @@ tests: asserts: - equal: path: spec.template.spec.containers[0].image - value: docker.ozg-sh.de/admin-client:0.1.0 + value: docker.ozg-sh.de/administration-client:0.1.0 - equal: path: spec.template.spec.containers[0].name - value: admin-client - - equal: + value: administration-client + - equal: path: spec.template.spec.containers[0].imagePullPolicy value: Always - #- it: should have correct values for container ports - # asserts: - # - contains: - # path: spec.template.spec.containers[0].ports - # content: - # containerPort: 8081 - # name: metrics - # protocol: TCP - - diff --git a/alfa-client/apps/admin/src/test/helm/deployment_container_other_values_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_container_other_values_test.yaml index e73cabb2634d16f2c3c8cd104ecc0ade2af46d78..32d7a2be8b0340082991856a15d75408a3e86360 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_container_other_values_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_container_other_values_test.yaml @@ -22,8 +22,7 @@ # unter der Lizenz sind dem Lizenztext zu entnehmen. # - - # +# # Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den # Ministerpräsidenten des Landes Schleswig-Holstein # Staatskanzlei @@ -49,7 +48,7 @@ suite: test deployment container other values release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/deployment.yaml @@ -57,7 +56,7 @@ set: ozgcloud: environment: dev imagePullSecret: test-image-secret - + tests: - it: should have correct values for container terminationMessagePolicy, terminationMessagePath, stdin, tty asserts: @@ -67,9 +66,9 @@ tests: - equal: path: spec.template.spec.containers[0].terminationMessagePath value: /dev/termination-log - - equal: + - equal: path: spec.template.spec.containers[0].stdin value: true - - equal: + - equal: path: spec.template.spec.containers[0].tty - value: true \ No newline at end of file + value: true diff --git a/alfa-client/apps/admin/src/test/helm/deployment_container_security_context_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_container_security_context_test.yaml index 6acb97512889899878f48eb3f510d1011ca7e0f4..6e42a6a93048f8577650fe57186df2a2db592b0f 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_container_security_context_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_container_security_context_test.yaml @@ -24,7 +24,7 @@ suite: test deployment container security context release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/deployment.yaml @@ -65,4 +65,4 @@ tests: asserts: - equal: path: spec.template.spec.containers[0].securityContext.runAsGroup - value: 1000 \ No newline at end of file + value: 1000 diff --git a/alfa-client/apps/admin/src/test/helm/deployment_defaults_labels_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_defaults_labels_test.yaml index 8a98ecee5e35f378e0bc537dd6fca032198083a4..e616ba28a7f21bc9cb1cdbed81e5b065dc9a290d 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_defaults_labels_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_defaults_labels_test.yaml @@ -24,7 +24,7 @@ suite: test deployment default labels release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/deployment.yaml @@ -39,33 +39,32 @@ tests: - equal: path: metadata.labels value: - app.kubernetes.io/instance: admin-client + app.kubernetes.io/instance: administration-client app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: admin-client + app.kubernetes.io/name: administration-client app.kubernetes.io/namespace: sh-helm-test app.kubernetes.io/part-of: ozgcloud app.kubernetes.io/version: 0.0.0-MANAGED-BY-JENKINS - helm.sh/chart: admin-client-0.0.0-MANAGED-BY-JENKINS - + helm.sh/chart: administration-client-0.0.0-MANAGED-BY-JENKINS + - it: should set spec.selector.matchLabels asserts: - equal: path: spec.selector.matchLabels value: - app.kubernetes.io/name: admin-client + app.kubernetes.io/name: administration-client app.kubernetes.io/namespace: sh-helm-test - - it: should have correct deyploment spec.template.metadata.labels asserts: - equal: path: spec.template.metadata.labels - value: - app.kubernetes.io/instance: admin-client + value: + app.kubernetes.io/instance: administration-client app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: admin-client + app.kubernetes.io/name: administration-client app.kubernetes.io/namespace: sh-helm-test app.kubernetes.io/part-of: ozgcloud app.kubernetes.io/version: 0.0.0-MANAGED-BY-JENKINS - component: admin-client - helm.sh/chart: admin-client-0.0.0-MANAGED-BY-JENKINS \ No newline at end of file + component: administration-client + helm.sh/chart: administration-client-0.0.0-MANAGED-BY-JENKINS diff --git a/alfa-client/apps/admin/src/test/helm/deployment_defaults_topologySpreadConstraints_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_defaults_topologySpreadConstraints_test.yaml index 7f1200536637e1584514d7bc504615702694dc01..dcef5143625d5da53e1333f52826c9ec15348efa 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_defaults_topologySpreadConstraints_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_defaults_topologySpreadConstraints_test.yaml @@ -24,11 +24,11 @@ suite: test deployment topology spread constrains release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/deployment.yaml -set: +set: ozgcloud: environment: test imagePullSecret: test-image-secret @@ -49,4 +49,4 @@ tests: value: ScheduleAnyway - equal: path: spec.template.spec.topologySpreadConstraints[0].labelSelector.matchLabels["app.kubernetes.io/name"] - value: admin-client \ No newline at end of file + value: administration-client diff --git a/alfa-client/apps/admin/src/test/helm/deployment_general_value_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_general_value_test.yaml index 85d1b98c99cefb1080d4db168395c6d764b9a050..114d9e309062cf45213faaeedc9170dc54da3665 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_general_value_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_general_value_test.yaml @@ -24,7 +24,7 @@ suite: test deployment general values release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/deployment.yaml @@ -33,25 +33,23 @@ set: environment: dev imagePullSecret: test-image-secret - tests: - it: should have correct apiVersion asserts: - isKind: of: Deployment - isAPIVersion: - of: "apps/v1" - - - it: should have correct deployment metadata - asserts: + of: 'apps/v1' + + - it: should have correct deployment metadata + asserts: - equal: path: metadata.name - value: admin-client - - equal: + value: administration-client + - equal: path: metadata.namespace value: sh-helm-test - - it: should have correct deyployment general spec values asserts: - equal: @@ -65,12 +63,10 @@ tests: value: 10 - it: should have correct deployment spec strategy values asserts: - - equal: + - equal: path: spec.strategy - value: + value: rollingUpdate: maxSurge: 1 maxUnavailable: 0 type: RollingUpdate - - diff --git a/alfa-client/apps/admin/src/test/helm/deployment_host_aliases_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_host_aliases_test.yaml index 4fe65ff025aa4892f639bd6452bc5e9cea316e09..fda8d70bf7892b11f9f383fae197207b86c499e2 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_host_aliases_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_host_aliases_test.yaml @@ -24,11 +24,11 @@ suite: deployment host aliases release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/deployment.yaml -set: +set: ozgcloud: environment: test imagePullSecret: test-image-secret @@ -40,15 +40,15 @@ tests: - it: should set spec.template.spec.hostAliases set: hostAliases: - - ip: "127.0.0.1" + - ip: '127.0.0.1' hostname: - - "eins" - - "zwei" + - 'eins' + - 'zwei' asserts: - contains: path: spec.template.spec.hostAliases content: - ip: "127.0.0.1" + ip: '127.0.0.1' hostname: - - "eins" - - "zwei" + - 'eins' + - 'zwei' diff --git a/alfa-client/apps/admin/src/test/helm/deployment_imagepull_secret_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_imagepull_secret_test.yaml index 7129ef48705458bca19a741136907126fbe09fa1..1671e8a154517a59520eba65fa4f73ea50d7f9bf 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_imagepull_secret_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_imagepull_secret_test.yaml @@ -24,7 +24,7 @@ suite: test deployment image pull secret release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/deployment.yaml @@ -38,4 +38,4 @@ tests: asserts: - equal: path: spec.template.spec.imagePullSecrets[0].name - value: test-image-secret \ No newline at end of file + value: test-image-secret diff --git a/alfa-client/apps/admin/src/test/helm/deployment_ozgcloud_base_values_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_ozgcloud_base_values_test.yaml index 9195d806882c3e6bc7c841b34169e8de7a2673e8..892f2a185541661a5aaed10337c9eee95c2744fc 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_ozgcloud_base_values_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_ozgcloud_base_values_test.yaml @@ -24,11 +24,11 @@ suite: test ozgcloud base values release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/deployment.yaml -set: +set: imagePullSecret: test-image-secret tests: diff --git a/alfa-client/apps/admin/src/test/helm/deployment_resources_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_resources_test.yaml index 487a6aa04c55c1636d661a7262635e573ab80276..ed640c855a3eb9a0a4c85087cf0275f72bf7b17e 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_resources_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_resources_test.yaml @@ -24,7 +24,7 @@ suite: test deployment container resources release: - name: admin-client + name: administration-client templates: - templates/deployment.yaml set: @@ -59,4 +59,3 @@ tests: asserts: - isEmpty: path: spec.template.spec.containers[0].resources - diff --git a/alfa-client/apps/admin/src/test/helm/deployment_service_account_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_service_account_test.yaml index 98246ba981cba50008edd137c04040383faa5ecc..e8544c9249516ef141e947003588e565bc138929 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_service_account_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_service_account_test.yaml @@ -24,7 +24,7 @@ suite: deployment service account release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/deployment.yaml @@ -41,7 +41,7 @@ tests: asserts: - equal: path: spec.template.spec.serviceAccountName - value: admin-client-service-account + value: administration-client-service-account - it: should use service account with name set: serviceAccount: @@ -54,4 +54,4 @@ tests: - it: should use default service account asserts: - isNull: - path: spec.template.spec.serviceAccountName \ No newline at end of file + path: spec.template.spec.serviceAccountName diff --git a/alfa-client/apps/admin/src/test/helm/deployment_springProfile_test.yaml b/alfa-client/apps/admin/src/test/helm/deployment_springProfile_test.yaml index c42ea85d177c3daf475aa403f636efc4f8594707..483f9ef354b4d5bedeaf9fbbede3b1da763afc92 100644 --- a/alfa-client/apps/admin/src/test/helm/deployment_springProfile_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/deployment_springProfile_test.yaml @@ -24,11 +24,11 @@ suite: test deployment spring profiles release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/deployment.yaml -set: +set: ozgcloud: environment: test imagePullSecret: test-image-secret @@ -53,4 +53,4 @@ tests: path: spec.template.spec.containers[0].env content: name: spring_profiles_active - value: oc, test \ No newline at end of file + value: oc, test diff --git a/alfa-client/apps/admin/src/test/helm/ingress-tests.yaml b/alfa-client/apps/admin/src/test/helm/ingress-tests.yaml index 55974c06a961fefc8b43b985118eaf0eff5cc92a..ffcbcd43dc757d2bc0bc454dc88bcb556afb10f9 100644 --- a/alfa-client/apps/admin/src/test/helm/ingress-tests.yaml +++ b/alfa-client/apps/admin/src/test/helm/ingress-tests.yaml @@ -24,11 +24,11 @@ suite: test ingress.yaml release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/ingress.yaml -set: +set: ozgcloud: bezeichner: helm baseUrl: test.by.ozg-cloud.de @@ -42,7 +42,7 @@ tests: asserts: - equal: path: spec.tls[0].secretName - value: helm-admin-client-tls + value: helm-administration-client-tls - it: should not create ingress tls/ingressClass set: @@ -52,7 +52,7 @@ tests: path: spec.ingressClassName - isNull: path: spec.tls[0].secretName - + - it: should use default letsencrypt-prod cluster-issuer asserts: - equal: @@ -79,35 +79,34 @@ tests: asserts: - equal: path: spec.tls[0].hosts[0] - value: helm-admin.test.by.ozg-cloud.de + value: helm-administration.test.by.ozg-cloud.de - it: should create rules correctly asserts: - equal: path: spec.rules[0].http.paths[0] - value: - path: /api - pathType: Prefix - backend: - service: - name: administration - port: - number: 8080 + value: + path: /api + pathType: Prefix + backend: + service: + name: administration + port: + number: 8080 - equal: path: spec.rules[0].http.paths[1] - value: - path: / - pathType: Prefix - backend: - service: - name: admin-client - port: - number: 8080 - + value: + path: / + pathType: Prefix + backend: + service: + name: administration-client + port: + number: 8080 - it: should set hostname asserts: - equal: path: spec.rules[0].host - value: helm-admin.test.by.ozg-cloud.de + value: helm-administration.test.by.ozg-cloud.de diff --git a/alfa-client/apps/admin/src/test/helm/network_policy_test.yaml b/alfa-client/apps/admin/src/test/helm/network_policy_test.yaml index 2032db88a659c5b7fcf67aa199d6dba893bf5397..b9b5bce398183921d186fd26e480b1b0d6090110 100644 --- a/alfa-client/apps/admin/src/test/helm/network_policy_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/network_policy_test.yaml @@ -22,9 +22,9 @@ # unter der Lizenz sind dem Lizenztext zu entnehmen. # -suite: network policy admin-client test +suite: network policy administration-client test release: - name: admin-client + name: administration-client namespace: by-helm-test templates: - templates/network_policy.yaml @@ -52,7 +52,7 @@ tests: - equal: path: metadata value: - name: network-policy-admin-client + name: network-policy-administration-client namespace: by-helm-test - it: should add egress rule to administration service @@ -63,8 +63,8 @@ tests: - contains: path: spec.egress content: - to: - - namespaceSelector: + to: + - namespaceSelector: matchLabels: kubernetes.io/metadata.name: administration @@ -76,87 +76,86 @@ tests: - contains: path: spec.egress content: - to: + to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: test-dns-namespace - ports: - - port: 53 - protocol: UDP - - port: 53 - protocol: TCP - - port: 5353 - protocol: UDP - - port: 5353 - protocol: TCP + ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + - port: 5353 + protocol: UDP + - port: 5353 + protocol: TCP - it: should add additionalIngressConfig local set: networkPolicy: dnsServerNamespace: test-namespace-dns additionalIngressConfigLocal: - - from: - - podSelector: - matchLabels: - component: client2 + - from: + - podSelector: + matchLabels: + component: client2 asserts: - contains: path: spec.ingress content: from: - - podSelector: - matchLabels: - component: client2 + - podSelector: + matchLabels: + component: client2 - it: should add additionalIngressConfig global set: networkPolicy: dnsServerNamespace: test-namespace-dns additionalIngressConfigGlobal: - - from: - - podSelector: - matchLabels: - component: client2 + - from: + - podSelector: + matchLabels: + component: client2 asserts: - contains: path: spec.ingress content: from: - - podSelector: - matchLabels: - component: client2 + - podSelector: + matchLabels: + component: client2 - it: should add additionalEgressConfig local set: networkPolicy: dnsServerNamespace: test-dns-namespace additionalEgressConfigLocal: - - to: - - ipBlock: - cidr: 1.2.3.4/32 + - to: + - ipBlock: + cidr: 1.2.3.4/32 asserts: - - contains: - path: spec.egress - content: - to: - - ipBlock: - cidr: 1.2.3.4/32 + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: 1.2.3.4/32 - it: should add additionalEgressConfig global set: networkPolicy: dnsServerNamespace: test-dns-namespace additionalEgressConfigGlobal: - - to: - - ipBlock: - cidr: 1.2.3.4/32 + - to: + - ipBlock: + cidr: 1.2.3.4/32 asserts: - - contains: - path: spec.egress - content: - to: - - ipBlock: - cidr: 1.2.3.4/32 - + - contains: + path: spec.egress + content: + to: + - ipBlock: + cidr: 1.2.3.4/32 - it: test network policy disabled set: @@ -188,4 +187,4 @@ tests: dnsServerNamespace: test-dns-server-namespace asserts: - hasDocuments: - count: 1 \ No newline at end of file + count: 1 diff --git a/alfa-client/apps/admin/src/test/helm/service_account_test.yaml b/alfa-client/apps/admin/src/test/helm/service_account_test.yaml index e80dde85e375a7ba052a403807c460969c9fdc4e..ed45acb09d1975f2cde3e28d2ffa53ca44603e8e 100644 --- a/alfa-client/apps/admin/src/test/helm/service_account_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/service_account_test.yaml @@ -24,7 +24,7 @@ suite: test service account release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/service_account.yaml @@ -40,7 +40,7 @@ tests: of: v1 - equal: path: metadata.name - value: admin-client-service-account + value: administration-client-service-account - equal: path: metadata.namespace value: sh-helm-test @@ -61,4 +61,4 @@ tests: - it: should not create service account asserts: - hasDocuments: - count: 0 \ No newline at end of file + count: 0 diff --git a/alfa-client/apps/admin/src/test/helm/service_test.yaml b/alfa-client/apps/admin/src/test/helm/service_test.yaml index 6496fb494b71c6d91a23ee3979b611ebd06a9fda..02bff8a2fc9e152c2db8f5ec57eb91e6a592ff88 100644 --- a/alfa-client/apps/admin/src/test/helm/service_test.yaml +++ b/alfa-client/apps/admin/src/test/helm/service_test.yaml @@ -24,20 +24,20 @@ suite: test service release: - name: admin-client + name: administration-client namespace: sh-helm-test templates: - templates/service.yaml tests: - - it: should have the label component with correct value + - it: should have the label component with correct value asserts: - isKind: of: Service - isAPIVersion: - of: v1 + of: v1 - equal: path: metadata.labels.component - value: admin-client-service + value: administration-client-service - it: should be of type ClusterIP asserts: - equal: @@ -48,19 +48,19 @@ tests: asserts: - equal: path: spec.selector.component - value: admin-client + value: administration-client - it: selector should contain helm recommended labels name and namespace asserts: - equal: path: spec.selector value: - app.kubernetes.io/name: admin-client + app.kubernetes.io/name: administration-client app.kubernetes.io/namespace: sh-helm-test - component: admin-client + component: administration-client - it: check component label for service asserts: - equal: path: metadata.labels["component"] - value: admin-client-service \ No newline at end of file + value: administration-client-service diff --git a/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-forwarding-dialog.e2e.component.ts b/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-forwarding-dialog.e2e.component.ts index 7f5d897628057b0c929c61fd9596ad7de3dad49a..34702bb369f5ae2c4bb777ea001a14562c8d5b9c 100644 --- a/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-forwarding-dialog.e2e.component.ts +++ b/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-forwarding-dialog.e2e.component.ts @@ -5,9 +5,9 @@ export class ForwardingDialogE2EComponent { private readonly forwardingButton: string = 'forwarding-dialog-forwarding-button'; private readonly searchText: string = 'instant_search-text-input'; private readonly searchEntry: string = 'item-button'; - private readonly forwardingItem: string = 'forwarding-item'; + private readonly selectedSearchItem: string = 'selected-search-item'; private readonly changeButton: string = 'forwarding-item-change-button'; - private readonly zufiSearch: string = 'zufi-search'; + private readonly organisationsEinheitSearch: string = 'organisations-einheit-search'; public getRoot() { return cy.getTestElement(this.root); @@ -37,15 +37,15 @@ export class ForwardingDialogE2EComponent { cy.getTestElement(this.searchEntry).eq(index).click(); } - public getForwardingItem() { - return cy.getTestElement(this.forwardingItem); + public getSelectedSearchItem() { + return cy.getTestElement(this.selectedSearchItem); } public getChangeButton() { return cy.getTestElement(this.changeButton); } - public getZufiSearch() { - return cy.getTestElement(this.zufiSearch); + public getOrganisationsEinheitSearch() { + return cy.getTestElement(this.organisationsEinheitSearch); } } diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-detailansicht/vorgang-forwarding.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-detailansicht/vorgang-forwarding.cy.ts index d91019905c1bc53a44fb0a28e0fb882a416933f3..8634b339b87e025f62705edaefa760c86c16575b 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-detailansicht/vorgang-forwarding.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-detailansicht/vorgang-forwarding.cy.ts @@ -2,6 +2,7 @@ import { registerLocaleData } from '@angular/common'; import localeDe from '@angular/common/locales/de'; import localeDeExtra from '@angular/common/locales/extra/de'; import { VorgangFormularButtonsE2EComponent } from 'apps/alfa-e2e/src/components/vorgang/vorgang-formular-buttons.e2e.components'; +import { ForwardingDialogE2EComponent } from 'apps/alfa-e2e/src/components/vorgang/vorgang-forwarding-dialog.e2e.component'; import { VorgangListE2EComponent } from '../../../components/vorgang/vorgang-list.e2e.component'; import { E2EVorgangNavigator } from '../../../helper/vorgang/vorgang.navigator'; import { E2EVorgangVerifier } from '../../../helper/vorgang/vorgang.verifier'; @@ -9,10 +10,9 @@ import { VorgangE2E } from '../../../model/vorgang'; import { MainPage, waitForSpinnerToDisappear } from '../../../page-objects/main.po'; import { VorgangPage } from '../../../page-objects/vorgang.po'; import { dropCollections } from '../../../support/cypress-helper'; -import { beDisabled, contains, exist, notBeDisabled, notExist } from '../../../support/cypress.util'; +import { beAriaDisabled, contains, exist, notBeAriaDisabled, notExist } from '../../../support/cypress.util'; import { loginAsPeter, loginAsSabine } from '../../../support/user-util'; import { createVorgang, initVorgaenge } from '../../../support/vorgang-util'; -import { ForwardingDialogE2EComponent } from 'apps/alfa-e2e/src/components/vorgang/vorgang-forwarding-dialog.e2e.component'; registerLocaleData(localeDe, 'de', localeDeExtra); @@ -59,12 +59,12 @@ describe('Vorgang weiterleiten', () => { exist(forwardingDialog.getRoot()); }); - it('should have zufi search', () => { - exist(forwardingDialog.getZufiSearch()); + it('should have organisations einheit search', () => { + exist(forwardingDialog.getOrganisationsEinheitSearch()); }); it('should have disabled forwarding button', () => { - beDisabled(forwardingDialog.getForwardingButton()); + beAriaDisabled(forwardingDialog.getForwardingButton()); }); it('should close dialog on escape', () => { @@ -87,30 +87,30 @@ describe('Vorgang weiterleiten', () => { notExist(forwardingDialog.getRoot()); }); - it('should show forwarding item on search select', () => { + it('should show selected search item on search select', () => { vorgangFormularButtons.getForwardButton().click(); forwardingDialog.search(organisationsEinheitName); forwardingDialog.clickSearchEntry(0); - exist(forwardingDialog.getForwardingItem()); - contains(forwardingDialog.getForwardingItem(), organisationsEinheitName); - contains(forwardingDialog.getForwardingItem(), organisationsEinheitAddress); + exist(forwardingDialog.getSelectedSearchItem()); + contains(forwardingDialog.getSelectedSearchItem(), organisationsEinheitName); + contains(forwardingDialog.getSelectedSearchItem(), organisationsEinheitAddress); }); it('should not show zufi search on search select', () => { - notExist(forwardingDialog.getZufiSearch()); + notExist(forwardingDialog.getOrganisationsEinheitSearch()); }); it('should not disable forwarding button on search select', () => { - notBeDisabled(forwardingDialog.getForwardingButton()); + notBeAriaDisabled(forwardingDialog.getForwardingButton()); }); - it('should clear forwarding item on change button click', () => { + it('should clear selected search item on change button click', () => { forwardingDialog.getChangeButton().click(); - notExist(forwardingDialog.getForwardingItem()); - exist(forwardingDialog.getZufiSearch()); - beDisabled(forwardingDialog.getForwardingButton()); + notExist(forwardingDialog.getSelectedSearchItem()); + exist(forwardingDialog.getOrganisationsEinheitSearch()); + beAriaDisabled(forwardingDialog.getForwardingButton()); }); it('should not display Weiterleiten button in status Angenommen', () => { diff --git a/alfa-client/apps/alfa-e2e/src/helper/forwarding/forwarding.executor.ts b/alfa-client/apps/alfa-e2e/src/helper/forwarding/forwarding.executor.ts index 22295bb180d00c9d23b787fde72a001bea29cab2..659d24215d7275c70ae2ee1b710f196dd0b81a46 100644 --- a/alfa-client/apps/alfa-e2e/src/helper/forwarding/forwarding.executor.ts +++ b/alfa-client/apps/alfa-e2e/src/helper/forwarding/forwarding.executor.ts @@ -9,7 +9,7 @@ export class E2EForwardingExecutor { this.forwardingDialog.getSearchText().type(organisationsEinheit); waitForSpinnerToDisappear(); this.forwardingDialog.clickSearchEntry(0); - exist(this.forwardingDialog.getForwardingItem()); + exist(this.forwardingDialog.getSelectedSearchItem()); this.forwardingDialog.getForwardingButton().click(); waitForSpinnerToDisappear(); } diff --git a/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts b/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts index f62a66e73e504ddd9952afd438ecdd7cf242866a..768db5ed90ce7662d1e2358b58aff85d86655f6b 100644 --- a/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts +++ b/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts @@ -97,12 +97,12 @@ export function notBeChecked(element: any): void { element.should('not.be.checked'); } -export function beDisabled(element: any): void { - element.should('be.disabled'); +export function beAriaDisabled(element: any): void { + element.should('have.attr', 'aria-disabled', 'true'); } -export function notBeDisabled(element: any): void { - element.should('not.be.disabled'); +export function notBeAriaDisabled(element: any): void { + element.should('have.attr', 'aria-disabled', 'false'); } //TODO: "first()" rausnehmen -> im html eine entprechende data-test-id ansprechen?! | trennen in "get" und "verify" diff --git a/alfa-client/apps/alfa/package.json b/alfa-client/apps/alfa/package.json index db8d098a8244e0cfde3ebcd7f8e148c07ffd9316..d2b0c691be9b12310cb1f527368dbf97b1ac764e 100644 --- a/alfa-client/apps/alfa/package.json +++ b/alfa-client/apps/alfa/package.json @@ -1,4 +1,4 @@ { "name": "alfa", - "version": "2.22.0-SNAPSHOT" + "version": "2.23.0-SNAPSHOT" } diff --git a/alfa-client/apps/info/package.json b/alfa-client/apps/info/package.json index f7b9ea61038da3ea7801ac1121b57c21816c49a6..b0116c864ac85254663de4319855171ec1183e33 100644 --- a/alfa-client/apps/info/package.json +++ b/alfa-client/apps/info/package.json @@ -1,4 +1,4 @@ { "name": "info", - "version": "1.7.0-SNAPSHOT" + "version": "1.8.0-SNAPSHOT" } diff --git a/alfa-client/libs/admin/statistik/.eslintrc.json b/alfa-client/libs/admin/aggregation-mapping/.eslintrc.json similarity index 100% rename from alfa-client/libs/admin/statistik/.eslintrc.json rename to alfa-client/libs/admin/aggregation-mapping/.eslintrc.json diff --git a/alfa-client/libs/admin/aggregation-mapping/README.md b/alfa-client/libs/admin/aggregation-mapping/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4e7c28147dedbfc615cb130aa675974839ef5485 --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/README.md @@ -0,0 +1 @@ +# Aggregation Mapping \ No newline at end of file diff --git a/alfa-client/libs/admin/statistik/jest.config.ts b/alfa-client/libs/admin/aggregation-mapping/jest.config.ts similarity index 83% rename from alfa-client/libs/admin/statistik/jest.config.ts rename to alfa-client/libs/admin/aggregation-mapping/jest.config.ts index fc41bd8816868cdd6f860b9908d0f06dbc9defc9..8ae2a4a3db388eb314ca1fbd2a5f4472c536e795 100644 --- a/alfa-client/libs/admin/statistik/jest.config.ts +++ b/alfa-client/libs/admin/aggregation-mapping/jest.config.ts @@ -1,8 +1,8 @@ export default { - displayName: 'admin-statistik', + displayName: 'admin-aggregation-mapping', preset: '../../../jest.preset.js', setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], - coverageDirectory: '../../../coverage/libs/admin/statistik', + coverageDirectory: '../../../coverage/libs/admin/aggregation-mapping', transform: { '^.+\\.(ts|mjs|js|html)$': [ 'jest-preset-angular', diff --git a/alfa-client/libs/admin/statistik/project.json b/alfa-client/libs/admin/aggregation-mapping/project.json similarity index 62% rename from alfa-client/libs/admin/statistik/project.json rename to alfa-client/libs/admin/aggregation-mapping/project.json index a5c36fc013da6fcc3504172dad9627628b42cfd9..ab4704d303d7ff8c812f90168092a7f4de11d91e 100644 --- a/alfa-client/libs/admin/statistik/project.json +++ b/alfa-client/libs/admin/aggregation-mapping/project.json @@ -1,7 +1,7 @@ { - "name": "admin-statistik", + "name": "admin-aggregation-mapping", "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/admin/statistik/src", + "sourceRoot": "libs/admin/aggregation-mapping/src", "prefix": "admin", "projectType": "library", "tags": [], @@ -10,8 +10,8 @@ "executor": "@nx/jest:jest", "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "options": { - "tsConfig": "libs/admin/statistik/tsconfig.lib.json", - "jestConfig": "libs/admin/statistik/jest.config.ts" + "tsConfig": "libs/admin/aggregation-mapping/tsconfig.lib.json", + "jestConfig": "libs/admin/aggregation-mapping/jest.config.ts" } }, "lint": { diff --git a/alfa-client/libs/admin/aggregation-mapping/src/index.ts b/alfa-client/libs/admin/aggregation-mapping/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa299f4458131fceb97d7979da73dbd1c23d3446 --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/aggregation-mapping-form-container/aggregation-mapping-form-container.component'; +export * from './lib/aggregation-mapping-list-container/aggregation-mapping-list-container.component'; diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form-container.component.html b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form-container.component.html new file mode 100644 index 0000000000000000000000000000000000000000..4cccfbe689b149cd7712935505a4bb3c95b67fec --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form-container.component.html @@ -0,0 +1,3 @@ +<ods-spinner [stateResource]="listStateResource$ | async"> + <admin-aggregation-mapping-form data-test-id="aggregation-mapping-form" /> +</ods-spinner> diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form-container.component.spec.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form-container.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d192a6d428de34ad1b7abefa0b79d0a2ce1158ef --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form-container.component.spec.ts @@ -0,0 +1,51 @@ +import { AggregationMappingFormContainerComponent } from '@admin-client/aggregation-mapping'; +import { AggregationMappingService } from '@admin-client/reporting-shared'; +import { Mock, mock } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect } from '@jest/globals'; +import { SpinnerComponent } from '@ods/component'; +import { MockComponent } from 'ng-mocks'; +import { AggregationMappingFormComponent } from './aggregation-mapping-form/aggregation-mapping-form.component'; + +describe('AggregationMappingFormContainerComponent', () => { + let component: AggregationMappingFormContainerComponent; + let fixture: ComponentFixture<AggregationMappingFormContainerComponent>; + + let aggregationMappingService: Mock<AggregationMappingService>; + + beforeEach(async () => { + aggregationMappingService = mock(AggregationMappingService); + + await TestBed.configureTestingModule({ + imports: [ + AggregationMappingFormContainerComponent, + MockComponent(SpinnerComponent), + MockComponent(AggregationMappingFormComponent), + ], + providers: [ + { + provide: AggregationMappingService, + useValue: aggregationMappingService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AggregationMappingFormContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('component', () => { + describe('on destroy', () => { + it('should reload list', () => { + component.ngOnDestroy(); + + expect(aggregationMappingService.refreshList).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form-container.component.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form-container.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f20feab09a9b98b0e1860ce01517220110d44b8c --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form-container.component.ts @@ -0,0 +1,24 @@ +import { AggregationMappingListResource, AggregationMappingService } from '@admin-client/reporting-shared'; +import { StateResource } from '@alfa-client/tech-shared'; +import { AsyncPipe } from '@angular/common'; +import { Component, inject, OnDestroy } from '@angular/core'; +import { SpinnerComponent } from '@ods/component'; +import { Observable } from 'rxjs'; +import { AggregationMappingFormComponent } from './aggregation-mapping-form/aggregation-mapping-form.component'; + +@Component({ + selector: 'admin-aggregation-mapping-form-container', + standalone: true, + imports: [SpinnerComponent, AsyncPipe, AggregationMappingFormComponent], + templateUrl: './aggregation-mapping-form-container.component.html', +}) +export class AggregationMappingFormContainerComponent implements OnDestroy { + public readonly aggregationMappingService = inject(AggregationMappingService); + + public readonly listStateResource$: Observable<StateResource<AggregationMappingListResource>> = + this.aggregationMappingService.getList(); + + ngOnDestroy(): void { + this.aggregationMappingService.refreshList(); + } +} diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-form/aggregation-mapping-field-form.component.html b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-form/aggregation-mapping-field-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..a70383f4bcb5c99d3c73a19cc00d3ade15c5b9e5 --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-form/aggregation-mapping-field-form.component.html @@ -0,0 +1,40 @@ +<form [formGroup]="formService.form"> + <ng-container [formArrayName]="AggregationMappingFormService.FIELD_MAPPINGS"> + <ng-container [formGroupName]="index"> + <div class="mt-4 flex w-full flex-col rounded-md bg-background-150 p-4"> + <div class="flex flex-row items-center justify-between"> + <p class="mb-2 block text-lg font-medium text-text">Datenfeld</p> + <ods-button + class="self-end" + variant="ghost" + size="fit" + destructive="true" + (clickEmitter)="formService.removeMapping(index)" + [dataTestId]="'remove-mapping-button-' + index" + [attr.data-test-id]="'remove-mapping-' + index" + > + <ods-delete-icon icon /> + </ods-button> + </div> + <div class="flex flex-col gap-4"> + <ods-text-editor + [formControlName]="AggregationMappingFormService.FIELD_MAPPING_SOURCE_PATH" + label="Pfad" + placeholder="Tragen Sie hier den gesamten Pfad des Datenfeldes ein, das Sie auswerten möchten." + isRequired="true" + [dataTestId]="'source-mapping-field-' + index" + [attr.data-test-id]="'source-mapping-field-' + index" + ></ods-text-editor> + <ods-text-editor + [formControlName]="AggregationMappingFormService.FIELD_MAPPING_TARGET_PATH" + label="Zielfeld" + isRequired="true" + placeholder="Tragen Sie hier den gesamten Pfad des Datenfeldes ein, das Sie auswerten möchten." + [dataTestId]="'target-mapping-field-' + index" + [attr.data-test-id]="'target-mapping-field-' + index" + ></ods-text-editor> + </div> + </div> + </ng-container> + </ng-container> +</form> diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-form/aggregation-mapping-field-form.component.spec.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-form/aggregation-mapping-field-form.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5f466dd455aa9a62f69769e5e51c6fa4a22a7a5 --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-form/aggregation-mapping-field-form.component.spec.ts @@ -0,0 +1,99 @@ +import { ADMIN_FORMSERVICE } from '@admin-client/shared'; +import { EMPTY_STRING } from '@alfa-client/tech-shared'; +import { existsAsHtmlElement, mock, Mock, MockEvent, mockGetValue, triggerEvent } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import { expect } from '@jest/globals'; +import { TextEditorComponent } from '@ods/component'; +import { ButtonComponent, DeleteIconComponent } from '@ods/system'; +import { MockComponent } from 'ng-mocks'; +import { getDataTestIdOf } from '../../../../../../../../tech-shared/test/data-test'; +import { AggregationMappingFormservice } from '../../aggregation-mapping.formservice'; +import { AggregationMappingFieldFormComponent } from './aggregation-mapping-field-form.component'; + +describe('AggregationMappingFieldFormComponent', () => { + let component: AggregationMappingFieldFormComponent; + let fixture: ComponentFixture<AggregationMappingFieldFormComponent>; + + const formBuilder: FormBuilder = new FormBuilder(); + const fieldIndex: number = 0; + const sourcePathEditorTestId: string = getDataTestIdOf('source-mapping-field-0'); + const targetPathEditorTestId: string = getDataTestIdOf('target-mapping-field-0'); + const removeMappingButtonTestId: string = getDataTestIdOf('remove-mapping-0'); + + let formService: Mock<AggregationMappingFormservice>; + + beforeEach(async () => { + const form: FormGroup = formBuilder.group({ + [AggregationMappingFormservice.FIELD_MAPPINGS]: formBuilder.array([ + new FormGroup({ + [AggregationMappingFormservice.FIELD_MAPPING_SOURCE_PATH]: new FormControl(EMPTY_STRING), + [AggregationMappingFormservice.FIELD_MAPPING_TARGET_PATH]: new FormControl(EMPTY_STRING), + }), + ]), + }); + + formService = <any>{ + ...mock(AggregationMappingFormservice), + form, + addMapping: jest.fn(), + removeMapping: jest.fn(), + }; + + mockGetValue( + formService, + AggregationMappingFormservice.FIELD_MAPPINGS, + form.controls[AggregationMappingFormservice.FIELD_MAPPINGS], + ); + + await TestBed.configureTestingModule({ + imports: [ + AggregationMappingFieldFormComponent, + MockComponent(TextEditorComponent), + MockComponent(ButtonComponent), + MockComponent(DeleteIconComponent), + ], + providers: [ + { + provide: ADMIN_FORMSERVICE, + useValue: formService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AggregationMappingFieldFormComponent); + component = fixture.componentInstance; + component.index = fieldIndex; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('template', () => { + describe('remove mapping button', () => { + it('should exists', () => { + existsAsHtmlElement(fixture, removeMappingButtonTestId); + }); + + it('should remove mapping on click', () => { + triggerEvent({ fixture, elementSelector: removeMappingButtonTestId, name: MockEvent.CLICK, data: fieldIndex }); + + expect(formService.removeMapping).toHaveBeenCalledWith(fieldIndex); + }); + }); + + describe('source path text editor', () => { + it('should exists', () => { + existsAsHtmlElement(fixture, sourcePathEditorTestId); + }); + }); + + describe('target path text editor', () => { + it('should should exists', () => { + existsAsHtmlElement(fixture, targetPathEditorTestId); + }); + }); + }); +}); diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-form/aggregation-mapping-field-form.component.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-form/aggregation-mapping-field-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..346d5f27049b36d31cdb49547ffc49607f94496a --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-form/aggregation-mapping-field-form.component.ts @@ -0,0 +1,20 @@ +import { ADMIN_FORMSERVICE } from '@admin-client/shared'; +import { Component, inject, Input } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TextEditorComponent } from '@ods/component'; +import { ButtonComponent, DeleteIconComponent } from '@ods/system'; +import { AggregationMappingFormservice } from '../../aggregation-mapping.formservice'; + +@Component({ + selector: 'admin-aggregation-mapping-field-form', + standalone: true, + templateUrl: './aggregation-mapping-field-form.component.html', + imports: [ButtonComponent, DeleteIconComponent, ReactiveFormsModule, TextEditorComponent], +}) +export class AggregationMappingFieldFormComponent { + @Input({ required: true }) index: number; + + public readonly formService = <AggregationMappingFormservice>inject(ADMIN_FORMSERVICE); + + public readonly AggregationMappingFormService = AggregationMappingFormservice; +} diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-list-form.component.html b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-list-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..965537eeb053cbb0003784d3e2adaebdbae084cb --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-list-form.component.html @@ -0,0 +1,8 @@ +<form [formGroup]="formService.form"> + <div *ngFor="let ignore of mappingsFormArray; let i = index"> + <admin-aggregation-mapping-field-form + [index]="i" + [attr.data-test-id]="'aggregation-mapping-field-mapping-form-' + i" + ></admin-aggregation-mapping-field-form> + </div> +</form> diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-list-form.component.spec.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-list-form.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..724a1e08cc31da67b155908df64530bda5b3cc4d --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-list-form.component.spec.ts @@ -0,0 +1,84 @@ +import { ADMIN_FORMSERVICE } from '@admin-client/shared'; +import { EMPTY_STRING } from '@alfa-client/tech-shared'; +import { existsAsHtmlElement, getElementComponentFromFixtureByCss, mock, Mock, mockGetValue } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { expect } from '@jest/globals'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { MockComponent } from 'ng-mocks'; +import { AggregationMappingFormservice } from '../aggregation-mapping.formservice'; +import { AggregationMappingFieldFormComponent } from './aggregation-mapping-field-form/aggregation-mapping-field-form.component'; +import { AggregationMappingFieldListFormComponent } from './aggregation-mapping-field-list-form.component'; + +describe('AggregationMappingFieldListFormComponent', () => { + let component: AggregationMappingFieldListFormComponent; + let fixture: ComponentFixture<AggregationMappingFieldListFormComponent>; + + const mappingForm: string = getDataTestIdOf('aggregation-mapping-field-mapping-form-0'); + + const formBuilder: FormBuilder = new FormBuilder(); + + let formService: Mock<AggregationMappingFormservice>; + + beforeEach(async () => { + const form: FormGroup = formBuilder.group({ + [AggregationMappingFormservice.FIELD_MAPPINGS]: formBuilder.array([ + new FormGroup({ + [AggregationMappingFormservice.FIELD_MAPPING_SOURCE_PATH]: new FormControl(EMPTY_STRING), + [AggregationMappingFormservice.FIELD_MAPPING_TARGET_PATH]: new FormControl(EMPTY_STRING), + }), + ]), + }); + + formService = <any>{ + ...mock(AggregationMappingFormservice), + form, + addMapping: jest.fn(), + removeMapping: jest.fn(), + }; + + mockGetValue( + formService, + AggregationMappingFormservice.FIELD_MAPPINGS, + form.controls[AggregationMappingFormservice.FIELD_MAPPINGS], + ); + + await TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + AggregationMappingFieldListFormComponent, + MockComponent(AggregationMappingFieldFormComponent), + ], + providers: [ + { + provide: ADMIN_FORMSERVICE, + useValue: formService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AggregationMappingFieldListFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('template', () => { + describe('mapping input', () => { + it('should exists', () => { + fixture.detectChanges(); + + existsAsHtmlElement(fixture, mappingForm); + }); + + it('should have inputs', () => { + const mappingComponent: AggregationMappingFieldFormComponent = getElementComponentFromFixtureByCss(fixture, mappingForm); + + expect(mappingComponent.index).toEqual(0); + }); + }); + }); +}); diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-list-form.component.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-list-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..7b751ad91466cdf1b42f2f275bcaa9416882ddb1 --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-field-list-form/aggregation-mapping-field-list-form.component.ts @@ -0,0 +1,20 @@ +import { ADMIN_FORMSERVICE } from '@admin-client/shared'; +import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { AbstractControl, ReactiveFormsModule } from '@angular/forms'; +import { AggregationMappingFormservice } from '../aggregation-mapping.formservice'; +import { AggregationMappingFieldFormComponent } from './aggregation-mapping-field-form/aggregation-mapping-field-form.component'; + +@Component({ + selector: 'admin-aggregation-mapping-field-list-form', + templateUrl: './aggregation-mapping-field-list-form.component.html', + standalone: true, + imports: [CommonModule, ReactiveFormsModule, AggregationMappingFieldFormComponent], +}) +export class AggregationMappingFieldListFormComponent { + public readonly formService = <AggregationMappingFormservice>inject(ADMIN_FORMSERVICE); + + public readonly mappingsFormArray: AbstractControl[] = this.formService.mappings.controls; + + public readonly AggregationMappingFormService = AggregationMappingFormservice; +} diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-form.component.html b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-form.component.html new file mode 100644 index 0000000000000000000000000000000000000000..346b7322ddeef11cd3c74b98dc1577243bf7714e --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-form.component.html @@ -0,0 +1,53 @@ +<h2 class="heading-2" data-test-id="aggregation-mapping-fields-form-header-text">Felder zur Auswertung hinzufügen</h2> + +<ods-spinner [stateResource]="aggregationMappingStateResource$ | async"> + <div class="flex max-w-4xl flex-col gap-4"> + <form class="form flex-col" [formGroup]="formService.form" class="flex flex-col gap-4"> + <div class="flex flex-col gap-4 lg:flex-row"> + <ods-text-editor + class="basis-1/2 lg:pr-2" + [formControlName]="AggregationMappingFormService.FIELD_NAME" + label="Name" + placeholder="" + isRequired="true" + data-test-id="aggregation-mapping-name-text-editor" + dataTestId="aggregation-mapping-name" + ></ods-text-editor> + </div> + <div [formGroupName]="AggregationMappingFormService.FIELD_FORM_IDENTIFIER" class="flex flex-col gap-4 lg:flex-row"> + <ods-text-editor + class="flex-1" + [formControlName]="AggregationMappingFormService.FIELD_FORM_ENGINE_NAME" + label="Formengine" + placeholder="Tragen Sie hier die Formengine des Formulars ein." + isRequired="true" + data-test-id="form-engine-name-text-editor" + dataTestId="form-engine-name" + ></ods-text-editor> + <ods-text-editor + class="flex-1" + [formControlName]="AggregationMappingFormService.FIELD_FORM_ID" + label="FormID" + placeholder="Tragen Sie hier die FormID des Formulars ein." + isRequired="true" + data-test-id="form-id-text-editor" + dataTestId="form-id" + ></ods-text-editor> + </div> + <admin-aggregation-mapping-field-list-form /> + </form> + <ods-button + text="Datenfeld hinzufügen" + dataTestId="add-mapping-button" + data-test-id="add-mapping" + (clickEmitter)="formService.addMapping()" + > + <ods-plus-icon icon class="fill-whitetext" /> + </ods-button> + + <div class="mt-4 flex gap-4"> + <admin-save-button [successLinkPath]="Routes.AGGREGATION_MAPPING" /> + <admin-cancel-button [linkPath]="Routes.AGGREGATION_MAPPING" /> + </div> + </div> +</ods-spinner> diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-form.component.spec.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-form.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2abba71105c82b854633725a837836ec7f278f0a --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-form.component.spec.ts @@ -0,0 +1,154 @@ +import { AggregationMappingResource } from '@admin-client/reporting-shared'; +import { ADMIN_FORMSERVICE, AdminCancelButtonComponent, AdminSaveButtonComponent, ROUTES } from '@admin-client/shared'; +import { createStateResource, EMPTY_STRING, StateResource } from '@alfa-client/tech-shared'; +import { + dispatchEventFromFixture, + existsAsHtmlElement, + getElementFromFixtureByType, + mock, + Mock, + MockEvent, +} from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { expect } from '@jest/globals'; +import { TextEditorComponent } from '@ods/component'; +import { ButtonComponent, PlusIconComponent } from '@ods/system'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { getDataTestIdOf } from '../../../../../../tech-shared/test/data-test'; +import { singleColdCompleted } from '../../../../../../tech-shared/test/marbles'; +import { createAggregationMappingResource } from '../../../../../reporting-shared/test/aggregation-mapping'; +import { AggregationMappingFieldListFormComponent } from './aggregation-mapping-field-list-form/aggregation-mapping-field-list-form.component'; +import { AggregationMappingFormComponent } from './aggregation-mapping-form.component'; +import { AggregationMappingFormservice } from './aggregation-mapping.formservice'; + +describe('AggregationMappingFormComponent', () => { + let component: AggregationMappingFormComponent; + let fixture: ComponentFixture<AggregationMappingFormComponent>; + + const formEngineNameInputTestId: string = getDataTestIdOf('form-engine-name-text-editor'); + const formIdInputTestId: string = getDataTestIdOf('form-id-text-editor'); + const addMappingButton: string = getDataTestIdOf('add-mapping'); + + const aggregationMappingStateResource: StateResource<AggregationMappingResource> = createStateResource( + createAggregationMappingResource(), + ); + + const formBuilder: FormBuilder = new FormBuilder(); + + let formService: Mock<AggregationMappingFormservice>; + + beforeEach(async () => { + const form: FormGroup = formBuilder.group({ + [AggregationMappingFormservice.FIELD_NAME]: new FormControl(EMPTY_STRING), + [AggregationMappingFormservice.FIELD_FORM_IDENTIFIER]: formBuilder.group({ + [AggregationMappingFormservice.FIELD_FORM_ENGINE_NAME]: new FormControl(EMPTY_STRING), + [AggregationMappingFormservice.FIELD_FORM_ID]: new FormControl(EMPTY_STRING), + }), + }); + + formService = <any>{ ...mock(AggregationMappingFormservice), form }; + formService.get = jest.fn().mockReturnValue(of(aggregationMappingStateResource)); + + await TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + AggregationMappingFormComponent, + MockComponent(TextEditorComponent), + MockComponent(ButtonComponent), + MockComponent(PlusIconComponent), + MockComponent(AdminSaveButtonComponent), + MockComponent(AdminCancelButtonComponent), + MockComponent(AggregationMappingFieldListFormComponent), + ], + }) + .overrideComponent(AggregationMappingFormComponent, { + set: { + providers: [ + { + provide: AggregationMappingFormservice, + useValue: formService, + }, + { + provide: ADMIN_FORMSERVICE, + useValue: formService, + }, + ], + }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(AggregationMappingFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('component', () => { + it('should get aggregation mapping from form service', () => { + expect(formService.get).toHaveBeenCalled(); + }); + + it('should initial values', () => { + expect(component.aggregationMappingStateResource$).toBeObservable(singleColdCompleted(aggregationMappingStateResource)); + }); + }); + + describe('template', () => { + describe('form engine input', () => { + it('should exists', () => { + fixture.detectChanges(); + + existsAsHtmlElement(fixture, formEngineNameInputTestId); + }); + }); + + describe('form id input', () => { + it('should exists', () => { + fixture.detectChanges(); + + existsAsHtmlElement(fixture, formIdInputTestId); + }); + }); + + describe('add mapping button', () => { + it('should exists', () => { + fixture.detectChanges(); + + existsAsHtmlElement(fixture, addMappingButton); + }); + + describe('output', () => { + describe('clickEmitter', () => { + it('should call formService', () => { + fixture.detectChanges(); + + dispatchEventFromFixture(fixture, addMappingButton, MockEvent.CLICK); + + expect(formService.addMapping).toHaveBeenCalled(); + }); + }); + }); + }); + + describe('save button', () => { + it('should have link path', () => { + const comp: AdminSaveButtonComponent = getElementFromFixtureByType(fixture, AdminSaveButtonComponent); + + expect(comp.successLinkPath).toEqual(ROUTES.AGGREGATION_MAPPING); + }); + }); + + describe('cancel button', () => { + it('should have link path', () => { + const comp: AdminCancelButtonComponent = getElementFromFixtureByType(fixture, AdminCancelButtonComponent); + + expect(comp.linkPath).toEqual(ROUTES.AGGREGATION_MAPPING); + }); + }); + }); +}); diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-form.component.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-form.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4865e5521255cca5a9d421d8fc7a42e4764d433a --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping-form.component.ts @@ -0,0 +1,38 @@ +import { AggregationMappingResource } from '@admin-client/reporting-shared'; +import { ADMIN_FORMSERVICE, AdminCancelButtonComponent, AdminSaveButtonComponent, ROUTES } from '@admin-client/shared'; +import { StateResource } from '@alfa-client/tech-shared'; +import { AsyncPipe } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { SpinnerComponent, TextEditorComponent } from '@ods/component'; +import { ButtonComponent, PlusIconComponent } from '@ods/system'; +import { Observable } from 'rxjs'; +import { AggregationMappingFieldListFormComponent } from './aggregation-mapping-field-list-form/aggregation-mapping-field-list-form.component'; +import { AggregationMappingFormservice } from './aggregation-mapping.formservice'; + +@Component({ + selector: 'admin-aggregation-mapping-form', + templateUrl: './aggregation-mapping-form.component.html', + standalone: true, + imports: [ + ButtonComponent, + PlusIconComponent, + ReactiveFormsModule, + AdminSaveButtonComponent, + AdminCancelButtonComponent, + AggregationMappingFieldListFormComponent, + SpinnerComponent, + AsyncPipe, + TextEditorComponent, + ], + providers: [{ provide: ADMIN_FORMSERVICE, useClass: AggregationMappingFormservice }], +}) +export class AggregationMappingFormComponent { + public readonly formService = <AggregationMappingFormservice>inject(ADMIN_FORMSERVICE); + + public readonly aggregationMappingStateResource$: Observable<StateResource<AggregationMappingResource>> = + this.formService.get(); + + public readonly AggregationMappingFormService = AggregationMappingFormservice; + public readonly Routes = ROUTES; +} diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping.formservice.spec.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping.formservice.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8aae7f66f74abf27206092e6df5e05413295bc9d --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping.formservice.spec.ts @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +import { AggregationMappingResource, AggregationMappingService } from '@admin-client/reporting-shared'; +import { createStateResource, EMPTY_STRING, StateResource } from '@alfa-client/tech-shared'; +import { Mock, mock } from '@alfa-client/test-utils'; +import { TestBed } from '@angular/core/testing'; +import { FormArray, FormControl, FormGroup } from '@angular/forms'; +import { expect } from '@jest/globals'; +import { createAggregationMapping, createAggregationMappingResource } from 'libs/admin/reporting-shared/test/aggregation-mapping'; +import { omit } from 'lodash-es'; +import { of } from 'rxjs'; +import { singleColdCompleted } from '../../../../../../tech-shared/test/marbles'; +import { AggregationMappingFormservice } from './aggregation-mapping.formservice'; + +describe('AggregationMappingFormService', () => { + let formService: AggregationMappingFormservice; + + let service: Mock<AggregationMappingService>; + + beforeEach(() => { + service = mock(AggregationMappingService); + + TestBed.configureTestingModule({ + providers: [AggregationMappingFormservice, { provide: AggregationMappingService, useValue: service }], + }); + + formService = TestBed.inject(AggregationMappingFormservice); + }); + + it('should create', () => { + expect(formService).toBeTruthy(); + }); + + describe('on do submit', () => { + const stateResource: StateResource<AggregationMappingResource> = createStateResource(createAggregationMappingResource()); + + beforeEach(() => { + service.create.mockReturnValue(of(stateResource)); + service.save.mockReturnValue(of(stateResource)); + }); + + it('should create', () => { + formService.isPatch = jest.fn().mockReturnValue(false); + formService.form = <any>createAggregationMapping(); + + formService.submit(); + + expect(service.create).toHaveBeenCalledWith(formService.form.value); + }); + + it('should save', () => { + formService.isPatch = jest.fn().mockReturnValue(true); + formService.form = <any>createAggregationMapping(); + + formService.submit(); + + expect(service.save).toHaveBeenCalledWith(formService.form.value); + }); + }); + + describe('add mapping', () => { + it('should add mapping control', () => { + formService.addMapping(); + + const mappingFormArray: FormArray = <FormArray>formService.form.controls[AggregationMappingFormservice.FIELD_MAPPINGS]; + expect(mappingFormArray).toHaveLength(2); + expect(mappingFormArray.controls[0].value).toEqual({ + [AggregationMappingFormservice.FIELD_MAPPING_SOURCE_PATH]: EMPTY_STRING, + [AggregationMappingFormservice.FIELD_MAPPING_TARGET_PATH]: EMPTY_STRING, + }); + }); + }); + + describe('remove mapping', () => { + it('should remove mapping control', () => { + (<FormArray>formService.form.controls[AggregationMappingFormservice.FIELD_MAPPINGS]).push( + new FormGroup({ + [AggregationMappingFormservice.FIELD_MAPPING_SOURCE_PATH]: new FormControl('controlToRemove'), + [AggregationMappingFormservice.FIELD_MAPPING_TARGET_PATH]: new FormControl('controlToRemove'), + }), + ); + + formService.removeMapping(1); + + const mappingFormArray: FormArray = <FormArray>formService.form.controls[AggregationMappingFormservice.FIELD_MAPPINGS]; + expect(mappingFormArray).toHaveLength(1); + expect(mappingFormArray.controls[0].value).toEqual({ + [AggregationMappingFormservice.FIELD_MAPPING_SOURCE_PATH]: EMPTY_STRING, + [AggregationMappingFormservice.FIELD_MAPPING_TARGET_PATH]: EMPTY_STRING, + }); + }); + }); + + describe('get mappings', () => { + it('should return mappings as array', () => { + const mappings: FormArray = formService.mappings; + + expect(mappings).toHaveLength(1); + expect(mappings.controls[0].value).toEqual({ + [AggregationMappingFormservice.FIELD_MAPPING_SOURCE_PATH]: EMPTY_STRING, + [AggregationMappingFormservice.FIELD_MAPPING_TARGET_PATH]: EMPTY_STRING, + }); + }); + }); + + describe('get', () => { + const stateResource: StateResource<AggregationMappingResource> = createStateResource(createAggregationMappingResource()); + + beforeEach(() => { + service.get.mockReturnValue(of(stateResource)); + formService._patchForm = jest.fn(); + }); + + it('should get by current url', () => { + formService.get(); + + expect(service.get).toHaveBeenCalled(); + }); + + it('should patch form', () => { + formService.get().subscribe(); + + expect(formService._patchForm).toHaveBeenCalledWith(stateResource.resource); + }); + + it('should emit state resource', () => { + expect(formService.get()).toBeObservable(singleColdCompleted(stateResource)); + }); + }); + + describe('patch form', () => { + const aggregationMappingResource: AggregationMappingResource = createAggregationMappingResource(); + + it('should patch', () => { + formService._patchForm(aggregationMappingResource); + + expect(formService.form.value).toEqual(omit(aggregationMappingResource, '_links')); + }); + }); +}); diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping.formservice.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping.formservice.ts new file mode 100644 index 0000000000000000000000000000000000000000..09c620e51c53f37b5db731b7488d9538c1bdee06 --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-form-container/aggregation-mapping-form/aggregation-mapping.formservice.ts @@ -0,0 +1,75 @@ +import { AggregationMappingResource, AggregationMappingService, FieldMapping } from '@admin-client/reporting-shared'; +import { AbstractFormService, EMPTY_STRING, isLoaded, StateResource } from '@alfa-client/tech-shared'; +import { inject, Injectable } from '@angular/core'; +import { FormArray, FormControl, FormGroup, UntypedFormGroup } from '@angular/forms'; +import { filter, Observable, tap } from 'rxjs'; + +@Injectable() +export class AggregationMappingFormservice extends AbstractFormService<AggregationMappingResource> { + public static readonly FIELD_NAME: string = 'name'; + public static readonly FIELD_FORM_IDENTIFIER: string = 'formIdentifier'; + public static readonly FIELD_FORM_ENGINE_NAME: string = 'formEngineName'; + public static readonly FIELD_FORM_ID: string = 'formId'; + public static readonly FIELD_MAPPINGS: string = 'mappings'; + public static readonly FIELD_MAPPING_SOURCE_PATH = 'sourcePath'; + public static readonly FIELD_MAPPING_TARGET_PATH = 'targetPath'; + + private readonly aggregationMappingService = inject(AggregationMappingService); + + protected initForm(): UntypedFormGroup { + return this.formBuilder.group({ + [AggregationMappingFormservice.FIELD_NAME]: new FormControl(EMPTY_STRING), + [AggregationMappingFormservice.FIELD_FORM_IDENTIFIER]: this.formBuilder.group({ + [AggregationMappingFormservice.FIELD_FORM_ENGINE_NAME]: new FormControl(EMPTY_STRING), + [AggregationMappingFormservice.FIELD_FORM_ID]: new FormControl(EMPTY_STRING), + }), + [AggregationMappingFormservice.FIELD_MAPPINGS]: new FormArray([this.createArrayControl()]), + }); + } + + protected doSubmit(): Observable<StateResource<AggregationMappingResource>> { + if (this.isPatch()) { + return this.aggregationMappingService.save(this.getFormValue()); + } + return this.aggregationMappingService.create(this.getFormValue()); + } + + protected getPathPrefix(): string { + return EMPTY_STRING; + } + + public addMapping(): void { + this.mappings.push(this.createArrayControl()); + } + + private createArrayControl(sourcePath: string = EMPTY_STRING, targetPath: string = EMPTY_STRING): FormGroup { + return new FormGroup({ + [AggregationMappingFormservice.FIELD_MAPPING_SOURCE_PATH]: new FormControl(sourcePath), + [AggregationMappingFormservice.FIELD_MAPPING_TARGET_PATH]: new FormControl(targetPath), + }); + } + + public removeMapping(index: number): void { + this.mappings.removeAt(index); + } + + public get mappings(): FormArray { + return this.form.controls[AggregationMappingFormservice.FIELD_MAPPINGS] as FormArray; + } + + public get(): Observable<StateResource<AggregationMappingResource>> { + return this.aggregationMappingService.get().pipe( + filter(isLoaded), + tap((stateResource: StateResource<AggregationMappingResource>) => this._patchForm(stateResource.resource)), + ); + } + + _patchForm(value: AggregationMappingResource): void { + this.patch(value); + const mappingsFormArray: FormArray = this.mappings; + mappingsFormArray.clear(); + value.mappings.forEach((mapping: FieldMapping) => + mappingsFormArray.push(this.createArrayControl(mapping.sourcePath, mapping.targetPath)), + ); + } +} diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.html b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list-container.component.html similarity index 73% rename from alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.html rename to alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list-container.component.html index 1fd0441a39d71372d0c65fd84d0ce5386ac52fd2..a95bf3f3a42150b41b020b139387585105eeebbe 100644 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.html +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list-container.component.html @@ -23,13 +23,12 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<h1 class="heading-1" data-test-id="statistik-header-text">Statistik</h1> -<div class="mt-4 w-fit"> - <ods-routing-button - [linkPath]="ROUTES.STATISTIK_NEU" - text="Weitere Felder auswerten" - dataTestId="weitere-felder-auswerten-button" - ></ods-routing-button> -</div> +<h1 class="heading-1" data-test-id="aggregation-mapping-header-text">Statistik</h1> +<ods-routing-button + class="my-4 w-fit" + [linkPath]="ROUTES.AGGREGATION_MAPPING_NEU" + text="Weitere Felder auswerten" + dataTestId="weitere-felder-auswerten-button" +/> -<ng-container *ngIf="listStateResource$ | async"></ng-container> +<admin-aggregation-mapping-list [aggregationMappingListStateResource]="listStateResource$ | async" /> diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.spec.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list-container.component.spec.ts similarity index 55% rename from alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.spec.ts rename to alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list-container.component.spec.ts index 3abca44e4a1aa07b3bceb119a0caf010014c02ed..7548de0fe94f05ec6678cd5dec6156e98bdd3286 100644 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.spec.ts +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list-container.component.spec.ts @@ -23,17 +23,20 @@ */ import { AggregationMappingListResource, AggregationMappingService } from '@admin-client/reporting-shared'; import { createStateResource, StateResource } from '@alfa-client/tech-shared'; -import { mock, Mock } from '@alfa-client/test-utils'; +import { expectComponentExistsInTemplate, getElementFromFixtureByType, mock, Mock } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect } from '@jest/globals'; import { RoutingButtonComponent } from '@ods/component'; import { singleCold } from 'libs/tech-shared/test/marbles'; import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; import { createAggregationMappingListResource } from '../../../../reporting-shared/test/aggregation-mapping'; -import { StatistikContainerComponent } from './statistik-container.component'; +import { AggregationMappingListContainerComponent } from './aggregation-mapping-list-container.component'; +import { AggregationMappingListComponent } from './aggregation-mapping-list/aggregation-mapping-list.component'; -describe('StatistikContainerComponent', () => { - let component: StatistikContainerComponent; - let fixture: ComponentFixture<StatistikContainerComponent>; +describe('AggregationMappingListContainerComponent', () => { + let component: AggregationMappingListContainerComponent; + let fixture: ComponentFixture<AggregationMappingListContainerComponent>; let aggregationMappingService: Mock<AggregationMappingService>; @@ -41,22 +44,20 @@ describe('StatistikContainerComponent', () => { aggregationMappingService = mock(AggregationMappingService); await TestBed.configureTestingModule({ - imports: [StatistikContainerComponent], - declarations: [MockComponent(RoutingButtonComponent)], - }) - .overrideComponent(StatistikContainerComponent, { - set: { - providers: [ - { - provide: AggregationMappingService, - useValue: aggregationMappingService, - }, - ], + imports: [ + AggregationMappingListContainerComponent, + MockComponent(RoutingButtonComponent), + MockComponent(AggregationMappingListComponent), + ], + providers: [ + { + provide: AggregationMappingService, + useValue: aggregationMappingService, }, - }) - .compileComponents(); + ], + }).compileComponents(); - fixture = TestBed.createComponent(StatistikContainerComponent); + fixture = TestBed.createComponent(AggregationMappingListContainerComponent); component = fixture.componentInstance; fixture.detectChanges(); }); @@ -86,4 +87,32 @@ describe('StatistikContainerComponent', () => { expect(component.listStateResource$).toBeObservable(singleCold(stateResource)); }); }); + + describe('on destroy', () => { + it('should refresh aggregation mapping list', () => { + component.ngOnDestroy(); + + expect(aggregationMappingService.refreshList).toHaveBeenCalled(); + }); + }); + + describe('template', () => { + describe('aggregation mapping list', () => { + it('should exists', () => { + expectComponentExistsInTemplate(fixture, AggregationMappingListComponent); + }); + + it('should have inputs', () => { + const stateResource: StateResource<AggregationMappingListResource> = createStateResource( + createAggregationMappingListResource(), + ); + component.listStateResource$ = of(stateResource); + fixture.detectChanges(); + + const comp: AggregationMappingListComponent = getElementFromFixtureByType(fixture, AggregationMappingListComponent); + + expect(comp.aggregationMappingListStateResource).toEqual(stateResource); + }); + }); + }); }); diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list-container.component.ts similarity index 74% rename from alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.ts rename to alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list-container.component.ts index 90c3515896348b1c1d6ee77ba4f247428e79c4bf..622a61d5efc8a1466aa8af3c4e39e99f4a133369 100644 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.ts +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list-container.component.ts @@ -25,18 +25,18 @@ import { AggregationMappingListResource, AggregationMappingService } from '@admi import { ROUTES } from '@admin-client/shared'; import { StateResource } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; -import { Component, inject, OnInit } from '@angular/core'; +import { Component, inject, OnDestroy, OnInit } from '@angular/core'; import { RoutingButtonComponent } from '@ods/component'; import { Observable } from 'rxjs'; +import { AggregationMappingListComponent } from './aggregation-mapping-list/aggregation-mapping-list.component'; @Component({ - selector: 'admin-statistik-container', - templateUrl: './statistik-container.component.html', + selector: 'admin-aggregation-mapping-list-container', + templateUrl: './aggregation-mapping-list-container.component.html', standalone: true, - imports: [CommonModule, RoutingButtonComponent], - providers: [AggregationMappingService], + imports: [CommonModule, RoutingButtonComponent, AggregationMappingListComponent], }) -export class StatistikContainerComponent implements OnInit { +export class AggregationMappingListContainerComponent implements OnInit, OnDestroy { private service = inject(AggregationMappingService); public listStateResource$: Observable<StateResource<AggregationMappingListResource>>; @@ -46,4 +46,8 @@ export class StatistikContainerComponent implements OnInit { ngOnInit(): void { this.listStateResource$ = this.service.getList(); } + + ngOnDestroy(): void { + this.service.refreshList(); + } } diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list-item/aggregation-mapping-list-item.component.html b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list-item/aggregation-mapping-list-item.component.html new file mode 100644 index 0000000000000000000000000000000000000000..9c4788d829ced3e228a1f73861c33dcd63f647df --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list-item/aggregation-mapping-list-item.component.html @@ -0,0 +1,15 @@ +<ods-list-item [path]="aggregationMapping | toResourceUri" [attr.data-test-id]="aggregationMapping.name | convertForDataTest"> + <dl class="flex w-full"> + <div class="flex-1"> + <dt class="sr-only">Name</dt> + <dd class="font-semibold" data-test-class="list-item-name">{{ aggregationMapping.name }}</dd> + </div> + <div class="flex-wrap flex-1"> + <dt class="sr-only">Formengine</dt> + <dd data-test-class="list-item-form-engine-name">{{ aggregationMapping.formIdentifier.formEngineName }}</dd> + + <dt class="sr-only">Form ID</dt> + <dd data-test-class="list-item-form-id">{{ aggregationMapping.formIdentifier.formId }}</dd> + </div> + </dl> +</ods-list-item> diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list-item/aggregation-mapping-list-item.component.spec.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list-item/aggregation-mapping-list-item.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9a6061356da4568345c2ae526967310605b7d096 --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list-item/aggregation-mapping-list-item.component.spec.ts @@ -0,0 +1,42 @@ +import { AggregationMappingResource } from '@admin-client/reporting-shared'; +import { ToResourceUriPipe } from '@alfa-client/tech-shared'; +import { getElementFromFixtureByType } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect } from '@jest/globals'; +import { ListItemComponent } from '@ods/system'; +import { createAggregationMappingResource } from 'libs/admin/reporting-shared/test/aggregation-mapping'; +import { MockComponent } from 'ng-mocks'; +import { AggregationMappingListItemComponent } from './aggregation-mapping-list-item.component'; + +describe('AggregationMappingListItemComponent', () => { + let component: AggregationMappingListItemComponent; + let fixture: ComponentFixture<AggregationMappingListItemComponent>; + + const toResourceUriPipe: ToResourceUriPipe = new ToResourceUriPipe(); + const aggregationMappingResource: AggregationMappingResource = createAggregationMappingResource(); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AggregationMappingListItemComponent, ToResourceUriPipe, MockComponent(ListItemComponent)], + }).compileComponents(); + + fixture = TestBed.createComponent(AggregationMappingListItemComponent); + component = fixture.componentInstance; + component.aggregationMapping = aggregationMappingResource; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('template', () => { + describe('list item', () => { + it('should have inputs', () => { + const comp: ListItemComponent = getElementFromFixtureByType(fixture, ListItemComponent); + + expect(comp.path).toEqual(toResourceUriPipe.transform(aggregationMappingResource)); + }); + }); + }); +}); diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list-item/aggregation-mapping-list-item.component.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list-item/aggregation-mapping-list-item.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..6880096a779fa36e88b62dba199d952c7d34d9c7 --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list-item/aggregation-mapping-list-item.component.ts @@ -0,0 +1,15 @@ +import { AggregationMappingResource } from '@admin-client/reporting-shared'; +import { ConvertForDataTestPipe, ToResourceUriPipe } from '@alfa-client/tech-shared'; +import { Component, Input } from '@angular/core'; +import { ListItemComponent } from '@ods/system'; + +@Component({ + selector: 'admin-aggregation-mapping-list-item', + standalone: true, + templateUrl: './aggregation-mapping-list-item.component.html', + imports: [ConvertForDataTestPipe, ListItemComponent, ToResourceUriPipe], + styles: [':host {@apply block}'], +}) +export class AggregationMappingListItemComponent { + @Input({ required: true }) aggregationMapping: AggregationMappingResource; +} diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list.component.html b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..fb306dc34f8a492b6540186720ce02b350c89e50 --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list.component.html @@ -0,0 +1,7 @@ +<ods-spinner [stateResource]="aggregationMappingListStateResource"> + <ods-list data-test-id="aggregation-mapping-list"> + @for (aggregationMapping of (aggregationMappingListStateResource.resource | toEmbeddedResources: AggregationMappingListLinkRel.LIST); track $index) { + <admin-aggregation-mapping-list-item [aggregationMapping]="aggregationMapping" /> + } + </ods-list> +</ods-spinner> diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list.component.spec.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..37718ad397856c004d3b8eba65080a19bfceb4cb --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list.component.spec.ts @@ -0,0 +1,64 @@ +import { AggregationMappingListLinkRel, AggregationMappingListResource } from '@admin-client/reporting-shared'; +import { createStateResource, getEmbeddedResources, StateResource } from '@alfa-client/tech-shared'; +import { expectComponentExistsInTemplate, getElementFromFixtureByType } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect } from '@jest/globals'; +import { SpinnerComponent } from '@ods/component'; +import { ListComponent } from '@ods/system'; +import { MockComponent } from 'ng-mocks'; +import { createAggregationMappingListResource } from '../../../../../reporting-shared/test/aggregation-mapping'; +import { AggregationMappingListItemComponent } from './aggregation-mapping-list-item/aggregation-mapping-list-item.component'; +import { AggregationMappingListComponent } from './aggregation-mapping-list.component'; + +describe('AggregationMappingListComponent', () => { + let component: AggregationMappingListComponent; + let fixture: ComponentFixture<AggregationMappingListComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + AggregationMappingListComponent, + MockComponent(SpinnerComponent), + MockComponent(ListComponent), + MockComponent(AggregationMappingListItemComponent), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AggregationMappingListComponent); + component = fixture.componentInstance; + component.aggregationMappingListStateResource = createStateResource(createAggregationMappingListResource()); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('template', () => { + const aggregationMappingListStateResource: StateResource<AggregationMappingListResource> = createStateResource( + createAggregationMappingListResource(), + ); + + beforeEach(() => { + component.aggregationMappingListStateResource = aggregationMappingListStateResource; + fixture.detectChanges(); + }); + + it('should have list', () => { + expectComponentExistsInTemplate(fixture, ListComponent); + }); + + it('should have spinner with state resource', () => { + const comp: SpinnerComponent = getElementFromFixtureByType(fixture, SpinnerComponent); + + expect(comp.stateResource).toEqual(aggregationMappingListStateResource); + }); + + it('should have list item with mapping', () => { + const comp: AggregationMappingListItemComponent = getElementFromFixtureByType(fixture, AggregationMappingListItemComponent); + expect(comp.aggregationMapping).toEqual( + getEmbeddedResources(aggregationMappingListStateResource, AggregationMappingListLinkRel.LIST)[0], + ); + }); + }); +}); diff --git a/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list.component.ts b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac2c343c47d7ea879951589de6206c3b35d6ab77 --- /dev/null +++ b/alfa-client/libs/admin/aggregation-mapping/src/lib/aggregation-mapping-list-container/aggregation-mapping-list/aggregation-mapping-list.component.ts @@ -0,0 +1,19 @@ +import { AggregationMappingListResource } from '@admin-client/reporting-shared'; +import { StateResource, ToEmbeddedResourcesPipe } from '@alfa-client/tech-shared'; +import { Component, Input } from '@angular/core'; +import { SpinnerComponent } from '@ods/component'; +import { ListComponent } from '@ods/system'; +import { AggregationMappingListLinkRel } from 'libs/admin/reporting-shared/src/lib/aggregation-mapping.linkrel'; +import { AggregationMappingListItemComponent } from './aggregation-mapping-list-item/aggregation-mapping-list-item.component'; + +@Component({ + selector: 'admin-aggregation-mapping-list', + standalone: true, + templateUrl: './aggregation-mapping-list.component.html', + imports: [SpinnerComponent, ListComponent, AggregationMappingListItemComponent, ToEmbeddedResourcesPipe], +}) +export class AggregationMappingListComponent { + @Input({ required: true }) aggregationMappingListStateResource: StateResource<AggregationMappingListResource>; + + public readonly AggregationMappingListLinkRel = AggregationMappingListLinkRel; +} diff --git a/alfa-client/libs/admin/statistik/src/test-setup.ts b/alfa-client/libs/admin/aggregation-mapping/src/test-setup.ts similarity index 100% rename from alfa-client/libs/admin/statistik/src/test-setup.ts rename to alfa-client/libs/admin/aggregation-mapping/src/test-setup.ts diff --git a/alfa-client/libs/admin/statistik/tsconfig.json b/alfa-client/libs/admin/aggregation-mapping/tsconfig.json similarity index 100% rename from alfa-client/libs/admin/statistik/tsconfig.json rename to alfa-client/libs/admin/aggregation-mapping/tsconfig.json diff --git a/alfa-client/libs/admin/statistik/tsconfig.lib.json b/alfa-client/libs/admin/aggregation-mapping/tsconfig.lib.json similarity index 100% rename from alfa-client/libs/admin/statistik/tsconfig.lib.json rename to alfa-client/libs/admin/aggregation-mapping/tsconfig.lib.json diff --git a/alfa-client/libs/admin/statistik/tsconfig.spec.json b/alfa-client/libs/admin/aggregation-mapping/tsconfig.spec.json similarity index 100% rename from alfa-client/libs/admin/statistik/tsconfig.spec.json rename to alfa-client/libs/admin/aggregation-mapping/tsconfig.spec.json diff --git a/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.html b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.html index 0c1102a4c22c1603d4cc93f6baaaf5fcb2f94d40..c319836e14be9bee1d1314df5f293f09173a12b0 100644 --- a/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.html +++ b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.html @@ -4,7 +4,7 @@ </ods-nav-item> } @if (configurationStateResource.resource | hasLink: configurationLinkRel.AGGREGATION_MAPPINGS) { - <ods-nav-item data-test-id="statistik-navigation" caption="Statistik" path="/statistik"> + <ods-nav-item data-test-id="statistik-navigation" caption="Statistik" [path]="'/' + ROUTES.AGGREGATION_MAPPING"> <ods-statistic-icon icon /> </ods-nav-item> } diff --git a/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.ts b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.ts index 0c9cddefff0b13b58d8dd915f44352b78bd60f2b..c0845f3f13cf83a993f7cbe48f8021d42a2097e2 100644 --- a/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.ts +++ b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.ts @@ -1,4 +1,5 @@ import { ConfigurationLinkRel, ConfigurationResource } from '@admin-client/configuration-shared'; +import { ROUTES } from '@admin-client/shared'; import { HasLinkPipe, StateResource } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; @@ -14,4 +15,5 @@ export class MenuComponent { @Input() configurationStateResource: StateResource<ConfigurationResource>; public readonly configurationLinkRel = ConfigurationLinkRel; + public readonly ROUTES = ROUTES; } diff --git a/alfa-client/libs/admin/keycloak-shared/src/index.ts b/alfa-client/libs/admin/keycloak-shared/src/index.ts index 4197ca2f1a4afbeacb65191a3fbe565c72987da5..06df4b1a3e6c7b8508c6d3a1e6ae4e55844020da 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/index.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/index.ts @@ -1,3 +1,4 @@ +export * from './lib/keycloak-error.model'; export * from './lib/keycloak-formservice'; export * from './lib/keycloak-token.service'; export * from './lib/keycloak.resource.service'; diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.model.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..87bdce2f2d937a86c9358ddd751b5b1c36c59cd4 --- /dev/null +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.model.ts @@ -0,0 +1,31 @@ +export const KEYCLOAK_VALIDATION_ERROR_STATUS_CODES: number[] = [400, 409]; + +export enum KeycloakFieldName { + USERNAME = 'username', + EMAIL = 'email', +} + +export enum KeycloakErrorMessage { + USERNAME_IS_MISSING = 'User name is missing', + EMAIL_EXISTS = 'User exists with same email', + USERNAME_EXISTS = 'User exists with same username', + INVALID_LENGTH = 'error-invalid-length', + INVALID_EMAIL = 'error-invalid-email', +} + +export interface ErrorRepresentation { + errorMessage?: KeycloakErrorMessage; + field?: KeycloakFieldName; + params?: any[]; +} + +export interface KeycloakHttpErrorResponse { + response: { + status: number; + }; + responseData: KeycloakErrorResponseData; +} + +export interface KeycloakErrorResponseData extends ErrorRepresentation { + errors?: ErrorRepresentation[]; +} diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..29c1aea25abe2703d3f1feecba2be25d470e8faf --- /dev/null +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.spec.ts @@ -0,0 +1,253 @@ +import { describe, expect, it } from '@jest/globals'; +import { + ErrorRepresentation, + KeycloakErrorMessage, + KeycloakErrorResponseData, + KeycloakFieldName, + KeycloakHttpErrorResponse, +} from './keycloak-error.model'; +import { + canHandleKeycloakError, + extractErrorRepresentations, + isFieldErrorMessage, + isSingleErrorMessage, +} from './keycloak-error.util'; + +describe('keycloak error model', () => { + describe('extract error representations', () => { + it('should throw error', () => { + expect(() => extractErrorRepresentations({} as any)).toThrowError(); + }); + + it('should return errors', () => { + const keycloakError: KeycloakHttpErrorResponse = { + response: { + status: 400, + }, + responseData: { + errors: [ + { + field: KeycloakFieldName.USERNAME, + errorMessage: KeycloakErrorMessage.INVALID_LENGTH, + params: ['username', 3, 255], + }, + { + field: KeycloakFieldName.EMAIL, + errorMessage: KeycloakErrorMessage.INVALID_EMAIL, + params: ['email', 'f'], + }, + ], + }, + }; + + expect(extractErrorRepresentations(keycloakError)).toStrictEqual([ + { + errorMessage: KeycloakErrorMessage.INVALID_LENGTH, + params: ['username', 3, 255], + field: KeycloakFieldName.USERNAME, + } as ErrorRepresentation, + + { + errorMessage: KeycloakErrorMessage.INVALID_EMAIL, + params: ['email', 'f'], + field: KeycloakFieldName.EMAIL, + } as ErrorRepresentation, + ]); + }); + + it('should error response data', () => { + const keycloakError: KeycloakHttpErrorResponse = { + response: { + status: 400, + }, + responseData: { + field: KeycloakFieldName.USERNAME, + errorMessage: KeycloakErrorMessage.INVALID_LENGTH, + params: ['username', 3, 255], + }, + }; + + expect(extractErrorRepresentations(keycloakError)).toStrictEqual([ + { + errorMessage: KeycloakErrorMessage.INVALID_LENGTH, + params: ['username', 3, 255], + field: KeycloakFieldName.USERNAME, + } as ErrorRepresentation, + ]); + }); + }); + + describe('can handle keycloak error', () => { + const errorWithMessage: any = { + response: { + status: 400, + }, + responseData: { + errorMessage: 'User name is missing', + }, + }; + + const errorWithObject: any = { + response: { + status: 400, + }, + responseData: { + field: 'username', + errorMessage: 'error-invalid-length', + params: ['username', 3, 255], + }, + }; + + const errorWithArray: any = { + response: { + status: 400, + }, + responseData: { + errors: [ + { + field: 'username', + errorMessage: 'error-invalid-length', + params: ['username', 3, 255], + }, + { + field: 'email', + errorMessage: 'error-invalid-email', + params: ['email', 'f'], + }, + ], + }, + }; + + const errorWithoutErrorMessage: any = { + response: {}, + responseData: {}, + }; + + const errorWithoutResponseData: any = { + response: {}, + responseData: {}, + }; + + const error500: any = { + response: { + status: 500, + }, + responseData: { + errorMessage: 'internal error', + }, + }; + + it('should return true for text error message', () => { + expect(canHandleKeycloakError(errorWithMessage)).toBe(true); + }); + + it('should return true for error object', () => { + expect(canHandleKeycloakError(errorWithObject)).toBe(true); + }); + + it('should return true for error array', () => { + expect(canHandleKeycloakError(errorWithArray)).toBe(true); + }); + + it('should return false for error without error message', () => { + expect(canHandleKeycloakError(errorWithoutErrorMessage)).toBe(false); + }); + + it('should return false for error without response data', () => { + expect(canHandleKeycloakError(errorWithoutResponseData)).toBe(false); + }); + + it('should return false for 500', () => { + expect(canHandleKeycloakError(error500)).toBe(false); + }); + + describe('evaluate validation errors', () => {}); + }); + + describe('isSingleErrorMessage', () => { + it('should return false if contains field', () => { + const error: KeycloakErrorResponseData = { + errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING, + field: KeycloakFieldName.USERNAME, + }; + + expect(isSingleErrorMessage(error)).toBe(false); + }); + + it('should return false if contains params', () => { + const error: KeycloakErrorResponseData = { + errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING, + params: [], + }; + + expect(isSingleErrorMessage(error)).toBe(false); + }); + + it('should return false if contains errors', () => { + const error: KeycloakErrorResponseData = { + errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING, + errors: [], + }; + + expect(isSingleErrorMessage(error)).toBe(false); + }); + + it('should return true', () => { + const error: KeycloakErrorResponseData = { + errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING, + }; + + expect(isSingleErrorMessage(error)).toBe(true); + }); + }); + + describe('isFieldErrorMessage', () => { + it('should return false if error message missing', () => { + const error: KeycloakErrorResponseData = { + params: [], + field: KeycloakFieldName.USERNAME, + }; + + expect(isFieldErrorMessage(error)).toBe(false); + }); + + it('should return false if field missing', () => { + const error: KeycloakErrorResponseData = { + errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING, + params: [], + }; + + expect(isFieldErrorMessage(error)).toBe(false); + }); + + it('should return false if params missing', () => { + const error: KeycloakErrorResponseData = { + errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING, + field: KeycloakFieldName.USERNAME, + }; + + expect(isFieldErrorMessage(error)).toBe(false); + }); + + it('should return false if contains errors', () => { + const error: KeycloakErrorResponseData = { + errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING, + field: KeycloakFieldName.USERNAME, + params: [], + errors: [], + }; + + expect(isFieldErrorMessage(error)).toBe(false); + }); + + it('should return true', () => { + const error: KeycloakErrorResponseData = { + errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING, + field: KeycloakFieldName.USERNAME, + params: [], + }; + + expect(isFieldErrorMessage(error)).toBe(true); + }); + }); +}); diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f8cf30cada65b7045f7625bf301fa826bec902b --- /dev/null +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-error.util.ts @@ -0,0 +1,40 @@ +import { + ErrorRepresentation, + KEYCLOAK_VALIDATION_ERROR_STATUS_CODES, + KeycloakErrorResponseData, + KeycloakHttpErrorResponse, +} from '@admin/keycloak-shared'; +import { isNotNil } from '@alfa-client/tech-shared'; +import { isNil } from 'lodash-es'; + +export function extractErrorRepresentations(error: KeycloakHttpErrorResponse): ErrorRepresentation[] { + if (!canHandleKeycloakError(error)) { + throw new Error(`Cannot handle keycloak error: ${error}`); + } + const errorResponseData: KeycloakErrorResponseData = error.responseData; + return error.responseData.errors ?? [errorResponseData]; +} + +export function canHandleKeycloakError(err: KeycloakHttpErrorResponse): boolean { + if (!KEYCLOAK_VALIDATION_ERROR_STATUS_CODES.includes(err?.response?.status)) return false; + if (isNil(err?.responseData)) return false; + + if (containsErrorMessage(err)) return true; + return containsErrorArray(err) && isNotNil(err.responseData.errors[0]?.errorMessage); +} + +function containsErrorMessage(err: any): boolean { + return isNotNil(err?.responseData?.errorMessage); +} + +function containsErrorArray(err: any): boolean { + return isNotNil(err?.responseData?.errors) && err.responseData.errors.length > 0; +} + +export function isSingleErrorMessage(err: KeycloakErrorResponseData): boolean { + return isNotNil(err.errorMessage) && isNil(err.field) && isNil(err.params) && isNil(err.errors); +} + +export function isFieldErrorMessage(err: KeycloakErrorResponseData): boolean { + return isNotNil(err.errorMessage) && isNotNil(err.field) && isNotNil(err.params) && isNil(err.errors); +} diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts index 6fe0a332ba918b071d6bddfb712e7b4e177a4891..038187decc6f18d2bac42ab33d94cdf3a751912a 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.spec.ts @@ -21,20 +21,43 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { createEmptyStateResource, createStateResource, EMPTY_STRING, StateResource } from '@alfa-client/tech-shared'; +import { createEmptyStateResource, createStateResource, EMPTY_STRING, InvalidParam, setInvalidParamValidationError, StateResource, } from '@alfa-client/tech-shared'; import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup, UntypedFormGroup, Validators } from '@angular/forms'; import { ActivatedRoute, UrlSegment } from '@angular/router'; import { faker } from '@faker-js/faker/.'; import { createDummy, Dummy } from 'libs/tech-shared/test/dummy'; -import { singleCold, singleHot } from 'libs/tech-shared/test/marbles'; +import { singleCold, singleColdCompleted, singleHot } from 'libs/tech-shared/test/marbles'; import { createSpy, mock, Mock } from 'libs/test-utils/src/lib/mocking'; -import { Observable, of } from 'rxjs'; +import { Observable, of, throwError } from 'rxjs'; import { createUrlSegment } from '../../../../navigation-shared/test/navigation-test-factory'; import { KeycloakFormService, PatchConfig } from './keycloak-formservice'; +import { expect } from '@jest/globals'; +import { ValidationMessageCode } from '../../../../tech-shared/src/lib/validation/tech.validation.messages'; +import { createInvalidParam } from '../../../../tech-shared/test/error'; +import { createErrorRepresentation, createKeycloakHttpErrorResponse } from '../test/keycloak'; import * as FormUtil from './form.util'; +import { ErrorRepresentation, KeycloakErrorMessage, KeycloakFieldName, KeycloakHttpErrorResponse } from './keycloak-error.model'; +import { extractErrorRepresentations, isFieldErrorMessage, isSingleErrorMessage } from './keycloak-error.util'; + +jest.mock('./keycloak-error.util', () => ({ + ...jest.requireActual('./keycloak-error.util'), + extractErrorRepresentations: jest.fn(), + isSingleErrorMessage: jest.fn(), + isFieldErrorMessage: jest.fn(), +})); + +jest.mock('@alfa-client/tech-shared', () => ({ + ...jest.requireActual('@alfa-client/tech-shared'), + setInvalidParamValidationError: jest.fn(), +})); + +const setInvalidParamValidationErrorMock: jest.Mock = setInvalidParamValidationError as jest.Mock; +const extractErrorRepresentationsMock: jest.Mock = extractErrorRepresentations as jest.Mock; +const isSingleErrorMessageMock: jest.Mock = isSingleErrorMessage as jest.Mock; +const isFieldErrorMessageMock: jest.Mock = isFieldErrorMessage as jest.Mock; describe('KeycloakFormService', () => { let service: KeycloakFormService<Dummy>; @@ -53,6 +76,10 @@ describe('KeycloakFormService', () => { initFormSpy = createSpy(TestKeycloakFormService, '_initForm').mockReturnValue(formGroup); evaluateRouteSpy = createSpy(TestKeycloakFormService, '_evaluateRoute'); + extractErrorRepresentationsMock.mockClear(); + isSingleErrorMessageMock.mockClear(); + isFieldErrorMessageMock.mockClear(); + setInvalidParamValidationErrorMock.mockClear(); TestBed.configureTestingModule({ providers: [ @@ -186,10 +213,29 @@ describe('KeycloakFormService', () => { beforeEach(() => { service._doSubmit = jest.fn().mockReturnValue(singleHot(dummyStateResource)); + service._processInvalidForm = jest.fn().mockReturnValue(of(createEmptyStateResource())); + service._processResponseValidationErrors = jest.fn().mockReturnValue(of(createEmptyStateResource())); + service.form.setErrors(null); + }); + + describe('on client validation error', () => { + it('should process invalid form', () => { + service.form.setErrors({ dummy: 'dummy error' }); + + service.submit().subscribe(); + + expect(service._processInvalidForm).toHaveBeenCalled(); + }); + + it('should return invalid form processing result on invalid form', () => { + service.form.setErrors({ dummy: 'dummy error' }); + + expect(service.submit()).toBeObservable(singleColdCompleted(createEmptyStateResource())); + }); }); it('should call do submit', () => { - service.submit(); + service.submit().subscribe(); expect(service._doSubmit).toHaveBeenCalled(); }); @@ -199,6 +245,424 @@ describe('KeycloakFormService', () => { expect(submitResponse).toBeObservable(singleCold(dummyStateResource)); }); + + describe('on server validation error', () => { + const keycloakHttpError: KeycloakHttpErrorResponse = createKeycloakHttpErrorResponse(); + + beforeEach(() => { + service._doSubmit = jest.fn().mockReturnValue(throwError(() => keycloakHttpError)); + }); + + it('should process response validation errors', () => { + service.submit().subscribe(); + + expect(service._processResponseValidationErrors).toHaveBeenCalledWith(keycloakHttpError); + }); + + it('should return processing response validation errors result', () => { + expect(service.submit()).toBeObservable(singleColdCompleted(createEmptyStateResource())); + }); + }); + }); + + describe('process invalid form', () => { + beforeEach(() => { + service._showValidationErrorForAllInvalidControls = jest.fn(); + }); + + it('should show validation errors on all invalid controls', () => { + service._processInvalidForm(); + + expect(service._showValidationErrorForAllInvalidControls).toHaveBeenCalledWith(service.form); + }); + + it('should return emit state resource', () => { + expect(service._processInvalidForm()).toBeObservable(singleColdCompleted(createEmptyStateResource())); + }); + }); + + describe('process response validation errors', () => { + const keycloakHttpError: KeycloakHttpErrorResponse = createKeycloakHttpErrorResponse(); + + beforeEach(() => { + service._handleUnknownKeycloakError = jest.fn(); + service._setValidationErrorsOnControls = jest.fn(); + service._mapKeycloakErrorRepresentationsToInvalidParams = jest.fn(); + }); + + it('should set validation errors on controls', () => { + const invalidParams: InvalidParam[] = [createInvalidParam()]; + service._mapKeycloakErrorRepresentationsToInvalidParams = jest.fn().mockReturnValue(invalidParams); + + service._processResponseValidationErrors(keycloakHttpError); + + expect(service._setValidationErrorsOnControls).toHaveBeenCalledWith(invalidParams); + }); + + it('should map keycloak error representations to invalid params', () => { + const errorRepresentation: ErrorRepresentation = createErrorRepresentation(); + extractErrorRepresentationsMock.mockReturnValue([errorRepresentation]); + + service._processResponseValidationErrors(keycloakHttpError); + + expect(service._mapKeycloakErrorRepresentationsToInvalidParams).toHaveBeenCalledWith([errorRepresentation]); + }); + + it('should extract error representations', () => { + service._processResponseValidationErrors(keycloakHttpError); + + expect(extractErrorRepresentationsMock).toHaveBeenCalledWith(keycloakHttpError); + }); + + it('should emit empty state resource', () => { + expect(service._processResponseValidationErrors(keycloakHttpError)).toBeObservable( + singleColdCompleted(createEmptyStateResource()), + ); + }); + + it('should handle unknown keycloak error', () => { + const error: Error = new Error('Fehler'); + extractErrorRepresentationsMock.mockImplementation(() => { + throw error; + }); + + service._processResponseValidationErrors(keycloakHttpError); + + expect(service._handleUnknownKeycloakError).toHaveBeenCalledWith(keycloakHttpError); + }); + + it('should emit empty state resource exception', () => { + const error: Error = new Error('Fehler'); + extractErrorRepresentationsMock.mockImplementation(() => { + throw error; + }); + + expect(service._processResponseValidationErrors(keycloakHttpError)).toBeObservable( + singleColdCompleted(createEmptyStateResource()), + ); + }); + }); + + describe('show validation errors on all invalid controls', () => { + it('should update value and validity on invalid control', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); + + service._showValidationErrorForAllInvalidControls(control); + + expect(spy).toHaveBeenCalled(); + }); + + it('should update value and validity form invalid control from group', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(service, '_showValidationErrorForAllInvalidControls'); + const group: UntypedFormGroup = new FormGroup({ + someControl: control, + }); + + service._showValidationErrorForAllInvalidControls(group); + + expect(spy).toHaveBeenCalledWith(control); + }); + + it('should update value and validity on invalid control from group', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); + const group: UntypedFormGroup = new FormGroup({ + someControl: control, + }); + + service._showValidationErrorForAllInvalidControls(group); + + expect(spy).toHaveBeenCalled(); + }); + + it('should update value and validity form invalid control from array', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(service, '_showValidationErrorForAllInvalidControls'); + const array: FormArray = new FormArray([control]); + + service._showValidationErrorForAllInvalidControls(array); + + expect(spy).toHaveBeenCalledWith(control); + }); + + it('should update value and validity on invalid control from group', () => { + const control: AbstractControl = new FormControl(); + control.setErrors({ dummy: 'error' }); + const spy: jest.SpyInstance = jest.spyOn(control, 'updateValueAndValidity'); + const array: FormArray = new FormArray([control]); + + service._showValidationErrorForAllInvalidControls(array); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe('set validation errors on controls', () => { + it('should set invalid param validation error', () => { + const invalidParam: InvalidParam = createInvalidParam(); + + service._setValidationErrorsOnControls([invalidParam]); + + expect(setInvalidParamValidationErrorMock).toHaveBeenCalledWith(service.form, invalidParam); + }); + }); + + describe('map keycloak error representations to invalid params', () => { + it('should map', () => { + service._mapKeycloakErrorRepresentationToInvalidParam = jest.fn(); + const errorRepresentation: ErrorRepresentation = createErrorRepresentation(); + + service._mapKeycloakErrorRepresentationsToInvalidParams([errorRepresentation]); + + expect(service._mapKeycloakErrorRepresentationToInvalidParam).toHaveBeenCalledWith(errorRepresentation); + }); + }); + + describe('map keycloak error representation to invalid param', () => { + const controlName: string = faker.word.noun(); + const invalidParam: InvalidParam = createInvalidParam(); + + beforeEach(() => { + service._evaluateControlName = jest.fn().mockReturnValue(controlName); + service._mapInvalidEmail = jest.fn().mockReturnValue(invalidParam); + service._mapInvalidLength = jest.fn().mockReturnValue(invalidParam); + service._mapEmailExists = jest.fn().mockReturnValue(invalidParam); + service._mapUsernameExists = jest.fn().mockReturnValue(invalidParam); + service._mapUserNameIsMissing = jest.fn().mockReturnValue(invalidParam); + }); + + it('should map invalid email', () => { + service._mapKeycloakErrorRepresentationToInvalidParam({ + ...createErrorRepresentation(), + errorMessage: KeycloakErrorMessage.INVALID_EMAIL, + }); + + expect(service._mapInvalidEmail).toHaveBeenCalledWith(controlName); + }); + + it('should return invalid param for invalid email', () => { + const param: InvalidParam = service._mapKeycloakErrorRepresentationToInvalidParam({ + ...createErrorRepresentation(), + errorMessage: KeycloakErrorMessage.INVALID_EMAIL, + }); + + expect(param).toEqual(invalidParam); + }); + + it('should map invalid length', () => { + const errorRepresentation: ErrorRepresentation = { + ...createErrorRepresentation(), + errorMessage: KeycloakErrorMessage.INVALID_LENGTH, + }; + + service._mapKeycloakErrorRepresentationToInvalidParam(errorRepresentation); + + expect(service._mapInvalidLength).toHaveBeenCalledWith(controlName, errorRepresentation); + }); + + it('should return invalid param for invalid length', () => { + const param: InvalidParam = service._mapKeycloakErrorRepresentationToInvalidParam({ + ...createErrorRepresentation(), + errorMessage: KeycloakErrorMessage.INVALID_LENGTH, + }); + + expect(param).toEqual(invalidParam); + }); + + it('should map email exists', () => { + service._mapKeycloakErrorRepresentationToInvalidParam({ + ...createErrorRepresentation(), + errorMessage: KeycloakErrorMessage.EMAIL_EXISTS, + }); + + expect(service._mapEmailExists).toHaveBeenCalledWith(controlName); + }); + + it('should return invalid param for email exists', () => { + const param: InvalidParam = service._mapKeycloakErrorRepresentationToInvalidParam({ + ...createErrorRepresentation(), + errorMessage: KeycloakErrorMessage.EMAIL_EXISTS, + }); + + expect(param).toEqual(invalidParam); + }); + + it('should map username exists', () => { + service._mapKeycloakErrorRepresentationToInvalidParam({ + ...createErrorRepresentation(), + errorMessage: KeycloakErrorMessage.USERNAME_EXISTS, + }); + + expect(service._mapUsernameExists).toHaveBeenCalledWith(controlName); + }); + + it('should return invalid param for username exists', () => { + const param: InvalidParam = service._mapKeycloakErrorRepresentationToInvalidParam({ + ...createErrorRepresentation(), + errorMessage: KeycloakErrorMessage.USERNAME_EXISTS, + }); + + expect(param).toEqual(invalidParam); + }); + + it('should map username is missing', () => { + service._mapKeycloakErrorRepresentationToInvalidParam({ + ...createErrorRepresentation(), + errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING, + }); + + expect(service._mapUserNameIsMissing).toHaveBeenCalledWith(controlName); + }); + + it('should return invalid param for username is missing', () => { + const param: InvalidParam = service._mapKeycloakErrorRepresentationToInvalidParam({ + ...createErrorRepresentation(), + errorMessage: KeycloakErrorMessage.USERNAME_IS_MISSING, + }); + + expect(param).toEqual(invalidParam); + }); + + it('should throw error', () => { + expect(() => + service._mapKeycloakErrorRepresentationToInvalidParam({ + ...createErrorRepresentation(), + errorMessage: 'unknown' as KeycloakErrorMessage, + }), + ).toThrowError(); + }); + }); + + describe('map invalid email', () => { + it('should return invalid param', () => { + expect(service._mapInvalidEmail('control')).toEqual({ + name: 'control', + reason: ValidationMessageCode.FIELD_INVALID, + constraintParameters: [], + value: null, + } as InvalidParam); + }); + }); + + describe('map invalid length', () => { + it('should return invalid param', () => { + expect(service._mapInvalidLength('control', { params: ['email', 1, 2] })).toEqual({ + name: 'control', + reason: ValidationMessageCode.FIELD_SIZE, + constraintParameters: [ + { name: 'min', value: '1' }, + { name: 'max', value: '2' }, + ], + value: null, + } as InvalidParam); + }); + }); + + describe('map email exists', () => { + it('should return invalid param', () => { + expect(service._mapEmailExists('control')).toEqual({ + name: 'control', + reason: ValidationMessageCode.FIELD_VALUE_ALREADY_EXISTS, + constraintParameters: [{ name: 'value', value: 'Email-Adresse' }], + value: null, + } as InvalidParam); + }); + }); + + describe('map username exists', () => { + it('should return invalid param', () => { + expect(service._mapUsernameExists('control')).toEqual({ + name: 'control', + reason: ValidationMessageCode.FIELD_VALUE_ALREADY_EXISTS, + constraintParameters: [{ name: 'value', value: 'Benutzername' }], + value: null, + } as InvalidParam); + }); + }); + + describe('map username is missing', () => { + it('should return invalid param', () => { + expect(service._mapUserNameIsMissing('control')).toEqual({ + name: 'control', + reason: ValidationMessageCode.FIELD_EMPTY, + constraintParameters: [], + value: null, + } as InvalidParam); + }); + }); + + describe('evaluate control name', () => { + const errorRepresentation: ErrorRepresentation = createErrorRepresentation(); + + beforeEach(() => { + service._mapKeycloakErrorMessageToControlName = jest.fn(); + service._mapKeycloakFieldNameToControlName = jest.fn(); + }); + + describe('on single error message', () => { + beforeEach(() => { + isSingleErrorMessageMock.mockReturnValue(true); + isFieldErrorMessageMock.mockReturnValue(false); + }); + + it('should check if single error message', () => { + service._evaluateControlName(errorRepresentation); + + expect(isSingleErrorMessageMock).toHaveBeenCalledWith(errorRepresentation); + }); + + it('should map to control name', () => { + service._evaluateControlName(errorRepresentation); + + expect(service._mapKeycloakErrorMessageToControlName).toHaveBeenCalledWith(errorRepresentation.errorMessage); + }); + + it('should return control name', () => { + const controlName: string = faker.word.noun(); + service._mapKeycloakErrorMessageToControlName = jest.fn().mockReturnValue(controlName); + + expect(service._evaluateControlName(errorRepresentation)).toEqual(controlName); + }); + }); + + describe('on field error message', () => { + beforeEach(() => { + isSingleErrorMessageMock.mockReturnValue(false); + isFieldErrorMessageMock.mockReturnValue(true); + }); + + it('should check if field error message', () => { + service._evaluateControlName(errorRepresentation); + + expect(isFieldErrorMessageMock).toHaveBeenCalledWith(errorRepresentation); + }); + + it('should map to control name', () => { + service._evaluateControlName(errorRepresentation); + + expect(service._mapKeycloakFieldNameToControlName).toHaveBeenCalledWith(errorRepresentation.field); + }); + + it('should return control name', () => { + const controlName: string = faker.word.noun(); + service._mapKeycloakFieldNameToControlName = jest.fn().mockReturnValue(controlName); + + expect(service._evaluateControlName(errorRepresentation)).toEqual(controlName); + }); + }); + + it('should throw error', () => { + isSingleErrorMessageMock.mockReturnValue(false); + isFieldErrorMessageMock.mockReturnValue(false); + + expect(() => service._evaluateControlName(errorRepresentation)).toThrowError(); + }); }); describe('patch', () => { @@ -306,4 +770,16 @@ export class TestKeycloakFormService extends KeycloakFormService<Dummy> { _doDelete(): Observable<StateResource<Dummy>> { return TestKeycloakFormService.DELETE_OBSERVABLE(); } + + _mapKeycloakFieldNameToControlName(keycloakFieldName: KeycloakFieldName): string { + throw new Error('Method not implemented.'); + } + + _mapKeycloakErrorMessageToControlName(keycloakErrorMessage: KeycloakErrorMessage): string { + throw new Error('Method not implemented.'); + } + + _handleUnknownKeycloakError(error: KeycloakHttpErrorResponse): void { + throw new Error('Method not implemented.'); + } } diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts index 49a2e6c84e3fdbf69b22ca324ee0100a34ae6763..e1856df17c3952c3fde88e15d205b6838b9083ff 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak-formservice.ts @@ -21,13 +21,15 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { createEmptyStateResource, isLoaded, StateResource } from '@alfa-client/tech-shared'; +import { createEmptyStateResource, InvalidParam, isLoaded, setInvalidParamValidationError, StateResource, } from '@alfa-client/tech-shared'; import { inject, Injectable } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { AbstractControl, FormArray, FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, UrlSegment } from '@angular/router'; -import { first, Observable, of, tap } from 'rxjs'; - +import { catchError, first, Observable, of, tap } from 'rxjs'; +import { ValidationMessageCode } from '../../../../tech-shared/src/lib/validation/tech.validation.messages'; import * as FormUtil from './form.util'; +import { ErrorRepresentation, KeycloakErrorMessage, KeycloakFieldName, KeycloakHttpErrorResponse } from './keycloak-error.model'; +import { extractErrorRepresentations, isFieldErrorMessage, isSingleErrorMessage } from './keycloak-error.util'; @Injectable() export abstract class KeycloakFormService<T> { @@ -70,9 +72,127 @@ export abstract class KeycloakFormService<T> { } public submit(): Observable<StateResource<T>> { - return this._doSubmit(); + if (this.form.invalid) { + return this._processInvalidForm(); + } + return this._doSubmit().pipe( + catchError((keycloakError: KeycloakHttpErrorResponse) => this._processResponseValidationErrors(keycloakError)), + ); + } + + _processInvalidForm(): Observable<StateResource<T>> { + this._showValidationErrorForAllInvalidControls(this.form); + return of(createEmptyStateResource<T>()); + } + + _processResponseValidationErrors(keycloakError: KeycloakHttpErrorResponse): Observable<StateResource<T>> { + try { + this._setValidationErrorsOnControls( + this._mapKeycloakErrorRepresentationsToInvalidParams(extractErrorRepresentations(keycloakError)), + ); + } catch (error: any) { + console.error(error); + this._handleUnknownKeycloakError(keycloakError); + } + return of(createEmptyStateResource<T>()); + } + + _showValidationErrorForAllInvalidControls(control: AbstractControl): void { + if (control.invalid) control.updateValueAndValidity(); + if (control instanceof FormGroup || control instanceof FormArray) { + Object.values(control.controls).forEach((control) => this._showValidationErrorForAllInvalidControls(control)); + } } + _setValidationErrorsOnControls(invalidParams: InvalidParam[]): void { + invalidParams.forEach((invalidParam: InvalidParam) => { + setInvalidParamValidationError(this.form, invalidParam); + }); + } + + _mapKeycloakErrorRepresentationsToInvalidParams(errorRepresentations: ErrorRepresentation[]): InvalidParam[] { + return errorRepresentations.map((errorRepresentation: ErrorRepresentation) => + this._mapKeycloakErrorRepresentationToInvalidParam(errorRepresentation), + ); + } + + _mapKeycloakErrorRepresentationToInvalidParam(errorRepresentation: ErrorRepresentation): InvalidParam { + const controlName: string = this._evaluateControlName(errorRepresentation); + switch (errorRepresentation.errorMessage) { + case KeycloakErrorMessage.INVALID_EMAIL: + return this._mapInvalidEmail(controlName); + case KeycloakErrorMessage.INVALID_LENGTH: + return this._mapInvalidLength(controlName, errorRepresentation); + case KeycloakErrorMessage.EMAIL_EXISTS: + return this._mapEmailExists(controlName); + case KeycloakErrorMessage.USERNAME_EXISTS: + return this._mapUsernameExists(controlName); + case KeycloakErrorMessage.USERNAME_IS_MISSING: + return this._mapUserNameIsMissing(controlName); + default: + throw new Error(`Unknown keycloak error message ${errorRepresentation.errorMessage}`); + } + } + + _mapInvalidEmail(controlName: string): InvalidParam { + return { name: controlName, reason: ValidationMessageCode.FIELD_INVALID, constraintParameters: [], value: null }; + } + + _mapInvalidLength(controlName: string, errorRepresentation: ErrorRepresentation): InvalidParam { + return { + name: controlName, + reason: ValidationMessageCode.FIELD_SIZE, + constraintParameters: [ + { name: 'min', value: `${errorRepresentation.params[1]}` }, + { name: 'max', value: `${errorRepresentation.params[2]}` }, + ], + value: null, + }; + } + + _mapEmailExists(controlName: string): InvalidParam { + return { + name: controlName, + reason: ValidationMessageCode.FIELD_VALUE_ALREADY_EXISTS, + constraintParameters: [{ name: 'value', value: 'Email-Adresse' }], + value: null, + }; + } + + _mapUsernameExists(controlName: string): InvalidParam { + return { + name: controlName, + reason: ValidationMessageCode.FIELD_VALUE_ALREADY_EXISTS, + constraintParameters: [{ name: 'value', value: 'Benutzername' }], + value: null, + }; + } + + _mapUserNameIsMissing(controlName: string): InvalidParam { + return { + name: controlName, + reason: ValidationMessageCode.FIELD_EMPTY, + constraintParameters: [], + value: null, + }; + } + + _evaluateControlName(errorRepresentation: ErrorRepresentation): string { + if (isSingleErrorMessage(errorRepresentation)) { + return this._mapKeycloakErrorMessageToControlName(errorRepresentation.errorMessage); + } + if (isFieldErrorMessage(errorRepresentation)) { + return this._mapKeycloakFieldNameToControlName(errorRepresentation.field as KeycloakFieldName); + } + throw new Error(`Cannot evaluate control name for error ${errorRepresentation}`); + } + + abstract _mapKeycloakFieldNameToControlName(keycloakFieldName: KeycloakFieldName): string; + + abstract _mapKeycloakErrorMessageToControlName(keycloakErrorMessage: KeycloakErrorMessage): string; + + abstract _handleUnknownKeycloakError(error: KeycloakHttpErrorResponse): void; + abstract _doSubmit(): Observable<StateResource<T>>; _patch(valueToPatch: T): void { diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts index 3928e72cc9b20e768c905eedb69dc48c4ecfba3b..99426f1a908aa57d206ebc0d9880d1d20b08bcd8 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.spec.ts @@ -21,13 +21,13 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { faker } from '@faker-js/faker'; import { cold } from 'jest-marbles'; -import { StateResource, createEmptyStateResource, createStateResource } from 'libs/tech-shared/src/lib/resource/resource.util'; -import { Dummy, createDummy } from 'libs/tech-shared/test/dummy'; +import { createEmptyStateResource, createStateResource, StateResource } from 'libs/tech-shared/src/lib/resource/resource.util'; +import { createDummy, Dummy } from 'libs/tech-shared/test/dummy'; import { singleCold } from 'libs/tech-shared/test/marbles'; -import { Observable, of } from 'rxjs'; +import { Observable, of, throwError } from 'rxjs'; import { KeycloakResourceService } from './keycloak.resource.service'; import * as ResourceUtil from 'libs/tech-shared/src/lib/resource/resource.util'; @@ -211,6 +211,13 @@ describe('KeycloakResourceService', () => { expect(service.refresh).toHaveBeenCalled(); }); + it('should rethrow error', () => { + const error = new Error('dummy'); + service.refresh = jest.fn(); + + expect(service._refreshAfterEmit(throwError(() => error))).toBeObservable(cold('#', null, error)); + }); + it('should throw error refresh on error', () => { service.refresh = jest.fn(); diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts index 2b6255aa9bba1e4ac1424de5a9b56282cb683349..9e8f1f7828f030c702b617414ff8f46ad0de23dc 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/keycloak.resource.service.ts @@ -78,11 +78,14 @@ export abstract class KeycloakResourceService<T> { return action.pipe( first(), tap((): void => this.refresh()), - catchError((err: Error) => this.handleError(err)), + catchError((err) => { + this.refresh(); + return this._handleError(err); + }), ); } - handleError(err: Error): Observable<never> { + _handleError(err: Error): Observable<never> { this.refresh(); return throwError(() => err); } diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts index c0ef6b387b57946efc4336573809b55582b7912e..8508ad1aad9980fca4502a94f84a2591c77bf814 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.spec.ts @@ -36,7 +36,6 @@ import { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/rol import { Users } from '@keycloak/keycloak-admin-client/lib/resources/users'; import { cold } from 'jest-marbles'; import { omit, times } from 'lodash-es'; -import { throwError } from 'rxjs'; import { createUser } from '../../../user-shared/test/user'; import { UserFormService } from '../../../user/src/lib/user-form/user.formservice'; import { KeycloakDefaults } from './keycloak.model'; @@ -92,16 +91,6 @@ describe('UserRepository', () => { done(); }); }); - - it('should call handleError', fakeAsync(() => { - repository._handleCreateError = jest.fn(); - kcAdminClient.users['create'] = jest.fn().mockReturnValue(throwError(() => new Error('error'))); - - repository.createInKeycloak(user).subscribe({ error: () => {} }); - tick(); - - expect(repository._handleCreateError).toHaveBeenCalled(); - })); }); describe('saveInKeycloak', () => { @@ -133,16 +122,6 @@ describe('UserRepository', () => { done(); }); }); - - it('should call handleError', fakeAsync(() => { - repository._handleSaveError = jest.fn(); - kcAdminClient.users['update'] = jest.fn().mockReturnValue(throwError(() => new Error('error'))); - - repository.saveInKeycloak(user).subscribe({ error: () => {} }); - tick(); - - expect(repository._handleSaveError).toHaveBeenCalled(); - })); }); describe('updateUserGroups', () => { diff --git a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts index c510b595f5b6157f20093328d23fa31bcc11e242..da9b890518ef470364c4a30953894c255a328e4b 100644 --- a/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts +++ b/alfa-client/libs/admin/keycloak-shared/src/lib/user.repository.ts @@ -31,11 +31,12 @@ import MappingsRepresentation from '@keycloak/keycloak-admin-client/lib/defs/map import { RequiredActionAlias } from '@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation'; import { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; -import * as _ from 'lodash-es'; import { isNil, omit } from 'lodash-es'; -import { catchError, concatMap, forkJoin, from, map, mergeMap, Observable, tap, throwError } from 'rxjs'; +import { concatMap, forkJoin, from, map, mergeMap, Observable, tap, throwError } from 'rxjs'; import { KeycloakDefaults } from './keycloak.model'; +import * as _ from 'lodash-es'; + @Injectable({ providedIn: 'root', }) @@ -53,7 +54,6 @@ export class UserRepository { }), tap((response: { id: string }): void => this._sendActivationMail(response.id)), map((): User => user), - catchError((err: Error): Observable<never> => this._handleCreateError(err)), ); } @@ -65,7 +65,6 @@ export class UserRepository { await this._updateUserGroups(user.id, user.groupIds); }), map((): User => user), - catchError((err: Error): Observable<never> => this._handleSaveError(err)), ); } diff --git a/alfa-client/libs/admin/keycloak-shared/src/test/keycloak.ts b/alfa-client/libs/admin/keycloak-shared/src/test/keycloak.ts new file mode 100644 index 0000000000000000000000000000000000000000..32fdb0792d197579367961070e189301a786d222 --- /dev/null +++ b/alfa-client/libs/admin/keycloak-shared/src/test/keycloak.ts @@ -0,0 +1,23 @@ +import { + ErrorRepresentation, + KeycloakErrorMessage, + KeycloakFieldName, + KeycloakHttpErrorResponse, +} from '../lib/keycloak-error.model'; + +export function createErrorRepresentation(): ErrorRepresentation { + return { + field: KeycloakFieldName.USERNAME, + errorMessage: KeycloakErrorMessage.USERNAME_EXISTS, + params: [], + }; +} + +export function createKeycloakHttpErrorResponse(): KeycloakHttpErrorResponse { + return { + response: { + status: 400, + }, + responseData: createErrorRepresentation(), + }; +} diff --git a/alfa-client/libs/admin/reporting-shared/src/index.ts b/alfa-client/libs/admin/reporting-shared/src/index.ts index 44ab78233d97deaf497e327e28f71c431ae6960e..17fd61a17a54ff36c1cdc0799dbc16036c853571 100644 --- a/alfa-client/libs/admin/reporting-shared/src/index.ts +++ b/alfa-client/libs/admin/reporting-shared/src/index.ts @@ -1,4 +1,6 @@ +export * from './lib/aggregation-mapping-list-resource.service'; export * from './lib/aggregation-mapping-resource.service'; +export * from './lib/aggregation-mapping.linkrel'; export * from './lib/aggregation-mapping.model'; export * from './lib/aggregation-mapping.provider'; export * from './lib/aggregation-mapping.service'; diff --git a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping-list-resource.service.ts b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping-list-resource.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..1378b74475c88940614acdf93cd68218ee9927ea --- /dev/null +++ b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping-list-resource.service.ts @@ -0,0 +1,26 @@ +import { ConfigurationLinkRel, ConfigurationResource, ConfigurationService } from '@admin-client/configuration-shared'; +import { ListResourceServiceConfig, ResourceListService, ResourceRepository } from '@alfa-client/tech-shared'; +import { AggregationMappingListLinkRel } from './aggregation-mapping.linkrel'; +import { AggregationMappingListResource, AggregationMappingResource } from './aggregation-mapping.model'; + +export class AggregationMappingListResourceService extends ResourceListService< + ConfigurationResource, + AggregationMappingListResource, + AggregationMappingResource +> {} + +export function createAggregationMappingListResourceService( + repository: ResourceRepository, + configurationService: ConfigurationService, +) { + return new ResourceListService(buildConfig(configurationService), repository); +} + +function buildConfig(configurationService: ConfigurationService): ListResourceServiceConfig<ConfigurationResource> { + return { + baseResource: configurationService.get(), + listLinkRel: ConfigurationLinkRel.AGGREGATION_MAPPINGS, + listResourceListLinkRel: AggregationMappingListLinkRel.LIST, + createLinkRel: AggregationMappingListLinkRel.SELF, + }; +} diff --git a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping-resource.service.spec.ts b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping-resource.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d9131bbba356d5498790a473a7429705a8f3b92e --- /dev/null +++ b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping-resource.service.spec.ts @@ -0,0 +1,211 @@ +import { AggregationMappingResource } from '@admin-client/reporting-shared'; +import { ROUTES } from '@admin-client/shared'; +import { NavigationService, RouteData } from '@alfa-client/navigation-shared'; +import { + createEmptyStateResource, + createStateResource, + decodeUrlFromEmbedding, + ResourceRepository, + ResourceServiceConfig, + StateResource, +} from '@alfa-client/tech-shared'; +import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +import { UrlSegment } from '@angular/router'; +import { faker } from '@faker-js/faker'; +import { afterAll, expect } from '@jest/globals'; +import { ResourceUri } from '@ngxp/rest'; +import { Observable, of } from 'rxjs'; +import { createRouteData, createUrlSegment } from '../../../../navigation-shared/test/navigation-test-factory'; +import { singleColdCompleted } from '../../../../tech-shared/test/marbles'; +import { createAggregationMappingResource } from '../../test/aggregation-mapping'; +import * as self from './aggregation-mapping-resource.service'; + +jest.mock('@alfa-client/tech-shared', () => ({ + ...jest.requireActual('@alfa-client/tech-shared'), + decodeUrlFromEmbedding: jest.fn(), +})); + +const decodeUrlFromEmbeddingMock: jest.Mock = decodeUrlFromEmbedding as jest.Mock; + +describe('AggregationMappingResourceService', () => { + let repository: Mock<ResourceRepository>; + let navigationService: Mock<NavigationService>; + + beforeEach(() => { + repository = mock(ResourceRepository); + navigationService = mock(NavigationService); + }); + + describe('build config', () => { + const getResourceByNavigationRouteSpy: jest.SpyInstance = jest + .spyOn(self, '_getResourceByNavigationRoute') + .mockImplementation(); + + afterAll(() => { + getResourceByNavigationRouteSpy.mockRestore(); + }); + + it('should get resource by navigation route', () => { + self._buildResourceServiceConfig(useFromMock(repository), useFromMock(navigationService)); + + expect(getResourceByNavigationRouteSpy).toHaveBeenCalled(); + }); + + it('should have aggregation mapping static resource', () => { + const staticResource: StateResource<AggregationMappingResource> = createStateResource(createAggregationMappingResource()); + getResourceByNavigationRouteSpy.mockReturnValue(of(staticResource)); + + const config: ResourceServiceConfig<AggregationMappingResource> = self._buildResourceServiceConfig( + useFromMock(repository), + useFromMock(navigationService), + ); + + expect(config.resource).toBeObservable(singleColdCompleted(staticResource)); + }); + }); + + describe('get resource by navigation route', () => { + const routeData: RouteData = createRouteData(); + const _isAggregationMappingEditUrl: jest.SpyInstance = jest.spyOn(self, '_isAggregationMappingEditUrl').mockImplementation(); + const _getAggregationMappingResourceByRoute: jest.SpyInstance = jest + .spyOn(self, '_getAggregationMappingResourceByRoute') + .mockImplementation(); + + beforeEach(() => { + navigationService.getCurrentRouteData.mockReturnValue(of(routeData)); + }); + + afterAll(() => { + _isAggregationMappingEditUrl.mockRestore(); + _getAggregationMappingResourceByRoute.mockRestore(); + }); + + it('should get current route data', () => { + self._getResourceByNavigationRoute(useFromMock(repository), useFromMock(navigationService)).subscribe(); + + expect(navigationService.getCurrentRouteData).toHaveBeenCalled(); + }); + + it('should check if url contains resource uri', () => { + self._getResourceByNavigationRoute(useFromMock(repository), useFromMock(navigationService)).subscribe(); + + expect(_isAggregationMappingEditUrl).toHaveBeenCalled(); + }); + + it('should get aggregation mapping resource by route', () => { + self._getResourceByNavigationRoute(useFromMock(repository), useFromMock(navigationService)).subscribe(); + + expect(_getAggregationMappingResourceByRoute).toHaveBeenCalled(); + }); + + it('should return aggregation mapping by route', () => { + const stateResource: StateResource<AggregationMappingResource> = createStateResource(createAggregationMappingResource()); + _getAggregationMappingResourceByRoute.mockReturnValue(of(stateResource)); + _isAggregationMappingEditUrl.mockReturnValue(true); + + const resourceByRoute$: Observable<StateResource<AggregationMappingResource>> = self._getResourceByNavigationRoute( + useFromMock(repository), + useFromMock(navigationService), + ); + + expect(resourceByRoute$).toBeObservable(singleColdCompleted(stateResource)); + }); + + it('should return empty state resource', () => { + const stateResource: StateResource<AggregationMappingResource> = createEmptyStateResource(); + _isAggregationMappingEditUrl.mockReturnValue(false); + + const resourceByRoute$: Observable<StateResource<AggregationMappingResource>> = self._getResourceByNavigationRoute( + useFromMock(repository), + useFromMock(navigationService), + ); + + expect(resourceByRoute$).toBeObservable(singleColdCompleted(stateResource)); + }); + }); + + describe('is aggregation mapping edit url', () => { + it.each([0, 1, 3])('should return false of wrong number of segments = %d', (numberOfSegments: number) => { + const routeData: RouteData = { + ...createRouteData(), + urlSegments: Array(numberOfSegments).fill(createUrlSegment()), + }; + + expect(self._isAggregationMappingEditUrl(routeData)).toBe(false); + }); + + it('should return false if first path is wrong', () => { + const routeData: RouteData = { + ...createRouteData(), + urlSegments: Array(2).fill(createUrlSegment()), + }; + + expect(self._isAggregationMappingEditUrl(routeData)).toBe(false); + }); + + it('should return false if second path is wrong', () => { + const firstUrlSegment: UrlSegment = createUrlSegment(); + firstUrlSegment.path = ROUTES.AGGREGATION_MAPPING; + const secondUrlSegment: UrlSegment = createUrlSegment(); + secondUrlSegment.path = 'neu'; + const routeData: RouteData = { + ...createRouteData(), + urlSegments: [firstUrlSegment, secondUrlSegment], + }; + + expect(self._isAggregationMappingEditUrl(routeData)).toBe(false); + }); + + it('should return true', () => { + const firstUrlSegment: UrlSegment = createUrlSegment(); + firstUrlSegment.path = ROUTES.AGGREGATION_MAPPING; + const secondUrlSegment: UrlSegment = createUrlSegment(); + secondUrlSegment.path = faker.internet.url(); + const routeData: RouteData = { + ...createRouteData(), + urlSegments: [firstUrlSegment, secondUrlSegment], + }; + + expect(self._isAggregationMappingEditUrl(routeData)).toBe(true); + }); + }); + + describe('get aggregation mapping resource by route', () => { + const firstUrlSegment: UrlSegment = createUrlSegment(); + firstUrlSegment.path = ROUTES.AGGREGATION_MAPPING; + const secondUrlSegment: UrlSegment = createUrlSegment(); + secondUrlSegment.path = faker.internet.url(); + const routeData: RouteData = { + ...createRouteData(), + urlSegments: [firstUrlSegment, secondUrlSegment], + }; + const uri: ResourceUri = faker.internet.url(); + const resource: AggregationMappingResource = createAggregationMappingResource(); + + beforeEach(() => { + decodeUrlFromEmbeddingMock.mockReturnValue(uri); + repository.getResource.mockReturnValue(of(resource)); + }); + + it('should decode url', () => { + self._getAggregationMappingResourceByRoute(useFromMock(repository), routeData); + + expect(decodeUrlFromEmbeddingMock).toHaveBeenCalledWith(secondUrlSegment.path); + }); + + it('should get resource', () => { + self._getAggregationMappingResourceByRoute(useFromMock(repository), routeData); + + expect(repository.getResource).toHaveBeenCalledWith(uri); + }); + + it('should return state resource', () => { + const stateResource$: Observable<StateResource<AggregationMappingResource>> = self._getAggregationMappingResourceByRoute( + useFromMock(repository), + routeData, + ); + + expect(stateResource$).toBeObservable(singleColdCompleted(createStateResource(resource))); + }); + }); +}); diff --git a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping-resource.service.ts b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping-resource.service.ts index 8f96d053a24b8d10c4878d99dc75ce7fae2506a9..0bfa0689a8c24df11542605a2f33dfbe4a4401eb 100644 --- a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping-resource.service.ts +++ b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping-resource.service.ts @@ -1,26 +1,70 @@ -import { ConfigurationLinkRel, ConfigurationResource, ConfigurationService } from '@admin-client/configuration-shared'; -import { ApiListResourceService, ListResourceServiceConfig, ResourceRepository } from '@alfa-client/tech-shared'; -import { AggregationMappingListLinkRel } from './aggregation-mapping.linkrel'; -import { AggregationMappingListResource, AggregationMappingResource } from './aggregation-mapping.model'; +import { ROUTES } from '@admin-client/shared'; +import { NavigationService, RouteData } from '@alfa-client/navigation-shared'; +import { + ApiResourceService, + createEmptyStateResource, + createStateResource, + decodeUrlFromEmbedding, + ResourceRepository, + ResourceServiceConfig, + StateResource, +} from '@alfa-client/tech-shared'; +import { UrlSegment } from '@angular/router'; +import { iif, map, Observable, of, switchMap } from 'rxjs'; +import * as self from './aggregation-mapping-resource.service'; +import { AggregationMappingLinkRel } from './aggregation-mapping.linkrel'; +import { AggregationMappingResource } from './aggregation-mapping.model'; -export class AggregationMappingListResourceService extends ApiListResourceService< - ConfigurationResource, - AggregationMappingListResource, +export class AggregationMappingResourceService extends ApiResourceService< + AggregationMappingResource, AggregationMappingResource > {} export function createAggregationMappingResourceService( repository: ResourceRepository, - configurationService: ConfigurationService, -) { - return new ApiListResourceService(buildConfig(configurationService), repository); + navigationService: NavigationService, +): AggregationMappingResourceService { + return new AggregationMappingResourceService(_buildResourceServiceConfig(repository, navigationService), repository); } -function buildConfig(configurationService: ConfigurationService): ListResourceServiceConfig<ConfigurationResource> { +export function _buildResourceServiceConfig( + repository: ResourceRepository, + navigationService: NavigationService, +): ResourceServiceConfig<AggregationMappingResource> { return { - baseResource: configurationService.get(), - listResourceListLinkRel: AggregationMappingListLinkRel.LIST, - getLinkRel: ConfigurationLinkRel.SETTING, - create: { linkRel: AggregationMappingListLinkRel.SELF }, + resource: self._getResourceByNavigationRoute(repository, navigationService), + getLinkRel: AggregationMappingLinkRel.SELF, + edit: { linkRel: AggregationMappingLinkRel.SELF }, + delete: { linkRel: AggregationMappingLinkRel.SELF }, }; } + +export function _getResourceByNavigationRoute( + repository: ResourceRepository, + navigationService: NavigationService, +): Observable<StateResource<AggregationMappingResource>> { + return navigationService + .getCurrentRouteData() + .pipe( + switchMap((route: RouteData) => + iif( + () => self._isAggregationMappingEditUrl(route), + self._getAggregationMappingResourceByRoute(repository, route), + of(createEmptyStateResource<AggregationMappingResource>()), + ), + ), + ); +} + +export function _isAggregationMappingEditUrl(route: RouteData): boolean { + const urlSegments: UrlSegment[] = route.urlSegments; + return urlSegments.length === 2 && urlSegments[0].path === ROUTES.AGGREGATION_MAPPING && urlSegments[1].path !== 'neu'; +} + +export function _getAggregationMappingResourceByRoute( + repository: ResourceRepository, + route: RouteData, +): Observable<StateResource<AggregationMappingResource>> { + const uri: string = decodeUrlFromEmbedding(route.urlSegments[1].path); + return repository.getResource(uri).pipe(map((resource: AggregationMappingResource) => createStateResource(resource))); +} diff --git a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.linkrel.ts b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.linkrel.ts index a4a02bfd7087b105a9d0d2dd816aa2b180a4db9c..6aea64156f045b33fd70c20aa2427aaebb4ece32 100644 --- a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.linkrel.ts +++ b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.linkrel.ts @@ -2,3 +2,7 @@ export enum AggregationMappingListLinkRel { LIST = 'aggregationMappings', SELF = 'self', } + +export enum AggregationMappingLinkRel { + SELF = 'self', +} diff --git a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.model.ts b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.model.ts index 0db6cb4e397768b10781b3c34c9379fc866cec2b..5d1e7e3feec8053e7e77eedc54d709c46da15deb 100644 --- a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.model.ts +++ b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.model.ts @@ -2,6 +2,7 @@ import { ListResource } from '@alfa-client/tech-shared'; import { Resource } from '@ngxp/rest'; export interface AggregationMapping { + name: string; formIdentifier: FormIdentifier; mappings: FieldMapping[]; } @@ -17,4 +18,5 @@ export interface FieldMapping { } export interface AggregationMappingResource extends AggregationMapping, Resource {} + export interface AggregationMappingListResource extends ListResource {} diff --git a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.provider.ts b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.provider.ts index 888d7154e81dbfe53fb7476e4de8835463de6d25..76d294de177ea685a3e9cbd849e5a032f5423101 100644 --- a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.provider.ts +++ b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.provider.ts @@ -1,8 +1,13 @@ +import { NavigationService } from '@alfa-client/navigation-shared'; import { ResourceRepository } from '@alfa-client/tech-shared'; import { Provider } from '@angular/core'; import { ConfigurationService } from 'libs/admin/configuration-shared/src/lib/configuration.service'; import { AggregationMappingListResourceService, + createAggregationMappingListResourceService, +} from './aggregation-mapping-list-resource.service'; +import { + AggregationMappingResourceService, createAggregationMappingResourceService, } from './aggregation-mapping-resource.service'; import { AggregationMappingService } from './aggregation-mapping.service'; @@ -10,8 +15,13 @@ import { AggregationMappingService } from './aggregation-mapping.service'; export const AggregationMappingProvider: Provider[] = [ { provide: AggregationMappingListResourceService, - useFactory: createAggregationMappingResourceService, + useFactory: createAggregationMappingListResourceService, deps: [ResourceRepository, ConfigurationService], }, + { + provide: AggregationMappingResourceService, + useFactory: createAggregationMappingResourceService, + deps: [ResourceRepository, NavigationService], + }, AggregationMappingService, ]; diff --git a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.service.spec.ts b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.service.spec.ts index ecf4387ceee6a3dbdd6775f8ef4fa3a306144002..bca88ee563cf743b74f44776c11ab3e848f73241 100644 --- a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.service.spec.ts +++ b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.service.spec.ts @@ -1,26 +1,34 @@ -import { StateResource, createStateResource } from '@alfa-client/tech-shared'; +import { createStateResource, StateResource } from '@alfa-client/tech-shared'; import { Mock, mock } from '@alfa-client/test-utils'; import { TestBed } from '@angular/core/testing'; -import { singleCold } from 'libs/tech-shared/test/marbles'; -import { Observable } from 'rxjs'; +import { expect } from '@jest/globals'; +import { singleCold, singleColdCompleted } from 'libs/tech-shared/test/marbles'; +import { Observable, of } from 'rxjs'; import { createAggregationMapping, createAggregationMappingListResource, createAggregationMappingResource, } from '../../test/aggregation-mapping'; -import { AggregationMappingListResourceService } from './aggregation-mapping-resource.service'; +import { AggregationMappingListResourceService } from './aggregation-mapping-list-resource.service'; +import { AggregationMappingResourceService } from './aggregation-mapping-resource.service'; import { AggregationMapping, AggregationMappingListResource, AggregationMappingResource } from './aggregation-mapping.model'; import { AggregationMappingService } from './aggregation-mapping.service'; describe('AggregationMappingService', () => { let service: AggregationMappingService; let listResourceService: Mock<AggregationMappingListResourceService>; + let resourceService: Mock<AggregationMappingResourceService>; beforeEach(() => { listResourceService = mock(AggregationMappingListResourceService); + resourceService = mock(AggregationMappingResourceService); TestBed.configureTestingModule({ - providers: [AggregationMappingService, { provide: AggregationMappingListResourceService, useValue: listResourceService }], + providers: [ + AggregationMappingService, + { provide: AggregationMappingListResourceService, useValue: listResourceService }, + { provide: AggregationMappingResourceService, useValue: resourceService }, + ], }); service = TestBed.inject(AggregationMappingService); @@ -76,4 +84,54 @@ describe('AggregationMappingService', () => { expect(loadedAggregationMappingResource).toBeObservable(singleCold(aggregationMappingStateResource)); }); }); + + describe('refresh list', () => { + it('should call list resource service', () => { + listResourceService.refresh = jest.fn(); + + service.refreshList(); + + expect(listResourceService.refresh).toHaveBeenCalled(); + }); + }); + + describe('get', () => { + const stateResource: StateResource<AggregationMappingResource> = createStateResource(createAggregationMappingResource()); + + beforeEach(() => { + resourceService.get = jest.fn().mockReturnValue(of(stateResource)); + }); + + it('should call resource service', () => { + service.get(); + + expect(resourceService.get).toHaveBeenCalled(); + }); + + it('should emit resource', () => { + const stateResource$: Observable<StateResource<AggregationMappingResource>> = service.get(); + + expect(stateResource$).toBeObservable(singleColdCompleted(stateResource)); + }); + }); + + describe('save', () => { + const aggregationMapping: AggregationMapping = createAggregationMapping(); + const stateResource: StateResource<AggregationMappingResource> = createStateResource(createAggregationMappingResource()); + beforeEach(() => { + resourceService.save = jest.fn().mockReturnValue(of(stateResource)); + }); + + it('should call resource service', () => { + service.save(aggregationMapping); + + expect(resourceService.save).toHaveBeenCalledWith(aggregationMapping); + }); + + it('should emit saved state resource', () => { + const saved$: Observable<StateResource<AggregationMappingResource>> = service.save(aggregationMapping); + + expect(saved$).toBeObservable(singleColdCompleted(stateResource)); + }); + }); }); diff --git a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.service.ts b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.service.ts index 20a8ce6c1ccda133323bac99a671d887e77afb8e..96c72825e5794c7affdb1019d5febd4624b885a7 100644 --- a/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.service.ts +++ b/alfa-client/libs/admin/reporting-shared/src/lib/aggregation-mapping.service.ts @@ -1,12 +1,14 @@ import { StateResource } from '@alfa-client/tech-shared'; -import { Injectable, inject } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { AggregationMappingListResourceService } from './aggregation-mapping-resource.service'; +import { AggregationMappingListResourceService } from './aggregation-mapping-list-resource.service'; +import { AggregationMappingResourceService } from './aggregation-mapping-resource.service'; import { AggregationMapping, AggregationMappingListResource, AggregationMappingResource } from './aggregation-mapping.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class AggregationMappingService { - readonly listService = inject(AggregationMappingListResourceService); + private readonly listService = inject(AggregationMappingListResourceService); + private readonly resourceService = inject(AggregationMappingResourceService); public getList(): Observable<StateResource<AggregationMappingListResource>> { return this.listService.getList(); @@ -15,4 +17,16 @@ export class AggregationMappingService { public create(toCreate: AggregationMapping): Observable<StateResource<AggregationMappingResource>> { return this.listService.create(toCreate); } + + public refreshList(): void { + this.listService.refresh(); + } + + public get(): Observable<StateResource<AggregationMappingResource>> { + return this.resourceService.get(); + } + + public save(aggregationMapping: AggregationMapping): Observable<StateResource<AggregationMappingResource>> { + return this.resourceService.save(aggregationMapping); + } } diff --git a/alfa-client/libs/admin/reporting-shared/test/aggregation-mapping.ts b/alfa-client/libs/admin/reporting-shared/test/aggregation-mapping.ts index acbcc9fa75722396e4a410ea77e9157a2a8ce8fc..3506ea96a8c9387cf6258756bc880e63c44fe535 100644 --- a/alfa-client/libs/admin/reporting-shared/test/aggregation-mapping.ts +++ b/alfa-client/libs/admin/reporting-shared/test/aggregation-mapping.ts @@ -1,12 +1,13 @@ +import { AggregationMappingListLinkRel } from '@admin-client/reporting-shared'; import { faker } from '@faker-js/faker'; import { times } from 'lodash-es'; import { LinkRelationName } from '../../../tech-shared/src'; import { toResource } from '../../../tech-shared/test/resource'; import { AggregationMapping, AggregationMappingListResource, AggregationMappingResource } from '../src'; -import { AggregationMappingListLinkRel } from '../src/lib/aggregation-mapping.linkrel'; export function createAggregationMapping(): AggregationMapping { return { + name: faker.word.noun(), formIdentifier: { formEngineName: faker.lorem.word(), formId: faker.string.uuid(), diff --git a/alfa-client/libs/admin/shared/src/lib/admin-save-button/admin-save-button.component.spec.ts b/alfa-client/libs/admin/shared/src/lib/admin-save-button/admin-save-button.component.spec.ts index baee873bfd7c0db07a9295de75844d638edaead5..d6ff6dfa2d7082c4e314dcd7a7d05bb919aeb8fa 100644 --- a/alfa-client/libs/admin/shared/src/lib/admin-save-button/admin-save-button.component.spec.ts +++ b/alfa-client/libs/admin/shared/src/lib/admin-save-button/admin-save-button.component.spec.ts @@ -1,20 +1,32 @@ import { ADMIN_FORMSERVICE } from '@admin-client/shared'; -import { AbstractFormService, createStateResource, StateResource } from '@alfa-client/tech-shared'; -import { dispatchEventFromFixture, getMockComponent, Mock, MockEvent } from '@alfa-client/test-utils'; +import { NavigationService } from '@alfa-client/navigation-shared'; +import { AbstractFormService, createStateResource, isLoaded, StateResource } from '@alfa-client/tech-shared'; +import { dispatchEventFromFixture, getMockComponent, mock, Mock, MockEvent } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { faker } from '@faker-js/faker/.'; +import { expect } from '@jest/globals'; import { Resource } from '@ngxp/rest'; import { ButtonWithSpinnerComponent } from '@ods/component'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; -import { singleCold } from 'libs/tech-shared/test/marbles'; +import { singleColdCompleted } from 'libs/tech-shared/test/marbles'; import { createDummyResource } from 'libs/tech-shared/test/resource'; import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; import { AdminSaveButtonComponent } from './admin-save-button.component'; +jest.mock('@alfa-client/tech-shared', () => { + return { + ...jest.requireActual('@alfa-client/tech-shared'), + isLoaded: jest.fn(), + }; +}); +const isLoadedMock: jest.Mock = isLoaded as jest.Mock; + describe('AdminSaveButtonComponent', () => { let component: AdminSaveButtonComponent; let fixture: ComponentFixture<AdminSaveButtonComponent>; + let navigationService: Mock<NavigationService>; let formService: Mock<AbstractFormService<Resource>>; const saveButton: string = getDataTestIdOf('save'); @@ -22,7 +34,8 @@ describe('AdminSaveButtonComponent', () => { const stateResource: StateResource<Resource> = createStateResource(createDummyResource()); beforeEach(async () => { - formService = <any>{ submit: jest.fn().mockReturnValue(singleCold(stateResource)) }; + formService = <any>{ submit: jest.fn().mockReturnValue(of(stateResource)) }; + navigationService = mock(NavigationService); await TestBed.configureTestingModule({ imports: [AdminSaveButtonComponent], @@ -32,6 +45,10 @@ describe('AdminSaveButtonComponent', () => { provide: ADMIN_FORMSERVICE, useValue: formService, }, + { + provide: NavigationService, + useValue: navigationService, + }, ], }).compileComponents(); @@ -44,17 +61,50 @@ describe('AdminSaveButtonComponent', () => { expect(component).toBeTruthy(); }); - describe('on submit', () => { - it('should call formService', () => { - dispatchEventFromFixture(fixture, saveButton, MockEvent.CLICK); + describe('component', () => { + describe('on successful form submission', () => { + it('should navigate to success link path', () => { + const successLinkPath: string = faker.internet.url(); + component.successLinkPath = successLinkPath; + isLoadedMock.mockReturnValue(true); + + component._navigateOnSuccessfulSubmission(createStateResource(createDummyResource())); + + expect(navigationService.navigate).toHaveBeenCalledWith(successLinkPath); + }); + + it('should NOT navigate', () => { + isLoadedMock.mockReturnValue(false); - expect(formService.submit).toHaveBeenCalled(); + component._navigateOnSuccessfulSubmission(createStateResource(createDummyResource())); + + expect(navigationService.navigate).not.toHaveBeenCalled(); + }); }); - it('should assign state resource', () => { - dispatchEventFromFixture(fixture, saveButton, MockEvent.CLICK); + describe('on submit', () => { + beforeEach(() => { + component._navigateOnSuccessfulSubmission = jest.fn(); + }); + + it('should call formService', () => { + dispatchEventFromFixture(fixture, saveButton, MockEvent.CLICK); + + expect(formService.submit).toHaveBeenCalled(); + }); + + it('should assign state resource', () => { + dispatchEventFromFixture(fixture, saveButton, MockEvent.CLICK); + + expect(component.stateResource$).toBeObservable(singleColdCompleted(stateResource)); + }); + + it('should navigate on successful submit', () => { + dispatchEventFromFixture(fixture, saveButton, MockEvent.CLICK); + component.stateResource$.subscribe(); - expect(component.stateResource$).toBeObservable(singleCold(stateResource)); + expect(component._navigateOnSuccessfulSubmission).toHaveBeenCalledWith(stateResource); + }); }); }); diff --git a/alfa-client/libs/admin/shared/src/lib/admin-save-button/admin-save-button.component.ts b/alfa-client/libs/admin/shared/src/lib/admin-save-button/admin-save-button.component.ts index 60abd08c7f311ecabe6d56ecc47fbfcd16412877..598d056ea60aeb0edc17497fd923982ed4533474 100644 --- a/alfa-client/libs/admin/shared/src/lib/admin-save-button/admin-save-button.component.ts +++ b/alfa-client/libs/admin/shared/src/lib/admin-save-button/admin-save-button.component.ts @@ -1,10 +1,11 @@ import { ADMIN_FORMSERVICE } from '@admin-client/shared'; -import { AbstractFormService, createEmptyStateResource, StateResource } from '@alfa-client/tech-shared'; +import { NavigationService } from '@alfa-client/navigation-shared'; +import { AbstractFormService, createEmptyStateResource, isLoaded, StateResource } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; -import { Component, inject } from '@angular/core'; +import { Component, inject, Input } from '@angular/core'; import { Resource } from '@ngxp/rest'; import { ButtonWithSpinnerComponent } from '@ods/component'; -import { Observable, of } from 'rxjs'; +import { Observable, of, tap } from 'rxjs'; @Component({ selector: 'admin-save-button', @@ -13,11 +14,24 @@ import { Observable, of } from 'rxjs'; templateUrl: './admin-save-button.component.html', }) export class AdminSaveButtonComponent { - private formService: AbstractFormService<Resource> = inject(ADMIN_FORMSERVICE); + @Input() successLinkPath: string; + + private readonly formService: AbstractFormService<Resource> = inject(ADMIN_FORMSERVICE); + private readonly navigationService: NavigationService = inject(NavigationService); public stateResource$: Observable<StateResource<Resource>> = of(createEmptyStateResource<Resource>()); public submit(): void { - this.stateResource$ = this.formService.submit(); + this.stateResource$ = this.formService.submit().pipe( + tap((stateResource: StateResource<Resource>) => { + this._navigateOnSuccessfulSubmission(stateResource); + }), + ); + } + + _navigateOnSuccessfulSubmission(stateResource: StateResource<Resource>) { + if (isLoaded(stateResource)) { + this.navigationService.navigate(this.successLinkPath); + } } } diff --git a/alfa-client/libs/admin/shared/src/lib/routes.ts b/alfa-client/libs/admin/shared/src/lib/routes.ts index 090a9f3a7d82d6c3489bfa5c542b31592c177e06..9f1496843e54d2c7ff25e132f0b68b4edee6469d 100644 --- a/alfa-client/libs/admin/shared/src/lib/routes.ts +++ b/alfa-client/libs/admin/shared/src/lib/routes.ts @@ -28,6 +28,7 @@ export enum ROUTES { BENUTZER_ID = 'benutzer/:userid', ORGANISATIONSEINHEITEN = 'organisationseinheiten', UNAVAILABLE = 'unavailable', - STATISTIK = 'statistik', - STATISTIK_NEU = 'statistik/neu', + AGGREGATION_MAPPING = 'auswertungen', + AGGREGATION_MAPPING_NEU = 'auswertungen/neu', + AGGREGATION_MAPPING_ID = 'auswertungen/:aggregationMappingId', } diff --git a/alfa-client/libs/admin/statistik/README.md b/alfa-client/libs/admin/statistik/README.md deleted file mode 100644 index ad651ba5c6b2577aae0af1c1ac56ca839db65022..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/statistik/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# statistik - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test statistik` to execute the unit tests. diff --git a/alfa-client/libs/admin/statistik/src/index.ts b/alfa-client/libs/admin/statistik/src/index.ts deleted file mode 100644 index 157271feb3bf8625864f12faec9623307c3b7690..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/statistik/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lib/statistik-container/statistik-container.component'; -export * from './lib/statistik-fields-form/admin-statistik-fields-form.component'; diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/admin-statistik-fields-form.component.html b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/admin-statistik-fields-form.component.html deleted file mode 100644 index ce24e7184c9f832dec08b205237303ce3cd25af7..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/admin-statistik-fields-form.component.html +++ /dev/null @@ -1,36 +0,0 @@ -<h2 class="heading-2" data-test-id="statistik-fields-form-header-text">Felder zur Auswertung hinzufügen</h2> - -<div class="flex max-w-4xl flex-col gap-4"> - <form class="form flex-col" [formGroup]="formService.form" class="flex flex-col gap-4"> - <div [formGroupName]="StatistikFieldsFormService.FIELD_FORM_IDENTIFIER" class="flex flex-col gap-4"> - <ods-text-editor - [formControlName]="StatistikFieldsFormService.FIELD_FORM_ENGINE_NAME" - label="Formengine" - placeholder="Tragen Sie hier die Formengine des Formulars ein." - data-test-id="form-engine-name" - dataTestId="form-engine-name" - ></ods-text-editor> - <ods-text-editor - [formControlName]="StatistikFieldsFormService.FIELD_FORM_ID" - label="FormID" - placeholder="Tragen Sie hier die FormID des Formulars ein." - data-test-id="form-id" - dataTestId="form-id" - ></ods-text-editor> - </div> - <statistik-fields-form-mapping /> - </form> - <ods-button - text="Datenfeld hinzufügen" - dataTestId="add-mapping-button" - data-test-id="add-mapping" - (clickEmitter)="formService.addMapping()" - > - <ods-plus-icon icon class="fill-whitetext" /> - </ods-button> - - <div class="mt-4 flex gap-4"> - <admin-save-button /> - <admin-cancel-button [linkPath]="Routes.STATISTIK" /> - </div> -</div> diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/admin-statistik-fields-form.component.spec.ts b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/admin-statistik-fields-form.component.spec.ts deleted file mode 100644 index e124d07cf71b81b1c0ee692d87cd25ed8e8b59cd..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/admin-statistik-fields-form.component.spec.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { ADMIN_FORMSERVICE } from '@admin-client/shared'; -import { EMPTY_STRING } from '@alfa-client/tech-shared'; -import { dispatchEventFromFixture, existsAsHtmlElement, mock, Mock, MockEvent } from '@alfa-client/test-utils'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { TextEditorComponent } from '@ods/component'; -import { ButtonComponent, PlusIconComponent } from '@ods/system'; -import { MockComponent } from 'ng-mocks'; -import { getDataTestIdOf } from '../../../../../tech-shared/test/data-test'; -import { AdminCancelButtonComponent } from '../../../../shared/src/lib/admin-cancel-button/admin-cancel-button.component'; -import { AdminSaveButtonComponent } from '../../../../shared/src/lib/admin-save-button/admin-save-button.component'; -import { AdminStatistikFieldsFormComponent } from './admin-statistik-fields-form.component'; -import { AdminStatistikFieldsFormMappingComponent } from './statistik-fields-form-mapping/statistik-fields-form-mapping.component'; -import { StatistikFieldsFormService } from './statistik-fields.formservice'; - -describe('AdminStatistikFieldsFormComponent', () => { - let component: AdminStatistikFieldsFormComponent; - let fixture: ComponentFixture<AdminStatistikFieldsFormComponent>; - - const formEngineNameInputTestId: string = getDataTestIdOf('form-engine-name'); - const formIdInputTestId: string = getDataTestIdOf('form-id'); - const addMappingButton: string = getDataTestIdOf('add-mapping'); - - const formBuilder: FormBuilder = new FormBuilder(); - - let formService: Mock<StatistikFieldsFormService>; - - beforeEach(async () => { - const form: FormGroup = formBuilder.group({ - [StatistikFieldsFormService.FIELD_FORM_IDENTIFIER]: formBuilder.group({ - [StatistikFieldsFormService.FIELD_FORM_ENGINE_NAME]: new FormControl(EMPTY_STRING), - [StatistikFieldsFormService.FIELD_FORM_ID]: new FormControl(EMPTY_STRING), - }), - }); - - formService = <any>{ ...mock(StatistikFieldsFormService), form }; - - await TestBed.configureTestingModule({ - declarations: [ - AdminStatistikFieldsFormComponent, - MockComponent(TextEditorComponent), - MockComponent(ButtonComponent), - MockComponent(PlusIconComponent), - MockComponent(AdminSaveButtonComponent), - MockComponent(AdminCancelButtonComponent), - MockComponent(AdminStatistikFieldsFormMappingComponent), - ], - imports: [ReactiveFormsModule], - }) - .overrideComponent(AdminStatistikFieldsFormComponent, { - set: { - providers: [ - { - provide: StatistikFieldsFormService, - useValue: formService, - }, - { - provide: ADMIN_FORMSERVICE, - useValue: formService, - }, - ], - }, - }) - .compileComponents(); - - fixture = TestBed.createComponent(AdminStatistikFieldsFormComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('template', () => { - describe('form engine input', () => { - it('should exists', () => { - fixture.detectChanges(); - - existsAsHtmlElement(fixture, formEngineNameInputTestId); - }); - }); - - describe('form id input', () => { - it('should exists', () => { - fixture.detectChanges(); - - existsAsHtmlElement(fixture, formIdInputTestId); - }); - }); - - describe('add mapping button', () => { - it('should exists', () => { - fixture.detectChanges(); - - existsAsHtmlElement(fixture, addMappingButton); - }); - - describe('output', () => { - describe('clickEmitter', () => { - it('should call formService', () => { - fixture.detectChanges(); - - dispatchEventFromFixture(fixture, addMappingButton, MockEvent.CLICK); - - expect(formService.addMapping).toHaveBeenCalled(); - }); - }); - }); - }); - }); -}); diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/admin-statistik-fields-form.component.ts b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/admin-statistik-fields-form.component.ts deleted file mode 100644 index 0298196202e623a3d080468c792f3c1c800a8750..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/admin-statistik-fields-form.component.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ADMIN_FORMSERVICE, AdminCancelButtonComponent, AdminSaveButtonComponent, ROUTES } from '@admin-client/shared'; -import { Component, inject } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { TextEditorComponent } from '@ods/component'; -import { ButtonComponent, DeleteIconComponent, PlusIconComponent } from '@ods/system'; -import { AdminStatistikFieldsFormMappingComponent } from './statistik-fields-form-mapping/statistik-fields-form-mapping.component'; -import { StatistikFieldsFormService } from './statistik-fields.formservice'; - -@Component({ - selector: 'admin-statistik-fields-form', - templateUrl: './admin-statistik-fields-form.component.html', - standalone: true, - imports: [ - ButtonComponent, - PlusIconComponent, - ReactiveFormsModule, - TextEditorComponent, - DeleteIconComponent, - AdminSaveButtonComponent, - AdminCancelButtonComponent, - AdminStatistikFieldsFormMappingComponent, - ], - providers: [{ provide: ADMIN_FORMSERVICE, useClass: StatistikFieldsFormService }], -}) -export class AdminStatistikFieldsFormComponent { - public readonly formService = <StatistikFieldsFormService>inject(ADMIN_FORMSERVICE); - - public readonly StatistikFieldsFormService = StatistikFieldsFormService; - public readonly Routes = ROUTES; -} diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form-mapping/statistik-fields-form-mapping.component.html b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form-mapping/statistik-fields-form-mapping.component.html deleted file mode 100644 index 569fc993f66170c76ad2cad6c28679b36e86e210..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form-mapping/statistik-fields-form-mapping.component.html +++ /dev/null @@ -1,29 +0,0 @@ -<form [formGroup]="formService.form"> - <div class="flex flex-col gap-4" [formArrayName]="StatistikFieldsFormService.FIELD_MAPPINGS"> - <div - *ngFor="let mappingControl of formService.mappings.controls; let i = index" - [formGroupName]="i" - class="flex w-full gap-2" - > - <ods-text-editor - class="flex-1" - formControlName="sourcePath" - label="Pfad des Datenfeldes" - placeholder="Tragen Sie hier den gesamten Pfad des Datenfeldes ein, das Sie auswerten möchten." - [dataTestId]="'mapping-field-' + i" - [attr.data-test-id]="'mapping-field-' + i" - ></ods-text-editor> - <ods-button - class="self-end" - variant="ghost" - size="fit" - destructive="true" - (clickEmitter)="formService.removeMapping(i)" - [dataTestId]="'remove-mapping-button-' + i" - [attr.data-test-id]="'remove-mapping-' + i" - > - <ods-delete-icon icon /> - </ods-button> - </div> - </div> -</form> diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form-mapping/statistik-fields-form-mapping.component.spec.ts b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form-mapping/statistik-fields-form-mapping.component.spec.ts deleted file mode 100644 index d06746e48f9660298e3c4e2d305a1404cd0e8605..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form-mapping/statistik-fields-form-mapping.component.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { ADMIN_FORMSERVICE } from '@admin-client/shared'; -import { EMPTY_STRING } from '@alfa-client/tech-shared'; -import { dispatchEventFromFixture, existsAsHtmlElement, mock, Mock, MockEvent, mockGetValue } from '@alfa-client/test-utils'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; -import { TextEditorComponent } from '@ods/component'; -import { ButtonComponent, DeleteIconComponent } from '@ods/system'; -import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; -import { MockComponent } from 'ng-mocks'; -import { StatistikFieldsFormService } from '../statistik-fields.formservice'; -import { AdminStatistikFieldsFormMappingComponent } from './statistik-fields-form-mapping.component'; - -describe('AdminStatistikFieldsFormMappingComponent', () => { - let component: AdminStatistikFieldsFormMappingComponent; - let fixture: ComponentFixture<AdminStatistikFieldsFormMappingComponent>; - - const mappingField: string = getDataTestIdOf('mapping-field-0'); - const removeMappingButton: string = getDataTestIdOf('remove-mapping-0'); - - const formBuilder: FormBuilder = new FormBuilder(); - - let formService: Mock<StatistikFieldsFormService>; - - beforeEach(async () => { - const form: FormGroup = formBuilder.group({ - [StatistikFieldsFormService.FIELD_MAPPINGS]: formBuilder.array([ - new FormGroup({ sourcePath: new FormControl(EMPTY_STRING) }), - ]), - }); - - formService = <any>{ - ...mock(StatistikFieldsFormService), - form, - addMapping: jest.fn(), - removeMapping: jest.fn(), - }; - - mockGetValue( - formService, - StatistikFieldsFormService.FIELD_MAPPINGS, - form.controls[StatistikFieldsFormService.FIELD_MAPPINGS], - ); - - await TestBed.configureTestingModule({ - declarations: [ - AdminStatistikFieldsFormMappingComponent, - MockComponent(TextEditorComponent), - MockComponent(ButtonComponent), - MockComponent(DeleteIconComponent), - ], - imports: [ReactiveFormsModule], - providers: [ - { - provide: StatistikFieldsFormService, - useValue: formService, - }, - { - provide: ADMIN_FORMSERVICE, - useValue: formService, - }, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(AdminStatistikFieldsFormMappingComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('template', () => { - describe('mapping input', () => { - it('should exists', () => { - fixture.detectChanges(); - - existsAsHtmlElement(fixture, mappingField); - }); - }); - - describe('remove mapping button', () => { - it('should call formservice', () => { - dispatchEventFromFixture(fixture, removeMappingButton, MockEvent.CLICK); - - expect(formService.removeMapping).toHaveBeenCalledWith(0); - }); - }); - }); -}); diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form-mapping/statistik-fields-form-mapping.component.ts b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form-mapping/statistik-fields-form-mapping.component.ts deleted file mode 100644 index 48bc3eaddd2a2fe51b983939dd8920b02e7f8dae..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields-form-mapping/statistik-fields-form-mapping.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { ADMIN_FORMSERVICE, AdminCancelButtonComponent, AdminSaveButtonComponent, ROUTES } from '@admin-client/shared'; -import { CommonModule } from '@angular/common'; -import { Component, inject } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { TextEditorComponent } from '@ods/component'; -import { ButtonComponent, DeleteIconComponent, PlusIconComponent } from '@ods/system'; -import { StatistikFieldsFormService } from '../statistik-fields.formservice'; - -@Component({ - selector: 'statistik-fields-form-mapping', - templateUrl: './statistik-fields-form-mapping.component.html', - standalone: true, - imports: [ - CommonModule, - ButtonComponent, - PlusIconComponent, - ReactiveFormsModule, - TextEditorComponent, - DeleteIconComponent, - AdminSaveButtonComponent, - AdminCancelButtonComponent, - ], -}) -export class AdminStatistikFieldsFormMappingComponent { - public readonly formService = <StatistikFieldsFormService>inject(ADMIN_FORMSERVICE); - - public readonly StatistikFieldsFormService = StatistikFieldsFormService; - public readonly Routes = ROUTES; -} diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields.formservice.spec.ts b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields.formservice.spec.ts deleted file mode 100644 index 58a2273cb692762c4f930e583c8c49f210099517..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields.formservice.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2025 Das Land Schleswig-Holstein vertreten durch den - * Ministerpräsidenten des Landes Schleswig-Holstein - * Staatskanzlei - * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung - * - * Lizenziert unter der EUPL, Version 1.2 oder - sobald - * diese von der Europäischen Kommission genehmigt wurden - - * Folgeversionen der EUPL ("Lizenz"); - * Sie dürfen dieses Werk ausschließlich gemäß - * dieser Lizenz nutzen. - * Eine Kopie der Lizenz finden Sie hier: - * - * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * Sofern nicht durch anwendbare Rechtsvorschriften - * gefordert oder in schriftlicher Form vereinbart, wird - * die unter der Lizenz verbreitete Software "so wie sie - * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - - * ausdrücklich oder stillschweigend - verbreitet. - * Die sprachspezifischen Genehmigungen und Beschränkungen - * unter der Lizenz sind dem Lizenztext zu entnehmen. - */ -import { AggregationMappingResource, AggregationMappingService } from '@admin-client/reporting-shared'; -import { EMPTY_STRING, StateResource, createStateResource } from '@alfa-client/tech-shared'; -import { Mock, mock } from '@alfa-client/test-utils'; -import { TestBed } from '@angular/core/testing'; -import { FormArray, FormControl, FormGroup } from '@angular/forms'; -import { createAggregationMapping, createAggregationMappingResource } from 'libs/admin/reporting-shared/test/aggregation-mapping'; -import { of } from 'rxjs'; -import { StatistikFieldsFormService } from './statistik-fields.formservice'; - -describe('StatistikFieldsFormService', () => { - let formService: StatistikFieldsFormService; - - let service: Mock<AggregationMappingService>; - - beforeEach(() => { - service = mock(AggregationMappingService); - - TestBed.configureTestingModule({ - providers: [StatistikFieldsFormService, { provide: AggregationMappingService, useValue: service }], - }); - - formService = TestBed.inject(StatistikFieldsFormService); - }); - - it('should create', () => { - expect(formService).toBeTruthy(); - }); - - describe('on do submit', () => { - const stateResource: StateResource<AggregationMappingResource> = createStateResource(createAggregationMappingResource()); - - beforeEach(() => { - service.create.mockReturnValue(of(stateResource)); - }); - - it('should call service', () => { - formService.form = <any>createAggregationMapping(); - - formService.submit(); - - expect(service.create).toHaveBeenCalledWith(formService.form.value); - }); - }); - - describe('add mapping', () => { - it('should add mapping control', () => { - formService.addMapping(); - - const mappingFormArray: FormArray = <FormArray>formService.form.controls[StatistikFieldsFormService.FIELD_MAPPINGS]; - expect(mappingFormArray).toHaveLength(2); - expect(mappingFormArray.controls[0].value).toEqual({ sourcePath: EMPTY_STRING }); - }); - }); - - describe('remove mapping', () => { - it('should remove mapping control', () => { - (<FormArray>formService.form.controls[StatistikFieldsFormService.FIELD_MAPPINGS]).push( - new FormGroup({ sourcePath: new FormControl('controlToRemove') }), - ); - - formService.removeMapping(1); - - const mappingFormArray: FormArray = <FormArray>formService.form.controls[StatistikFieldsFormService.FIELD_MAPPINGS]; - expect(mappingFormArray).toHaveLength(1); - expect(mappingFormArray.controls[0].value).toEqual({ sourcePath: EMPTY_STRING }); - }); - }); - - describe('get mappings', () => { - it('should return mappings as array', () => { - const mappings: FormArray = formService.mappings; - - expect(mappings).toHaveLength(1); - expect(mappings.controls[0].value).toEqual({ sourcePath: EMPTY_STRING }); - }); - }); -}); diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields.formservice.ts b/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields.formservice.ts deleted file mode 100644 index 1b746e12f42b44cc1261ce6de543380ea61d9ec4..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/statistik/src/lib/statistik-fields-form/statistik-fields.formservice.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AggregationMappingResource, AggregationMappingService } from '@admin-client/reporting-shared'; -import { AbstractFormService, EMPTY_STRING, StateResource } from '@alfa-client/tech-shared'; -import { inject, Injectable } from '@angular/core'; -import { FormArray, FormControl, FormGroup, UntypedFormGroup } from '@angular/forms'; -import { Observable } from 'rxjs'; - -@Injectable() -export class StatistikFieldsFormService extends AbstractFormService<AggregationMappingResource> { - private service = inject(AggregationMappingService); - - public static readonly FIELD_FORM_IDENTIFIER: string = 'formIdentifier'; - public static readonly FIELD_FORM_ENGINE_NAME: string = 'formEngineName'; - public static readonly FIELD_FORM_ID: string = 'formId'; - - public static readonly FIELD_MAPPINGS: string = 'mappings'; - - protected initForm(): UntypedFormGroup { - return this.formBuilder.group({ - [StatistikFieldsFormService.FIELD_FORM_IDENTIFIER]: this.formBuilder.group({ - [StatistikFieldsFormService.FIELD_FORM_ENGINE_NAME]: new FormControl(EMPTY_STRING), - [StatistikFieldsFormService.FIELD_FORM_ID]: new FormControl(EMPTY_STRING), - }), - [StatistikFieldsFormService.FIELD_MAPPINGS]: new FormArray([this.createArrayControl()]), - }); - } - - protected doSubmit(): Observable<StateResource<AggregationMappingResource>> { - return this.service.create(this.getFormValue()); - } - - protected getPathPrefix(): string { - return 'settingBody'; - } - - public addMapping(): void { - this.mappings.push(this.createArrayControl()); - } - - private createArrayControl(): FormGroup { - return new FormGroup({ sourcePath: new FormControl(EMPTY_STRING) }); - } - - public removeMapping(index: number): void { - this.mappings.removeAt(index); - } - - public get mappings(): FormArray { - return this.form.controls[StatistikFieldsFormService.FIELD_MAPPINGS] as FormArray; - } -} diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-data/user-form-data.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-data/user-form-data.component.html index 830c7960ac97bb94267cdb6737d3a83001e44951..fb1574bf9b8090e385af452584610fb4d03f457f 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-data/user-form-data.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-data/user-form-data.component.html @@ -1,17 +1,18 @@ <div [formGroup]="formGroupParent"> <div class="mb-4 grid gap-4 xl:grid-cols-2"> - <ods-text-editor [formControlName]="UserFormService.FIRST_NAME" [isRequired]="true" label="Vorname" /> - <ods-text-editor [formControlName]="UserFormService.LAST_NAME" [isRequired]="true" label="Nachname" /> + <ods-text-editor [formControlName]="UserFormService.FIRST_NAME" [isRequired]="true" label="Vorname" dataTestId="firstName" /> + <ods-text-editor [formControlName]="UserFormService.LAST_NAME" [isRequired]="true" label="Nachname" dataTestId="lastName" /> @if (isPatch) { - <admin-user-form-data-name [userName]="userName" data-test-id="user-name-info" /> + <admin-user-form-data-name [userName]="userName" data-test-id="user-name-info" dataTestId="username" /> } @else { <ods-text-editor [formControlName]="UserFormService.USERNAME" [isRequired]="true" label="Benutzername" data-test-id="user-name-editor" + dataTestId="username" /> } - <ods-text-editor [formControlName]="UserFormService.EMAIL" [isRequired]="true" label="E-Mail" /> + <ods-text-editor [formControlName]="UserFormService.EMAIL" [isRequired]="true" label="E-Mail" dataTestId="email" /> </div> </div> diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-organisations-einheit-list/user-form-organisations-einheit-list.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-organisations-einheit-list/user-form-organisations-einheit-list.component.html index 5d3fea2f35377a4c9a2a62d99d356fe38bbbf7d7..2c1d3520d18708d5b7861627130f35cec586c927 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-organisations-einheit-list/user-form-organisations-einheit-list.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-organisations-einheit-list/user-form-organisations-einheit-list.component.html @@ -1,11 +1,14 @@ -<div class="mb-12 block"> - <h2 class="heading-2 mt-4">Organisationseinheiten</h2> - <div [formGroup]="formGroupParent"> - <div [formGroupName]="UserFormService.GROUPS" class="flex flex-col gap-2"> - <p class="font-medium">Dem Benutzer sind folgende Organisationseinheiten zugewiesen</p> - @for (controlName of formGroupOrganisationsEinheiten.controls | keyvalue; track controlName.key) { - <ods-checkbox-editor [formControlName]="controlName.key" [label]="controlName.key" [inputId]="controlName.key" /> - } - </div> +<div + [formGroup]="formGroupParent" + role="group" + aria-labelledby="organisationseinheiten-heading organisationseinheiten-desc" + class="mb-12" +> + <h2 id="organisationseinheiten-heading" class="heading-2 mt-4">Organisationseinheiten</h2> + <p id="organisationseinheiten-desc" class="mb-2 font-medium">Dem Benutzer sind folgende Organisationseinheiten zugewiesen</p> + <div [formGroupName]="UserFormService.GROUPS" class="flex flex-col gap-2"> + @for (controlName of formGroupOrganisationsEinheiten.controls | keyvalue; track controlName.key) { + <ods-checkbox-editor [formControlName]="controlName.key" [label]="controlName.key" [inputId]="controlName.key" /> + } </div> </div> diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html index 1dda4319a297a47b72c1d1c8bc6e1401a796a7ee..d3d39a71ceb69e639c38e1ac0219f27eb1c5e6f5 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.html @@ -1,6 +1,12 @@ -<h2 class="heading-2 mt-4">Rollen für OZG-Cloud</h2> -<div [formGroup]="formGroupParent"> - <div [formGroupName]="UserFormService.CLIENT_ROLES" class="mb-8 flex flex-col gap-4 md:flex-row"> +<h2 class="heading-2 mt-4" id="rollen-ozg-heading">Rollen für OZG-Cloud<i aria-hidden="true">*</i></h2> +<div + [formGroup]="formGroupParent" + role="group" + aria-required="true" + aria-labelledby="rollen-ozg-heading" + [attr.aria-invalid]="!isValid" +> + <div [formGroupName]="UserFormService.CLIENT_ROLES" class="flex flex-col gap-4 md:flex-row"> <div [formGroupName]="UserFormService.ADMINISTRATION_GROUP" class="flex flex-1 flex-col gap-2"> <h3 class="text-md block font-medium text-text">Administration</h3> <div class="flex items-center gap-2"> @@ -54,4 +60,10 @@ </div> </div> </div> + <ods-validation-error + [id]="validationErrorId" + [invalidParams]="invalidParams$ | async" + label="Rollen" + data-test-id="rollen-error" + ></ods-validation-error> </div> diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts index b27337b2786891b5738a6d09bae607cecd492cdb..5f21edb7a5722e98bd9a0069b3dcb3f9ce8ad10a 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.spec.ts @@ -1,14 +1,24 @@ +import { InvalidParam } from '@alfa-client/tech-shared'; +import { existsAsHtmlElement, getElementComponentFromFixtureByCss } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; +import { AbstractControl, FormControl, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; +import { expect } from '@jest/globals'; import { InfoIconComponent, TooltipDirective } from '@ods/system'; import { MockComponent, MockDirective } from 'ng-mocks'; +import { of } from 'rxjs'; +import { ValidationErrorComponent } from '../../../../../../design-component/src/lib/form/validation-error/validation-error.component'; +import { getDataTestIdOf } from '../../../../../../tech-shared/test/data-test'; +import { createInvalidParam } from '../../../../../../tech-shared/test/error'; import { createUserFormGroup } from '../../../../test/form'; +import { UserFormService } from '../user.formservice'; import { UserFormRolesComponent } from './user-form-roles.component'; describe('UserFormRolesComponent', () => { let component: UserFormRolesComponent; let fixture: ComponentFixture<UserFormRolesComponent>; + const validationErrorTestId: string = getDataTestIdOf('rollen-error'); + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserFormRolesComponent, ReactiveFormsModule, MockComponent(InfoIconComponent), MockDirective(TooltipDirective)], @@ -23,4 +33,84 @@ describe('UserFormRolesComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('component', () => { + describe('ngOnInit', () => { + it('should emit invalid params on invalid control', (done) => { + const invalidParam: InvalidParam = createInvalidParam(); + const error: any = { dummy: invalidParam }; + const control: AbstractControl = new FormControl(); + component.formGroupParent = new UntypedFormGroup({ [UserFormService.CLIENT_ROLES]: control }); + + component.ngOnInit(); + component.invalidParams$.subscribe((result) => { + expect(result).toEqual([invalidParam]); + done(); + }); + control.setErrors(error); + }); + + it('should emit empty array on valid control', (done) => { + const control: AbstractControl = new FormControl(); + component.formGroupParent = new UntypedFormGroup({ [UserFormService.CLIENT_ROLES]: control }); + + component.ngOnInit(); + component.invalidParams$.subscribe((result) => { + expect(result).toEqual([]); + done(); + }); + + control.setErrors(null); + }); + + it('should mark as invalid', (done) => { + const invalidParam: InvalidParam = createInvalidParam(); + const error: any = { dummy: invalidParam }; + const control: AbstractControl = new FormControl(); + component.formGroupParent = new UntypedFormGroup({ [UserFormService.CLIENT_ROLES]: control }); + + component.ngOnInit(); + component.invalidParams$.subscribe(() => { + expect(component.isValid).toBe(false); + done(); + }); + + control.setErrors(error); + }); + + it('should mark as valid', (done) => { + const control: AbstractControl = new FormControl(); + component.formGroupParent = new UntypedFormGroup({ [UserFormService.CLIENT_ROLES]: control }); + + component.ngOnInit(); + component.invalidParams$.subscribe(() => { + expect(component.isValid).toBe(true); + done(); + }); + + control.setErrors(null); + }); + }); + }); + + describe('template', () => { + describe('validation error', () => { + it('should exists', () => { + existsAsHtmlElement(fixture, validationErrorTestId); + }); + + it('should have inputs', () => { + const invalidParam: InvalidParam = createInvalidParam(); + component.invalidParams$ = of([invalidParam]); + + fixture.detectChanges(); + const validationErrorComponent: ValidationErrorComponent = getElementComponentFromFixtureByCss( + fixture, + validationErrorTestId, + ); + + expect(validationErrorComponent.invalidParams).toEqual([invalidParam]); + }); + }); + }); }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts index c7d46e948f2ddc5b4c50fd5880ddf7ff2221030c..7a8c901dab035b8a8364b9ce60c1453a7374711c 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user-form-roles/user-form-roles.component.ts @@ -1,17 +1,40 @@ -import { Component, Input } from '@angular/core'; -import { ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; -import { CheckboxEditorComponent } from '@ods/component'; +import { generateValidationErrorId, InvalidParam } from '@alfa-client/tech-shared'; +import { AsyncPipe } from '@angular/common'; +import { Component, Input, OnInit } from '@angular/core'; +import { AbstractControl, FormControlStatus, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms'; +import { CheckboxEditorComponent, ValidationErrorComponent } from '@ods/component'; import { InfoIconComponent, TooltipDirective } from '@ods/system'; +import { isEmpty } from 'lodash-es'; +import { map, Observable, of, tap } from 'rxjs'; import { UserFormService } from '../user.formservice'; @Component({ selector: 'admin-user-form-roles', standalone: true, - imports: [CheckboxEditorComponent, ReactiveFormsModule, TooltipDirective, InfoIconComponent], + imports: [ + CheckboxEditorComponent, + ReactiveFormsModule, + TooltipDirective, + InfoIconComponent, + ValidationErrorComponent, + AsyncPipe, + ], templateUrl: './user-form-roles.component.html', }) -export class UserFormRolesComponent { +export class UserFormRolesComponent implements OnInit { @Input() formGroupParent: UntypedFormGroup; + public invalidParams$: Observable<InvalidParam[]> = of([]); + public readonly UserFormService = UserFormService; + public readonly validationErrorId: string = generateValidationErrorId(); + public isValid: boolean = true; + + ngOnInit(): void { + const control: AbstractControl = this.formGroupParent.controls[UserFormService.CLIENT_ROLES]; + this.invalidParams$ = control.statusChanges.pipe( + map((status: FormControlStatus) => (status === 'INVALID' ? Object.values(control.errors) : [])), + tap((invalidParams: InvalidParam[]) => (this.isValid = isEmpty(invalidParams))), + ); + } } diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts index 99e28aabc946516a2076a89eb850c1df7b5594cd..caabe30992d06bce0dffefa1c988f2e405a8793a 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts @@ -24,9 +24,14 @@ import { AdminOrganisationsEinheit, AdminOrganisationsEinheitService } from '@admin-client/organisations-einheit-shared'; import { ROUTES } from '@admin-client/shared'; import { User, UserService } from '@admin-client/user-shared'; -import { PatchConfig } from '@admin/keycloak-shared'; +import { KeycloakErrorMessage, KeycloakFieldName, PatchConfig } from '@admin/keycloak-shared'; import { NavigationService } from '@alfa-client/navigation-shared'; -import { createEmptyStateResource, createLoadingStateResource, createStateResource, StateResource, } from '@alfa-client/tech-shared'; +import { + createEmptyStateResource, + createLoadingStateResource, + createStateResource, + StateResource, +} from '@alfa-client/tech-shared'; import { Mock, mock, mockWindowError } from '@alfa-client/test-utils'; import { SnackBarService } from '@alfa-client/ui'; import { fakeAsync, TestBed, tick } from '@angular/core/testing'; @@ -34,24 +39,24 @@ import { AbstractControl, FormControl, FormGroup, UntypedFormBuilder, UntypedFor import { ActivatedRoute, UrlSegment } from '@angular/router'; import { faker } from '@faker-js/faker/locale/de'; import { expect } from '@jest/globals'; -import { cold } from 'jest-marbles'; import { createUser } from 'libs/admin/user-shared/test/user'; -import { Observable, of, throwError } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { createUrlSegment } from '../../../../../navigation-shared/test/navigation-test-factory'; import { singleCold, singleColdCompleted, singleHot } from '../../../../../tech-shared/test/marbles'; +import { createKeycloakHttpErrorResponse } from '../../../../keycloak-shared/src/test/keycloak'; import { createAdminOrganisationsEinheit } from '../../../../organisations-einheit-shared/src/test/organisations-einheit'; import { UserFormService } from './user.formservice'; import SpyInstance = jest.SpyInstance; describe('UserFormService', () => { - let formService: UserFormService; + let service: UserFormService; let roleGroup: UntypedFormGroup; let alfaGroup: UntypedFormGroup; let organisationsEinheitenGroup: UntypedFormGroup; let administrationGroup: UntypedFormGroup; - let service: Mock<UserService>; + let userService: Mock<UserService>; let adminOrganisationsEinheitService: Mock<AdminOrganisationsEinheitService>; let navigationService: Mock<NavigationService>; let snackBarService: Mock<SnackBarService>; @@ -63,7 +68,7 @@ describe('UserFormService', () => { ]); beforeEach(() => { - service = { + userService = { ...mock(UserService), refresh: jest.fn(), create: jest.fn(), @@ -83,7 +88,7 @@ describe('UserFormService', () => { providers: [ UserFormService, UntypedFormBuilder, - { provide: UserService, useValue: service }, + { provide: UserService, useValue: userService }, { provide: AdminOrganisationsEinheitService, useValue: adminOrganisationsEinheitService }, { provide: NavigationService, useValue: navigationService }, { provide: SnackBarService, useValue: snackBarService }, @@ -91,15 +96,15 @@ describe('UserFormService', () => { ], }); - formService = TestBed.inject(UserFormService); - organisationsEinheitenGroup = <UntypedFormGroup>formService.form.get(UserFormService.GROUPS); - roleGroup = <UntypedFormGroup>formService.form.get(UserFormService.CLIENT_ROLES); + service = TestBed.inject(UserFormService); + organisationsEinheitenGroup = <UntypedFormGroup>service.form.get(UserFormService.GROUPS); + roleGroup = <UntypedFormGroup>service.form.get(UserFormService.CLIENT_ROLES); alfaGroup = <UntypedFormGroup>roleGroup.get(UserFormService.ALFA_GROUP); administrationGroup = <UntypedFormGroup>roleGroup.get(UserFormService.ADMINISTRATION_GROUP); }); it('should create', () => { - expect(formService).toBeTruthy(); + expect(service).toBeTruthy(); }); describe('build patch config', () => { @@ -108,7 +113,7 @@ describe('UserFormService', () => { const benutzerUrlSegment: UrlSegment = <UrlSegment>{ ...createUrlSegment(), path: 'benutzer' }; const idUrlSegment: UrlSegment = <UrlSegment>{ ...createUrlSegment(), path: 'dummyId' }; - const patchConfig: PatchConfig = formService._buildPatchConfig([benutzerUrlSegment, idUrlSegment]); + const patchConfig: PatchConfig = service._buildPatchConfig([benutzerUrlSegment, idUrlSegment]); expect(patchConfig).toEqual({ id: 'dummyId', doPatch: true }); }); @@ -118,7 +123,7 @@ describe('UserFormService', () => { const benutzerUrlSegment: UrlSegment = <UrlSegment>{ ...createUrlSegment(), path: 'benutzer' }; const idUrlSegment: UrlSegment = <UrlSegment>{ ...createUrlSegment(), path: 'neu' }; - const patchConfig: PatchConfig = formService._buildPatchConfig([benutzerUrlSegment, idUrlSegment]); + const patchConfig: PatchConfig = service._buildPatchConfig([benutzerUrlSegment, idUrlSegment]); expect(patchConfig).toEqual({ doPatch: false }); }); @@ -130,17 +135,17 @@ describe('UserFormService', () => { const loadedUser: StateResource<User> = createStateResource(createUser()); beforeEach(() => { - service.getUserById.mockReturnValue(singleHot(loadedUser)); + userService.getUserById.mockReturnValue(singleHot(loadedUser)); }); it('should call service to get user by id', () => { - formService._load(id); + service._load(id); - expect(service.getUserById).toHaveBeenCalledWith(id); + expect(userService.getUserById).toHaveBeenCalledWith(id); }); it('should return loaded user', () => { - const response: Observable<StateResource<User>> = formService._load(id); + const response: Observable<StateResource<User>> = service._load(id); expect(response).toBeObservable(singleCold(loadedUser)); }); @@ -148,65 +153,63 @@ describe('UserFormService', () => { describe('listenToAlfaGroupChanges', () => { it('should call handleAlfaGroupChange on initial change', () => { - formService._handleAlfaGroupChange = jest.fn(); + service._handleAlfaGroupChange = jest.fn(); - formService.listenToAlfaGroupChanges(); + service.listenToAlfaGroupChanges(); - expect(formService._handleAlfaGroupChange).toHaveBeenCalled(); + expect(service._handleAlfaGroupChange).toHaveBeenCalled(); }); it('should call handleAlfaGroupChange when value of form element changes', fakeAsync(() => { - formService._handleAlfaGroupChange = jest.fn(); + service._handleAlfaGroupChange = jest.fn(); alfaGroup.get(UserFormService.LOESCHEN).setValue(true); tick(); - expect(formService._handleAlfaGroupChange).toHaveBeenCalled(); + expect(service._handleAlfaGroupChange).toHaveBeenCalled(); })); }); describe('initOrganisationsEinheiten', () => { beforeEach(() => { - formService._addOrganisationsEinheitenToForm = jest.fn(); + service._addOrganisationsEinheitenToForm = jest.fn(); }); it('should call adminOrganisationsEinheitService getAll', () => { - formService._initOrganisationsEinheiten(); + service._initOrganisationsEinheiten(); expect(adminOrganisationsEinheitService.getAll).toHaveBeenCalled(); }); it('should return resource', () => { - const result: Observable<AdminOrganisationsEinheit[]> = formService._initOrganisationsEinheiten(); + const result: Observable<AdminOrganisationsEinheit[]> = service._initOrganisationsEinheiten(); expect(result).toBeObservable(singleColdCompleted(adminOrganisationsEinheitList.resource)); }); it('should call addOrganisationsEinheitenToForm ', fakeAsync(() => { - formService._initOrganisationsEinheiten().subscribe(); + service._initOrganisationsEinheiten().subscribe(); tick(); - expect(formService._addOrganisationsEinheitenToForm).toHaveBeenCalled(); + expect(service._addOrganisationsEinheitenToForm).toHaveBeenCalled(); })); it('should save ids in map', () => { - formService._initOrganisationsEinheiten().subscribe(); + service._initOrganisationsEinheiten().subscribe(); - expect(formService._organisationsEinheitToGroupIdMap.get(adminOrganisationsEinheit.name)).toEqual( - adminOrganisationsEinheit.id, - ); + expect(service._organisationsEinheitToGroupIdMap.get(adminOrganisationsEinheit.name)).toEqual(adminOrganisationsEinheit.id); }); it('should set initOrganisationsEinheiten$', () => { - expect(formService['_initOrganisationsEinheiten$']).toBeDefined(); + expect(service['_initOrganisationsEinheiten$']).toBeDefined(); }); it('should not throw any exception on loading state resource', () => { adminOrganisationsEinheitService.getAll.mockReturnValue(of(createLoadingStateResource())); const errorMock: any = mockWindowError(); - formService._initOrganisationsEinheiten(); + service._initOrganisationsEinheiten(); expect(errorMock).not.toHaveBeenCalled(); }); @@ -214,53 +217,37 @@ describe('UserFormService', () => { describe('addOrganisationsEinheitenToForm', () => { it('should add organisationsEinheiten to form', () => { - const organisationsEinheitenGroup: UntypedFormGroup = <UntypedFormGroup>formService.form.get(UserFormService.GROUPS); + const organisationsEinheitenGroup: UntypedFormGroup = <UntypedFormGroup>service.form.get(UserFormService.GROUPS); - formService._addOrganisationsEinheitenToForm(adminOrganisationsEinheitList.resource, organisationsEinheitenGroup); + service._addOrganisationsEinheitenToForm(adminOrganisationsEinheitList.resource, organisationsEinheitenGroup); expect(organisationsEinheitenGroup.value).toEqual({ [adminOrganisationsEinheit.name]: false }); }); }); - describe('roleValidator', () => { - it('should return error if no role is selected', () => { - const result = formService.roleValidator()(roleGroup); - - expect(result).toEqual({ atLeastOneRoleSelected: true }); - }); - - it('should return null if at least one role is selected', () => { - alfaGroup.get(UserFormService.LOESCHEN).setValue(true); - - const result = formService.roleValidator()(roleGroup); - - expect(result).toBeNull(); - }); - }); - describe('handleAlfaGroupChange', () => { it('should call disableUncheckedCheckboxes if any checkbox is checked', () => { - formService._isAnyChecked = jest.fn().mockReturnValue(true); - formService._disableUncheckedCheckboxes = jest.fn(); + service._isAnyChecked = jest.fn().mockReturnValue(true); + service._disableUncheckedCheckboxes = jest.fn(); - formService._handleAlfaGroupChange(alfaGroup); + service._handleAlfaGroupChange(alfaGroup); - expect(formService._disableUncheckedCheckboxes).toHaveBeenCalled(); + expect(service._disableUncheckedCheckboxes).toHaveBeenCalled(); }); it('should call enableAllCheckboxes if not any checkbox is checked', () => { - formService._isAnyChecked = jest.fn().mockReturnValue(false); - formService._enableAllCheckboxes = jest.fn(); + service._isAnyChecked = jest.fn().mockReturnValue(false); + service._enableAllCheckboxes = jest.fn(); - formService._handleAlfaGroupChange(alfaGroup); + service._handleAlfaGroupChange(alfaGroup); - expect(formService._enableAllCheckboxes).toHaveBeenCalled(); + expect(service._enableAllCheckboxes).toHaveBeenCalled(); }); }); describe('isAnyChecked', () => { it('should return false if no checkbox is checked', () => { - const result = formService._isAnyChecked(alfaGroup); + const result = service._isAnyChecked(alfaGroup); expect(result).toBe(false); }); @@ -268,7 +255,7 @@ describe('UserFormService', () => { it('should return true if any checkbox is checked', () => { alfaGroup.get(UserFormService.LOESCHEN).setValue(true); - const result = formService._isAnyChecked(alfaGroup); + const result = service._isAnyChecked(alfaGroup); expect(result).toBe(true); }); @@ -279,7 +266,7 @@ describe('UserFormService', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); control.setValue(false); - formService._disableUncheckedCheckboxes(alfaGroup); + service._disableUncheckedCheckboxes(alfaGroup); expect(control.disabled).toBe(true); }); @@ -288,7 +275,7 @@ describe('UserFormService', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); control.setValue(true); - formService._disableUncheckedCheckboxes(alfaGroup); + service._disableUncheckedCheckboxes(alfaGroup); expect(control.disabled).toBe(false); }); @@ -299,7 +286,7 @@ describe('UserFormService', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); control.setValue(false); - formService._disableUncheckedCheckboxes(alfaGroup); + service._disableUncheckedCheckboxes(alfaGroup); expect(control.disabled).toBe(true); }); @@ -310,7 +297,7 @@ describe('UserFormService', () => { const control: AbstractControl = alfaGroup.get(UserFormService.LOESCHEN); const enableSpy = jest.spyOn(control, 'enable'); - formService._enableAllCheckboxes(alfaGroup); + service._enableAllCheckboxes(alfaGroup); expect(enableSpy).toHaveBeenCalled(); }); @@ -320,63 +307,44 @@ describe('UserFormService', () => { const user: User = createUser(); beforeEach(() => { - formService.isInvalid = jest.fn().mockReturnValue(false); - service.create.mockReturnValue(of(createStateResource(user))); - }); - - it('should return empty stateResource if form is invalid', () => { - formService.isInvalid = jest.fn().mockReturnValue(true); - - const result: Observable<StateResource<User>> = formService._doSubmit(); - - expect(result).toBeObservable(singleColdCompleted(createEmptyStateResource<User>())); + service.isInvalid = jest.fn().mockReturnValue(false); + userService.create.mockReturnValue(of(createStateResource(user))); }); it('should call createUser', () => { - formService._createUser = jest.fn(); + service._createUser = jest.fn(); - formService.submit(); + service._doSubmit().subscribe(); - expect(formService._createUser).toHaveBeenCalled(); + expect(service._createUser).toHaveBeenCalled(); }); it('should call _createOrSave', () => { - formService._createUser = jest.fn().mockReturnValue(user); - formService._createOrSave = jest.fn().mockReturnValue(of(user)); + service._createUser = jest.fn().mockReturnValue(user); + service._createOrSave = jest.fn().mockReturnValue(of(user)); - formService.submit(); + service._doSubmit().subscribe(); - expect(formService._createOrSave).toHaveBeenCalledWith(user); + expect(service._createOrSave).toHaveBeenCalledWith(user); }); it('should call handleOnCreateUserSuccess if not loading', fakeAsync(() => { - const handleOnCreateUserSuccessSpy: SpyInstance = jest.spyOn(formService, 'handleOnCreateUserSuccess'); + const handleOnCreateUserSuccessSpy: SpyInstance = jest.spyOn(service, 'handleOnCreateUserSuccess'); - formService.submit().subscribe(); - tick(); + service._doSubmit().subscribe(); expect(handleOnCreateUserSuccessSpy).toHaveBeenCalled(); })); it('should not call handleOnCreateUserSuccess if loading', fakeAsync(() => { - service.create.mockReturnValue(of(createEmptyStateResource(true))); - const handleOnCreateUserSuccessSpy: SpyInstance = jest.spyOn(formService, 'handleOnCreateUserSuccess'); + userService.create.mockReturnValue(of(createEmptyStateResource(true))); + const handleOnCreateUserSuccessSpy: SpyInstance = jest.spyOn(service, 'handleOnCreateUserSuccess'); - formService.submit().subscribe(); + service.submit().subscribe(); tick(); expect(handleOnCreateUserSuccessSpy).not.toHaveBeenCalled(); })); - - it('should call handleSubmitError on error', fakeAsync(() => { - service.create.mockReturnValue(throwError(() => new Error())); - const handleSubmitErrorSpy: SpyInstance = jest.spyOn(formService, 'handleSubmitError'); - - formService.submit().subscribe(); - tick(); - - expect(handleSubmitErrorSpy).toHaveBeenCalled(); - })); }); describe('createOrSave', () => { @@ -387,57 +355,50 @@ describe('UserFormService', () => { const patchConfig: PatchConfig = { id, doPatch: true }; it('should call save if patched', () => { - formService.isPatch = jest.fn().mockReturnValue(true); - formService._patchConfig = patchConfig; + service.isPatch = jest.fn().mockReturnValue(true); + service._patchConfig = patchConfig; user.groups = [adminOrganisationsEinheit.name]; - formService._organisationsEinheitToGroupIdMap.set(adminOrganisationsEinheit.name, adminOrganisationsEinheit.id); + service._organisationsEinheitToGroupIdMap.set(adminOrganisationsEinheit.name, adminOrganisationsEinheit.id); - formService._createOrSave(user); + service._createOrSave(user); - expect(service.save).toHaveBeenCalledWith({ ...user, id, groupIds: [adminOrganisationsEinheit.id] }); + expect(userService.save).toHaveBeenCalledWith({ ...user, id, groupIds: [adminOrganisationsEinheit.id] }); }); it('should call create if not patched', () => { - formService.isPatch = jest.fn().mockReturnValue(false); + service.isPatch = jest.fn().mockReturnValue(false); - formService._createOrSave(user); + service._createOrSave(user); - expect(service.create).toHaveBeenCalledWith(user); + expect(userService.create).toHaveBeenCalledWith(user); }); }); describe('handleOnCreateUserSuccess', () => { it('should show success message for create', () => { - formService.handleOnCreateUserSuccess(); + service.handleOnCreateUserSuccess(); expect(snackBarService.showInfo).toHaveBeenCalledWith('Der Benutzer wurde angelegt.'); }); it('should show success message for patch', () => { - formService.isPatch = jest.fn().mockReturnValue(true); + service.isPatch = jest.fn().mockReturnValue(true); - formService.handleOnCreateUserSuccess(); + service.handleOnCreateUserSuccess(); expect(snackBarService.showInfo).toHaveBeenCalledWith('Der Benutzer wurde gespeichert.'); }); it('should navigate back to user list', () => { - formService.handleOnCreateUserSuccess(); + service.handleOnCreateUserSuccess(); expect(navigationService.navigate).toHaveBeenCalledWith(ROUTES.BENUTZER); }); }); - describe('handleSubmitError', () => { - it('should return empty stateResource', () => { - const result = formService.handleSubmitError(); - - expect(result).toBeObservable(cold('(a|)', { a: createEmptyStateResource<User>() })); - }); - + describe('handle unknown keycloak error', () => { it('should show error message', fakeAsync(() => { - formService.handleSubmitError().subscribe(); - tick(); + service._handleUnknownKeycloakError(createKeycloakHttpErrorResponse()); expect(snackBarService.showError).toHaveBeenCalledWith('Der Benutzer konnte nicht angelegt werden.'); })); @@ -445,7 +406,7 @@ describe('UserFormService', () => { describe('getRoles', () => { it('should return no roles when none are active', () => { - const result: string[] = formService._getRoles(UserFormService.ALFA_GROUP); + const result: string[] = service._getRoles(UserFormService.ALFA_GROUP); expect(result).toEqual([]); }); @@ -453,7 +414,7 @@ describe('UserFormService', () => { it('should return poststelle role when active', () => { alfaGroup.get(UserFormService.POSTSTELLE).setValue(true); - const result: string[] = formService._getRoles(UserFormService.ALFA_GROUP); + const result: string[] = service._getRoles(UserFormService.ALFA_GROUP); expect(result).toEqual([UserFormService.POSTSTELLE]); }); @@ -461,7 +422,7 @@ describe('UserFormService', () => { describe('getActiveOrganisationsEinheiten', () => { it('should return no groups when none are active', () => { - const result: string[] = formService._getActiveOrganisationsEinheiten(); + const result: string[] = service._getActiveOrganisationsEinheiten(); expect(result).toEqual([]); }); @@ -470,7 +431,7 @@ describe('UserFormService', () => { const organisationsEinheit: AdminOrganisationsEinheit = createAdminOrganisationsEinheit(); organisationsEinheitenGroup.addControl(organisationsEinheit.name, new FormControl(true)); - const result: string[] = formService._getActiveOrganisationsEinheiten(); + const result: string[] = service._getActiveOrganisationsEinheiten(); expect(result).toEqual([organisationsEinheit.name]); }); @@ -478,29 +439,65 @@ describe('UserFormService', () => { describe('ngOnDestroy', () => { it('should unsubscribe from initOrganisationsEinheiten$', () => { - formService._initOrganisationsEinheiten$.unsubscribe = jest.fn(); + service._initOrganisationsEinheiten$.unsubscribe = jest.fn(); - formService.ngOnDestroy(); + service.ngOnDestroy(); - expect(formService._initOrganisationsEinheiten$.unsubscribe).toHaveBeenCalled(); + expect(service._initOrganisationsEinheiten$.unsubscribe).toHaveBeenCalled(); }); it('should unsubscribe from initOrganisationsEinheiten$', () => { - formService._alfaGroupChanges.unsubscribe = jest.fn(); + service._alfaGroupChanges.unsubscribe = jest.fn(); - formService.ngOnDestroy(); + service.ngOnDestroy(); - expect(formService._alfaGroupChanges.unsubscribe).toHaveBeenCalled(); + expect(service._alfaGroupChanges.unsubscribe).toHaveBeenCalled(); }); }); describe('get userName', () => { it('should return form control value of userName', () => { - formService.form = new FormGroup({ [UserFormService.USERNAME]: new FormControl('userNameDummy') }); + service.form = new FormGroup({ [UserFormService.USERNAME]: new FormControl('userNameDummy') }); - const userName: string = formService.getUserName(); + const userName: string = service.getUserName(); expect(userName).toBe('userNameDummy'); }); }); + + describe('map keycloak error message to control name', () => { + it('should map username is missing error message to username', () => { + expect(service._mapKeycloakErrorMessageToControlName(KeycloakErrorMessage.USERNAME_IS_MISSING)).toEqual( + UserFormService.USERNAME, + ); + }); + + it('should user name already exists error message to username', () => { + expect(service._mapKeycloakErrorMessageToControlName(KeycloakErrorMessage.USERNAME_EXISTS)).toEqual( + UserFormService.USERNAME, + ); + }); + + it('should map email exists error message to email', () => { + expect(service._mapKeycloakErrorMessageToControlName(KeycloakErrorMessage.EMAIL_EXISTS)).toEqual(UserFormService.EMAIL); + }); + + it('should throw error', () => { + expect(() => service._mapKeycloakErrorMessageToControlName('unknown' as KeycloakErrorMessage)).toThrowError(); + }); + }); + + describe('map keycloak field name to control name', () => { + it('should map username to username', () => { + expect(service._mapKeycloakFieldNameToControlName(KeycloakFieldName.USERNAME)).toEqual(UserFormService.USERNAME); + }); + + it('should map email to email', () => { + expect(service._mapKeycloakFieldNameToControlName(KeycloakFieldName.EMAIL)).toEqual(UserFormService.EMAIL); + }); + + it('should throw error', () => { + expect(() => service._mapKeycloakFieldNameToControlName('unknown' as KeycloakFieldName)).toThrowError(); + }); + }); }); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts index 666d4d192605be0c6e14cca6008225b86570e1d5..b0cb7867c9cd8330c274fd9ee944d16f9a5507bb 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.ts @@ -24,14 +24,29 @@ import { AdminOrganisationsEinheit, AdminOrganisationsEinheitService } from '@admin-client/organisations-einheit-shared'; import { ROUTES } from '@admin-client/shared'; import { AdminRoles, User, UserService } from '@admin-client/user-shared'; -import { KeycloakFormService, PatchConfig } from '@admin/keycloak-shared'; +import { + KeycloakErrorMessage, + KeycloakFieldName, + KeycloakFormService, + KeycloakHttpErrorResponse, + PatchConfig, +} from '@admin/keycloak-shared'; import { NavigationService } from '@alfa-client/navigation-shared'; -import { createEmptyStateResource, EMPTY_STRING, isLoaded, mapToResource, StateResource } from '@alfa-client/tech-shared'; +import { + checkBoxGroupsEmptyValidator, + EMPTY_STRING, + fieldEmptyValidator, + fieldInvalidValidator, + fieldLengthValidator, + isLoaded, + mapToResource, + StateResource, +} from '@alfa-client/tech-shared'; import { SnackBarService } from '@alfa-client/ui'; import { Injectable, OnDestroy } from '@angular/core'; -import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup, ValidationErrors, ValidatorFn, Validators, } from '@angular/forms'; +import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { UrlSegment } from '@angular/router'; -import { catchError, filter, Observable, of, Subscription, tap } from 'rxjs'; +import { filter, Observable, Subscription, tap } from 'rxjs'; @Injectable() export class UserFormService extends KeycloakFormService<User> implements OnDestroy { @@ -86,10 +101,12 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest _initForm(): UntypedFormGroup { return this.formBuilder.group({ - [UserFormService.FIRST_NAME]: new FormControl(EMPTY_STRING, Validators.required), - [UserFormService.LAST_NAME]: new FormControl(EMPTY_STRING, Validators.required), - [UserFormService.USERNAME]: new FormControl(EMPTY_STRING, Validators.required), - [UserFormService.EMAIL]: new FormControl(EMPTY_STRING, [Validators.required]), + [UserFormService.FIRST_NAME]: new FormControl(EMPTY_STRING, fieldEmptyValidator(UserFormService.FIRST_NAME)), + [UserFormService.LAST_NAME]: new FormControl(EMPTY_STRING, fieldEmptyValidator(UserFormService.LAST_NAME)), + [UserFormService.USERNAME]: new FormControl(EMPTY_STRING, [fieldLengthValidator(UserFormService.USERNAME, 3, 255)]), + [UserFormService.EMAIL]: new FormControl(EMPTY_STRING, [ + fieldInvalidValidator(UserFormService.EMAIL, /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/), + ]), [UserFormService.CLIENT_ROLES]: this.formBuilder.group( { [UserFormService.ADMINISTRATION_GROUP]: this.formBuilder.group({ @@ -102,33 +119,17 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest [UserFormService.POSTSTELLE]: new FormControl(false), }), }, - { validators: this.roleValidator() }, + { + validators: checkBoxGroupsEmptyValidator(UserFormService.CLIENT_ROLES, [ + UserFormService.ADMINISTRATION_GROUP, + UserFormService.ALFA_GROUP, + ]), + }, ), [UserFormService.GROUPS]: this.formBuilder.group({}), }); } - roleValidator(): ValidatorFn { - return (control: AbstractControl<UntypedFormGroup>): ValidationErrors | null => { - const rolesGroups: UntypedFormGroup = control.value; - - if (this.anyRoleSelected(rolesGroups)) { - return null; - } - - return { atLeastOneRoleSelected: true }; - }; - } - - private anyRoleSelected(rolesGroups: UntypedFormGroup): boolean { - for (const subGroup of Object.values(rolesGroups)) { - if (Object.values(subGroup).includes(true)) { - return true; - } - } - return false; - } - _initOrganisationsEinheiten(): Observable<AdminOrganisationsEinheit[]> { const organisationsEinheitenGroup: UntypedFormGroup = this.getOrganisationsEinheitenGroup(); return this.adminOrganisationsEinheitService.getAll().pipe( @@ -190,16 +191,11 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest } _doSubmit(): Observable<StateResource<User>> { - if (this.isInvalid()) { - return of(createEmptyStateResource<User>()); - } - const user: User = this._createUser(); return this._createOrSave(user).pipe( tap((state: StateResource<User>): void => { if (!state.loading) this.handleOnCreateUserSuccess(); }), - catchError((): Observable<StateResource<User>> => this.handleSubmitError()), ); } @@ -220,11 +216,6 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest this.navigationService.navigate(ROUTES.BENUTZER); } - handleSubmitError(): Observable<StateResource<User>> { - this.snackBarService.showError('Der Benutzer konnte nicht angelegt werden.'); - return of(createEmptyStateResource<User>()); - } - _createUser(): User { return { ...this._getFormValue(), @@ -265,4 +256,33 @@ export class UserFormService extends KeycloakFormService<User> implements OnDest public getUserName(): string { return this.form.get(UserFormService.USERNAME).value; } + + _mapKeycloakErrorMessageToControlName(keycloakErrorMessage: KeycloakErrorMessage): string { + switch (keycloakErrorMessage) { + case KeycloakErrorMessage.USERNAME_IS_MISSING: + case KeycloakErrorMessage.USERNAME_EXISTS: + return UserFormService.USERNAME; + case KeycloakErrorMessage.EMAIL_EXISTS: + return UserFormService.EMAIL; + default: + throw new Error(`Cannot map keycloak error message ${keycloakErrorMessage} to control name`); + } + } + + _mapKeycloakFieldNameToControlName(keycloakFieldName: KeycloakFieldName): string { + switch (keycloakFieldName) { + case KeycloakFieldName.USERNAME: + return UserFormService.USERNAME; + case KeycloakFieldName.EMAIL: + return UserFormService.EMAIL; + default: + throw new Error(`Cannot map keycloak field name ${keycloakFieldName} to control name`); + } + } + + _handleUnknownKeycloakError(error: KeycloakHttpErrorResponse): void { + this.snackBarService.showError( + this.isPatch() ? 'Der Benutzer konnte nicht gespeichert werden.' : 'Der Benutzer konnte nicht angelegt werden.', + ); + } } diff --git a/alfa-client/libs/design-component/src/index.ts b/alfa-client/libs/design-component/src/index.ts index 9eec827c3a019115cc42c751521816fdde5c87a7..27e3b6381b8dc83a99533d4440b7fac8845e9f48 100644 --- a/alfa-client/libs/design-component/src/index.ts +++ b/alfa-client/libs/design-component/src/index.ts @@ -31,6 +31,7 @@ export * from './lib/form/formcontrol-editor.abstract.component'; export * from './lib/form/single-file-upload-editor/single-file-upload-editor.component'; export * from './lib/form/text-editor/text-editor.component'; export * from './lib/form/textarea-editor/textarea-editor.component'; +export * from './lib/form/validation-error/validation-error.component'; export * from './lib/open-dialog-button/open-dialog-button.component'; export * from './lib/routing-button/routing-button.component'; export * from './lib/spinner/spinner.component'; diff --git a/alfa-client/libs/design-component/src/lib/button-with-spinner/button-with-spinner.component.ts b/alfa-client/libs/design-component/src/lib/button-with-spinner/button-with-spinner.component.ts index 1a97d12351e15c734c2108bf255e4f6836b31d08..b92ebcbbb4afa26d585285c50c807934c3ea735f 100644 --- a/alfa-client/libs/design-component/src/lib/button-with-spinner/button-with-spinner.component.ts +++ b/alfa-client/libs/design-component/src/lib/button-with-spinner/button-with-spinner.component.ts @@ -49,7 +49,7 @@ type ButtonVariants = VariantProps<typeof buttonVariants>; [dataTestId]="dataTestId" [isLoading]="isLoading" [disabled]="disabled" - (click)="clickEmitter.emit()" + (clickEmitter)="clickEmitter.emit()" > <ng-content icon select="[icon]" /> </ods-button>`, diff --git a/alfa-client/libs/design-component/src/lib/cancel-dialog-button/cancel-dialog-button.component.ts b/alfa-client/libs/design-component/src/lib/cancel-dialog-button/cancel-dialog-button.component.ts index df1f5fa4b1732a9b60879f6353ec9f006832cb5f..03ac3acbe39b7907df7af242da47d4d3f70a5fa3 100644 --- a/alfa-client/libs/design-component/src/lib/cancel-dialog-button/cancel-dialog-button.component.ts +++ b/alfa-client/libs/design-component/src/lib/cancel-dialog-button/cancel-dialog-button.component.ts @@ -27,7 +27,11 @@ import { ButtonComponent, CloseIconComponent, TooltipDirective } from '@ods/syst text="Abbrechen" dataTestId="cancel-dialog-button" data-test-id="cancel-dialog-button-container" - /> + > + <ng-container icon> + <ods-close-icon class="fill-primary" /> + </ng-container> + </ods-button> }`, }) export class CancelDialogButtonComponent { diff --git a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html index 2533dc1ed4b63eec0da6aac3d7969a8b34172740..1cc8b07470d8086854cee7b310342c8697b4eed5 100644 --- a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html +++ b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.html @@ -29,9 +29,11 @@ [label]="label" [disabled]="control.disabled" [hasError]="hasError" + [ariaDescribedBy]="validationErrorId" > <ods-validation-error error + [id]="validationErrorId" [invalidParams]="invalidParams" [label]="label" [attr.data-test-id]="(label | convertForDataTest) + '-checkbox-editor-error'" diff --git a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts index dfab859671455c3354bd14297d069ffde5474cde..bbb9f693fc54a9760f0bb33b006848b12aba6554 100644 --- a/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/checkbox-editor/checkbox-editor.component.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; +import { ConvertForDataTestPipe, generateValidationErrorId } from '@alfa-client/tech-shared'; import { Component, Input } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { CheckboxComponent } from '@ods/system'; @@ -38,6 +38,8 @@ export class CheckboxEditorComponent extends FormControlEditorAbstractComponent @Input() inputId: string; @Input() label: string; + public readonly validationErrorId: string = generateValidationErrorId(); + get hasError(): boolean { return this.invalidParams.length > 0; } diff --git a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html index c4c009107436b4b7a8837c0fa9c31c4add1008d6..3c116fb0b27c3f1b3b24807e7a0e26affb514b00 100644 --- a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html +++ b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html @@ -34,9 +34,11 @@ [required]="isRequired" [focus]="focus" [showLabel]="showLabel" + [ariaDescribedBy]="validationErrorId" > <ods-validation-error error + [id]="validationErrorId" [invalidParams]="invalidParams" [label]="label" [attr.data-test-id]="dataTestId + '-text-editor-error'" diff --git a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts index 45f7c42c8f3f609567e89d3075411ac652eb8882..19ae4d4d435edb97ec43d2512eda5f6ab371c7aa 100644 --- a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; +import { ConvertForDataTestPipe, generateValidationErrorId } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; @@ -44,6 +44,8 @@ export class TextEditorComponent extends FormControlEditorAbstractComponent { @Input() showLabel: boolean = true; @Input() dataTestId: string; + public readonly validationErrorId: string = generateValidationErrorId(); + get variant(): string { return this.invalidParams.length > 0 ? 'error' : 'default'; } diff --git a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html index 39ac3109643ea6771e5199ba3e9bba49cef69c37..753a2c385a768d890c272847d220b89f8de34c9e 100644 --- a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html +++ b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html @@ -34,9 +34,11 @@ [focus]="focus" [isResizable]="isResizable" [showLabel]="showLabel" + [ariaDescribedBy]="validationErrorId" > <ods-validation-error error + [id]="validationErrorId" [invalidParams]="invalidParams" [label]="label" [attr.data-test-id]="(label | convertForDataTest) + '-textarea-editor-error'" diff --git a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts index c7032ef03b58627369f4857d0b2bda4546d092b5..9203da053f1a13a071244c009c6516c213de5434 100644 --- a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; +import { ConvertForDataTestPipe, generateValidationErrorId } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; @@ -44,6 +44,8 @@ export class TextareaEditorComponent extends FormControlEditorAbstractComponent @Input() isResizable: boolean = true; @Input() showLabel: boolean = true; + public readonly validationErrorId: string = generateValidationErrorId(); + get variant(): string { return this.invalidParams.length > 0 ? 'error' : 'default'; } diff --git a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html index 224dbe8cc5d73df87e45d77700b0ffca1efdb03e..cb566bafe0c5a5a804ae3e10491c0ca19faf4c40 100644 --- a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html +++ b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.html @@ -24,5 +24,5 @@ --> <ng-container *ngFor="let invalidParam of invalidParams" - ><ods-error-message [text]="message(invalidParam)"></ods-error-message + ><ods-error-message [id]="id" [text]="message(invalidParam)"></ods-error-message ></ng-container> diff --git a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts index 8ae6e949f8ed75b49c309609d0ff9419acb46582..fca8d2a2ecfbec56310a3ec835f03459edb4ec27 100644 --- a/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/validation-error/validation-error.component.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { InvalidParam, getMessageForInvalidParam } from '@alfa-client/tech-shared'; +import { getMessageForInvalidParam, InvalidParam } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { ErrorMessageComponent } from '@ods/system'; @@ -33,6 +33,7 @@ import { ErrorMessageComponent } from '@ods/system'; templateUrl: './validation-error.component.html', }) export class ValidationErrorComponent { + @Input({ required: true }) id: string; @Input() label: string; @Input() invalidParams: InvalidParam[]; diff --git a/alfa-client/libs/design-system/src/index.ts b/alfa-client/libs/design-system/src/index.ts index d05d44db8ce46777152a6f48145ac2000dd5d0a3..c17fdb29f53e0948d6f7b3a61d0d3725c18e2ffc 100644 --- a/alfa-client/libs/design-system/src/index.ts +++ b/alfa-client/libs/design-system/src/index.ts @@ -28,6 +28,7 @@ export * from './lib/bescheid-status-text/bescheid-status-text.component'; export * from './lib/bescheid-wrapper/bescheid-wrapper.component'; export * from './lib/button-card/button-card.component'; export * from './lib/button/button.component'; +export * from './lib/dialog-container/dialog-container.component'; export * from './lib/dropdown-menu/dropdown-menu-button-item/dropdown-menu-button-item.component'; export * from './lib/dropdown-menu/dropdown-menu-item/dropdown-menu-item.component'; export * from './lib/dropdown-menu/dropdown-menu-link-item/dropdown-menu-link-item.component'; @@ -41,6 +42,7 @@ export * from './lib/form/file-upload-button/file-upload-button.component'; export * from './lib/form/radio-button-card/radio-button-card.component'; export * from './lib/form/text-input/text-input.component'; export * from './lib/form/textarea/textarea.component'; +export * from './lib/forwarding-item/forwarding-item-info/forwarding-item-info.component'; export * from './lib/forwarding-item/forwarding-item.component'; export * from './lib/icons/accessibility-icon/accessibility-icon.component'; export * from './lib/icons/account-circle-icon/account-circle-icon.component'; diff --git a/alfa-client/libs/design-system/src/lib/button/button.component.spec.ts b/alfa-client/libs/design-system/src/lib/button/button.component.spec.ts index ecd327e0aa7ec13701a789698d80c163ea5d3dfa..fc5322e9e5371ac6f5a6dcc897224fce9b0a4c5e 100644 --- a/alfa-client/libs/design-system/src/lib/button/button.component.spec.ts +++ b/alfa-client/libs/design-system/src/lib/button/button.component.spec.ts @@ -41,4 +41,26 @@ describe('ButtonComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + describe('component', () => { + beforeEach(() => { + component.clickEmitter.emit = jest.fn(); + }); + + describe('onClick', () => { + it('should emit click', () => { + component.onClick(); + + expect(component.clickEmitter.emit).toHaveBeenCalled(); + }); + + it('should NOT emit click if button is disabled', () => { + component.disabled = true; + + component.onClick(); + + expect(component.clickEmitter.emit).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/alfa-client/libs/design-system/src/lib/button/button.component.ts b/alfa-client/libs/design-system/src/lib/button/button.component.ts index 47e76172adb5e6f9600be54291ee79a841defb78..3f88a32abc1bdb41fc4c12cabc4543f7c2def3c1 100644 --- a/alfa-client/libs/design-system/src/lib/button/button.component.ts +++ b/alfa-client/libs/design-system/src/lib/button/button.component.ts @@ -31,18 +31,18 @@ import { SpinnerIconComponent } from '../icons/spinner-icon/spinner-icon.compone export const buttonVariants = cva( [ 'flex items-center gap-3 rounded-lg text-sm font-medium box-border', - 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', + 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 outline-focus', ], { variants: { variant: { - primary: 'bg-primary text-whitetext shadow-md hover:enabled:bg-primary-hover focus-visible:bg-primary-hover', + primary: 'bg-primary text-whitetext shadow-md hover:bg-primary-hover focus-visible:bg-primary-hover', outline: - 'border border-primary bg-background-50 text-primary shadow-md hover:enabled:bg-ghost-hover focus-visible:bg-ghost-hover focus-visible:border-background-200', + 'border border-primary bg-background-50 text-primary shadow-md hover:bg-ghost-hover focus-visible:bg-ghost-hover focus-visible:border-background-200', outline_error: - 'border border-error bg-background-50 text-error shadow-md hover:enabled:bg-ghost-hover focus-visible:bg-ghost-hover focus-visible:border-background-200', + 'border border-error bg-background-50 text-error shadow-md hover:bg-ghost-hover focus-visible:bg-ghost-hover focus-visible:border-background-200', ghost: - 'border border-transparent hover:enabled:bg-ghost-hover text-primary focus-visible:border-background-200 focus-visible:bg-ghost-hover font-semibold [&]:focus-visible:outline-offset-1', + 'border border-transparent hover:bg-ghost-hover text-primary focus-visible:border-background-200 focus-visible:bg-ghost-hover font-semibold [&]:focus-visible:outline-offset-1', }, size: { medium: 'h-9 py-2 px-4 min-w-32', @@ -50,11 +50,11 @@ export const buttonVariants = cva( }, disabled: { false: null, - true: ['opacity-70', 'cursor-not-allowed'], + true: '[&]:outline-disabled-button cursor-not-allowed', }, destructive: { - false: 'outline-focus', - true: 'outline-destructive', + false: null, + true: '[&]:outline-destructive', }, }, defaultVariants: { @@ -66,19 +66,34 @@ export const buttonVariants = cva( { variant: 'primary', destructive: true, - class: - '[&]:hover:enabled:bg-destructive-primary-hover [&]:bg-destructive [&]:outline-destructive [&]:focus-visible:bg-destructive-primary-hover', + class: '[&]:hover:bg-destructive-primary-hover [&]:bg-destructive [&]:focus-visible:bg-destructive-primary-hover', }, { variant: 'outline', destructive: true, class: - '[&]:border-destructive [&]:text-destructive [&]:hover:enabled:bg-destructive-hover [&]:focus-visible:bg-destructive-hover', + '[&]:border-destructive [&]:text-destructive [&]:hover:bg-destructive-hover [&]:focus-visible:bg-destructive-hover', }, { variant: 'ghost', destructive: true, - class: '[&]:text-destructive [&]:hover:enabled:bg-destructive-hover [&]:focus-visible:bg-destructive-hover', + class: '[&]:text-destructive [&]:hover:bg-destructive-hover [&]:focus-visible:bg-destructive-hover', + }, + { + variant: 'primary', + disabled: true, + class: '[&]:bg-disabled-button [&]:hover:bg-disabled-button/90 [&]:focus-visible:bg-disabled-button/90', + }, + { + variant: 'outline', + disabled: true, + class: + '[&]:text-disabled-button [&]:border-disabled-button [&]:hover:bg-disabled-button/10 [&]:focus-visible:bg-disabled-button/10', + }, + { + variant: 'ghost', + disabled: true, + class: '[&]:text-disabled-button [&]:hover:bg-disabled-button/10 [&]:focus-visible:bg-disabled-button/10', }, ], }, @@ -89,27 +104,31 @@ export type ButtonVariants = VariantProps<typeof buttonVariants>; selector: 'ods-button', standalone: true, imports: [CommonModule, SpinnerIconComponent], - template: ` <button + template: `<button type="button" - [ngClass]="buttonVariants({ size, variant, disabled, destructive })" - [disabled]="isDisabled" + [ngClass]="buttonVariants({ size, variant, disabled: isDisabled, destructive })" [attr.aria-disabled]="isDisabled" [attr.aria-label]="text" [attr.data-test-id]="dataTestId" [attr.data-test-class]="dataTestClass" - (click)="clickEmitter.emit()" + (click)="onClick()" > - <ng-content *ngIf="!isLoading" select="[icon]"></ng-content> - <ods-spinner-icon *ngIf="isLoading" [size]="spinnerSize" data-test-class="spinner"></ods-spinner-icon> - <div *ngIf="text" class="flex-grow">{{ text }}</div> + @if (isLoading) { + <ods-spinner-icon [class]="isDisabled && 'fill-disabled-button'" [size]="spinnerSize" data-test-class="spinner" /> + } @else { + <ng-content select="[icon]" /> + } + @if (text) { + <p class="flex-grow">{{ text }}</p> + } </button>`, }) export class ButtonComponent { @Input() text: string = ''; @Input() dataTestId: string = ''; @Input() dataTestClass: string = ''; - @Input() disabled: boolean = false; - @Input() isLoading: boolean = false; + @Input({ transform: booleanAttribute }) disabled: boolean = false; + @Input({ transform: booleanAttribute }) isLoading: boolean = false; @Input({ transform: booleanAttribute }) destructive: boolean = false; @Input() variant: ButtonVariants['variant']; @Input() size: ButtonVariants['size']; @@ -121,5 +140,11 @@ export class ButtonComponent { return this.disabled || this.isLoading; } + public onClick(): void { + if (!this.isDisabled) { + this.clickEmitter.emit(); + } + } + readonly buttonVariants = buttonVariants; } diff --git a/alfa-client/libs/design-system/src/lib/dialog-container/dialog-container.component.spec.ts b/alfa-client/libs/design-system/src/lib/dialog-container/dialog-container.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1f894406fa1f6c9468c1d7ba356a56947a05c25 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/dialog-container/dialog-container.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DialogContainerComponent } from './dialog-container.component'; + +describe('DialogContainerComponent', () => { + let component: DialogContainerComponent; + let fixture: ComponentFixture<DialogContainerComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DialogContainerComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(DialogContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/dialog-container/dialog-container.component.ts b/alfa-client/libs/design-system/src/lib/dialog-container/dialog-container.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3df7faf24a83fb4ed3965b741004cdc53aaeabb6 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/dialog-container/dialog-container.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ods-dialog-container', + standalone: true, + imports: [], + template: ` + <div class="static flex w-[calc(100vw-2rem)] justify-center"> + <div class="flex max-w-4xl grow flex-col rounded-lg bg-background-50 p-6 shadow-md"> + <ng-content /> + </div> + </div> + `, +}) +export class DialogContainerComponent {} diff --git a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts index ffadf796c9031418fda5463896a1452351f2e109..3b3750bd3174bfaeecf2821d4dc29d9e5c6a4d4e 100644 --- a/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/checkbox/checkbox.component.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; +import { ConvertForDataTestPipe, EMPTY_STRING } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; @@ -45,6 +45,7 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms'; [attr.id]="inputId" [attr.disabled]="disabled ? true : null" [attr.data-test-id]="(label | convertForDataTest) + '-checkbox-editor'" + [attr.aria-describedby]="ariaDescribedBy" /> <label class="leading-5 text-text" [attr.for]="inputId">{{ label }}</label> <svg @@ -69,4 +70,5 @@ export class CheckboxComponent { @Input() label: string; @Input() disabled: boolean = false; @Input() hasError: boolean = false; + @Input() ariaDescribedBy: string = EMPTY_STRING; } diff --git a/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts b/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts index 66ed08e7f973dad14e092c899de2880ff7d8f515..7e8cc5ae0ed07c1d6de152f018842bb9f05ab11c 100644 --- a/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/error-message/error-message.component.ts @@ -21,6 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { EMPTY_STRING } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { ExclamationIconComponent } from '../../icons/exclamation-icon/exclamation-icon.component'; @@ -31,13 +32,14 @@ import { ExclamationIconComponent } from '../../icons/exclamation-icon/exclamati imports: [CommonModule, ExclamationIconComponent], styles: [':host {@apply flex text-error my-2 text-sm items-center font-medium}'], template: `<ods-exclamation-icon class="mr-1"></ods-exclamation-icon> - <div class="flex-grow break-all"> + <div class="flex-grow break-words" [id]="id"> {{ text }} <br *ngIf="subText" aria-hidden="true" /> {{ subText }} </div> `, }) export class ErrorMessageComponent { + @Input() id: string = EMPTY_STRING; @Input({ required: true }) text!: string; @Input() subText: string = ''; } diff --git a/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts b/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts index cd01ba0190c68b4f9fec34f3e94496e6d86f73aa..c84948d1e2db9eba1d7cd0071f193f1010ad825e 100644 --- a/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts @@ -55,29 +55,31 @@ type TextInputVariants = VariantProps<typeof textInputVariants>; template: ` <div class="relative"> <label *ngIf="showLabel" [for]="id" class="text-md mb-2 block font-medium text-text"> - {{ inputLabel }}<ng-container *ngIf="required"><i aria-hidden="true">*</i></ng-container> + {{ inputLabel }} + <ng-container *ngIf="required"><i aria-hidden="true">*</i></ng-container> </label> - <div class="mt-2"> - <div *ngIf="withPrefix" class="pointer-events-none absolute bottom-2 left-2 flex size-6 items-center justify-center"> - <ng-content select="[prefix]" /> - </div> - <input - type="text" - [id]="id" - [formControl]="fieldControl" - [ngClass]="[textInputVariants({ variant }), withPrefix ? 'pl-10' : '', withSuffix ? 'pr-10' : '']" - [placeholder]="placeholder" - [autocomplete]="autocomplete" - [attr.aria-required]="required" - [attr.aria-invalid]="variant === 'error'" - [attr.data-test-id]="_dataTestId + '-text-input'" - (click)="clickEmitter.emit()" - #inputElement - /> - <div *ngIf="withSuffix" class="absolute bottom-2 right-2 flex size-6 items-center justify-center"> - <ng-content select="[suffix]" /> - </div> + + <div *ngIf="withPrefix" class="pointer-events-none absolute bottom-2 left-2 flex size-6 items-center justify-center"> + <ng-content select="[prefix]"/> + </div> + <input + type="text" + [id]="id" + [formControl]="fieldControl" + [ngClass]="[textInputVariants({ variant }), withPrefix ? 'pl-10' : '', withSuffix ? 'pr-10' : '']" + [placeholder]="placeholder" + [autocomplete]="autocomplete" + [attr.aria-required]="required" + [attr.aria-invalid]="variant === 'error'" + [attr.data-test-id]="_dataTestId + '-text-input'" + [attr.aria-describedby]="ariaDescribedBy" + (click)="clickEmitter.emit()" + #inputElement + /> + <div *ngIf="withSuffix" class="absolute bottom-2 right-2 flex size-6 items-center justify-center"> + <ng-content select="[suffix]"/> </div> + <ng-content select="[error]"></ng-content> </div> `, @@ -102,6 +104,7 @@ export class TextInputComponent implements AfterViewInit { @Input() set dataTestId(value: string) { if (isNotUndefined(value)) this._dataTestId = value; } + @Input() ariaDescribedBy: string = EMPTY_STRING; @Output() clickEmitter: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>(); diff --git a/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts b/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts index f3eb84af9d638567a3bcbdc644b400abf344643e..d3c04b6c82bb491b5f25652bd89e6af37595dd85 100644 --- a/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts @@ -57,6 +57,7 @@ type TextareaVariants = VariantProps<typeof textareaVariants>; {{ inputLabel }}<ng-container *ngIf="required"><i aria-hidden="true">*</i></ng-container> </label> <textarea + #textAreaElement [id]="id" [formControl]="fieldControl" [rows]="rows" @@ -66,7 +67,7 @@ type TextareaVariants = VariantProps<typeof textareaVariants>; [attr.aria-required]="required" [attr.aria-invalid]="variant === 'error'" [attr.data-test-id]="(inputLabel | convertForDataTest) + '-textarea'" - #textAreaElement + [attr.aria-describedby]="ariaDescribedBy" ></textarea> <ng-content select="[error]"></ng-content> </div> @@ -94,6 +95,7 @@ export class TextareaComponent { this.textAreaElement.nativeElement.focus(); } } + @Input() ariaDescribedBy: string = EMPTY_STRING; inputLabel: string; id: string; diff --git a/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item-info/forwarding-item-info.component.spec.ts b/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item-info/forwarding-item-info.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa29de43383f328ce1179743c2fb6bbd0b435954 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item-info/forwarding-item-info.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ForwardingItemInfoComponent } from './forwarding-item-info.component'; + +describe('ForwardingItemInfoComponent', () => { + let component: ForwardingItemInfoComponent; + let fixture: ComponentFixture<ForwardingItemInfoComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ForwardingItemInfoComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ForwardingItemInfoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item-info/forwarding-item-info.component.ts b/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item-info/forwarding-item-info.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..abace4dec64981bfc6fd2e5cd8e3c66abeca881e --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item-info/forwarding-item-info.component.ts @@ -0,0 +1,12 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ods-forwarding-item-info', + standalone: true, + template: `<p class="font-medium">{{ label }}</p> + <p>{{ address }}</p>`, +}) +export class ForwardingItemInfoComponent { + @Input({ required: true }) label!: string; + @Input({ required: true }) address!: string; +} diff --git a/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item.component.ts b/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item.component.ts index 7f8ebfd800c662910c5ec0905935c3d12487b7f3..e1322cfcdef6610df2a458541bcd3436fc12d7d2 100644 --- a/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item.component.ts +++ b/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item.component.ts @@ -21,7 +21,6 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; import { ForwardVorgangIconComponent } from '../icons/forward-vorgang-icon/forward-vorgang-icon.component'; @@ -33,25 +32,18 @@ export enum ForwardingDirection { @Component({ selector: 'ods-forwarding-item', standalone: true, - imports: [CommonModule, ForwardVorgangIconComponent], - template: `<div - class="flex flex-col items-start justify-between gap-2 rounded-lg border border-grayborder p-3 md:flex-row md:items-center md:gap-0" - > - <div class="flex gap-3"> - <ods-forward-vorgang-icon class="fill-text" /> - <p class="text-gray-500">{{ direction }}:</p> - <div> - <p class="font-medium">{{ label }}</p> - <p>{{ address }}</p> + imports: [ForwardVorgangIconComponent], + template: ` <div class="h-full rounded-lg border border-grayborder"> + <div class="flex flex-col gap-3 p-3 align-top sm:flex-row"> + <div class="flex gap-3"> + <ods-forward-vorgang-icon class="fill-text" /> + <div class="font-medium text-gray-500">{{ direction }}:</div> </div> - </div> - <div class="text-end empty:hidden"> - <ng-content /> + <div class="grow"><ng-content /></div> + <div class="flex items-center"><ng-content select="[end-content]" /></div> </div> </div>`, }) export class ForwardingItemComponent { - @Input({ required: true }) label!: string; - @Input({ required: true }) address!: string; @Input() direction: ForwardingDirection = ForwardingDirection.TO; } diff --git a/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item.stories.ts b/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item.stories.ts index 53d2116e9f314c9e2194cd8a9ff7c72e39dd1b12..d70b202b1147e34a030258198844ac19afc7b1b6 100644 --- a/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item.stories.ts +++ b/alfa-client/libs/design-system/src/lib/forwarding-item/forwarding-item.stories.ts @@ -24,11 +24,9 @@ import { argsToTemplate, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { ButtonComponent } from '../button/button.component'; +import { ForwardingItemInfoComponent } from './forwarding-item-info/forwarding-item-info.component'; import { ForwardingDirection, ForwardingItemComponent } from './forwarding-item.component'; -const label: string = 'Bund für Umwelt und Naturschutz Kreisgruppe Kiel'; -const address: string = 'Kaiserstraße 25, 12443 Kiel'; - const meta: Meta<ForwardingItemComponent> = { title: 'Forwarding item', component: ForwardingItemComponent, @@ -41,7 +39,7 @@ const meta: Meta<ForwardingItemComponent> = { }, decorators: [ moduleMetadata({ - imports: [ForwardingItemComponent, ButtonComponent], + imports: [ForwardingItemComponent, ForwardingItemInfoComponent, ButtonComponent], }), ], excludeStories: /.*Data$/, @@ -53,40 +51,39 @@ type Story = StoryObj<ForwardingItemComponent>; export const Default: Story = { args: { - label: label, - address: address, direction: ForwardingDirection.TO, }, argTypes: { direction: { control: 'select', options: [ForwardingDirection.TO, ForwardingDirection.FROM] }, }, + render: () => ({ + template: `<ods-forwarding-item> + <ods-forwarding-item-info label="Bund für Umwelt und Naturschutz Kreisgruppe Kiel" address="Kaiserstraße 25, 12443 Kiel" /> + </ods-forwarding-item>`, + }), }; export const WithButton: Story = { - args: { - label: label, - address: address, - }, - - render: (args: ForwardingItemComponent) => ({ - props: args, - template: `<ods-forwarding-item ${argsToTemplate(args)}> - <ods-button variant="outline" text="Stelle ändern" /> + render: () => ({ + template: `<ods-forwarding-item> + <ods-forwarding-item-info label="Bund für Umwelt und Naturschutz Kreisgruppe Kiel" address="Kaiserstraße 25, 12443 Kiel" /> + <ods-button variant="outline" text="Stelle ändern" end-content /> </ods-forwarding-item>`, }), }; export const WithCreationInfo: Story = { args: { - label: label, - address: address, direction: ForwardingDirection.FROM, }, render: (args: ForwardingItemComponent) => ({ props: args, template: `<ods-forwarding-item ${argsToTemplate(args)}> - <p>20. Dez. 09:35</p> - <p class="text-sm">Karin Wanowski-Müller</p> + <ods-forwarding-item-info label="Bund für Umwelt und Naturschutz Kreisgruppe Kiel" address="Kaiserstraße 25, 12443 Kiel" /> + <div end-content> + <p>20. Dez. 09:35</p> + <p class="text-sm">Karin Wanowski-Müller</p> + </div> </ods-forwarding-item>`, }), }; diff --git a/alfa-client/libs/design-system/src/lib/icons/spinner-icon/spinner-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/spinner-icon/spinner-icon.component.ts index d361292b08e026c2f05e245276dae53b82fa2805..20ca2f1c2affe64e1a2aab8b81576c2c40a68e3e 100644 --- a/alfa-client/libs/design-system/src/lib/icons/spinner-icon/spinner-icon.component.ts +++ b/alfa-client/libs/design-system/src/lib/icons/spinner-icon/spinner-icon.component.ts @@ -24,6 +24,7 @@ import { NgClass } from '@angular/common'; import { Component, Input } from '@angular/core'; +import { twMerge } from 'tailwind-merge'; import { IconVariants, iconVariants } from '../iconVariants'; @Component({ @@ -33,8 +34,7 @@ import { IconVariants, iconVariants } from '../iconVariants'; template: ` <svg xmlns="http://www.w3.org/2000/svg" - [ngClass]="iconVariants({ size })" - class="animate-spin fill-primary text-gray-200 dark:text-gray-600" + [ngClass]="twMerge('animate-spin fill-primary text-gray-200 dark:text-gray-600', iconVariants({ size }), class)" aria-hidden="true" viewBox="0 0 100 100" fill="none" @@ -53,6 +53,8 @@ import { IconVariants, iconVariants } from '../iconVariants'; }) export class SpinnerIconComponent { @Input() size: IconVariants['size'] = 'full'; + @Input() class: string; - iconVariants = iconVariants; + readonly iconVariants = iconVariants; + readonly twMerge = twMerge; } diff --git a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.ts b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.ts index 48a8a502165e5795ba32bd93485def62486d4de9..73cf86fa263ec18bad66efdb06b5ad28e6b48041 100644 --- a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.ts +++ b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.ts @@ -71,7 +71,7 @@ import { InstantSearchQuery, InstantSearchResult } from './instant-search.model' <ods-aria-live-region [text]="ariaLiveText" /> <ods-search-result-layer *ngIf="results.length && areResultsVisible" - containerClass="absolute z-50 mt-3 max-h-[calc(50vh)] w-full overflow-y-auto" + containerClass="absolute z-50 mt-3 max-h-[calc(37vh)] w-full overflow-y-auto" id="results" > <ods-search-result-header *ngIf="headerText" [text]="headerText" [count]="results.length" header /> diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.ts index 3f5364877b5c08806ff6990851a7863abdcd74a5..3d765154891b5f39c3168a8a07e39c973eee840b 100644 --- a/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.ts +++ b/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.ts @@ -32,7 +32,7 @@ import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@ *ngIf="title" [ngClass]="[ 'flex w-full justify-between border-2 border-transparent px-6 py-3', - 'hover:border-focus focus:border-focus focus:outline-none', + 'hover:bg-background-150 focus:border-focus focus:outline-none', ]" role="listitem" tabindex="-1" diff --git a/alfa-client/libs/design-system/src/lib/list/list.component.ts b/alfa-client/libs/design-system/src/lib/list/list.component.ts index 95c4bd3e7251e650b4a34670ebee420ee7f57f5f..3621cbb31fc7848a1b986a72ed3ad3521cc7b385 100644 --- a/alfa-client/libs/design-system/src/lib/list/list.component.ts +++ b/alfa-client/libs/design-system/src/lib/list/list.component.ts @@ -23,12 +23,11 @@ */ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { ListItemComponent } from './list-item/list-item.component'; @Component({ selector: 'ods-list', standalone: true, - imports: [CommonModule, ListItemComponent], + imports: [CommonModule], template: ` <ul class="divide-y divide-gray-300 rounded-md bg-background-50 text-text shadow-sm ring-1 ring-gray-300 empty:hidden"> <ng-content /> diff --git a/alfa-client/libs/design-system/src/lib/tailwind-preset/root.css b/alfa-client/libs/design-system/src/lib/tailwind-preset/root.css index a3c8f7b01b5e1b8342761bf3190841ef059b5cc8..333257d60e54034a148f8f864ae42132d1a7643e 100644 --- a/alfa-client/libs/design-system/src/lib/tailwind-preset/root.css +++ b/alfa-client/libs/design-system/src/lib/tailwind-preset/root.css @@ -18,6 +18,7 @@ --color-disabled: 206 14% 95%; --color-disabled-dark: 208 12% 65%; + --color-disabled-button: 0 0% 42%; --color-destructive: 360, 71%, 49%, 1; --color-destructive-hover: 360, 71%, 49%, 0.07; @@ -59,6 +60,7 @@ --color-disabled: 206 14% 15%; --color-disabled-dark: 208 12% 33%; + --color-disabled-button: 0 0% 68%; --color-destructive: 360, 71%, 49%, 1; --color-destructive-hover: 360, 71%, 49%, 0.2; diff --git a/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js b/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js index cf2abfcb2f32821bcb410f4d9bcd817f2aa2efb0..9c5ba7a4e53f673e8ac6c1b27e08f63936647d7f 100644 --- a/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js +++ b/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js @@ -146,6 +146,7 @@ module.exports = { focus: 'hsl(var(--color-focus))', disabled: { dark: 'hsl(var(--color-disabled-dark) / <alpha-value>)', + button: 'hsl(var(--color-disabled-button) / <alpha-value>)', DEFAULT: 'hsl(var(--color-disabled) / <alpha-value>)', }, destructive: 'hsla(var(--color-destructive))', diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.html b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.html index 5bf44a14c8978d8fea1e07d51b0ccc1279935ca4..6995cd3356290db9eb8f6011c21e013009b29734 100644 --- a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.html +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.html @@ -1,11 +1,12 @@ <ods-button-with-spinner [stateResource]="stateResource" [disabled]="disabled" + [tooltip]="tooltip" (clickEmitter)="clickEmitter.emit()" - text="Weiterleiten" - variant="outline" + text="Jetzt weiterleiten" + tooltipPosition="above" dataTestId="forwarding-dialog-forwarding-button" data-test-id="forwarding-button-container" > - <ods-forward-vorgang-icon icon class="fill-primary" /> + <ods-forward-vorgang-icon icon class="fill-whitetext" /> </ods-button-with-spinner> diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.spec.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.spec.ts index 9e9a9d2118e1a9758f7ba58715f822822ead4cbf..661105c1ad208f595ae5b3f7f1158e0529d9fe2f 100644 --- a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.spec.ts +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.spec.ts @@ -1,3 +1,4 @@ +import { EMPTY_STRING } from '@alfa-client/tech-shared'; import { dispatchEventFromFixture, MockEvent } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ButtonWithSpinnerComponent } from '@ods/component'; @@ -27,13 +28,31 @@ describe('ForwardingButtonComponent', () => { expect(component).toBeTruthy(); }); - describe('on button click', () => { - it('should emit', () => { - component.clickEmitter.emit = jest.fn(); + describe('component', () => { + describe('set disabled', () => { + it('should set tooltip text', () => { + component.disabled = true; - dispatchEventFromFixture(fixture, button, MockEvent.CLICK); + expect(component.tooltip).not.toBe(EMPTY_STRING); + }); - expect(component.clickEmitter.emit).toHaveBeenCalled(); + it('should set empty tooltip', () => { + component.disabled = false; + + expect(component.tooltip).toBe(EMPTY_STRING); + }); + }); + }); + + describe('template', () => { + describe('on button click', () => { + it('should emit', () => { + component.clickEmitter.emit = jest.fn(); + + dispatchEventFromFixture(fixture, button, MockEvent.CLICK); + + expect(component.clickEmitter.emit).toHaveBeenCalled(); + }); }); }); }); diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.ts index 767fe5e4ce40e7589725973d37e9a84c12fd0564..e881014e936c13d57632ed0c9d059093bcb1d76a 100644 --- a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.ts +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-button/forwarding-button.component.ts @@ -1,18 +1,28 @@ import { CommandResource } from '@alfa-client/command-shared'; -import { StateResource } from '@alfa-client/tech-shared'; +import { EMPTY_STRING, StateResource } from '@alfa-client/tech-shared'; import { Component, EventEmitter, Input, Output } from '@angular/core'; import { ButtonWithSpinnerComponent } from '@ods/component'; -import { ForwardVorgangIconComponent } from '@ods/system'; +import { ForwardVorgangIconComponent, TooltipDirective } from '@ods/system'; @Component({ selector: 'alfa-forwarding-button', standalone: true, - imports: [ButtonWithSpinnerComponent, ForwardVorgangIconComponent], + imports: [ButtonWithSpinnerComponent, ForwardVorgangIconComponent, TooltipDirective], templateUrl: './forwarding-button.component.html', }) export class ForwardingButtonComponent { - @Input() disabled: boolean; + @Input() set disabled(value: boolean) { + this._disabled = value; + this.tooltip = value ? 'Bitte ein Amt oder Stelle auswählen' : EMPTY_STRING; + } @Input() stateResource: StateResource<CommandResource>; @Output() clickEmitter: EventEmitter<void> = new EventEmitter(); + + public tooltip: string; + private _disabled: boolean; + + get disabled() { + return this._disabled; + } } diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.html b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.html index 5aaf99a340455262073812254cf8c7d74adf578e..54327bb8b611751959f462fc5415b16e0fcde6d7 100644 --- a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.html +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.html @@ -1,22 +1,25 @@ -<div class="flex w-[620px] max-w-full flex-col gap-4 bg-background-100 p-8"> - <div class="flex items-center justify-between"> - <h1 class="text-xl font-semibold text-primary">Vorgang weiterleiten</h1> - <ods-cancel-dialog-button showAsIconButton="true" /> - </div> - - @if (!selectedSearchResult) { - <alfa-search-zustaendige-stelle-form-container cdkFocusInitial focusOnSearchField="true" data-test-id="zufi-search" /> - } @else { - <alfa-forwarding-item-in-dialog [organisationsEinheitResource]="selectedSearchResult" data-test-id="forwarding-item" /> - } +<ods-dialog-container> + <div class="flex grow flex-col gap-6"> + <div class="flex items-center justify-between"> + <h1 class="text-lg font-medium text-primary">Vorgang weiterleiten</h1> + <ods-cancel-dialog-button showAsIconButton="true" /> + </div> + <div class="h-[calc(37vh+4.5rem)] grow"> + @if (!selectedSearchResult) { + <alfa-forwarding-search-organisations-einheit cdkFocusInitial data-test-id="organisations-einheit-search" /> + } @else { + <alfa-selected-search-item [organisationsEinheitResource]="selectedSearchResult" data-test-id="selected-search-item" /> + } + </div> - <div class="flex gap-4"> - <alfa-forwarding-button - [stateResource]="forwardCommandStateResource" - [disabled]="!selectedSearchResult" - (clickEmitter)="onForwarding()" - data-test-id="foward-dialog-forward-button" - /> - <ods-cancel-dialog-button /> + <div class="flex gap-4"> + <alfa-forwarding-button + [stateResource]="forwardCommandStateResource" + [disabled]="!selectedSearchResult" + (clickEmitter)="onForwarding()" + data-test-id="foward-dialog-forward-button" + /> + <ods-cancel-dialog-button /> + </div> </div> -</div> +</ods-dialog-container> diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.spec.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.spec.ts index 7e6114236be2ad7d2dddfcdfd309853b32568cdd..be35437daced36c2d43a532852f0d78cbc4fa8b9 100644 --- a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.spec.ts +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.spec.ts @@ -5,35 +5,36 @@ import { MockEvent, notExistsAsHtmlElement, } from '@alfa-client/test-utils'; -import { ZustaendigeStelleModule } from '@alfa-client/zustaendige-stelle'; import { OrganisationsEinheitResource } from '@alfa-client/zustaendige-stelle-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { getUrl } from '@ngxp/rest'; import { CancelDialogButtonComponent } from '@ods/component'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { getDataTestIdOf } from '../../../../../tech-shared/test/data-test'; import { createOrganisationsEinheitResource } from '../../../../../zustaendige-stelle-shared/test/organisations-einheit'; import { ForwardingButtonComponent } from './forwarding-button/forwarding-button.component'; import { ForwardingDialogComponent } from './forwarding-dialog.component'; -import { ForwardingItemInDialogComponent } from './forwarding-item/forwarding-item.component'; +import { SelectedSearchItemComponent } from './selected-search-item/selected-search-item.component'; +import { ForwardingSearchOrganisationsEinheitComponent } from './search-organisations-einheit/search-organisations-einheit.component'; describe('ForwardingDialogComponent', () => { let component: ForwardingDialogComponent; let fixture: ComponentFixture<ForwardingDialogComponent>; - const zufiSearch: string = getDataTestIdOf('zufi-search'); - const forwardingItem: string = getDataTestIdOf('forwarding-item'); + const organisationsEinheitSearch: string = getDataTestIdOf('organisations-einheit-search'); + const selectedSearchItem: string = getDataTestIdOf('selected-search-item'); const forwardButton: string = getDataTestIdOf('foward-dialog-forward-button'); const organisationsEinheitResource: OrganisationsEinheitResource = createOrganisationsEinheitResource(); + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ForwardingDialogComponent], declarations: [ MockComponent(CancelDialogButtonComponent), MockComponent(ForwardingButtonComponent), - MockComponent(ForwardingItemInDialogComponent), - MockModule(ZustaendigeStelleModule), + MockComponent(SelectedSearchItemComponent), + MockComponent(ForwardingSearchOrganisationsEinheitComponent), ], }).compileComponents(); @@ -53,7 +54,7 @@ describe('ForwardingDialogComponent', () => { fixture.detectChanges(); - existsAsHtmlElement(fixture, zufiSearch); + existsAsHtmlElement(fixture, organisationsEinheitSearch); }); it('should NOT render if selectedSearchResult is NOT null', () => { @@ -61,7 +62,7 @@ describe('ForwardingDialogComponent', () => { fixture.detectChanges(); - notExistsAsHtmlElement(fixture, zufiSearch); + notExistsAsHtmlElement(fixture, organisationsEinheitSearch); }); }); }); @@ -78,7 +79,7 @@ describe('ForwardingDialogComponent', () => { fixture.detectChanges(); - existsAsHtmlElement(fixture, forwardingItem); + existsAsHtmlElement(fixture, selectedSearchItem); }); it('should NOT render if selectedSearchResult is null', () => { @@ -86,7 +87,7 @@ describe('ForwardingDialogComponent', () => { fixture.detectChanges(); - notExistsAsHtmlElement(fixture, forwardingItem); + notExistsAsHtmlElement(fixture, selectedSearchItem); }); }); diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.ts index 178d84465273bc421ee5bd9a49dca84d213cb299..9ff338f0a8f472af342aa6ccd809b3dcbcfca407 100644 --- a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.ts +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-dialog.component.ts @@ -7,8 +7,10 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; import { getUrl, ResourceUri } from '@ngxp/rest'; import { CancelDialogButtonComponent } from '@ods/component'; +import { DialogContainerComponent } from '@ods/system'; import { ForwardingButtonComponent } from './forwarding-button/forwarding-button.component'; -import { ForwardingItemInDialogComponent } from './forwarding-item/forwarding-item.component'; +import { SelectedSearchItemComponent } from './selected-search-item/selected-search-item.component'; +import { ForwardingSearchOrganisationsEinheitComponent } from './search-organisations-einheit/search-organisations-einheit.component'; @Component({ selector: 'alfa-forwarding-dialog', @@ -19,7 +21,9 @@ import { ForwardingItemInDialogComponent } from './forwarding-item/forwarding-it ReactiveFormsModule, ZustaendigeStelleModule, ForwardingButtonComponent, - ForwardingItemInDialogComponent, + ForwardingSearchOrganisationsEinheitComponent, + SelectedSearchItemComponent, + DialogContainerComponent, ], templateUrl: './forwarding-dialog.component.html', }) diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item.component.html b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item.component.html deleted file mode 100644 index bd41e43d6a1c75eb03bca208ca264c7553aaa5a8..0000000000000000000000000000000000000000 --- a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item.component.html +++ /dev/null @@ -1,3 +0,0 @@ -<ods-forwarding-item [label]="organisationsEinheitResource.name" [address]="organisationsEinheitResource.anschrift | anschriftToString" > - <alfa-forwarding-item-change-button-container /> -</ods-forwarding-item> \ No newline at end of file diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item.component.spec.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item.component.spec.ts deleted file mode 100644 index b8ffb55d8ab7ff80771ddd8cdd2d1e6611dbe459..0000000000000000000000000000000000000000 --- a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item.component.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { getMockComponent } from '@alfa-client/test-utils'; -import { Anschrift, OrganisationsEinheitResource } from '@alfa-client/zustaendige-stelle-shared'; -import { ForwardingItemComponent } from '@ods/system'; -import { MockComponent } from 'ng-mocks'; -import { createOrganisationsEinheitResource } from '../../../../../../zustaendige-stelle-shared/test/organisations-einheit'; -import { ForwardingItemChangeButtonContainerComponent } from './forwarding-item-change-button-container/forwarding-item-change-button-container.component'; -import { ForwardingItemInDialogComponent } from './forwarding-item.component'; - -describe('ForwardingDialogForwardingItemComponent', () => { - let component: ForwardingItemInDialogComponent; - let fixture: ComponentFixture<ForwardingItemInDialogComponent>; - - const organisationsEinheitResource: OrganisationsEinheitResource = createOrganisationsEinheitResource(); - const anschrift: Anschrift = organisationsEinheitResource.anschrift; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ForwardingItemInDialogComponent], - declarations: [MockComponent(ForwardingItemChangeButtonContainerComponent), MockComponent(ForwardingItemComponent)], - }).compileComponents(); - - fixture = TestBed.createComponent(ForwardingItemInDialogComponent); - component = fixture.componentInstance; - component.organisationsEinheitResource = organisationsEinheitResource; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('forwarding item', () => { - it('should exist with input', () => { - const forwardingItem: ForwardingItemComponent = getMockComponent(fixture, ForwardingItemComponent); - - expect(forwardingItem.label).toBe(organisationsEinheitResource.name); - expect(forwardingItem.address).toBe(`${anschrift.strasse} ${anschrift.hausnummer}, ${anschrift.plz} ${anschrift.ort}`); - }); - }); -}); diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item.component.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item.component.ts deleted file mode 100644 index 9828f682d8860e851cc15cde31fef24150120f4a..0000000000000000000000000000000000000000 --- a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item.component.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AnschriftToStringPipe, OrganisationsEinheitResource } from '@alfa-client/zustaendige-stelle-shared'; -import { Component, Input } from '@angular/core'; -import { ForwardingItemComponent } from '@ods/system'; -import { ForwardingItemChangeButtonContainerComponent } from './forwarding-item-change-button-container/forwarding-item-change-button-container.component'; - -@Component({ - selector: 'alfa-forwarding-item-in-dialog', - standalone: true, - imports: [ForwardingItemChangeButtonContainerComponent, AnschriftToStringPipe, ForwardingItemComponent], - templateUrl: './forwarding-item.component.html', -}) -export class ForwardingItemInDialogComponent { - @Input() organisationsEinheitResource: OrganisationsEinheitResource; -} diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/search-organisations-einheit/search-organisations-einheit.component.html b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/search-organisations-einheit/search-organisations-einheit.component.html new file mode 100644 index 0000000000000000000000000000000000000000..410a1ed8605cd28872a594b69559885e6e707d5d --- /dev/null +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/search-organisations-einheit/search-organisations-einheit.component.html @@ -0,0 +1,3 @@ +<ods-forwarding-item> + <alfa-search-zustaendige-stelle-form-container focusOnSearchField="true"/> +</ods-forwarding-item> \ No newline at end of file diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/search-organisations-einheit/search-organisations-einheit.component.spec.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/search-organisations-einheit/search-organisations-einheit.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..84b088454368fd60b3f91e74363ce1aadae01ee0 --- /dev/null +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/search-organisations-einheit/search-organisations-einheit.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ZustaendigeStelleModule } from '@alfa-client/zustaendige-stelle'; +import { ForwardingItemComponent } from '@ods/system'; +import { MockComponent, MockModule } from 'ng-mocks'; +import { ForwardingSearchOrganisationsEinheitComponent } from './search-organisations-einheit.component'; + +describe('ForwardingSearchOrganisationsEinheitComponent', () => { + let component: ForwardingSearchOrganisationsEinheitComponent; + let fixture: ComponentFixture<ForwardingSearchOrganisationsEinheitComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ForwardingSearchOrganisationsEinheitComponent], + declarations: [MockComponent(ForwardingItemComponent), MockModule(ZustaendigeStelleModule)], + }).compileComponents(); + + fixture = TestBed.createComponent(ForwardingSearchOrganisationsEinheitComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/search-organisations-einheit/search-organisations-einheit.component.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/search-organisations-einheit/search-organisations-einheit.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..bc974a66d4a87b9d32b81acf1e8eff03d5f40430 --- /dev/null +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/search-organisations-einheit/search-organisations-einheit.component.ts @@ -0,0 +1,11 @@ +import { ZustaendigeStelleModule } from '@alfa-client/zustaendige-stelle'; +import { Component } from '@angular/core'; +import { ForwardingItemComponent } from '@ods/system'; + +@Component({ + selector: 'alfa-forwarding-search-organisations-einheit', + standalone: true, + imports: [ForwardingItemComponent, ZustaendigeStelleModule], + templateUrl: './search-organisations-einheit.component.html', +}) +export class ForwardingSearchOrganisationsEinheitComponent {} diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item-change-button-container/forwarding-item-change-button-container.component.html b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/change-button-container/change-button-container.component.html similarity index 100% rename from alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item-change-button-container/forwarding-item-change-button-container.component.html rename to alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/change-button-container/change-button-container.component.html diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item-change-button-container/forwarding-item-change-button-container.component.spec.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/change-button-container/change-button-container.component.spec.ts similarity index 79% rename from alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item-change-button-container/forwarding-item-change-button-container.component.spec.ts rename to alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/change-button-container/change-button-container.component.spec.ts index 49e50eaa845262e74914cd7f4717c66e549ca924..86ce46346d2fa0592dc93cc2c83c75f3f4512d83 100644 --- a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item-change-button-container/forwarding-item-change-button-container.component.spec.ts +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/change-button-container/change-button-container.component.spec.ts @@ -5,11 +5,11 @@ import { OrganisationsEinheitService, ZUSTAENDIGE_STELLE_SERVICE } from '@alfa-c import { ButtonComponent } from '@ods/system'; import { MockComponent } from 'ng-mocks'; import { getDataTestIdOf } from '../../../../../../../tech-shared/test/data-test'; -import { ForwardingItemChangeButtonContainerComponent } from './forwarding-item-change-button-container.component'; +import { ChangeButtonContainerComponent } from './change-button-container.component'; describe('ForwardingItemChangeButtonContainerComponent', () => { - let component: ForwardingItemChangeButtonContainerComponent; - let fixture: ComponentFixture<ForwardingItemChangeButtonContainerComponent>; + let component: ChangeButtonContainerComponent; + let fixture: ComponentFixture<ChangeButtonContainerComponent>; const buttonContainer: string = getDataTestIdOf('forwarding-item-change-button-container'); @@ -19,12 +19,12 @@ describe('ForwardingItemChangeButtonContainerComponent', () => { service = mock(OrganisationsEinheitService); TestBed.configureTestingModule({ - imports: [ForwardingItemChangeButtonContainerComponent], + imports: [ChangeButtonContainerComponent], declarations: [MockComponent(ButtonComponent)], providers: [{ provide: ZUSTAENDIGE_STELLE_SERVICE, useValue: service }], }).compileComponents(); - fixture = TestBed.createComponent(ForwardingItemChangeButtonContainerComponent); + fixture = TestBed.createComponent(ChangeButtonContainerComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item-change-button-container/forwarding-item-change-button-container.component.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/change-button-container/change-button-container.component.ts similarity index 71% rename from alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item-change-button-container/forwarding-item-change-button-container.component.ts rename to alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/change-button-container/change-button-container.component.ts index 5a0ca9ccbe7730e0eb8d54ef013b17d12719b0ec..549bd4342b5c274724a0a55a6fdde303d1209d69 100644 --- a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/forwarding-item/forwarding-item-change-button-container/forwarding-item-change-button-container.component.ts +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/change-button-container/change-button-container.component.ts @@ -3,12 +3,12 @@ import { Component, inject } from '@angular/core'; import { ButtonComponent } from '@ods/system'; @Component({ - selector: 'alfa-forwarding-item-change-button-container', + selector: 'alfa-change-button-container', standalone: true, imports: [ButtonComponent], - templateUrl: './forwarding-item-change-button-container.component.html', + templateUrl: './change-button-container.component.html', }) -export class ForwardingItemChangeButtonContainerComponent { +export class ChangeButtonContainerComponent { private readonly organisationsEinheitService = inject(ZUSTAENDIGE_STELLE_SERVICE) as OrganisationsEinheitService; public onClick(): void { diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/selected-search-item.component.html b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/selected-search-item.component.html new file mode 100644 index 0000000000000000000000000000000000000000..66f8b9cac15d60112010465b2632f3e607b0dfa7 --- /dev/null +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/selected-search-item.component.html @@ -0,0 +1,7 @@ +<ods-forwarding-item class="block"> + <ods-forwarding-item-info + [label]="organisationsEinheitResource.name" + [address]="organisationsEinheitResource.anschrift | anschriftToString" + /> + <alfa-change-button-container end-content /> +</ods-forwarding-item> diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/selected-search-item.component.spec.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/selected-search-item.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c95fd052a78482a1a5aa3a15a60e3e3cd0b35ec3 --- /dev/null +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/selected-search-item.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { getMockComponent } from '@alfa-client/test-utils'; +import { Anschrift, OrganisationsEinheitResource } from '@alfa-client/zustaendige-stelle-shared'; +import { ForwardingItemComponent, ForwardingItemInfoComponent } from '@ods/system'; +import { MockComponent } from 'ng-mocks'; +import { createOrganisationsEinheitResource } from '../../../../../../zustaendige-stelle-shared/test/organisations-einheit'; +import { ChangeButtonContainerComponent } from './change-button-container/change-button-container.component'; +import { SelectedSearchItemComponent } from './selected-search-item.component'; + +describe('ForwardingSearchOrganisationsEinheitComponent', () => { + let component: SelectedSearchItemComponent; + let fixture: ComponentFixture<SelectedSearchItemComponent>; + + const organisationsEinheitResource: OrganisationsEinheitResource = createOrganisationsEinheitResource(); + const anschrift: Anschrift = organisationsEinheitResource.anschrift; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SelectedSearchItemComponent], + declarations: [ + MockComponent(ChangeButtonContainerComponent), + MockComponent(ForwardingItemComponent), + MockComponent(ForwardingItemInfoComponent), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SelectedSearchItemComponent); + component = fixture.componentInstance; + component.organisationsEinheitResource = organisationsEinheitResource; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('forwarding item info', () => { + it('should exist with input', () => { + const forwardingItemInfo: ForwardingItemInfoComponent = getMockComponent(fixture, ForwardingItemInfoComponent); + + expect(forwardingItemInfo.label).toBe(organisationsEinheitResource.name); + expect(forwardingItemInfo.address).toBe(`${anschrift.strasse} ${anschrift.hausnummer}, ${anschrift.plz} ${anschrift.ort}`); + }); + }); +}); diff --git a/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/selected-search-item.component.ts b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/selected-search-item.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c40737820507149807e1fe17be6de8a6033b6e6d --- /dev/null +++ b/alfa-client/libs/forwarding/src/lib/forwarding-dialog-container/forwarding-dialog/selected-search-item/selected-search-item.component.ts @@ -0,0 +1,14 @@ +import { AnschriftToStringPipe, OrganisationsEinheitResource } from '@alfa-client/zustaendige-stelle-shared'; +import { Component, Input } from '@angular/core'; +import { ForwardingItemComponent, ForwardingItemInfoComponent } from '@ods/system'; +import { ChangeButtonContainerComponent } from './change-button-container/change-button-container.component'; + +@Component({ + selector: 'alfa-selected-search-item', + standalone: true, + imports: [ChangeButtonContainerComponent, AnschriftToStringPipe, ForwardingItemComponent, ForwardingItemInfoComponent], + templateUrl: './selected-search-item.component.html', +}) +export class SelectedSearchItemComponent { + @Input() organisationsEinheitResource: OrganisationsEinheitResource; +} diff --git a/alfa-client/libs/tech-shared/src/index.ts b/alfa-client/libs/tech-shared/src/index.ts index dcfa58a7cb36883ea69342990b5e6f1880dabd86..bc0367096e77bd152634a643e3b2026a3b3d9bfe 100644 --- a/alfa-client/libs/tech-shared/src/index.ts +++ b/alfa-client/libs/tech-shared/src/index.ts @@ -66,3 +66,4 @@ export * from './lib/service/formservice.abstract'; export * from './lib/tech.model'; export * from './lib/tech.util'; export * from './lib/validation/tech.validation.util'; +export * from './lib/validation/tech.validators'; diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts index accb4de66726417404f156a36d8b732731199c50..2045e5afa6740ac78ea86e2a9057d3ea9066122a 100644 --- a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts +++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.messages.ts @@ -32,21 +32,20 @@ export enum ValidationMessageCode { FIELD_INVALID = 'validation_field_invalid', FIELD_DATE_FORMAT_INVALID = 'validation_field_date_format_invalid', FIELD_ASSIGN_BEARBEITER_NOT_EXIST = 'fe_only_validation_bearbeiter_not_exist', + FIELD_VALUE_ALREADY_EXISTS = 'validation_field_value_already_exists', } export const VALIDATION_MESSAGES: { [code: string]: string } = { [ValidationMessageCode.FIELD_EMPTY]: 'Bitte {field} ausfüllen', [ValidationMessageCode.FIELD_MAX_SIZE]: '{field} darf höchstens {max} Zeichen enthalten', [ValidationMessageCode.FIELD_MIN_SIZE]: '{field} muss aus mindestens {min} Zeichen bestehen', - [ValidationMessageCode.FIELD_SIZE]: - '{field} muss mindestens {min} und darf höchstens {max} Zeichen enthalten', + [ValidationMessageCode.FIELD_SIZE]: '{field} muss mindestens {min} und darf höchstens {max} Zeichen enthalten', [ValidationMessageCode.FIELD_DATE_PAST]: 'Das Datum für {field} muss in der Zukunft liegen', [ValidationMessageCode.FIELD_INVALID]: 'Bitte {field} korrekt ausfüllen', - [ValidationMessageCode.FIELD_FILE_SIZE_EXCEEDED]: - 'Anhänge größer {max}{unit} können nicht hinzugefügt werden.', + [ValidationMessageCode.FIELD_FILE_SIZE_EXCEEDED]: 'Anhänge größer {max}{unit} können nicht hinzugefügt werden.', fe_only_validation_bearbeiter_not_exist: 'Der Bearbeiter existiert nicht', [ValidationMessageCode.FIELD_DATE_FORMAT_INVALID]: 'Geben Sie ein gültiges Datum ein', - [ValidationMessageCode.FIELD_FILE_CONTENT_TYPE_INVALID]: - 'Erlaubte Dateiendungen: pdf, jpg, png, jpeg', + [ValidationMessageCode.FIELD_FILE_CONTENT_TYPE_INVALID]: 'Erlaubte Dateiendungen: pdf, jpg, png, jpeg', + [ValidationMessageCode.FIELD_VALUE_ALREADY_EXISTS]: '{value} bereits verwendet', }; diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts index 32ce38f3c400ca02773522f97c30b6e7fab1b246..b4a1c442684dc9101a616d957e0897d8003df9d0 100644 --- a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.spec.ts @@ -26,7 +26,16 @@ import { faker } from '@faker-js/faker'; import { createInvalidParam, createIssue, createProblemDetail } from '../../../test/error'; import { InvalidParam, Issue } from '../tech.model'; import { VALIDATION_MESSAGES, ValidationMessageCode } from './tech.validation.messages'; -import { getControlForInvalidParam, getControlForIssue, getFieldPath, getMessageForInvalidParam, getMessageForIssue, getMessageReason, setInvalidParamValidationError, setIssueValidationError } from './tech.validation.util'; +import { + getControlForInvalidParam, + getControlForIssue, + getFieldPath, + getMessageForInvalidParam, + getMessageForIssue, + getMessageReason, + setInvalidParamValidationError, + setIssueValidationError, +} from './tech.validation.util'; describe('ValidationUtils', () => { const baseField1Control: FormControl = new UntypedFormControl(); @@ -44,7 +53,7 @@ describe('ValidationUtils', () => { describe('set issue validation error', () => { describe('get control for issue', () => { it('should return base field control', () => { - const issue: Issue = { ...createIssue(), field: 'class.resource.baseField1' }; + const issue: Issue = { ...createIssue(), field: 'baseField1' }; const control: AbstractControl = getControlForIssue(form, issue); @@ -69,7 +78,7 @@ describe('ValidationUtils', () => { }); describe('in base field', () => { - const issue: Issue = { ...createIssue(), field: 'class.resource.baseField1' }; + const issue: Issue = { ...createIssue(), field: 'baseField1' }; it('should set error in control', () => { setIssueValidationError(form, issue); @@ -144,7 +153,7 @@ describe('ValidationUtils', () => { it('should return base field control', () => { const invalidParam: InvalidParam = { ...createInvalidParam(), - name: 'class.resource.baseField1', + name: 'baseField1', }; const control: AbstractControl = getControlForInvalidParam(form, invalidParam); @@ -155,7 +164,7 @@ describe('ValidationUtils', () => { it('should return sub group field', () => { const invalidParam: InvalidParam = { ...createInvalidParam(), - name: 'class.resource.subGroup.subGroupField1', + name: 'resource.subGroup.subGroupField1', }; const control: AbstractControl = getControlForInvalidParam(form, invalidParam, 'resource'); @@ -166,7 +175,7 @@ describe('ValidationUtils', () => { it('should ignore path prefix', () => { const invalidParam: InvalidParam = { ...createInvalidParam(), - name: 'class.resource.baseField1', + name: 'resource.baseField1', }; const control: AbstractControl = getControlForInvalidParam(form, invalidParam, 'resource'); @@ -178,7 +187,7 @@ describe('ValidationUtils', () => { describe('in base field', () => { const invalidParam: InvalidParam = { ...createInvalidParam(), - name: 'class.resource.baseField1', + name: 'baseField1', }; it('should set error in control', () => { @@ -209,7 +218,7 @@ describe('ValidationUtils', () => { describe('in subGroup Field', () => { const invalidParam: InvalidParam = { ...createInvalidParam(), - name: 'class.resource.subGroup.subGroupField1', + name: 'resource.subGroup.subGroupField1', }; it('should set error in control', () => { @@ -243,12 +252,9 @@ describe('ValidationUtils', () => { }); it('should return field from full path when resource is undefined', () => { - const fieldPath: string = 'field1'; - const fullPath: string = `${backendClassName}.${resource}.${fieldPath}`; - - const result: string = getFieldPath(fullPath, undefined); + const result: string = getFieldPath('field1', undefined); - expect(result).toBe(fieldPath); + expect(result).toBe('field1'); }); it('should return field from field when resource is undefined', () => { @@ -309,9 +315,7 @@ describe('ValidationUtils', () => { ...invalidParam, reason: ValidationMessageCode.FIELD_INVALID, }); - expect(message).toEqual( - VALIDATION_MESSAGES[ValidationMessageCode.FIELD_INVALID].replace('{field}', label), - ); + expect(message).toEqual(VALIDATION_MESSAGES[ValidationMessageCode.FIELD_INVALID].replace('{field}', label)); }); it('should return message with placeholders', () => { diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts index b0cb78f8a1d59efdddeb25e26c4deda6a746d7fa..0701d11447b842b06de8d18e9798c117a61b6f1f 100644 --- a/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validation.util.ts @@ -22,7 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { AbstractControl, UntypedFormGroup } from '@angular/forms'; -import { isEmpty, isNil } from 'lodash-es'; +import { isEmpty, isNil, uniqueId } from 'lodash-es'; import { ApiError, InvalidParam, Issue, IssueParam, ProblemDetail } from '../tech.model'; import { replacePlaceholder } from '../tech.util'; import { VALIDATION_MESSAGES, ValidationMessageCode } from './tech.validation.messages'; @@ -31,28 +31,18 @@ export function isValidationError(issue: Issue): boolean { return issue.messageCode.includes('javax.validation.constraints'); } -export function setIssueValidationError( - form: UntypedFormGroup, - issue: Issue, - pathPrefix?: string, -): void { +export function setIssueValidationError(form: UntypedFormGroup, issue: Issue, pathPrefix?: string): void { const control: AbstractControl = getControlForIssue(form, issue, pathPrefix); control.setErrors({ [issue.messageCode]: issue }); control.markAsTouched(); } -export function getControlForIssue( - form: UntypedFormGroup, - issue: Issue, - pathPrefix?: string, -): AbstractControl { +export function getControlForIssue(form: UntypedFormGroup, issue: Issue, pathPrefix?: string): AbstractControl { const fieldPath: string = getFieldPath(issue.field, pathPrefix); let curControl: AbstractControl = form; - fieldPath - .split('.') - .forEach((field) => (curControl = (<UntypedFormGroup>curControl).controls[field])); + fieldPath.split('.').forEach((field) => (curControl = (<UntypedFormGroup>curControl).controls[field])); return curControl; } @@ -66,9 +56,7 @@ export function getMessageForIssue(label: string, issue: Issue): string { } msg = replacePlaceholder(msg, 'field', label); - issue.parameters.forEach( - (param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)), - ); + issue.parameters.forEach((param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value))); return msg; } @@ -84,11 +72,7 @@ export function getMessageCode(apiError: ApiError): string { return apiError.issues[0].messageCode; } -export function setInvalidParamValidationError( - form: UntypedFormGroup, - invalidParam: InvalidParam, - pathPrefix?: string, -): void { +export function setInvalidParamValidationError(form: UntypedFormGroup, invalidParam: InvalidParam, pathPrefix?: string): void { const control: AbstractControl = getControlForInvalidParam(form, invalidParam, pathPrefix); control.setErrors({ [invalidParam.reason]: invalidParam }); @@ -112,17 +96,25 @@ export function getMessageForInvalidParam(label: string, invalidParam: InvalidPa } msg = replacePlaceholder(msg, 'field', label); - invalidParam.constraintParameters.forEach( - (param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value)), - ); + invalidParam.constraintParameters.forEach((param: IssueParam) => (msg = replacePlaceholder(msg, param.name, param.value))); return msg; } export function getFieldPath(name: string, pathPrefix: string): string { + const path: string = _mapFormArrayElementNameToPath(name); if (isEmpty(pathPrefix)) { - return name.split('.').pop(); + return path; } - const indexOfField = name.lastIndexOf(pathPrefix) + pathPrefix.length + 1; - return name.slice(indexOfField); + const indexOfField = path.lastIndexOf(pathPrefix) + pathPrefix.length + 1; + return path.slice(indexOfField); +} + +export function _mapFormArrayElementNameToPath(name: string): string { + const formArrayControlIndexCaptureGroup: RegExp = /\[(\d+?)]\./g; + return name.replace(formArrayControlIndexCaptureGroup, '.$1.'); +} + +export function generateValidationErrorId(): string { + return `${uniqueId()}-validation-error`; } diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.spec.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..00ef101f7dd00dde3d7c6452d5dc771cc41d0c6f --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.spec.ts @@ -0,0 +1,155 @@ +import { + checkBoxGroupsEmptyValidator, + fieldEmptyValidator, + fieldInvalidValidator, + fieldLengthValidator, + InvalidParam, +} from '@alfa-client/tech-shared'; +import { AbstractControl, FormControl, UntypedFormGroup } from '@angular/forms'; +import { expect } from '@jest/globals'; +import { ValidationMessageCode } from './tech.validation.messages'; + +describe('Tech Validators', () => { + describe('field empty validator', () => { + it('should return null', () => { + const control: AbstractControl = new FormControl('test'); + + expect(fieldEmptyValidator('')(control)).toBeNull(); + }); + + it('should return invalid param', () => { + const control: AbstractControl = new FormControl(null); + + expect(fieldEmptyValidator('')(control)).toEqual({ + [ValidationMessageCode.FIELD_EMPTY]: { + name: '', + value: null, + reason: ValidationMessageCode.FIELD_EMPTY, + constraintParameters: [], + } as InvalidParam, + }); + }); + }); + + describe('field invalid validator', () => { + it('should return null', () => { + const control: AbstractControl = new FormControl('test'); + + expect(fieldInvalidValidator('', /^test$/)(control)).toBeNull(); + }); + + it('should return invalid param', () => { + const control: AbstractControl = new FormControl('test2'); + + expect(fieldInvalidValidator('', /^test$/)(control)).toEqual({ + [ValidationMessageCode.FIELD_INVALID]: { + name: '', + value: null, + reason: ValidationMessageCode.FIELD_INVALID, + constraintParameters: [], + } as InvalidParam, + }); + }); + }); + + describe('field length validator', () => { + it('should return null', () => { + const control: AbstractControl = new FormControl('test'); + + expect(fieldLengthValidator('', 1, 5)(control)).toBeNull(); + }); + + it('should return invalid param with min length', () => { + const control: AbstractControl = new FormControl('t'); + + expect(fieldLengthValidator('', 2, 5)(control)).toEqual({ + [ValidationMessageCode.FIELD_SIZE]: { + name: '', + value: null, + reason: ValidationMessageCode.FIELD_SIZE, + constraintParameters: [ + { name: 'min', value: '2' }, + { name: 'max', value: '5' }, + ], + } as InvalidParam, + }); + }); + + it('should return invalid param with max length', () => { + const control: AbstractControl = new FormControl('test test test'); + + expect(fieldLengthValidator('', 2, 5)(control)).toEqual({ + [ValidationMessageCode.FIELD_SIZE]: { + name: '', + value: null, + reason: ValidationMessageCode.FIELD_SIZE, + constraintParameters: [ + { name: 'min', value: '2' }, + { name: 'max', value: '5' }, + ], + } as InvalidParam, + }); + }); + }); + + describe('check box group empty validator', () => { + it('should return null', () => { + const control: UntypedFormGroup = new UntypedFormGroup({ + group1: new UntypedFormGroup({ + check1: new FormControl(true), + check2: new FormControl(false), + }), + group2: new UntypedFormGroup({ + check1: new FormControl(false), + check2: new FormControl(false), + }), + }); + + expect(checkBoxGroupsEmptyValidator('', ['group1', 'group2'])(control)).toBeNull(); + }); + + it('should return invalid param', () => { + const control: UntypedFormGroup = new UntypedFormGroup({ + group1: new UntypedFormGroup({ + check1: new FormControl(false), + check2: new FormControl(false), + }), + group2: new UntypedFormGroup({ + check1: new FormControl(false), + check2: new FormControl(false), + }), + }); + + expect(checkBoxGroupsEmptyValidator('', ['group1', 'group2'])(control)).toEqual({ + [ValidationMessageCode.FIELD_EMPTY]: { + name: '', + value: null, + reason: ValidationMessageCode.FIELD_EMPTY, + constraintParameters: [], + } as InvalidParam, + }); + }); + + it('should return invalid param and ignore group', () => { + const control: UntypedFormGroup = new UntypedFormGroup({ + group1: new UntypedFormGroup({ + check1: new FormControl(false), + check2: new FormControl(false), + }), + group2: new UntypedFormGroup({ + check1: new FormControl(true), + check2: new FormControl(false), + }), + }); + + expect(checkBoxGroupsEmptyValidator('', ['group1'])(control)).toEqual({ + [ValidationMessageCode.FIELD_EMPTY]: { + name: '', + value: null, + reason: ValidationMessageCode.FIELD_EMPTY, + constraintParameters: [], + } as InvalidParam, + }); + }); + }); +}); diff --git a/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.ts b/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.ts new file mode 100644 index 0000000000000000000000000000000000000000..798a98d8ebe415a7d9e97cae7c6bd6000a79926b --- /dev/null +++ b/alfa-client/libs/tech-shared/src/lib/validation/tech.validators.ts @@ -0,0 +1,81 @@ +import { InvalidParam, isNotEmpty, isNotNil } from '@alfa-client/tech-shared'; +import { AbstractControl, UntypedFormGroup, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { ValidationMessageCode } from './tech.validation.messages'; + +export function fieldEmptyValidator(controlName: string): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (isNotEmpty(control.value)) return null; + return { + [ValidationMessageCode.FIELD_EMPTY]: { + name: controlName, + value: null, + reason: ValidationMessageCode.FIELD_EMPTY, + constraintParameters: [], + } as InvalidParam, + }; + }; +} + +export function fieldInvalidValidator(controlName: string, regex: RegExp): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + if (regex.test(control.value)) return null; + return { + [ValidationMessageCode.FIELD_INVALID]: { + name: controlName, + value: null, + reason: ValidationMessageCode.FIELD_INVALID, + constraintParameters: [], + } as InvalidParam, + }; + }; +} + +export function fieldLengthValidator(controlName: string, min: number, max: number): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value: string = control.value; + if (isNotEmpty(control.value) && value.length >= min && value.length <= max) return null; + return { + [ValidationMessageCode.FIELD_SIZE]: { + name: controlName, + value: null, + reason: ValidationMessageCode.FIELD_SIZE, + constraintParameters: [ + { name: 'min', value: min.toString() }, + { name: 'max', value: max.toString() }, + ], + } as InvalidParam, + }; + }; +} + +/** + * A simplified version of validation subgroups of check boxes. Could be extended for more advanced forms. + * It only looks up direct children of check box groups. + */ +export function checkBoxGroupsEmptyValidator(controlName: string, groupNames: string[]): ValidatorFn { + return (control: AbstractControl<UntypedFormGroup>): ValidationErrors | null => { + const group: UntypedFormGroup = control as UntypedFormGroup; + const found: boolean = groupNames + .filter((groupName: string) => _existsUntypedFormSubGroup(group, groupName)) + .map((groupName: string) => group.controls[groupName] as UntypedFormGroup) + .map(_isAtLeastOneChecked) + .reduce((a: boolean, b: boolean) => a || b); + if (found) return null; + return { + [ValidationMessageCode.FIELD_EMPTY]: { + name: controlName, + value: null, + reason: ValidationMessageCode.FIELD_EMPTY, + constraintParameters: [], + } as InvalidParam, + }; + }; +} + +export function _existsUntypedFormSubGroup(group: UntypedFormGroup, subGroupName: string): boolean { + return isNotNil(group.controls[subGroupName]) && group.controls[subGroupName] instanceof UntypedFormGroup; +} + +export function _isAtLeastOneChecked(group: UntypedFormGroup): boolean { + return Object.values(group.value).findIndex((isChecked: boolean) => isChecked) > -1; +} diff --git a/alfa-client/libs/test-utils/src/lib/jest.helper.ts b/alfa-client/libs/test-utils/src/lib/jest.helper.ts index 60df8592afa840fd2f06b5f967e8f5cb295a9a2c..4d0c827161f6290c7db00fd85f2bc7745f38c38d 100644 --- a/alfa-client/libs/test-utils/src/lib/jest.helper.ts +++ b/alfa-client/libs/test-utils/src/lib/jest.helper.ts @@ -21,10 +21,11 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { Type } from '@angular/core'; import { ComponentFixture } from '@angular/core/testing'; import { expect } from '@jest/globals'; import { TooltipDirective } from '@ods/system'; -import { getDebugElementFromFixtureByCss, getElementFromFixture } from './helper'; +import { getDebugElementFromFixtureByCss, getElementFromFixture, getElementFromFixtureByType } from './helper'; export function notExistsAsHtmlElement(fixture: ComponentFixture<any>, domElement: string): void { expect(getElementFromFixture(fixture, domElement)).not.toBeInstanceOf(HTMLElement); @@ -34,6 +35,10 @@ export function existsAsHtmlElement(fixture: ComponentFixture<any>, domElement: expect(getElementFromFixture(fixture, domElement)).toBeInstanceOf(HTMLElement); } +export function expectComponentExistsInTemplate<T>(fixture: ComponentFixture<any>, component: Type<T>): void { + expect(getElementFromFixtureByType(fixture, component)).toBeInstanceOf(component); +} + export function tooltipExistsWithText(fixture: ComponentFixture<any>, domElement: string, tooltipText: string) { const tooltipInstance = getDebugElementFromFixtureByCss(fixture, domElement).injector.get(TooltipDirective); expect(tooltipInstance.componentRef.instance.text).toBe(tooltipText); diff --git a/alfa-client/package.json b/alfa-client/package.json index d6a7bffddc6984305f4bc77149fa653d2083dfd3..d4e51a736a56e6aebfb6e3c8943ca998363833d7 100644 --- a/alfa-client/package.json +++ b/alfa-client/package.json @@ -17,9 +17,9 @@ "test:lib": "nx test ${npm_config_lib}", "test:debug:lib": "nx test ${npm_config_lib} --detectOpenHandles --watchAll", "ci-build": "nx run alfa:build --outputHashing=all", - "ci-build-admin": "nx container admin", + "ci-build-administration": "nx container admin", "ci-prodBuild": "nx run alfa:build --outputHashing=all --configuration production", - "ci-prodBuild-admin": "nx container admin", + "ci-prodBuild-administration": "nx container admin", "ci-test": "nx run-many --target=test --parallel 20 -- --runInBand", "ci-sonar": "nx run-many --target=test --parallel 20 -- --runInBand --codeCoverage --coverageReporters=lcov --testResultsProcessor=jest-sonar-reporter && pnpm exec sonar-scanner", "lint": "nx workspace-lint && nx lint", diff --git a/alfa-client/tsconfig.base.json b/alfa-client/tsconfig.base.json index 615131502b18ad4b0d693d9d2baeed734f62c4ce..210fd016c76a0f4c30f30f797af24cca6c9d11dd 100644 --- a/alfa-client/tsconfig.base.json +++ b/alfa-client/tsconfig.base.json @@ -25,7 +25,7 @@ "@admin-client/reporting-shared": ["libs/admin/reporting-shared/src/index.ts"], "@admin-client/settings-shared": ["libs/admin/settings-shared/src/index.ts"], "@admin-client/shared": ["libs/admin/shared/src/index.ts"], - "@admin-client/statistik": ["libs/admin/statistik/src/index.ts"], + "@admin-client/aggregation-mapping": ["libs/admin/aggregation-mapping/src/index.ts"], "@admin-client/user": ["libs/admin/user/src/index.ts"], "@admin-client/user-shared": ["libs/admin/user-shared/src/index.ts"], "@admin/keycloak-shared": ["libs/admin/keycloak-shared/src/index.ts"],