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/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/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/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/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/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/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/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/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 65eb7a8e1680b97f12cdba2556ae8d9d9041883c..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 { ListResourceServiceConfig, ResourceListService, 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 ResourceListService< - ConfigurationResource, - AggregationMappingListResource, +export class AggregationMappingResourceService extends ApiResourceService< + AggregationMappingResource, AggregationMappingResource > {} export function createAggregationMappingResourceService( repository: ResourceRepository, - configurationService: ConfigurationService, -) { - return new ResourceListService(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(), - listLinkRel: ConfigurationLinkRel.AGGREGATION_MAPPINGS, - listResourceListLinkRel: AggregationMappingListLinkRel.LIST, - createLinkRel: 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/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/tech-shared/src/lib/resource/list-resource.service.spec.ts b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts index aa2dee9b6966288e6326463b82a4568abef68ecd..def11057cef1266514b993ab56701985896498be 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts @@ -28,14 +28,17 @@ import { Resource, ResourceUri } from '@ngxp/rest'; import { cold } from 'jest-marbles'; import { DummyLinkRel, DummyListLinkRel } from 'libs/tech-shared/test/dummy'; import { createDummyListResource, createDummyResource, createFilledDummyListResource } from 'libs/tech-shared/test/resource'; -import { BehaviorSubject, Observable, of } from 'rxjs'; -import { multipleCold, singleCold, singleHot } from '../../../test/marbles'; +import { BehaviorSubject, Observable, of, throwError } from 'rxjs'; +import { multipleCold, singleCold, singleColdCompleted, singleHot } from '../../../test/marbles'; import { ResourceListService } from './list-resource.service'; import { CreateResourceData, LinkRelationName, ListItemResource, ListResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; -import { ListResource, StateResource, createEmptyStateResource, createStateResource } from './resource.util'; - import * as ResourceUtil from './resource.util'; +import { createEmptyStateResource, createErrorStateResource, createLoadingStateResource, createStateResource, ListResource, StateResource } from './resource.util'; + +import { ProblemDetail } from '@alfa-client/tech-shared'; +import { expect } from '@jest/globals'; +import { createProblemDetail } from '../../../test/error'; describe('ListResourceService', () => { let service: ResourceListService<Resource, ListResource, ListItemResource>; @@ -348,6 +351,40 @@ describe('ListResourceService', () => { expect(createdResource).toBeObservable(multipleCold(createEmptyStateResource(true), createStateResource(returnResource))); }); + + it('should handle error', () => { + const error: ProblemDetail = createProblemDetail(); + service._handleError = jest.fn(); + resourceRepository.createResource.mockReturnValue(throwError(() => error)); + + service.create(toCreate).subscribe(); + + expect(service._handleError).toHaveBeenCalledWith(error); + }); + + it('should emit on error', () => { + const error: ProblemDetail = createProblemDetail(); + service._handleError = jest.fn().mockReturnValue(of(createErrorStateResource(error))); + resourceRepository.createResource.mockReturnValue(cold('-#', null, error)); + + expect(service.create(toCreate)).toBeObservable( + cold('a(b|)', { a: createLoadingStateResource(), b: createErrorStateResource(error) }), + ); + }); + }); + + describe('handle error', () => { + it('should return error state resource on unprocessable entity', () => { + const error: ProblemDetail = createProblemDetail(); + + expect(service._handleError(error)).toBeObservable(singleColdCompleted(createErrorStateResource(error))); + }); + + it('should throw error', () => { + const error: ProblemDetail = { ...createProblemDetail(), status: 500 }; + + expect(service._handleError(error)).toBeObservable(cold('#', null, error)); + }); }); describe('select', () => { diff --git a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts index 852a8299c56d256d93bf778e9600ab0717edc00f..c98b96a0bf2e7a3a7d0a886e33ee8aa8de416a35 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.ts @@ -21,24 +21,16 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Resource, ResourceUri, getUrl, hasLink } from '@ngxp/rest'; +import { getUrl, hasLink, Resource, ResourceUri } from '@ngxp/rest'; import { isEqual, isNil, isNull } from 'lodash-es'; -import { BehaviorSubject, Observable, combineLatest, debounceTime, filter, first, map, startWith, tap } from 'rxjs'; +import { BehaviorSubject, catchError, combineLatest, debounceTime, filter, first, map, Observable, of, startWith, tap, throwError, } from 'rxjs'; +import { isUnprocessableEntity } from '../http.util'; +import { ProblemDetail } from '../tech.model'; import { isNotNull, isNotUndefined } from '../tech.util'; import { CreateResourceData, ListItemResource, ListResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; import { mapToFirst, mapToResource } from './resource.rxjs.operator'; -import { - ListResource, - StateResource, - createEmptyStateResource, - createStateResource, - doIfLoadingRequired, - getEmbeddedResources, - isInvalidResourceCombination, - isLoadingRequired, - isStateResoureStable, -} from './resource.util'; +import { createEmptyStateResource, createErrorStateResource, createStateResource, doIfLoadingRequired, getEmbeddedResources, isInvalidResourceCombination, isLoadingRequired, isStateResoureStable, ListResource, StateResource, } from './resource.util'; /** * B = Type of baseresource @@ -110,6 +102,7 @@ export class ResourceListService<B extends Resource, T extends ListResource, I e this.verifyBeforeCreation(); return this.repository.createResource(this.buildCreateResourceData(toCreate, this.config.createLinkRel)).pipe( map((listItemResource: I) => createStateResource(listItemResource)), + catchError((error: ProblemDetail) => this._handleError(error)), startWith(createEmptyStateResource<I>(true)), ); } @@ -131,6 +124,13 @@ export class ResourceListService<B extends Resource, T extends ListResource, I e return this.hasLinkRel(this.config.createLinkRel); } + _handleError(error: ProblemDetail): Observable<StateResource<I>> { + if (isUnprocessableEntity(error.status)) { + return of(createErrorStateResource(error)); + } + return throwError(() => error); + } + public select(uri: ResourceUri): void { this.setSelectedResourceLoading(); this.repository 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 7f8f416bf9b8a56b3f17bf41d79572d012bdbdcb..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 @@ -101,12 +101,18 @@ export function getMessageForInvalidParam(label: string, invalidParam: InvalidPa } 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 { 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/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"],