diff --git a/alfa-client/Jenkinsfile.e2e b/alfa-client/Jenkinsfile.e2e index a662c618cc0a6e26e7188e55024c4aeb1edca764..94fcdb512628bbe15ac09a1640bc9e89f8b68920 100644 --- a/alfa-client/Jenkinsfile.e2e +++ b/alfa-client/Jenkinsfile.e2e @@ -556,6 +556,7 @@ Void generateAdminNamespaceYaml() { envValues.ozgcloud.bezeichner = bezeichner envValues.administration.put("image", ['tag': env.ADMINISTRATION_IMAGE_TAG]) envValues.administration.put("helm", ['version': env.ADMINISTRATION_HELM_CHART_VERSION, 'repoUrl': env.ADMINISTRATION_HELM_REPO_URL]) + envValues.administration.put("ozgcloud", ['feature': ['organisationsEinheiten' : "true"], 'organisationEinheit': ['zufiSearchUri': generateZufiSearchUri(bezeichner)]]) envValues.admin_client.put("image", ['tag': env.ADMIN_CLIENT_IMAGE_TAG]) envValues.admin_client.put("helm", ['version': env.ADMIN_CLIENT_HELM_CHART_VERSION, 'repoUrl': env.ADMIN_CLIENT_HELM_REPO_URL]) @@ -563,6 +564,10 @@ Void generateAdminNamespaceYaml() { return writeYamlToGitOps(bezeichner, envValues); } +String generateZufiSearchUri(String bezeichner) { + return "https://${bezeichner}.dev.by.ozg-cloud.de/api/organisationseinheits?searchBy={searchBy}" +} + String generateEaNamespaceYaml() { return generateNamespaceYaml(env.EA_BEZEICHNER, "by-ea-dev.yaml"); } diff --git a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts index 95761b4df7188b50522c3b6a5d4b845503a64499..018f353c5f8cfca304adf2b0bd15a6f0792482c9 100644 --- a/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts +++ b/alfa-client/apps/admin-e2e/src/components/benutzer/benutzer.e2e.component.ts @@ -36,6 +36,7 @@ export class BenutzerE2EComponent { private readonly loeschenCheckbox: string = 'Loschen-checkbox-editor'; private readonly userCheckbox: string = 'User-checkbox-editor'; private readonly postCheckbox: string = 'Poststelle-checkbox-editor'; + private readonly saveButton: string = 'save-button'; public getHinzufuegenButton(): Cypress.Chainable<Element> { return cy.getTestElement(this.benutzerHinzufuegenButton); @@ -60,18 +61,34 @@ export class BenutzerE2EComponent { return cy.getTestElement(this.userVorname); } + public enterVorname(vorname: string): void { + this.getVornameInput().type(vorname); + } + public getNachnameInput(): Cypress.Chainable<Element> { return cy.getTestElement(this.userNachname); } + public enterNachname(nachname: string): void { + this.getNachnameInput().type(nachname); + } + public getBenutzernameInput(): Cypress.Chainable<Element> { return cy.getTestElement(this.userBenutzername); } + public enterBenutzername(benutzername: string): void { + this.getBenutzernameInput().type(benutzername); + } + public getMailInput(): Cypress.Chainable<Element> { return cy.getTestElement(this.userMail); } + public enterMail(mail: string): void { + this.getMailInput().type(mail); + } + public getOEButton(): Cypress.Chainable<Element> { return cy.getTestElement(this.addOEButton); } @@ -111,4 +128,12 @@ export class BenutzerE2EComponent { public clickPostCheckbox(): void { this.getPostCheckbox().click(); } + + public getSaveButton(): Cypress.Chainable<Element> { + return cy.getTestElement(this.saveButton); + } + + public saveUser(): void { + this.getSaveButton().click(); + } } diff --git a/alfa-client/apps/admin-e2e/src/components/statistik/statistik-component.ts b/alfa-client/apps/admin-e2e/src/components/statistik/statistik-component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c088cd85910ceafc4c0f4df93edc403abfd4a630 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/components/statistik/statistik-component.ts @@ -0,0 +1,9 @@ +import { exist } from '../../support/cypress.util'; + +export class StatistikE2EComponent { + private readonly headerText: string = 'statistik-header-text'; + + public isHeaderTextVisible(): void { + exist(cy.getTestElement(this.headerText)); + } +} diff --git a/alfa-client/apps/admin-e2e/src/components/ui/snackbar.e2e.component.ts b/alfa-client/apps/admin-e2e/src/components/ui/snackbar.e2e.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..903e0d97a994405891c4ae19fa4cc33afa754d01 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/components/ui/snackbar.e2e.component.ts @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 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. + */ +export class SnackBarE2EComponent { + private readonly locatorMessage: string = 'snackbar-message'; + private readonly locatorCloseButton: string = 'snackbar-close-button'; + private readonly locatorRevokeButton: string = 'snackbar-revoke-button'; + + public getMessage() { + return cy.getTestElement(this.locatorMessage); + } + + public getCloseButton() { + return cy.getTestElement(this.locatorCloseButton); + } + + public getRevokeButton() { + return cy.getTestElement(this.locatorRevokeButton); + } +} diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..08b27109920f0cc6a67779bfb8d3d262b63bd7bb --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/benutzer_rollen/benutzer-anlegen.cy.ts @@ -0,0 +1,73 @@ + +import { MainPage } from 'apps/admin-e2e/src/page-objects/main.po'; +import { BenutzerE2EComponent } from '../../../components/benutzer/benutzer.e2e.component'; +import { beChecked, beEnabled, contains, exist, notBeChecked, notBeEnabled } from '../../../support/cypress.util'; +import { AlfaRollen, AlfaUsers, loginAsAriane } from '../../../support/user-util'; +import { SnackBarE2EComponent } from '../../../components/ui/snackbar.e2e.component'; +import { SnackbarMessagesE2E } from '../../../model/util'; +import { wait, waitOfInterceptor } from 'apps/admin-e2e/src/support/cypress-helper'; + +const mainPage: MainPage = new MainPage(); +const benutzerPage: BenutzerE2EComponent = new BenutzerE2EComponent(); +const snackBar: SnackBarE2EComponent = new SnackBarE2EComponent(); + +const vorname: string = 'Theo'; +const nachname: string = 'Testuser'; +const benutzername: string = 'testtheo'; +const emailAddress: string = 'theo.test@ozg-sh.de'; + +describe('Benutzer anlegen', () => { + before(() => { + loginAsAriane(); + }); + + it('should open Benutzer tab and show Hinzufuegen button', () => { + mainPage.clickBenutzerTab(); + + exist(benutzerPage.getHinzufuegenButton()); + }); + + it.skip('should not do anything after click on save with missing data', () => { + benutzerPage.addUser(); + benutzerPage.saveUser(); + + //TODO: Fehlermeldungen hinzufügen + }); + + it('should show snackbar message on error', () => { + const interceptor: string = 'postUser'; + const url: string = 'https://sso.dev.by.ozg-cloud.de/admin/realms/by-e2e-tests-local-dev/users' + const errorCode: number = 500; + const errorBody: string = 'Internal Server Error'; + + cy.intercept('POST', url, {statusCode: errorCode, body: errorBody}).as(interceptor); + + benutzerPage.addUser(); + benutzerPage.saveUser(); + + waitOfInterceptor(interceptor).then(() => { + contains(snackBar.getMessage(), SnackbarMessagesE2E.NUTZER_FEHLGESCHLAGEN); + }); + }); + + it('should show snackbar message on saving user', () => { + snackBar.getCloseButton().click(); + + benutzerPage.enterVorname(vorname); + benutzerPage.enterNachname(nachname); + benutzerPage.enterBenutzername(benutzername); + benutzerPage.enterMail(emailAddress); + benutzerPage.clickAdminCheckbox(); + benutzerPage.clickUserCheckbox(); + + benutzerPage.saveUser(); + contains(snackBar.getMessage(), SnackbarMessagesE2E.NUTZER_ANGELEGT); + }); + + it('should display new user in users table', () => { + benutzerPage.stringExistsInUserEntry(AlfaRollen.USER, benutzername); + benutzerPage.stringExistsInUserEntry(vorname, benutzername); + benutzerPage.stringExistsInUserEntry(nachname, benutzername); + // FEHLT NOCH: benutzerPage.stringExistsInUserEntry(AlfaRollen.ADMIN, benutzername); + }); +}); diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..10be61e410d4b9332e05ce3efdd12e65bfd6dc4b --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/ariane.cy.ts @@ -0,0 +1,31 @@ +import { MainPage, waitForSpinnerToDisappear } from 'apps/admin-e2e/src/page-objects/main.po'; +import { exist, notExist } from 'apps/admin-e2e/src/support/cypress.util'; +import { loginAsAriane } from 'apps/admin-e2e/src/support/user-util'; + +describe('Navigation', () => { + const mainPage: MainPage = new MainPage(); + + describe('with user ariane', () => { + before(() => { + loginAsAriane(); + + waitForSpinnerToDisappear(); + }); + + it('should show benutzer navigation item', () => { + exist(mainPage.getBenutzerTab()); + }); + + it('should show postfach navigation item', () => { + exist(mainPage.getPostfachNavigationItem()); + }); + + it('should hide organisationseinheiten navigation item', () => { + notExist(mainPage.getOrganisationEinheitNavigationItem()); + }); + + it('should hide statistik navigation item', () => { + notExist(mainPage.getStatistikNavigationItem()); + }); + }); +}); diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/daria.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/daria.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..7082da14ad51b94c655f92de71e898742c07a15d --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/daria.cy.ts @@ -0,0 +1,44 @@ +import { StatistikE2EComponent } from 'apps/admin-e2e/src/components/statistik/statistik-component'; +import { MainPage, waitForSpinnerToDisappear } from 'apps/admin-e2e/src/page-objects/main.po'; +import { exist, notExist } from 'apps/admin-e2e/src/support/cypress.util'; +import { loginAsDaria } from 'apps/admin-e2e/src/support/user-util'; + +describe('Navigation', () => { + const mainPage: MainPage = new MainPage(); + + const statistikPage: StatistikE2EComponent = new StatistikE2EComponent(); + + describe('with user daria', () => { + before(() => { + loginAsDaria(); + + waitForSpinnerToDisappear(); + }); + + it('should hide other navigation item', () => { + notExist(mainPage.getBenutzerTab()); + }); + + it('should hide postfach navigation item', () => { + notExist(mainPage.getPostfachNavigationItem()); + }); + + it('should hide organisationseinheiten navigation item', () => { + notExist(mainPage.getOrganisationEinheitNavigationItem()); + }); + + describe('statistik', () => { + it('should be visible', () => { + exist(mainPage.getStatistikNavigationItem()); + }); + + it('should be initial selected', () => { + mainPage.isStatistikNavigationItemSelected(); + }); + + it('should show header text', () => { + statistikPage.isHeaderTextVisible(); + }); + }); + }); +}); diff --git a/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/safira.cy.ts b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/safira.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b1c1023b9ba8d3bb73e34ce2c36a937816ca716 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/e2e/main-tests/navigation/safira.cy.ts @@ -0,0 +1,50 @@ +import { StatistikE2EComponent } from 'apps/admin-e2e/src/components/statistik/statistik-component'; +import { MainPage, waitForSpinnerToDisappear } from 'apps/admin-e2e/src/page-objects/main.po'; +import { exist } from 'apps/admin-e2e/src/support/cypress.util'; +import { loginAsSafira } from 'apps/admin-e2e/src/support/user-util'; + +describe('Navigation', () => { + const mainPage: MainPage = new MainPage(); + + const statistikPage: StatistikE2EComponent = new StatistikE2EComponent(); + + describe('with user safira', () => { + before(() => { + loginAsSafira(); + + waitForSpinnerToDisappear(); + }); + + it('should show benutzer navigation item', () => { + exist(mainPage.getBenutzerTab()); + }); + + it('should show postfach navigation item', () => { + exist(mainPage.getPostfachNavigationItem()); + }); + + it('should show organisationseinheiten navigation item', () => { + exist(mainPage.getOrganisationEinheitNavigationItem()); + }); + + describe('statistik', () => { + it('should be visible', () => { + exist(mainPage.getStatistikNavigationItem()); + }); + + describe('on selection', () => { + before(() => { + mainPage.openStatistik(); + }); + + it('should show page on selection', () => { + statistikPage.isHeaderTextVisible(); + }); + + it('should mark navigation item as selected', () => { + mainPage.isStatistikNavigationItemSelected(); + }); + }); + }); + }); +}); diff --git a/alfa-client/apps/admin-e2e/src/fixtures/argocd/by-admin-dev.yaml b/alfa-client/apps/admin-e2e/src/fixtures/argocd/by-admin-dev.yaml index dadb3ca3ebd8236064c6e473e191ace3552a84e1..cebf220d954ac403b96721579ba03a294698c986 100644 --- a/alfa-client/apps/admin-e2e/src/fixtures/argocd/by-admin-dev.yaml +++ b/alfa-client/apps/admin-e2e/src/fixtures/argocd/by-admin-dev.yaml @@ -33,6 +33,8 @@ project: administration: enabled: true + env: + overrideSpringProfiles: 'oc,e2e,dev' sso: keycloak_users: - name: ariane diff --git a/alfa-client/apps/admin-e2e/src/fixtures/user/user_ariane.json b/alfa-client/apps/admin-e2e/src/fixtures/user/user_ariane.json index 144c0dc0cfb644f8d47795c6a5605900c831639a..47c5184edc2a2062bdcfa44b89b72ea3d29a01d2 100644 --- a/alfa-client/apps/admin-e2e/src/fixtures/user/user_ariane.json +++ b/alfa-client/apps/admin-e2e/src/fixtures/user/user_ariane.json @@ -1,6 +1,7 @@ { "name": "ariane", - "password": "Y9nk43yrQ_zzIPpfFU-I", + "password_new": "Y9nk43yrQ_zzIPpfFU-I", + "password": "123Test!", "firstName": "Ariane", "lastName": "Admin", "fullName": "Ariane Admin", diff --git a/alfa-client/apps/admin-e2e/src/fixtures/user/user_daria.json b/alfa-client/apps/admin-e2e/src/fixtures/user/user_daria.json new file mode 100644 index 0000000000000000000000000000000000000000..0ac9cc99af58292dfa0c13ffac4c7f36d49fef93 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/fixtures/user/user_daria.json @@ -0,0 +1,12 @@ +{ + "name": "daria", + "password": "Y9nk43yrQ_zzIPpfFU-I", + "firstName": "Daria", + "lastName": "Data", + "fullName": "Daria Data", + "email": "daria.data@ozg-sh.de", + "initials": "DD", + "dataTestId": "DARIA_DATENBEAUFTRAGUNG", + "clientRoles": ["DATENBEAUFTRAGUNG"], + "groups": ["E2E Tests"] +} diff --git a/alfa-client/apps/admin-e2e/src/fixtures/user/user_safira.json b/alfa-client/apps/admin-e2e/src/fixtures/user/user_safira.json new file mode 100644 index 0000000000000000000000000000000000000000..1a978c211c729ec44d0d827034d07bf7cd9d9745 --- /dev/null +++ b/alfa-client/apps/admin-e2e/src/fixtures/user/user_safira.json @@ -0,0 +1,12 @@ +{ + "name": "safira", + "password": "Y9nk43yrQ_zzIPpfFU-I", + "firstName": "Safira", + "lastName": "Super", + "fullName": "Safira Super", + "email": "safira.super@ozg-sh.de", + "initials": "SS", + "dataTestId": "SAFIRA_ADMIN_DATENBEAUFTRAGUNG", + "clientRoles": ["DATENBEAUFTRAGUNG", "ADMIN_ADMIN"], + "groups": ["E2E Tests"] +} diff --git a/alfa-client/apps/admin-e2e/src/model/util.ts b/alfa-client/apps/admin-e2e/src/model/util.ts index 93af4b06cb32e05a076a6b4b2343964356d6ecd5..efd196d5bb608c2f48ef32ec1f46524847da91fd 100644 --- a/alfa-client/apps/admin-e2e/src/model/util.ts +++ b/alfa-client/apps/admin-e2e/src/model/util.ts @@ -24,3 +24,8 @@ export class ObjectIdE2E { $oid: string; } + +export enum SnackbarMessagesE2E { + NUTZER_ANGELEGT = 'Der Benutzer wurde hinzugefügt.', + NUTZER_FEHLGESCHLAGEN = 'Der Benutzer konnte nicht hinzugefügt werden.' +} 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 8137fc536242c75461320a28b5d49f2e056f52d0..360f51ce884d2e15ec285c10d7b7c2d8eb66856e 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 @@ -22,15 +22,21 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { BuildInfoE2EComponent } from '../components/buildinfo/buildinfo.e2e.component'; +import { containClass } from '../support/cypress.util'; import { HeaderE2EComponent } from './header.po'; export class MainPage { private readonly buildInfo: BuildInfoE2EComponent = new BuildInfoE2EComponent(); private readonly header: HeaderE2EComponent = new HeaderE2EComponent(); + private readonly benutzerTab: string = 'caption-Benutzer__Rollen'; private readonly postfachTab: string = 'caption-Postfach'; private readonly organisationsEinheitenTab: string = 'caption-Organisationseinheiten'; + private readonly postfachNavigationItem: string = 'postfach-navigation'; + private readonly organisationEinheitNavigation: string = 'organisations-einheiten-navigation'; + private readonly statistikNavigationItem: string = 'statistik-navigation'; + public getBuildInfo(): BuildInfoE2EComponent { return this.buildInfo; } @@ -47,6 +53,14 @@ export class MainPage { this.getBenutzerTab().click(); } + public getPostfachNavigationItem(): Cypress.Chainable<Element> { + return cy.getTestElement(this.postfachNavigationItem); + } + + public getOrganisationEinheitNavigationItem(): Cypress.Chainable<Element> { + return cy.getTestElement(this.organisationEinheitNavigation); + } + public getOrganisationsEinheitenMenu(): Cypress.Chainable<Element> { return cy.getTestElement(this.organisationsEinheitenTab); } @@ -54,6 +68,18 @@ export class MainPage { public openOrganisationsEinheiten(): void { this.getOrganisationsEinheitenMenu().click(); } + + public getStatistikNavigationItem(): Cypress.Chainable<Element> { + return cy.getTestElement(this.statistikNavigationItem); + } + + public isStatistikNavigationItemSelected(): void { + containClass(this.getStatistikNavigationItem().get('a'), 'border-selected'); + } + + public openStatistik(): void { + this.getStatistikNavigationItem().click(); + } } export function waitForSpinnerToDisappear(): boolean { diff --git a/alfa-client/apps/admin-e2e/src/support/angular.util.ts b/alfa-client/apps/admin-e2e/src/support/angular.util.ts index 7dbeb3b9cc5ff07185971dacf56729a404222d2c..8345e751b5781769282164d36aa7fb6b0bd4636d 100644 --- a/alfa-client/apps/admin-e2e/src/support/angular.util.ts +++ b/alfa-client/apps/admin-e2e/src/support/angular.util.ts @@ -39,8 +39,7 @@ enum AngularElementE2E { export function hasTooltip(element: any, value: string) { mouseEnter(element); - element.get('mat-tooltip-component').contains(value); - // element.get(`div[title="${value}"]`); + element.get('ods-tooltip').contains(value); } export function isChecked(element: any) { diff --git a/alfa-client/apps/admin-e2e/src/support/cypress.util.ts b/alfa-client/apps/admin-e2e/src/support/cypress.util.ts index 60db5d0f8610ec03e2cd6c0c34a71e45555612ca..505762fe8f5193921ee7b5bffb76234f5b21393b 100644 --- a/alfa-client/apps/admin-e2e/src/support/cypress.util.ts +++ b/alfa-client/apps/admin-e2e/src/support/cypress.util.ts @@ -25,7 +25,7 @@ import { wait } from './cypress-helper'; //TODO Naming der Methoden geradeziehen -export function containClass(element: Cypress.Chainable<Element>, cssClass: string): void { +export function containClass(element: Cypress.Chainable<any>, cssClass: string): void { element.should('have.class', cssClass); } diff --git a/alfa-client/apps/admin-e2e/src/support/user-util.ts b/alfa-client/apps/admin-e2e/src/support/user-util.ts index e05c80cd5947165fef7e2fd7a3dfca65885a0fe2..46f07823c27eee9c7872cc8f72a39a33a5326548 100644 --- a/alfa-client/apps/admin-e2e/src/support/user-util.ts +++ b/alfa-client/apps/admin-e2e/src/support/user-util.ts @@ -26,15 +26,23 @@ import { HeaderE2EComponent } from '../page-objects/header.po'; import { MainPage } from '../page-objects/main.po'; export function loginAsAriane(): void { - cy.fixture('user/user_ariane.json').then((user) => { - loginByUi(user); - }); + login(UserJsonPath.ARIANE); +} + +export function loginAsDaria(): void { + login(UserJsonPath.DARIA); } export function loginAsSabine(): void { - cy.fixture('user/user_sabine.json').then((user) => { - loginByUi(user); - }); + login(UserJsonPath.SABINE); +} + +export function loginAsSafira(): void { + login(UserJsonPath.SAFIRA); +} + +function login(userJson: string): void { + cy.fixture(userJson).then((user) => loginByUi(user)); } // Hinweis: cacheAcrossSpecs: true lässt Tests umfallen @@ -60,6 +68,13 @@ export function loginByUi(user: UserE2E): void { ); } +enum UserJsonPath { + ARIANE = 'user/user_ariane.json', + DARIA = 'user/user_daria.json', + SABINE = 'user/user_sabine.json', + SAFIRA = 'user/user_safira.json', +} + export function logout(): void { const mainPage: MainPage = new MainPage(); const header: HeaderE2EComponent = mainPage.getHeader(); @@ -70,6 +85,7 @@ export enum AlfaRollen { USER = 'VERWALTUNG_USER', LOESCHEN = 'VERWALTUNG_LOESCHEN', POSTSTELLE = 'VERWALTUNG_POSTSTELLE', + ADMIN = 'ADMIN_ADMIN' } export enum AlfaUsers { diff --git a/alfa-client/apps/admin/Jenkinsfile b/alfa-client/apps/admin/Jenkinsfile index 466c2b7d229a7fe30477cd5a464f2120c4307d7c..8ae0ccfcdb74541493558353c758eb9896742caa 100644 --- a/alfa-client/apps/admin/Jenkinsfile +++ b/alfa-client/apps/admin/Jenkinsfile @@ -72,7 +72,7 @@ pipeline { withNPM(npmrcConfig: 'npm-nexus-auth') { dir('alfa-client') { - sh 'pnpm install --frozen-lockfile --network-concurrency=8' + sh 'pnpm install --frozen-lockfile' if (isReleaseBranch()) { sh 'pnpm run ci-prodBuild-admin' diff --git a/alfa-client/apps/admin/src/app/app.component.html b/alfa-client/apps/admin/src/app/app.component.html index 1bf3181be49c2b39c5f0273266c0f5a07cf75d57..6409e8398c47509d3cfa668979eb5d0e1cd91b26 100644 --- a/alfa-client/apps/admin/src/app/app.component.html +++ b/alfa-client/apps/admin/src/app/app.component.html @@ -40,28 +40,24 @@ </header> <div class="flex h-screen w-full justify-center overflow-y-auto"> <ods-navbar data-test-id="navigation"> - <ng-container *ngIf="apiRoot | hasLink: apiRootLinkRel.CONFIGURATION"> - <ng-container *ngIf="environment.features.benutzerRollen"> - <ods-nav-item data-test-id="users-roles-navigation" caption="Benutzer & Rollen" path="/benutzer_und_rollen"> - <ods-users-icon class="stroke-text" icon /> - </ods-nav-item> - <hr /> - </ng-container> - <ng-container *ngIf="environment.features.postfach"> - <ods-nav-item data-test-id="postfach-navigation" caption="Postfach" path="/postfach"> - <ods-mailbox-icon icon /> - </ods-nav-item> - </ng-container> - <ng-container *ngIf="apiRoot | hasLink: apiRootLinkRel.ORGANISATIONS_EINHEIT"> - <ods-nav-item - data-test-id="organisations-einheiten-navigation" - caption="Organisationseinheiten" - path="/organisationseinheiten" - > - <ods-orga-unit-icon icon /> - </ods-nav-item> - </ng-container> - </ng-container> + @if (apiRoot | hasLink: apiRootLinkRel.USERS) { + <ods-nav-item data-test-id="users-roles-navigation" caption="Benutzer & Rollen" path="/benutzer_und_rollen"> + <ods-users-icon class="stroke-text" icon /> + </ods-nav-item> + } + @if (apiRoot | hasLink: apiRootLinkRel.ORGANISATIONS_EINHEIT) { + <ods-nav-item + data-test-id="organisations-einheiten-navigation" + caption="Organisationseinheiten" + path="/organisationseinheiten" + > + <ods-orga-unit-icon icon /> + </ods-nav-item> + <hr /> + } + @if (apiRoot | hasLink: apiRootLinkRel.CONFIGURATION) { + <admin-menu-container data-test-id="menu-container"></admin-menu-container> + } </ods-navbar> <main class="flex-1 overflow-y-auto bg-white px-6 py-4"> <router-outlet 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 47e52ca32268a84cd26ea728858fad6b0265e96a..86fda2c791f984af8fbb569cb101d7d7a5911151 100644 --- a/alfa-client/apps/admin/src/app/app.component.spec.ts +++ b/alfa-client/apps/admin/src/app/app.component.spec.ts @@ -21,20 +21,14 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { ConfigurationLinkRel, ConfigurationResource, ConfigurationService } from '@admin-client/configuration-shared'; import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; import { BuildInfoComponent } from '@alfa-client/common'; -import { getEnvironmentFactory } from '@alfa-client/environment-shared'; import { HasLinkPipe, createEmptyStateResource, createStateResource } from '@alfa-client/tech-shared'; -import { - Mock, - dispatchEventFromFixture, - existsAsHtmlElement, - getElementFromFixture, - mock, - notExistsAsHtmlElement, -} from '@alfa-client/test-utils'; +import { Mock, existsAsHtmlElement, 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, @@ -43,18 +37,16 @@ import { OrgaUnitIconComponent, UsersIconComponent, } from '@ods/system'; -import { AuthenticationService } from 'authentication'; +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'; -import { createEnvironment } from 'libs/environment-shared/test/environment'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { MockComponent, MockDirective } from 'ng-mocks'; -import { of } from 'rxjs'; +import { Subscription, of } from 'rxjs'; import { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component'; import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component'; import { AppComponent } from './app.component'; -jest.mock('@alfa-client/environment-shared'); - describe('AppComponent', () => { let component: AppComponent; let fixture: ComponentFixture<AppComponent>; @@ -62,15 +54,11 @@ describe('AppComponent', () => { const adminHeaderSelector: string = getDataTestIdOf('admin-header'); const buildInfoSelector: string = getDataTestIdOf('build-info'); const userProfileButtonSelector: string = getDataTestIdOf('user-profile-button'); - const navigationSelector: string = getDataTestIdOf('navigation'); const usersRolesNavigationSelector: string = getDataTestIdOf('users-roles-navigation'); - const postfachNavigationSelector: string = getDataTestIdOf('postfach-navigation'); + const organisationsEinheitenNavigationSelector: string = getDataTestIdOf('organisations-einheiten-navigation'); - const logoLink: string = getDataTestIdOf('logo-link'); const routerOutletSelector: string = getDataTestIdOf('router-outlet'); - - const environment = createEnvironment(); - (getEnvironmentFactory as jest.Mock).mockReturnValue(environment); + const menuContainer: string = getDataTestIdOf('menu-container'); const authenticationService: Mock<AuthenticationService> = { ...mock(AuthenticationService), @@ -91,9 +79,13 @@ describe('AppComponent', () => { }; const apiRootService: Mock<ApiRootService> = mock(ApiRootService); + let configurationService: Mock<ConfigurationService>; beforeEach(async () => { + configurationService = mock(ConfigurationService); + await TestBed.configureTestingModule({ + imports: [HasLinkPipe], declarations: [ AppComponent, MockComponent(AdminLogoIconComponent), @@ -105,7 +97,7 @@ describe('AppComponent', () => { MockComponent(NavbarComponent), MockComponent(NavItemComponent), MockComponent(BuildInfoComponent), - HasLinkPipe, + MockComponent(MenuContainerComponent), MockDirective(RouterOutlet), ], providers: [ @@ -117,6 +109,10 @@ describe('AppComponent', () => { provide: ApiRootService, useValue: apiRootService, }, + { + provide: ConfigurationService, + useValue: configurationService, + }, { provide: Router, useValue: router, @@ -159,127 +155,272 @@ describe('AppComponent', () => { }); describe('do after logged in', () => { + beforeEach(() => { + component.evaluateInitialNavigation = jest.fn(); + }); + it('should call apiRootService to getApiRoot', () => { component.doAfterLoggedIn(); expect(apiRootService.getApiRoot).toHaveBeenCalled(); }); - it('should call forwardWithoutAuthenticationParams', () => { - component.forwardWithoutAuthenticationParams = jest.fn(); + it('should call evaluateInitialNavigation', () => { + component.evaluateInitialNavigation = jest.fn(); component.doAfterLoggedIn(); - expect(component.forwardWithoutAuthenticationParams).toHaveBeenCalled(); + expect(component.evaluateInitialNavigation).toHaveBeenCalled(); }); }); - describe('forward without authentication params', () => { - it('should navigate to same route without authentication params', () => { - component.forwardWithoutAuthenticationParams(); + describe('evaluate initial navigation', () => { + beforeEach(() => { + component.evaluateNavigationByApiRoot = jest.fn(); + }); + + it('should call evaluate navigation by apiRoot', () => { + const apiRootResource: ApiRootResource = createApiRootResource(); + component.apiRootStateResource$ = of(createStateResource(apiRootResource)); - expect(router.navigate).toHaveBeenCalledWith([], { queryParams: {} }); + component.evaluateInitialNavigation(); + + expect(component.evaluateNavigationByApiRoot).toHaveBeenCalledWith(apiRootResource); + }); + + it('should NOT call evaluate navigation by apiRoot if resource is loading', () => { + component.apiRootStateResource$ = of(createEmptyStateResource<ApiRootResource>(true)); + component.evaluateNavigationByApiRoot = jest.fn(); + + component.evaluateInitialNavigation(); + + expect(component.evaluateNavigationByApiRoot).not.toHaveBeenCalled(); }); }); - }); - describe('template', () => { - it('show not show header if apiRoot is not loaded', () => { - component.apiRootStateResource$ = of(createEmptyStateResource<ApiRootResource>()); + describe('evaluate navigation api root', () => { + it('should evaluate navigation by configuration if link exists', () => { + component.evaluateNavigationByConfiguration = jest.fn(); + const apiRootResource: ApiRootResource = createApiRootResource([ApiRootLinkRel.CONFIGURATION]); - notExistsAsHtmlElement(fixture, adminHeaderSelector); + component.evaluateNavigationByApiRoot(apiRootResource); + + expect(component.evaluateNavigationByConfiguration).toHaveBeenCalled(); + }); + + it('should navigate by api root if link is missing', () => { + component.navigateByApiRoot = jest.fn(); + const apiRootResource: ApiRootResource = createApiRootResource(); + + component.evaluateNavigationByApiRoot(apiRootResource); + + expect(component.navigateByApiRoot).toHaveBeenCalledWith(apiRootResource); + }); }); - describe('user profile button', () => { + describe('evaluate navigation by configuration', () => { + const configurationResource: ConfigurationResource = createConfigurationResource(); + beforeEach(() => { - component.apiRootStateResource$ = of(createStateResource(createApiRootResource())); + configurationService.get.mockReturnValue(of(createStateResource(configurationResource))); + component.navigateByConfiguration = jest.fn(); }); - it('should show if apiRoot exists', () => { - fixture.detectChanges(); + it('should call configuration service to get resource', () => { + component.evaluateNavigationByConfiguration(); - existsAsHtmlElement(fixture, userProfileButtonSelector); + expect(configurationService.get).toHaveBeenCalled(); + }); + + it('should call navigate by configuration', () => { + component.evaluateNavigationByConfiguration(); + + expect(component.navigateByConfiguration).toHaveBeenCalledWith(configurationResource); }); - }); - describe('administration logo', () => { - const apiResource: ApiRootResource = createApiRootResource(); + it('should NOT call navigate by configuration if resource is loading', () => { + configurationService.get.mockReturnValue(of(createEmptyStateResource<ConfigurationResource>(true))); + component.evaluateNavigationByConfiguration(); + + expect(component.navigateByConfiguration).not.toHaveBeenCalled(); + }); + }); + + describe('navigate by configuration', () => { beforeEach(() => { - component.apiRootStateResource$ = of(createStateResource(apiResource)); - fixture.detectChanges(); + component.unsubscribe = jest.fn(); + }); + + it('should navigate to postfach if settings link exists', () => { + component.navigateByConfiguration(createConfigurationResource([ConfigurationLinkRel.SETTING])); + + expect(router.navigate).toHaveBeenCalledWith(['/postfach']); + }); + + it('should navigate to statistik if aggregation mapping link exists', () => { + component.navigateByConfiguration(createConfigurationResource([ConfigurationLinkRel.AGGREGATION_MAPPINGS])); + + expect(router.navigate).toHaveBeenCalledWith(['/statistik']); }); - it('should navigate to start page on click', () => { - dispatchEventFromFixture(fixture, logoLink, 'click'); + it('should navigate to unavailable page if no link exists', () => { + component.navigateByConfiguration(createConfigurationResource()); - expect(router.navigate).toHaveBeenCalledWith([], { queryParams: {} }); + expect(router.navigate).toHaveBeenCalledWith(['/unavailable']); + }); + + it('should call unsubscribe', () => { + component.navigateByConfiguration(createConfigurationResource()); + + expect(component.unsubscribe).toHaveBeenCalled(); }); }); - describe('navigation', () => { + describe('navigate by api root', () => { beforeEach(() => { - component.apiRootStateResource$ = of(createStateResource(createApiRootResource([ApiRootLinkRel.CONFIGURATION]))); + component.unsubscribe = jest.fn(); }); - it('should show links if configuration link exists', () => { - fixture.detectChanges(); + it('should navigate to benutzer und rollen if users link exists', () => { + const apiRootResource: ApiRootResource = createApiRootResource([ApiRootLinkRel.USERS]); - const navbarElement: HTMLElement = getElementFromFixture(fixture, navigationSelector); + component.navigateByApiRoot(apiRootResource); - expect(navbarElement.children.length).toBeGreaterThan(0); + expect(router.navigate).toHaveBeenCalledWith(['/benutzer_und_rollen']); }); - it('should not not show links if configuration resource not available', () => { - component.apiRootStateResource$ = of(createStateResource(createApiRootResource([]))); - fixture.detectChanges(); + it('should navigate to unavailable page if no link exists', () => { + component.navigateByApiRoot(createApiRootResource()); - const navbarElement: HTMLElement = getElementFromFixture(fixture, navigationSelector); + expect(router.navigate).toHaveBeenCalledWith(['/unavailable']); + }); - expect(navbarElement.children.length).toBe(0); + it('should call unsubscribe', () => { + component.navigateByApiRoot(createApiRootResource()); + + expect(component.unsubscribe).toHaveBeenCalled(); }); + }); - it('should show postfach link if postfach feature toggle is true', () => { - component.environment.features.postfach = true; - fixture.detectChanges(); + describe('unsubscribe', () => { + describe('apiRoot subscription', () => { + it('should unsubscribe if exists', () => { + component.apiRootSubscription = <any>mock(Subscription); + component.apiRootSubscription.unsubscribe = jest.fn(); + + component.unsubscribe(); + + expect(component.apiRootSubscription.unsubscribe).toHaveBeenCalled(); + }); + + it('should do nothing if not exists', () => { + component.apiRootSubscription = undefined; + const unsubscribeSpy: jest.SpyInstance = jest.spyOn(Subscription.prototype, 'unsubscribe'); - existsAsHtmlElement(fixture, postfachNavigationSelector); + component.unsubscribe(); + + expect(unsubscribeSpy).not.toHaveBeenCalled(); + unsubscribeSpy.mockRestore(); + }); }); - it('should not show postfach link if postfach feature toggle is false', () => { - component.environment.features.postfach = false; - fixture.detectChanges(); + describe('configuration subscription', () => { + it('should unsubscribe if exists', () => { + component.configurationSubscription = <any>mock(Subscription); + component.configurationSubscription.unsubscribe = jest.fn(); + + component.unsubscribe(); + + expect(component.configurationSubscription.unsubscribe).toHaveBeenCalled(); + }); + + it('should do nothing if not exists', () => { + component.apiRootSubscription = undefined; + const unsubscribeSpy: jest.SpyInstance = jest.spyOn(Subscription.prototype, 'unsubscribe'); + + component.unsubscribe(); - notExistsAsHtmlElement(fixture, postfachNavigationSelector); + expect(unsubscribeSpy).not.toHaveBeenCalled(); + unsubscribeSpy.mockRestore(); + }); }); + }); + }); - it('should show benutzer & rollen if benutzerRollen feature toggle is true', () => { - component.environment.features.benutzerRollen = true; - fixture.detectChanges(); + describe('template', () => { + it('show not show header if apiRoot is not loaded', () => { + component.apiRootStateResource$ = of(createEmptyStateResource<ApiRootResource>()); - existsAsHtmlElement(fixture, usersRolesNavigationSelector); + notExistsAsHtmlElement(fixture, adminHeaderSelector); + }); + + describe('user profile button', () => { + beforeEach(() => { + component.apiRootStateResource$ = of(createStateResource(createApiRootResource())); }); - it('should not show benutzer & rollen if benutzerRollen feature toggle is false', () => { - component.environment.features.benutzerRollen = false; + it('should show if apiRoot exists', () => { fixture.detectChanges(); - notExistsAsHtmlElement(fixture, usersRolesNavigationSelector); + existsAsHtmlElement(fixture, userProfileButtonSelector); }); + }); - it('should show organisationsEinheiten if link in apiRoot exists', () => { - component.apiRootStateResource$ = of( - createStateResource(createApiRootResource([ApiRootLinkRel.ORGANISATIONS_EINHEIT, ApiRootLinkRel.CONFIGURATION])), - ); - fixture.detectChanges(); + describe('navigation', () => { + describe('user and roles', () => { + it('should show if users link is present', () => { + component.apiRootStateResource$ = of( + createStateResource(createApiRootResource([ApiRootLinkRel.CONFIGURATION, ApiRootLinkRel.USERS])), + ); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, usersRolesNavigationSelector); + }); - existsAsHtmlElement(fixture, organisationsEinheitenNavigationSelector); + it('should hide if link is missing', () => { + component.apiRootStateResource$ = of(createStateResource(createApiRootResource([ApiRootLinkRel.CONFIGURATION]))); + + fixture.detectChanges(); + + notExistsAsHtmlElement(fixture, usersRolesNavigationSelector); + }); }); - it('should not show organisationsEinheiten if link in apiRoot does not exist', () => { - fixture.detectChanges(); + describe('organisationEinheiten', () => { + it('should show if link in apiRoot exists', () => { + component.apiRootStateResource$ = of( + createStateResource(createApiRootResource([ApiRootLinkRel.ORGANISATIONS_EINHEIT, ApiRootLinkRel.CONFIGURATION])), + ); + fixture.detectChanges(); + + existsAsHtmlElement(fixture, organisationsEinheitenNavigationSelector); + }); + + it('should hide if link in apiRoot does not exist', () => { + fixture.detectChanges(); + + notExistsAsHtmlElement(fixture, organisationsEinheitenNavigationSelector); + }); + }); + + describe('menu', () => { + it('should show if link exists', () => { + component.apiRootStateResource$ = of(createStateResource(createApiRootResource([ApiRootLinkRel.CONFIGURATION]))); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, menuContainer); + }); + + it('should hide if link is missing', () => { + component.apiRootStateResource$ = of(createStateResource(createApiRootResource())); + + fixture.detectChanges(); - notExistsAsHtmlElement(fixture, organisationsEinheitenNavigationSelector); + notExistsAsHtmlElement(fixture, menuContainer); + }); }); }); diff --git a/alfa-client/apps/admin/src/app/app.component.ts b/alfa-client/apps/admin/src/app/app.component.ts index 11944356c6944e3fcd7faf4a6e1cbb030ddc90a0..c9d3c4f536cf6cee9f74f43f206bf02bdc01f397 100644 --- a/alfa-client/apps/admin/src/app/app.component.ts +++ b/alfa-client/apps/admin/src/app/app.component.ts @@ -21,23 +21,25 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { MenuContainerComponent } from '@admin-client/configuration'; +import { ConfigurationLinkRel, ConfigurationResource, ConfigurationService } from '@admin-client/configuration-shared'; +import { ROUTES } from '@admin-client/shared'; import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; import { BuildInfoComponent } from '@alfa-client/common'; -import { Environment, getEnvironmentFactory } from '@alfa-client/environment-shared'; -import { StateResource, TechSharedModule } from '@alfa-client/tech-shared'; +import { isLoaded, isNotUndefined, mapToResource, StateResource, TechSharedModule } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Params, Router, RouterOutlet } from '@angular/router'; +import { Component, inject, OnInit } from '@angular/core'; +import { Router, RouterLink, RouterOutlet } from '@angular/router'; +import { AuthenticationService } from '@authentication'; +import { hasLink } from '@ngxp/rest'; import { AdminLogoIconComponent, - MailboxIconComponent, NavbarComponent, NavItemComponent, OrgaUnitIconComponent, UsersIconComponent, } from '@ods/system'; -import { AuthenticationService } from 'libs/authentication/src/lib/authentication.service'; -import { Observable } from 'rxjs'; +import { filter, Observable, Subscription } from 'rxjs'; import { UserProfileButtonContainerComponent } from '../common/user-profile-button-container/user-profile.button-container.component'; import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component'; @@ -53,28 +55,29 @@ import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/ UsersIconComponent, UserProfileButtonContainerComponent, AdminLogoIconComponent, - MailboxIconComponent, OrgaUnitIconComponent, RouterOutlet, UnavailablePageComponent, BuildInfoComponent, TechSharedModule, + MenuContainerComponent, + RouterLink, ], }) export class AppComponent implements OnInit { - readonly title = 'admin'; + readonly title: string = 'admin'; + + private readonly authenticationService = inject(AuthenticationService); + private readonly apiRootService = inject(ApiRootService); + private readonly router = inject(Router); + private readonly configurationService = inject(ConfigurationService); public apiRootStateResource$: Observable<StateResource<ApiRootResource>>; - public readonly environment: Environment = getEnvironmentFactory(); - public readonly apiRootLinkRel = ApiRootLinkRel; + apiRootSubscription: Subscription; + configurationSubscription: Subscription; - constructor( - public authenticationService: AuthenticationService, - private apiRootService: ApiRootService, - private router: Router, - private route: ActivatedRoute, - ) {} + public readonly apiRootLinkRel = ApiRootLinkRel; ngOnInit(): void { this.authenticationService.login().then(() => this.doAfterLoggedIn()); @@ -82,16 +85,56 @@ export class AppComponent implements OnInit { doAfterLoggedIn(): void { this.apiRootStateResource$ = this.apiRootService.getApiRoot(); - this.forwardWithoutAuthenticationParams(); + this.evaluateInitialNavigation(); + } + + evaluateInitialNavigation(): void { + this.apiRootSubscription = this.apiRootStateResource$ + .pipe(filter(isLoaded), mapToResource<ApiRootResource>()) + .subscribe((apiRootResource: ApiRootResource) => this.evaluateNavigationByApiRoot(apiRootResource)); + } + + evaluateNavigationByApiRoot(apiRootResource: ApiRootResource): void { + if (hasLink(apiRootResource, ApiRootLinkRel.CONFIGURATION)) { + this.evaluateNavigationByConfiguration(); + } else { + this.navigateByApiRoot(apiRootResource); + } + } + + evaluateNavigationByConfiguration(): void { + this.configurationSubscription = this.configurationService + .get() + .pipe(filter(isLoaded), mapToResource<ApiRootResource>()) + .subscribe((configurationResource: ConfigurationResource) => this.navigateByConfiguration(configurationResource)); + } + + navigateByConfiguration(configurationResource: ConfigurationResource): void { + if (hasLink(configurationResource, ConfigurationLinkRel.SETTING)) { + this.navigate(ROUTES.POSTFACH); + } else if (hasLink(configurationResource, ConfigurationLinkRel.AGGREGATION_MAPPINGS)) { + this.navigate(ROUTES.STATISTIK); + } else { + this.navigate(ROUTES.UNAVAILABLE); + } + this.unsubscribe(); + } + + navigateByApiRoot(apiRootResource: ApiRootResource): void { + if (hasLink(apiRootResource, ApiRootLinkRel.USERS)) { + this.navigate(ROUTES.BENUTZER_UND_ROLLEN); + } else { + this.navigate(ROUTES.UNAVAILABLE); + } + this.unsubscribe(); } - forwardWithoutAuthenticationParams() { - const queryParams = this.getQueryParamsWithoutAuthentication(); - this.router.navigate([], { queryParams }); + private navigate(routePath: string): void { + this.router.navigate(['/' + routePath]); } - private getQueryParamsWithoutAuthentication(): Params { - const { iss, state, session_state, code, ...queryParams } = this.route.snapshot.queryParams; - return queryParams; + unsubscribe(): void { + if (isNotUndefined(this.apiRootSubscription)) this.apiRootSubscription.unsubscribe(); + if (isNotUndefined(this.configurationSubscription)) this.configurationSubscription.unsubscribe(); } } diff --git a/alfa-client/apps/admin/src/app/app.guard.spec.ts b/alfa-client/apps/admin/src/app/app.guard.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..24e0a21eca4fb630c66112e19ba4b0024bf983a9 --- /dev/null +++ b/alfa-client/apps/admin/src/app/app.guard.spec.ts @@ -0,0 +1,230 @@ +/* + * 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 { ConfigurationResource, ConfigurationService } from '@admin-client/configuration-shared'; +import { ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; +import { createStateResource, LinkRelationName, StateResource } from '@alfa-client/tech-shared'; +import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +import { TestBed } from '@angular/core/testing'; +import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router'; +import { Resource } from '@ngxp/rest'; +import { createConfigurationResource } from 'libs/admin/configuration-shared/test/configuration'; +import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; +import { DummyLinkRel } from 'libs/tech-shared/test/dummy'; +import { createDummyResource } from 'libs/tech-shared/test/resource'; +import { Observable, of } from 'rxjs'; +import { GuardData } from './app.routes'; + +import * as Guard from './app.guard'; + +describe('Guard', () => { + const linkRelName: LinkRelationName = DummyLinkRel.DUMMY; + const route: ActivatedRouteSnapshot = <any>{ data: <GuardData>{ linkRelName } }; + + describe('api root guard', () => { + let apiRootService: Mock<ApiRootService>; + + const apiRootStateResource$: Observable<StateResource<ApiRootResource>> = of(createStateResource(createApiRootResource())); + + beforeEach(() => { + apiRootService = mock(ApiRootService); + + TestBed.configureTestingModule({ + providers: [ + { + provide: ApiRootService, + useValue: apiRootService, + }, + ], + }); + }); + + beforeEach(() => { + apiRootService.getApiRoot.mockReturnValue(apiRootStateResource$); + jest.spyOn(Guard, 'evaluate').mockReturnValue(of(true)); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should call apiRootService', () => { + runGuard().subscribe(); + + expect(apiRootService.getApiRoot).toHaveBeenCalled(); + }); + + it('should call evaluate', () => { + const evaluateSpy: jest.SpyInstance = jest.spyOn(Guard, 'evaluate').mockReturnValue(of(true)); + + runGuard().subscribe(); + + expect(evaluateSpy).toHaveBeenCalledWith(apiRootStateResource$, DummyLinkRel.DUMMY); + }); + + function runGuard(): Observable<boolean | UrlTree> { + return <Observable<boolean | UrlTree>>TestBed.runInInjectionContext(() => Guard.apiRootGuard(route, null)); + } + }); + + describe('configuration guard', () => { + let configurationService: Mock<ConfigurationService>; + + const configurationStateResource$: Observable<StateResource<ConfigurationResource>> = of( + createStateResource(createConfigurationResource()), + ); + + beforeEach(() => { + configurationService = mock(ConfigurationService); + + TestBed.configureTestingModule({ + providers: [ + { + provide: ConfigurationService, + useValue: configurationService, + }, + ], + }); + }); + + beforeEach(() => { + configurationService.get.mockReturnValue(configurationStateResource$); + jest.spyOn(Guard, 'evaluate').mockReturnValue(of(true)); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should call configurationService', () => { + runGuard().subscribe(); + + expect(configurationService.get).toHaveBeenCalled(); + }); + + it('should call evaluate', () => { + const evaluateSpy: jest.SpyInstance = jest.spyOn(Guard, 'evaluate').mockReturnValue(of(true)); + + runGuard().subscribe(); + + expect(evaluateSpy).toHaveBeenCalledWith(configurationStateResource$, DummyLinkRel.DUMMY); + }); + + function runGuard(): Observable<boolean | UrlTree> { + return <Observable<boolean | UrlTree>>TestBed.runInInjectionContext(() => Guard.configurationGuard(route, null)); + } + }); + + describe('evaluate', () => { + const resource: Resource = createDummyResource([DummyLinkRel.DUMMY]); + const stateResource$: Observable<StateResource<Resource>> = of(createStateResource(resource)); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should evaluate resource', () => { + const urlTreeMock: Mock<UrlTree> = mock(UrlTree); + const evaluateResourceSpy: jest.SpyInstance = jest + .spyOn(Guard, 'evaluateResource') + .mockReturnValue(useFromMock(urlTreeMock)); + + evaluate().subscribe(); + + expect(evaluateResourceSpy).toHaveBeenCalledWith(resource, DummyLinkRel.DUMMY); + }); + + it('should return evaluated boolean', (done) => { + jest.spyOn(Guard, 'evaluateResource').mockReturnValue(true); + + evaluate().subscribe((resolvedValue) => { + expect(resolvedValue).toEqual(true); + done(); + }); + }); + + it('should return evaluated url tree', (done) => { + const urlTreeMock: Mock<UrlTree> = mock(UrlTree); + jest.spyOn(Guard, 'evaluateResource').mockReturnValue(useFromMock(urlTreeMock)); + + evaluate().subscribe((resolvedValue) => { + expect(resolvedValue).toEqual(urlTreeMock); + done(); + }); + }); + + function evaluate(): Observable<boolean | UrlTree> { + return <Observable<boolean | UrlTree>>( + TestBed.runInInjectionContext(() => Guard.evaluate(stateResource$, DummyLinkRel.DUMMY)) + ); + } + }); + + describe('evaluate resource', () => { + let router: Mock<Router>; + + beforeEach(() => { + router = mock(Router); + + TestBed.configureTestingModule({ + providers: [ + { + provide: Router, + useValue: router, + }, + ], + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should return true if link exists', () => { + const result: boolean = <boolean>evaluateResource(createDummyResource([DummyLinkRel.DUMMY])); + + expect(result).toBeTruthy(); + }); + + describe('if link not exists', () => { + it('should call router', () => { + evaluateResource(createDummyResource()); + + expect(router.createUrlTree).toHaveBeenCalledWith(['/unavailable']); + }); + + it('should return redirect route', () => { + const urlTree: Mock<UrlTree> = mock(UrlTree); + router.createUrlTree.mockReturnValue(urlTree); + + const result: UrlTree = <UrlTree>evaluateResource(createDummyResource()); + + expect(result).toEqual(urlTree); + }); + }); + + function evaluateResource(dummyResource: Resource): boolean | UrlTree { + return <boolean | UrlTree>TestBed.runInInjectionContext(() => Guard.evaluateResource(dummyResource, DummyLinkRel.DUMMY)); + } + }); +}); diff --git a/alfa-client/apps/admin/src/app/app.guard.ts b/alfa-client/apps/admin/src/app/app.guard.ts new file mode 100644 index 0000000000000000000000000000000000000000..85625be095c256aa593faa2d0d415352258aa1e0 --- /dev/null +++ b/alfa-client/apps/admin/src/app/app.guard.ts @@ -0,0 +1,67 @@ +/* + * 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 { ROUTES } from '@admin-client/shared'; +import { ApiRootService } from '@alfa-client/api-root-shared'; +import { LinkRelationName, mapToResource, StateResource } from '@alfa-client/tech-shared'; +import { inject } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivateFn, Router, UrlTree } from '@angular/router'; +import { hasLink, Resource } from '@ngxp/rest'; +import { filter, map, Observable } from 'rxjs'; +import { GuardData } from './app.routes'; +import { ConfigurationService } from '@admin-client/configuration-shared'; + +import * as Guard from './app.guard'; + +export const apiRootGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => { + const apiRootService: ApiRootService = inject(ApiRootService); + return Guard.evaluate(apiRootService.getApiRoot(), getLinkRelationName(route)); +}; + +export const configurationGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => { + const configurationService: ConfigurationService = inject(ConfigurationService); + return Guard.evaluate(configurationService.get(), getLinkRelationName(route)); +}; + +export function evaluate( + stateResource$: Observable<StateResource<Resource>>, + linkRelationName: LinkRelationName, +): Observable<boolean | UrlTree> { + return stateResource$.pipe( + filter((stateResource: StateResource<Resource>) => stateResource.loaded), + mapToResource<Resource>(), + map((resource: Resource) => Guard.evaluateResource(resource, linkRelationName)), + ); +} + +function getLinkRelationName(route: ActivatedRouteSnapshot): string { + return (<GuardData>route.data).linkRelName; +} + +export function evaluateResource(resource: Resource, linkRelationName: LinkRelationName): boolean | UrlTree { + return hasLink(resource, linkRelationName) ? true : redirectToUnavailable(); +} + +function redirectToUnavailable(): UrlTree { + return inject(Router).createUrlTree(['/' + 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 0886fc15cf87a9708f8939ce22e603b1bf312cf1..a7ed852a6132c902566603a81021da5d46f2357f 100644 --- a/alfa-client/apps/admin/src/app/app.routes.ts +++ b/alfa-client/apps/admin/src/app/app.routes.ts @@ -21,34 +21,44 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { ConfigurationLinkRel } from '@admin-client/configuration-shared'; import { ROUTES } from '@admin-client/shared'; +import { ApiRootLinkRel } from '@alfa-client/api-root-shared'; import { Route } from '@angular/router'; import { OrganisationsEinheitFormPageComponent } from '../pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component'; import { OrganisationsEinheitPageComponent } from '../pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component'; import { PostfachPageComponent } from '../pages/postfach/postfach-page/postfach-page.component'; +import { StatistikPageComponent } from '../pages/statistik/statistik-page/statistik-page.component'; +import { UnavailablePageComponent } from '../pages/unavailable/unavailable-page/unavailable-page.component'; import { UserAddPageComponent } from '../pages/users-roles/user-add-page/user-add-page.component'; import { UserRolesPageComponent } from '../pages/users-roles/user-roles-page/user-roles-page.component'; +import { apiRootGuard, configurationGuard } from './app.guard'; + +export interface GuardData { + linkRelName: string; +} export const appRoutes: Route[] = [ - { - path: '', - redirectTo: ROUTES.POSTFACH, - pathMatch: 'full', - }, { path: ROUTES.POSTFACH, component: PostfachPageComponent, title: 'Admin | Postfach', + canActivate: [configurationGuard], + data: <GuardData>{ linkRelName: ConfigurationLinkRel.SETTING }, }, { path: ROUTES.BENUTZER_UND_ROLLEN, component: UserRolesPageComponent, title: 'Admin | Benutzer & Rollen', + canActivate: [apiRootGuard], + data: <GuardData>{ linkRelName: ApiRootLinkRel.USERS }, }, { path: ROUTES.BENUTZER_UND_ROLLEN_NEU, component: UserAddPageComponent, title: 'Admin | Benutzer anlegen', + canActivate: [apiRootGuard], + data: <GuardData>{ linkRelName: ApiRootLinkRel.USERS }, }, { path: ROUTES.ORGANISATIONSEINHEITEN, @@ -60,4 +70,16 @@ export const appRoutes: Route[] = [ component: OrganisationsEinheitFormPageComponent, title: 'Admin | Organisationseinheit', }, + { + path: ROUTES.UNAVAILABLE, + component: UnavailablePageComponent, + title: 'Unavailable', + }, + { + path: ROUTES.STATISTIK, + component: StatistikPageComponent, + title: 'Admin | Statistik', + canActivate: [configurationGuard], + data: <GuardData>{ linkRelName: ConfigurationLinkRel.AGGREGATION_MAPPINGS }, + }, ]; diff --git a/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.spec.ts b/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.spec.ts index 4593934bfd05732c53156c7d324a877c4de3a6f5..b40a661442d6728f6c7819a0548e556d615197a8 100644 --- a/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.spec.ts +++ b/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile-button-container.component.spec.ts @@ -25,7 +25,7 @@ import { dispatchEventFromFixture, getElementFromFixture, mock, Mock } from '@al import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { DropdownMenuButtonItemComponent, DropdownMenuComponent, LogoutIconComponent } from '@ods/system'; -import { AuthenticationService } from 'authentication'; +import { AuthenticationService } from '@authentication'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { MockComponent } from 'ng-mocks'; import { UserProfileButtonContainerComponent } from './user-profile.button-container.component'; diff --git a/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile.button-container.component.ts b/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile.button-container.component.ts index 439d995011c6250518b0b57bd70629bfd1e08055..f2f4bd8351b8eb6be525ce81a68c8aec3c081a6e 100644 --- a/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile.button-container.component.ts +++ b/alfa-client/apps/admin/src/common/user-profile-button-container/user-profile.button-container.component.ts @@ -23,7 +23,7 @@ */ import { Component, OnInit } from '@angular/core'; import { DropdownMenuButtonItemComponent, DropdownMenuComponent, LogoutIconComponent } from '@ods/system'; -import { AuthenticationService } from 'authentication'; +import { AuthenticationService } from '@authentication'; @Component({ selector: 'user-profile-button-container', diff --git a/alfa-client/apps/admin/src/main.ts b/alfa-client/apps/admin/src/main.ts index 6d84faade1f163c40b471a12d75778e7aba687ca..a11d1ca1749fda041fdb06e60534a92fa4da183a 100644 --- a/alfa-client/apps/admin/src/main.ts +++ b/alfa-client/apps/admin/src/main.ts @@ -38,7 +38,7 @@ import { StoreRouterConnectingModule } from '@ngrx/router-store'; import { StoreModule } from '@ngrx/store'; import { StoreDevtoolsModule } from '@ngrx/store-devtools'; import { OAuthModule } from 'angular-oauth2-oidc'; -import { HttpUnauthorizedInterceptor } from 'authentication'; +import { HttpUnauthorizedInterceptor } from '@authentication'; import { ConfigurationsProviders } from 'libs/admin/configuration-shared/src/lib/configuration.providers'; import { OrganisationEinheitProviders } from 'libs/admin/organisations-einheit-shared/src/lib/organisations-einheit.providers'; import { PostfachProviders } from 'libs/admin/postfach-shared/src/lib/postfach.providers'; diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.html b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.html index cb9bf3ffd04a1caacde740c71fc98b9ead6a057e..676e114ce2927e1c3fec27e3f602d572b1c9f978 100644 --- a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.html +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.html @@ -23,8 +23,4 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<ng-container *ngIf="(apiRootStateResource$ | async)?.resource as apiRoot"> - @if (apiRoot | hasLink: apiRootLinkRel.ORGANISATIONS_EINHEIT) { - <admin-organisations-einheit-form-container data-test-id="organisations-einheit-form" /> - } -</ng-container> \ No newline at end of file +<admin-organisations-einheit-form-container data-test-id="organisations-einheit-form" /> \ No newline at end of file diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.spec.ts b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.spec.ts index 081e864d889b4390e5951d5ae3a757b687cde6ab..4777b881b3bbf511f7effed9cfb8d48bf355fbe7 100644 --- a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.spec.ts +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.spec.ts @@ -22,33 +22,18 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { OrganisationsEinheitFormContainerComponent } from '@admin-client/organisations-einheit'; -import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; -import { createStateResource, StateResource, TechSharedModule } from '@alfa-client/tech-shared'; -import { existsAsHtmlElement, mock, Mock, notExistsAsHtmlElement } from '@alfa-client/test-utils'; -import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; -import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; -import { singleColdCompleted } from 'libs/tech-shared/test/marbles'; import { MockComponent } from 'ng-mocks'; -import { Observable, of } from 'rxjs'; import { OrganisationsEinheitFormPageComponent } from './organisationseinheit-form-page.component'; describe('OrganisationsEinheitFormPageComponent', () => { let component: OrganisationsEinheitFormPageComponent; let fixture: ComponentFixture<OrganisationsEinheitFormPageComponent>; - const organisationsEinheitFormSelector: string = getDataTestIdOf('organisations-einheit-form'); - - let apiRootService: Mock<ApiRootService>; - beforeEach(async () => { - apiRootService = mock(ApiRootService); - await TestBed.configureTestingModule({ - imports: [CommonModule, TechSharedModule], + imports: [], declarations: [OrganisationsEinheitFormPageComponent, MockComponent(OrganisationsEinheitFormContainerComponent)], - providers: [{ provide: ApiRootService, useValue: apiRootService }], }).compileComponents(); }); @@ -62,45 +47,4 @@ describe('OrganisationsEinheitFormPageComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - describe('component', () => { - describe('ngOnInit', () => { - const apiRootStateResource: StateResource<ApiRootResource> = createStateResource(createApiRootResource()); - const apiRootStateResource$: Observable<StateResource<ApiRootResource>> = of(apiRootStateResource); - - beforeEach(() => { - apiRootService.getApiRoot.mockReturnValue(apiRootStateResource$); - }); - - it('should call apiRootService getApiRoot', () => { - component.ngOnInit(); - - expect(apiRootService.getApiRoot).toHaveBeenCalled(); - }); - - it('should get apiRootStateResource$', () => { - component.ngOnInit(); - - expect(component.apiRootStateResource$).toBeObservable(singleColdCompleted(apiRootStateResource)); - }); - }); - }); - - describe('template', () => { - describe('admin-organisationseinheit-form-container', () => { - it('should be rendered if apiRootState has link', () => { - component.apiRootStateResource$ = of(createStateResource(createApiRootResource([ApiRootLinkRel.ORGANISATIONS_EINHEIT]))); - fixture.detectChanges(); - - existsAsHtmlElement(fixture, organisationsEinheitFormSelector); - }); - - it('should not be rendered if apiRootState has no link', () => { - component.apiRootStateResource$ = of(createStateResource(createApiRootResource())); - fixture.detectChanges(); - - notExistsAsHtmlElement(fixture, organisationsEinheitFormSelector); - }); - }); - }); }); diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.ts b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.ts index d92144b1e58138e50682790bfdede8198bb1ebbb..109509a3721d88b943d87b35ab75c76be7cd202c 100644 --- a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.ts +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-form-page/organisationseinheit-form-page.component.ts @@ -22,27 +22,12 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { OrganisationsEinheitFormContainerComponent } from '@admin-client/organisations-einheit'; -import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; -import { createEmptyStateResource, StateResource } from '@alfa-client/tech-shared'; -import { UiModule } from '@alfa-client/ui'; -import { CommonModule } from '@angular/common'; -import { Component, inject, OnInit } from '@angular/core'; -import { Observable, of } from 'rxjs'; +import { Component } from '@angular/core'; @Component({ selector: 'organisationseinheit-form-page', templateUrl: './organisationseinheit-form-page.component.html', standalone: true, - imports: [CommonModule, UiModule, OrganisationsEinheitFormContainerComponent], + imports: [OrganisationsEinheitFormContainerComponent], }) -export class OrganisationsEinheitFormPageComponent implements OnInit { - private apiRootService = inject(ApiRootService); - - public apiRootStateResource$: Observable<StateResource<ApiRootResource>> = of(createEmptyStateResource<ApiRootResource>()); - - public readonly apiRootLinkRel = ApiRootLinkRel; - - ngOnInit(): void { - this.apiRootStateResource$ = this.apiRootService.getApiRoot(); - } -} +export class OrganisationsEinheitFormPageComponent {} diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.html b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.html index f1c8abdc407045cf77eb1b673b22fe82c31ff1c7..33d2af56ffabe4eb1148b6b1f4feaf6f8a8c06e3 100644 --- a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.html +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.html @@ -23,8 +23,4 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<ng-container *ngIf="(apiRootStateResource$ | async)?.resource as apiRoot"> - @if (apiRoot | hasLink: apiRootLinkRel.ORGANISATIONS_EINHEIT) { - <admin-organisations-einheit-container data-test-id="organisations-einheit-container" /> - } -</ng-container> +<admin-organisations-einheit-container data-test-id="organisations-einheit-container" /> diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.spec.ts b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.spec.ts index d6b708a413afe74806a4e040ec5c5affc7490678..b106381fa75af3ab154e376ced46f3a288d7364b 100644 --- a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.spec.ts +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.spec.ts @@ -22,33 +22,18 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { OrganisationsEinheitContainerComponent } from '@admin-client/organisations-einheit'; -import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; -import { createStateResource, StateResource, TechSharedModule } from '@alfa-client/tech-shared'; -import { existsAsHtmlElement, mock, Mock, notExistsAsHtmlElement } from '@alfa-client/test-utils'; -import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; -import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; -import { singleColdCompleted } from 'libs/tech-shared/test/marbles'; import { MockComponent } from 'ng-mocks'; -import { Observable, of } from 'rxjs'; import { OrganisationsEinheitPageComponent } from './organisationseinheit-page.component'; describe('OrganisationsEinheitPageComponent', () => { let component: OrganisationsEinheitPageComponent; let fixture: ComponentFixture<OrganisationsEinheitPageComponent>; - const organisationsEinheitContainerSelector: string = getDataTestIdOf('organisations-einheit-container'); - - let apiRootService: Mock<ApiRootService>; - beforeEach(async () => { - apiRootService = mock(ApiRootService); - await TestBed.configureTestingModule({ - imports: [CommonModule, TechSharedModule], + imports: [], declarations: [OrganisationsEinheitPageComponent, MockComponent(OrganisationsEinheitContainerComponent)], - providers: [{ provide: ApiRootService, useValue: apiRootService }], }).compileComponents(); }); @@ -62,45 +47,4 @@ describe('OrganisationsEinheitPageComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - describe('component', () => { - describe('ngOnInit', () => { - const apiRootStateResource: StateResource<ApiRootResource> = createStateResource(createApiRootResource()); - const apiRootStateResource$: Observable<StateResource<ApiRootResource>> = of(apiRootStateResource); - - beforeEach(() => { - apiRootService.getApiRoot.mockReturnValue(apiRootStateResource$); - }); - - it('should call apiRootService getApiRoot', () => { - component.ngOnInit(); - - expect(apiRootService.getApiRoot).toHaveBeenCalled(); - }); - - it('should get apiRootStateResource$', () => { - component.ngOnInit(); - - expect(component.apiRootStateResource$).toBeObservable(singleColdCompleted(apiRootStateResource)); - }); - }); - }); - - describe('template', () => { - describe('admin-organisationseinheit-container', () => { - it('should be rendered if apiRootState has link', () => { - component.apiRootStateResource$ = of(createStateResource(createApiRootResource([ApiRootLinkRel.ORGANISATIONS_EINHEIT]))); - fixture.detectChanges(); - - existsAsHtmlElement(fixture, organisationsEinheitContainerSelector); - }); - - it('should not be rendered if apiRootState has no link', () => { - component.apiRootStateResource$ = of(createStateResource(createApiRootResource())); - fixture.detectChanges(); - - notExistsAsHtmlElement(fixture, organisationsEinheitContainerSelector); - }); - }); - }); }); diff --git a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.ts b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.ts index 031470f244f8776ca9d4448a1e55c1464a8ae2fe..d2d9e1334032279c2fd0aa5b32ffc76a89944534 100644 --- a/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.ts +++ b/alfa-client/apps/admin/src/pages/organisationseinheit/organisationseinheit-page/organisationseinheit-page.component.ts @@ -22,27 +22,12 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { OrganisationsEinheitContainerComponent } from '@admin-client/organisations-einheit'; -import { ApiRootLinkRel, ApiRootResource, ApiRootService } from '@alfa-client/api-root-shared'; -import { createEmptyStateResource, StateResource } from '@alfa-client/tech-shared'; -import { UiModule } from '@alfa-client/ui'; -import { CommonModule } from '@angular/common'; -import { Component, inject, OnInit } from '@angular/core'; -import { Observable, of } from 'rxjs'; +import { Component } from '@angular/core'; @Component({ selector: 'organisationseinheit-page', templateUrl: './organisationseinheit-page.component.html', standalone: true, - imports: [CommonModule, OrganisationsEinheitContainerComponent, UiModule], + imports: [OrganisationsEinheitContainerComponent], }) -export class OrganisationsEinheitPageComponent implements OnInit { - private apiRootService = inject(ApiRootService); - - public apiRootStateResource$: Observable<StateResource<ApiRootResource>> = of(createEmptyStateResource<ApiRootResource>()); - - public readonly apiRootLinkRel = ApiRootLinkRel; - - ngOnInit(): void { - this.apiRootStateResource$ = this.apiRootService.getApiRoot(); - } -} +export class OrganisationsEinheitPageComponent {} diff --git a/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.html b/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.html index 323961ecd53f966827744fef12654efaa6444c92..165154d705f18b6e09896e98a9b6ab6f7f845690 100644 --- a/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.html +++ b/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.html @@ -23,6 +23,4 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -@if (environment.features.postfach) { - <admin-postfach-container data-test-id="postfach-container" /> -} +<admin-postfach-container data-test-id="postfach-container" /> diff --git a/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.spec.ts b/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.spec.ts index 1b1555bcacff75394b0aa8a882090913b38e84bf..b5e58a8b8dbe2a45c5ec280cdc3349cf98c9c217 100644 --- a/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.spec.ts +++ b/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.spec.ts @@ -22,25 +22,14 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { PostfachContainerComponent } from '@admin-client/postfach'; -import { getEnvironmentFactory } from '@alfa-client/environment-shared'; -import { existsAsHtmlElement, notExistsAsHtmlElement } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { createEnvironment } from 'libs/environment-shared/test/environment'; -import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { MockComponent } from 'ng-mocks'; import { PostfachPageComponent } from './postfach-page.component'; -jest.mock('@alfa-client/environment-shared'); - describe('PostfachPageComponent', () => { let component: PostfachPageComponent; let fixture: ComponentFixture<PostfachPageComponent>; - const postfachContainerSelector: string = getDataTestIdOf('postfach-container'); - - const environment = createEnvironment(); - (getEnvironmentFactory as jest.Mock).mockReturnValue(environment); - beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [PostfachPageComponent, MockComponent(PostfachContainerComponent)], @@ -56,22 +45,4 @@ describe('PostfachPageComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - describe('template', () => { - describe('admin-postfach-container', () => { - it('should be rendered if feature toggle postfach is true', () => { - environment.features.postfach = true; - fixture.detectChanges(); - - existsAsHtmlElement(fixture, postfachContainerSelector); - }); - - it('should not be rendered if feature toggle postfach is false', () => { - environment.features.postfach = false; - fixture.detectChanges(); - - notExistsAsHtmlElement(fixture, postfachContainerSelector); - }); - }); - }); }); diff --git a/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.ts b/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.ts index c77fed5ff6e0e78283a2be7b408bf36db27dbab2..5089c002559f37c3dd5c1b288753032a93c74509 100644 --- a/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.ts +++ b/alfa-client/apps/admin/src/pages/postfach/postfach-page/postfach-page.component.ts @@ -21,7 +21,6 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Environment, getEnvironmentFactory } from '@alfa-client/environment-shared'; import { PostfachContainerComponent } from '@admin-client/postfach'; import { Component } from '@angular/core'; @@ -31,6 +30,4 @@ import { Component } from '@angular/core'; standalone: true, imports: [PostfachContainerComponent], }) -export class PostfachPageComponent { - public readonly environment: Environment = getEnvironmentFactory(); -} +export class PostfachPageComponent {} diff --git a/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.html b/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.html new file mode 100644 index 0000000000000000000000000000000000000000..5213f512591359ed5e619f95d17e27b36dc4d3a7 --- /dev/null +++ b/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.html @@ -0,0 +1,26 @@ +<!-- + + 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. + +--> +<admin-statistik-container data-test-id="statistik-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/statistik/statistik-page/statistik-page.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2166339888793c63b712e4b4d67cd286002ac6b0 --- /dev/null +++ b/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.spec.ts @@ -0,0 +1,50 @@ +/* + * 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 { StatistikContainerComponent } from '@admin-client/statistik'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; +import { StatistikPageComponent } from './statistik-page.component'; + +describe('StatistikPageComponent', () => { + let component: StatistikPageComponent; + let fixture: ComponentFixture<StatistikPageComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [], + declarations: [StatistikPageComponent, MockComponent(StatistikContainerComponent)], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(StatistikPageComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.ts b/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1e74b105df36af783a691e857f71f8697c4c95b2 --- /dev/null +++ b/alfa-client/apps/admin/src/pages/statistik/statistik-page/statistik-page.component.ts @@ -0,0 +1,33 @@ +/* + * 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 { StatistikContainerComponent } from '@admin-client/statistik'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-statistik-page', + standalone: true, + imports: [StatistikContainerComponent], + templateUrl: './statistik-page.component.html', +}) +export class StatistikPageComponent {} diff --git a/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.html b/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.html index 51a20e77113f69a4974e2886c91ea147f47fc9de..e8bcd63d2f35daea2cb655013ad7a28de1679d77 100644 --- a/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.html +++ b/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.html @@ -23,6 +23,4 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -@if (environment.features.benutzerRollen) { - <admin-user-add-form data-test-id="user-add-form" /> -} +<admin-user-add-form data-test-id="user-add-form" /> diff --git a/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.spec.ts b/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.spec.ts index facb1f6d4f1b2493b43c881222f3bfde83e90890..6c13973fa0b634e253c29ee4354b48da911efc3b 100644 --- a/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.spec.ts +++ b/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.spec.ts @@ -22,25 +22,14 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { UserAddFormComponent } from '@admin-client/user'; -import { getEnvironmentFactory } from '@alfa-client/environment-shared'; -import { existsAsHtmlElement, notExistsAsHtmlElement } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { createEnvironment } from 'libs/environment-shared/test/environment'; -import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { MockComponent } from 'ng-mocks'; import { UserAddPageComponent } from './user-add-page.component'; -jest.mock('@alfa-client/environment-shared'); - describe('UserAddPageComponent', () => { let component: UserAddPageComponent; let fixture: ComponentFixture<UserAddPageComponent>; - const userAddFormSelector: string = getDataTestIdOf('user-add-form'); - - const environment = createEnvironment(); - (getEnvironmentFactory as jest.Mock).mockReturnValue(environment); - beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [UserAddPageComponent, MockComponent(UserAddFormComponent)], @@ -54,22 +43,4 @@ describe('UserAddPageComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - describe('template', () => { - describe('admin-user-add-form', () => { - it('should be rendered if feature toggle for benutzerRollen is true', () => { - environment.features.benutzerRollen = true; - fixture.detectChanges(); - - existsAsHtmlElement(fixture, userAddFormSelector); - }); - - it('should not be rendered if feature toggle for benutzerRollen is false', () => { - environment.features.benutzerRollen = false; - fixture.detectChanges(); - - notExistsAsHtmlElement(fixture, userAddFormSelector); - }); - }); - }); }); diff --git a/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.ts b/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.ts index 0e022f312b04c2f22d3432683d58d2f324261f94..0df47d3f99a92af7ba262020ec16b2ad9466a8e6 100644 --- a/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.ts +++ b/alfa-client/apps/admin/src/pages/users-roles/user-add-page/user-add-page.component.ts @@ -21,7 +21,6 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Environment, getEnvironmentFactory } from '@alfa-client/environment-shared'; import { UserAddFormComponent } from '@admin-client/user'; import { Component } from '@angular/core'; @@ -31,6 +30,4 @@ import { Component } from '@angular/core'; standalone: true, imports: [UserAddFormComponent], }) -export class UserAddPageComponent { - public readonly environment: Environment = getEnvironmentFactory(); -} +export class UserAddPageComponent {} diff --git a/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.html b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.html index 35bbbc0007a18979bf7904595c5910d09316cb3b..b4f2c21ecb76e734d0a91b08d61db3e332ab3acb 100644 --- a/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.html +++ b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.html @@ -23,6 +23,4 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -@if (environment.features.benutzerRollen) { - <admin-users-roles data-test-id="users-roles" /> -} +<admin-users-roles data-test-id="users-roles" /> diff --git a/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.spec.ts b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.spec.ts index 77b415071c8a7e4d5ce966bae33b30bd48d4f14c..dcff9ea1f54b16ed94d2bf2f37957d37749c34b7 100644 --- a/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.spec.ts +++ b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.spec.ts @@ -22,25 +22,14 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { UsersRolesComponent } from '@admin-client/user'; -import { getEnvironmentFactory } from '@alfa-client/environment-shared'; -import { existsAsHtmlElement, notExistsAsHtmlElement } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { createEnvironment } from 'libs/environment-shared/test/environment'; -import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { MockComponent } from 'ng-mocks'; import { UserRolesPageComponent } from './user-roles-page.component'; -jest.mock('@alfa-client/environment-shared'); - describe('UserRolesPageComponent', () => { let component: UserRolesPageComponent; let fixture: ComponentFixture<UserRolesPageComponent>; - const usersRolesSelector: string = getDataTestIdOf('users-roles'); - - const environment = createEnvironment(); - (getEnvironmentFactory as jest.Mock).mockReturnValue(environment); - beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [UserRolesPageComponent], @@ -55,22 +44,4 @@ describe('UserRolesPageComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); - - describe('template', () => { - describe('admin-users-roles', () => { - it('should be rendered if feature toggle benutzerRollen is true', () => { - environment.features.benutzerRollen = true; - fixture.detectChanges(); - - existsAsHtmlElement(fixture, usersRolesSelector); - }); - - it('should not be rendered component if feature toggle benutzerRollen is false', () => { - environment.features.benutzerRollen = false; - fixture.detectChanges(); - - notExistsAsHtmlElement(fixture, usersRolesSelector); - }); - }); - }); }); diff --git a/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.ts b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.ts index b19feb8a0d942320087afe30f32bfe5872fef661..2d2cf9efdf2de388854a19148634c734b5587297 100644 --- a/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.ts +++ b/alfa-client/apps/admin/src/pages/users-roles/user-roles-page/user-roles-page.component.ts @@ -21,7 +21,6 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Environment, getEnvironmentFactory } from '@alfa-client/environment-shared'; import { UsersRolesComponent } from '@admin-client/user'; import { Component } from '@angular/core'; @@ -31,6 +30,4 @@ import { Component } from '@angular/core'; standalone: true, imports: [UsersRolesComponent], }) -export class UserRolesPageComponent { - public readonly environment: Environment = getEnvironmentFactory(); -} +export class UserRolesPageComponent {} diff --git a/alfa-client/apps/admin/src/styles.scss b/alfa-client/apps/admin/src/styles.scss index a70d56fcf0868ff075899d9cc14eeee7d26be531..5bf38c604f5517809686de44624f416bbea468d1 100644 --- a/alfa-client/apps/admin/src/styles.scss +++ b/alfa-client/apps/admin/src/styles.scss @@ -29,8 +29,9 @@ @import 'libs/design-system/src/lib/tailwind-preset/root.css'; @import 'libs/ui/src/lib/font/font_material'; -@import 'variables'; @import 'typeface-roboto/index.css'; +@import '../../alfa/src/styles/abstracts/variables'; +@import '../../alfa/src/styles/material/snackbar'; @include mat.all-component-typographies(); @include mat.core(); diff --git a/alfa-client/apps/alfa-e2e/src/components/user-profile/current-user-profile.component.e2e.ts b/alfa-client/apps/alfa-e2e/src/components/user-profile/current-user-profile.component.e2e.ts index 202c5d3ef538a45931d9dc855e1ad99dba5610fd..93a38796fd04ecade67ecf9995a417a73ce687ba 100644 --- a/alfa-client/apps/alfa-e2e/src/components/user-profile/current-user-profile.component.e2e.ts +++ b/alfa-client/apps/alfa-e2e/src/components/user-profile/current-user-profile.component.e2e.ts @@ -40,7 +40,7 @@ export class CurrentUserProfileE2EComponent { public logout(): void { this.getUserIconButton().click(); - this.getLogoutButton().click(); + this.getLogoutButton().click({ force: true }); } public getUserIconButton() { diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/init-users.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/init-users.cy.ts index b5325689ff01c2b61199ae06ef1e07ebc483a5b1..0558e553cf1915d2e288b730be3b06d6adbf4a61 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/init-users.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/init-users.cy.ts @@ -68,6 +68,6 @@ describe('Init users', () => { waitForSpinnerToDisappear(); exist(header.getLogo()); header.getCurrentUserProfile().getUserIconButton().click(); - header.getCurrentUserProfile().getLogoutButton().click(); + header.getCurrentUserProfile().getLogoutButton().click({ force: true }); } }); diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-detailansicht/vorgang-detailansicht.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-detailansicht/vorgang-detailansicht.cy.ts index ac09ea4046a74327aaa0e0460b1cd89afd6af003..7fa1b271ad7f923a8757a96e1e73854837fe101c 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-detailansicht/vorgang-detailansicht.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-detailansicht/vorgang-detailansicht.cy.ts @@ -37,18 +37,13 @@ import { contains, exist, haveText, + haveTextWithoutChildren, shouldFirstContains, shouldHaveAttributeBeGreaterThan, shouldHaveAttributeBeLowerThan, } from '../../../support/cypress.util'; import { loginAsSabine } from '../../../support/user-util'; -import { - AntragstellerE2ETestData, - buildVorgang, - createVorgang, - initVorgaenge, - objectIds, -} from '../../../support/vorgang-util'; +import { AntragstellerE2ETestData, buildVorgang, createVorgang, initVorgaenge, objectIds } from '../../../support/vorgang-util'; registerLocaleData(localeDe, 'de', localeDeExtra); @@ -57,13 +52,10 @@ describe('Vorgang Detailansicht', () => { const vorgangList: VorgangListE2EComponent = mainPage.getVorgangList(); const vorgangPage: VorgangPage = new VorgangPage(); - const vorgangDatenFormular: VorgangFormularDatenE2EComponent = - vorgangPage.getFormularDatenContainer(); + const vorgangDatenFormular: VorgangFormularDatenE2EComponent = vorgangPage.getFormularDatenContainer(); const vorgangHeader: VorgangDetailHeaderE2EComponent = vorgangPage.getVorgangDetailHeader(); - const expansionPanelContainer: ExpansionPanelE2Eomponent = vorgangPage - .getFormularDatenContainer() - .getExpansionPanelContainer(); + const expansionPanelContainer: ExpansionPanelE2Eomponent = vorgangPage.getFormularDatenContainer().getExpansionPanelContainer(); const antragsteller: AntragstellerE2EComponent = vorgangPage.getAntragstellerContainer(); const vorgang: VorgangE2E = { ...createVorgang(), name: 'TestVorgang' }; @@ -97,16 +89,10 @@ describe('Vorgang Detailansicht', () => { it('should have header data', () => { haveText(vorgangHeader.getStatus(), vorgangStatusLabelE2E[vorgang.status]); - haveText( - vorgangHeader.getAktenzeichen(), - vorgang.aktenzeichen ? vorgang.aktenzeichen : NO_AKTENZEICHEN, - ); - haveText(vorgangHeader.getVorgangNummer(), vorgang.nummer); + haveTextWithoutChildren(vorgangHeader.getAktenzeichen(), vorgang.aktenzeichen ? vorgang.aktenzeichen : NO_AKTENZEICHEN); + haveTextWithoutChildren(vorgangHeader.getVorgangNummer(), vorgang.nummer); haveText(vorgangHeader.getName(), vorgang.name); - haveText( - vorgangHeader.getCreatedAt(), - formatDate(vorgang.createdAt.$date, 'EEEE, dd. MMMM y, HH:mm', 'de'), - ); + haveText(vorgangHeader.getCreatedAt(), formatDate(vorgang.createdAt.$date, 'EEEE, dd. MMMM y, HH:mm', 'de')); }); it('should have header data after reload', () => { @@ -114,16 +100,10 @@ describe('Vorgang Detailansicht', () => { exist(vorgangHeader.getRoot()); haveText(vorgangHeader.getStatus(), vorgangStatusLabelE2E[vorgang.status]); - haveText( - vorgangHeader.getAktenzeichen(), - vorgang.aktenzeichen ? vorgang.aktenzeichen : NO_AKTENZEICHEN, - ); - haveText(vorgangHeader.getVorgangNummer(), vorgang.nummer); + haveTextWithoutChildren(vorgangHeader.getAktenzeichen(), vorgang.aktenzeichen ? vorgang.aktenzeichen : NO_AKTENZEICHEN); + haveTextWithoutChildren(vorgangHeader.getVorgangNummer(), vorgang.nummer); haveText(vorgangHeader.getName(), vorgang.name); - haveText( - vorgangHeader.getCreatedAt(), - formatDate(vorgang.createdAt.$date, 'EEEE, dd. MMMM y, HH:mm', 'de'), - ); + haveText(vorgangHeader.getCreatedAt(), formatDate(vorgang.createdAt.$date, 'EEEE, dd. MMMM y, HH:mm', 'de')); }); }); @@ -136,10 +116,7 @@ describe('Vorgang Detailansicht', () => { }); it('should show "empfangendestelle"', () => { - contains( - vorgangDatenFormular.getRoot(), - vorgang.eingangs[0].formData.empfangendestelle.emailadresse, - ); + contains(vorgangDatenFormular.getRoot(), vorgang.eingangs[0].formData.empfangendestelle.emailadresse); }); }); @@ -174,21 +151,13 @@ describe('Vorgang Detailansicht', () => { }); it('should have Formulardaten panel open initial', () => { - shouldHaveAttributeBeGreaterThan( - expansionPanelContainer.getExpansionPanel(), - 'outerHeight', - 50, - ); + shouldHaveAttributeBeGreaterThan(expansionPanelContainer.getExpansionPanel(), 'outerHeight', 50); }); it('should close the panel', () => { expansionPanelContainer.getExpansionPanelTitle().first().click(); - shouldHaveAttributeBeLowerThan( - expansionPanelContainer.getExpansionPanel(), - 'outerHeight', - 50, - ); + shouldHaveAttributeBeLowerThan(expansionPanelContainer.getExpansionPanel(), 'outerHeight', 50); }); }); }); diff --git a/alfa-client/apps/alfa-e2e/src/support/angular.util.ts b/alfa-client/apps/alfa-e2e/src/support/angular.util.ts index 4f8427775eba70bc3c69837580ff307ecdc2fded..a8929a5a22b1fc8073e1717241b8e7141a20ee99 100644 --- a/alfa-client/apps/alfa-e2e/src/support/angular.util.ts +++ b/alfa-client/apps/alfa-e2e/src/support/angular.util.ts @@ -40,8 +40,7 @@ enum AngularElementE2E { export function hasTooltip(element: any, value: string) { mouseEnter(element); - element.get('mat-tooltip-component').contains(value); - // element.get(`div[title="${value}"]`); + element.get('ods-tooltip').contains(value); } export function isChecked(element: any) { diff --git a/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts b/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts index ce27ac7e46551eb1cebe36055894341e23ae6106..69e7bccdfc44a17993598e6c49a8e81bd066db33 100644 --- a/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts +++ b/alfa-client/apps/alfa-e2e/src/support/cypress.util.ts @@ -52,6 +52,16 @@ export function haveText(element: any, text: string): void { .should('equal', text); } +export function haveTextWithoutChildren(element: any, text: string): void { + element + .invoke('clone') + .then((element) => { + element.children().remove(); + return element.text().trim(); + }) + .should('equal', text); +} + export function haveValue(element: any, value: string): void { element.should('have.value', value); } @@ -93,11 +103,7 @@ export function shouldFirstContains(element: any, containing: string) { element.first().should('exist').contains(containing); } -export function shouldHaveAttributeBeGreaterThan( - element: any, - attributeName: string, - value: number, -) { +export function shouldHaveAttributeBeGreaterThan(element: any, attributeName: string, value: number) { element.first().should('exist').invoke(attributeName).should('be.gt', value); } @@ -122,11 +128,7 @@ export function enter(element: any): void { element.clear().type(CypressKeyboardActions.ENTER); } -export function enterWith( - element: Cypress.Chainable<JQuery<HTMLElement>>, - value: string, - delayBeforeEnter: number = 200, -): void { +export function enterWith(element: Cypress.Chainable<JQuery<HTMLElement>>, value: string, delayBeforeEnter: number = 200): void { element.clear().type(value); wait(delayBeforeEnter); element.type(CypressKeyboardActions.ENTER); diff --git a/alfa-client/apps/alfa/Jenkinsfile b/alfa-client/apps/alfa/Jenkinsfile index 0dde2edb6c4f78bb6a77e54d7607b327db26a115..1464cec669b290d51f5bd201ea89f264cf7f3974 100644 --- a/alfa-client/apps/alfa/Jenkinsfile +++ b/alfa-client/apps/alfa/Jenkinsfile @@ -78,7 +78,7 @@ pipeline { sh 'node --version' dir('alfa-client') { - sh 'pnpm install --frozen-lockfile --network-concurrency=8' + sh 'pnpm install --frozen-lockfile' if (isMainBranch()) { withSonarQubeEnv('sonarqube-ozg-sh'){ diff --git a/alfa-client/apps/alfa/src/styles/main.scss b/alfa-client/apps/alfa/src/styles/main.scss index 38f3732fb91b31d208be4e0704ac2084f6506e3a..748d65e101e890ac85ac7c6edcd9bffc19b7dde8 100644 --- a/alfa-client/apps/alfa/src/styles/main.scss +++ b/alfa-client/apps/alfa/src/styles/main.scss @@ -43,8 +43,8 @@ @import 'material/icons'; @import 'material/list'; @import 'material/menu'; -@import 'material/snackbar'; @import 'material/tabs'; +@import 'material/snackbar'; @import 'material/typography'; @import 'libs/navigation/src/lib/header-container/header/header.theme'; @import 'libs/ui/src/lib/ui/expansion-panel/expansion-panel.theme'; diff --git a/alfa-client/apps/info/Jenkinsfile b/alfa-client/apps/info/Jenkinsfile index dd670c32603c9b721c60ac02dcfa3c32388c0a0a..e8c582e1e0332dc4618e99a2d145e602bb6c7998 100644 --- a/alfa-client/apps/info/Jenkinsfile +++ b/alfa-client/apps/info/Jenkinsfile @@ -46,7 +46,7 @@ pipeline { FAILED_STAGE = env.STAGE_NAME dir('alfa-client') { withNPM(npmrcConfig: 'npm-nexus-auth') { - sh 'pnpm install --frozen-lockfile --network-concurrency=8' + sh 'pnpm install --frozen-lockfile' sh 'pnpm exec nx run info:test' sh 'pnpm exec nx run info:test -- --runInBand --codeCoverage --coverageReporters=lcov --testResultsProcessor=jest-sonar-reporter && pnpm exec sonar-scanner' diff --git a/alfa-client/libs/admin/configuration-shared/README.md b/alfa-client/libs/admin/configuration-shared/README.md index 6f5030b70bcee5425a71ad582b478c8007bb6169..1e9f46fa0c35148f61b66d5b1e5d5a37a2da822c 100644 --- a/alfa-client/libs/admin/configuration-shared/README.md +++ b/alfa-client/libs/admin/configuration-shared/README.md @@ -4,4 +4,4 @@ This library was generated with [Nx](https://nx.dev). ## Running unit tests -Run `nx test configuration` to execute the unit tests. +Run `nx test configuration-shared` to execute the unit tests. diff --git a/alfa-client/libs/admin/configuration-shared/jest.config.ts b/alfa-client/libs/admin/configuration-shared/jest.config.ts index 62ec3a0768ca632eface9c5965a666571b6f526c..9689743d8f6262d45b94cf069ac946a79101cd06 100644 --- a/alfa-client/libs/admin/configuration-shared/jest.config.ts +++ b/alfa-client/libs/admin/configuration-shared/jest.config.ts @@ -1,5 +1,5 @@ export default { - displayName: 'admin-configuration', + displayName: 'admin-configuration-shared', preset: '../../../jest.preset.js', setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], coverageDirectory: '../../../coverage/libs/admin/configuration-shared', diff --git a/alfa-client/libs/admin/configuration-shared/project.json b/alfa-client/libs/admin/configuration-shared/project.json index ce13ed7f5533688a16237678622655a7117a4c41..da6c4ac258f5dae5682333c952fd6dad6a98a1e6 100644 --- a/alfa-client/libs/admin/configuration-shared/project.json +++ b/alfa-client/libs/admin/configuration-shared/project.json @@ -1,5 +1,5 @@ { - "name": "admin-configuration", + "name": "admin-configuration-shared", "$schema": "../../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/admin/configuration-shared/src", "prefix": "admin", diff --git a/alfa-client/libs/admin/configuration-shared/src/lib/configuration.linkrel.ts b/alfa-client/libs/admin/configuration-shared/src/lib/configuration.linkrel.ts index 1468b2b4e4354bd245f83cc28dd6f6c2c5fb00f3..f38803351591db0d6ac7fb13c7bef7c0a9ce7a34 100644 --- a/alfa-client/libs/admin/configuration-shared/src/lib/configuration.linkrel.ts +++ b/alfa-client/libs/admin/configuration-shared/src/lib/configuration.linkrel.ts @@ -23,4 +23,5 @@ */ export enum ConfigurationLinkRel { SETTING = 'settings', + AGGREGATION_MAPPINGS = 'aggregationMappings', } diff --git a/alfa-client/libs/admin/configuration-shared/test/configuration.ts b/alfa-client/libs/admin/configuration-shared/test/configuration.ts index 9b188cb1c2bd4bb8fec2db9dd4ba0d5830f09f41..5552d59c4ef83855927de673d83aee92ecba0725 100644 --- a/alfa-client/libs/admin/configuration-shared/test/configuration.ts +++ b/alfa-client/libs/admin/configuration-shared/test/configuration.ts @@ -1,6 +1,6 @@ -import { ConfigurationLinkRel, ConfigurationResource } from '@admin-client/configuration-shared'; +import { ConfigurationResource } from '@admin-client/configuration-shared'; import { toResource } from '../../../tech-shared/test/resource'; -export function createConfigurationResource(): ConfigurationResource { - return toResource({}, [ConfigurationLinkRel.SETTING]); +export function createConfigurationResource(linkRels: string[] = []): ConfigurationResource { + return toResource({}, linkRels); } diff --git a/alfa-client/libs/admin/configuration/.eslintrc.json b/alfa-client/libs/admin/configuration/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..b10f9813a8f5c59432cf245301dc4d01a8031fd1 --- /dev/null +++ b/alfa-client/libs/admin/configuration/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "lib", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "lib", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/alfa-client/libs/admin/configuration/README.md b/alfa-client/libs/admin/configuration/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6f5030b70bcee5425a71ad582b478c8007bb6169 --- /dev/null +++ b/alfa-client/libs/admin/configuration/README.md @@ -0,0 +1,7 @@ +# configuration + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test configuration` to execute the unit tests. diff --git a/alfa-client/libs/admin/configuration/jest.config.ts b/alfa-client/libs/admin/configuration/jest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..7b8ab3df6e12af6c69e5a52778822c7fd33ec9e3 --- /dev/null +++ b/alfa-client/libs/admin/configuration/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'admin-configuration', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/admin/configuration', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '<rootDir>/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/alfa-client/libs/admin/configuration/project.json b/alfa-client/libs/admin/configuration/project.json new file mode 100644 index 0000000000000000000000000000000000000000..29871bddc31df4176f2e079ff268b880111f1b8f --- /dev/null +++ b/alfa-client/libs/admin/configuration/project.json @@ -0,0 +1,20 @@ +{ + "name": "admin-configuration", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/admin/configuration/src", + "prefix": "lib", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/admin/configuration/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/alfa-client/libs/admin/configuration/src/index.ts b/alfa-client/libs/admin/configuration/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..221b8b737643d3ef9facd8ae2f520efc97099c91 --- /dev/null +++ b/alfa-client/libs/admin/configuration/src/index.ts @@ -0,0 +1 @@ +export * from './lib/menu-container/menu-container.component'; diff --git a/alfa-client/libs/admin/configuration/src/lib/menu-container/menu-container.component.html b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu-container.component.html new file mode 100644 index 0000000000000000000000000000000000000000..fd97589d60cd2ca0c583ca7ae1a143f7bba98121 --- /dev/null +++ b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu-container.component.html @@ -0,0 +1 @@ +<admin-menu [configurationStateResource]="configurationStateResource$ | async"></admin-menu> \ No newline at end of file diff --git a/alfa-client/libs/admin/configuration/src/lib/menu-container/menu-container.component.spec.ts b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu-container.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..69d8eb3bd3bb34964306f755e3c9d00acb7f58c6 --- /dev/null +++ b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu-container.component.spec.ts @@ -0,0 +1,55 @@ +import { ConfigurationResource, ConfigurationService } from '@admin-client/configuration-shared'; +import { createStateResource, StateResource } from '@alfa-client/tech-shared'; +import { getMockComponent, mock, Mock } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { createConfigurationResource } from '../../../../configuration-shared/test/configuration'; +import { MenuContainerComponent } from './menu-container.component'; +import { MenuComponent } from './menu/menu.component'; + +describe('MenuContainerComponent', () => { + let component: MenuContainerComponent; + let fixture: ComponentFixture<MenuContainerComponent>; + + let configurationService: Mock<ConfigurationService>; + + const configurationStateResource: StateResource<ConfigurationResource> = createStateResource(createConfigurationResource()); + + beforeEach(async () => { + configurationService = { ...mock(ConfigurationService), get: jest.fn().mockReturnValue(of(configurationStateResource)) }; + + await TestBed.configureTestingModule({ + imports: [MenuContainerComponent], + declarations: [MockComponent(MenuComponent)], + providers: [ + { + provide: ConfigurationService, + useValue: configurationService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(MenuContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('ngOnInit', () => { + it('should call service', () => { + component.ngOnInit(); + + expect(configurationService.get).toHaveBeenCalled(); + }); + }); + + it('should call menu', () => { + const menu: MenuComponent = getMockComponent(fixture, MenuComponent); + + expect(menu.configurationStateResource).toBe(configurationStateResource); + }); +}); diff --git a/alfa-client/libs/admin/configuration/src/lib/menu-container/menu-container.component.ts b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu-container.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..7cb838c535b0a87efd570d6ee6a6d58024fd8d19 --- /dev/null +++ b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu-container.component.ts @@ -0,0 +1,22 @@ +import { ConfigurationResource, ConfigurationService } from '@admin-client/configuration-shared'; +import { StateResource } from '@alfa-client/tech-shared'; +import { CommonModule } from '@angular/common'; +import { Component, inject, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { MenuComponent } from './menu/menu.component'; + +@Component({ + selector: 'admin-menu-container', + standalone: true, + imports: [CommonModule, MenuComponent], + templateUrl: './menu-container.component.html', +}) +export class MenuContainerComponent implements OnInit { + private readonly configurationService = inject(ConfigurationService); + + public configurationStateResource$: Observable<StateResource<ConfigurationResource>>; + + ngOnInit(): void { + this.configurationStateResource$ = this.configurationService.get(); + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..0c1102a4c22c1603d4cc93f6baaaf5fcb2f94d40 --- /dev/null +++ b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.html @@ -0,0 +1,10 @@ +@if (configurationStateResource.resource | hasLink: configurationLinkRel.SETTING) { + <ods-nav-item data-test-id="postfach-navigation" caption="Postfach" path="/postfach"> + <ods-mailbox-icon icon /> + </ods-nav-item> +} +@if (configurationStateResource.resource | hasLink: configurationLinkRel.AGGREGATION_MAPPINGS) { + <ods-nav-item data-test-id="statistik-navigation" caption="Statistik" path="/statistik"> + <ods-statistic-icon icon /> + </ods-nav-item> +} diff --git a/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.spec.ts b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f417e5febf09fcf523cd7c2d01a273d65630e79 --- /dev/null +++ b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.spec.ts @@ -0,0 +1,71 @@ +import { ConfigurationLinkRel } from '@admin-client/configuration-shared'; +import { createEmptyStateResource, createStateResource, HasLinkPipe } from '@alfa-client/tech-shared'; +import { existsAsHtmlElement, notExistsAsHtmlElement } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MailboxIconComponent, NavItemComponent } from '@ods/system'; +import { createConfigurationResource } from 'libs/admin/configuration-shared/test/configuration'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { MockComponent } from 'ng-mocks'; +import { MenuComponent } from './menu.component'; + +describe('MenuComponent', () => { + let component: MenuComponent; + let fixture: ComponentFixture<MenuComponent>; + + const postfachNavigation: string = getDataTestIdOf('postfach-navigation'); + const statistikNavigationSelector: string = getDataTestIdOf('statistik-navigation'); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MenuComponent], + declarations: [HasLinkPipe, MockComponent(NavItemComponent), MockComponent(MailboxIconComponent)], + }).compileComponents(); + + fixture = TestBed.createComponent(MenuComponent); + component = fixture.componentInstance; + component.configurationStateResource = createEmptyStateResource(); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('postfach navigation', () => { + it('should show if settings link is present', () => { + component.configurationStateResource = createStateResource(createConfigurationResource([ConfigurationLinkRel.SETTING])); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, postfachNavigation); + }); + + it('should hide if settings link is missing', () => { + component.configurationStateResource = createStateResource(createConfigurationResource()); + + fixture.detectChanges(); + + notExistsAsHtmlElement(fixture, postfachNavigation); + }); + }); + + describe('statistic navigation', () => { + it('should show if settings link is present', () => { + component.configurationStateResource = createStateResource( + createConfigurationResource([ConfigurationLinkRel.AGGREGATION_MAPPINGS]), + ); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, statistikNavigationSelector); + }); + + it('should hide if settings link is missing', () => { + component.configurationStateResource = createStateResource(createConfigurationResource()); + + fixture.detectChanges(); + + notExistsAsHtmlElement(fixture, statistikNavigationSelector); + }); + }); +}); 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 new file mode 100644 index 0000000000000000000000000000000000000000..19cc60bab45d9e098ace89450e3c7efa50e96c30 --- /dev/null +++ b/alfa-client/libs/admin/configuration/src/lib/menu-container/menu/menu.component.ts @@ -0,0 +1,17 @@ +import { ConfigurationLinkRel, ConfigurationResource } from '@admin-client/configuration-shared'; +import { StateResource, TechSharedModule } from '@alfa-client/tech-shared'; +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { MailboxIconComponent, NavItemComponent, StatisticIconComponent } from '@ods/system'; + +@Component({ + selector: 'admin-menu', + standalone: true, + imports: [CommonModule, NavItemComponent, MailboxIconComponent, TechSharedModule, StatisticIconComponent], + templateUrl: './menu.component.html', +}) +export class MenuComponent { + @Input() configurationStateResource: StateResource<ConfigurationResource>; + + public readonly configurationLinkRel = ConfigurationLinkRel; +} diff --git a/alfa-client/libs/admin/configuration/src/test-setup.ts b/alfa-client/libs/admin/configuration/src/test-setup.ts new file mode 100644 index 0000000000000000000000000000000000000000..c408668266d2fec3a9803c0ec044bc163fb987fe --- /dev/null +++ b/alfa-client/libs/admin/configuration/src/test-setup.ts @@ -0,0 +1,12 @@ +import '@testing-library/jest-dom'; +import 'jest-preset-angular/setup-jest'; + +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +getTestBed().resetTestEnvironment(); +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { + teardown: { destroyAfterEach: false }, + errorOnUnknownProperties: true, + errorOnUnknownElements: true, +}); diff --git a/alfa-client/libs/admin/configuration/tsconfig.json b/alfa-client/libs/admin/configuration/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..8ca9ad312c2bd4dc364383853ddd91a2ed8f86fd --- /dev/null +++ b/alfa-client/libs/admin/configuration/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "target": "es2022" + } +} diff --git a/alfa-client/libs/admin/configuration/tsconfig.lib.json b/alfa-client/libs/admin/configuration/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..8441346f6e5858b2ef4235cb3c3160eda256f94a --- /dev/null +++ b/alfa-client/libs/admin/configuration/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": ["src/**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/alfa-client/libs/admin/configuration/tsconfig.spec.json b/alfa-client/libs/admin/configuration/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..723782fbd367969806c5992aea882773ab65af8b --- /dev/null +++ b/alfa-client/libs/admin/configuration/tsconfig.spec.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"], + "target": "ES2022", + "useDefineForClassFields": false + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] +} diff --git a/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.html b/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.html index b90eaca0ebe2582b649a5ddc44cdc3cc84f76e67..8dc8279de55dc9926954798e2eece9dbc68eee0c 100644 --- a/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.html +++ b/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.html @@ -48,7 +48,7 @@ <dd class="mt-1" data-test-id="organisations-einheit-sync-error"> <ods-exclamation-icon *ngIf="organisationsEinheitResource.syncResult === AdminOrganisationsEinheitSyncResult.NOT_FOUND_IN_PVOG" - matTooltip="Organisationseinheit wurde nicht in den PVOG-Daten gefunden." + tooltip="Organisationseinheit wurde nicht in den PVOG-Daten gefunden." size="small" /> <ods-exclamation-icon diff --git a/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.spec.ts b/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.spec.ts index f7ce733a741f7c9c8a2162bca39ece86ad04b76f..0d5d5364e04fbe0e99026fb099091d7f7d641495 100644 --- a/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.spec.ts +++ b/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.spec.ts @@ -31,16 +31,15 @@ import { } from '@alfa-client/test-utils'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { ActivatedRoute } from '@angular/router'; -import { ExclamationIconComponent, ListComponent, ListItemComponent } from '@ods/system'; +import { ExclamationIconComponent, ListComponent, ListItemComponent, TooltipDirective } from '@ods/system'; import { AdminOrganisationsEinheitResource, AdminOrganisationsEinheitSyncResult, } from 'libs/admin/organisations-einheit-shared/src/lib/organisations-einheit.model'; import { createAdminOrganisationsEinheitResource } from 'libs/admin/organisations-einheit-shared/src/test/organisations-einheit'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { OrganisationsEinheitListComponent } from './organisations-einheit-list.component'; describe('OrganisationsEinheitListComponent', () => { @@ -71,7 +70,7 @@ describe('OrganisationsEinheitListComponent', () => { MockComponent(ListComponent), MockComponent(ListItemComponent), MockComponent(ExclamationIconComponent), - MockModule(MatTooltipModule), + MockDirective(TooltipDirective), ], }).compileComponents(); diff --git a/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.ts b/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.ts index 826556819ed09bee47196d4f0667a466d18fd3c0..fadda5a235ab06b9230cbd0d88552e8caa770f8d 100644 --- a/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.ts +++ b/alfa-client/libs/admin/organisations-einheit/src/lib/organisations-einheit-container/organisations-einheit-list/organisations-einheit-list.component.ts @@ -28,14 +28,13 @@ import { import { TechSharedModule } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; -import { MatTooltip } from '@angular/material/tooltip'; -import { ExclamationIconComponent, ListComponent, ListItemComponent } from '@ods/system'; +import { ExclamationIconComponent, ListComponent, ListItemComponent, TooltipDirective } from '@ods/system'; @Component({ selector: 'admin-organisations-einheit-list', templateUrl: './organisations-einheit-list.component.html', standalone: true, - imports: [CommonModule, ListComponent, ListItemComponent, ExclamationIconComponent, MatTooltip, TechSharedModule], + imports: [CommonModule, ListComponent, ListItemComponent, ExclamationIconComponent, TooltipDirective, TechSharedModule], }) export class OrganisationsEinheitListComponent { private _organisationsEinheitResources: AdminOrganisationsEinheitResource[] = []; diff --git a/alfa-client/libs/admin/shared/src/lib/routes.ts b/alfa-client/libs/admin/shared/src/lib/routes.ts index 4867284b8af315446b304fac9496612723910aaa..36d67d9ecc4785084d02668c47a63aa0392cd710 100644 --- a/alfa-client/libs/admin/shared/src/lib/routes.ts +++ b/alfa-client/libs/admin/shared/src/lib/routes.ts @@ -26,4 +26,6 @@ export const ROUTES = { BENUTZER_UND_ROLLEN: 'benutzer_und_rollen', BENUTZER_UND_ROLLEN_NEU: 'benutzer_und_rollen/neu', ORGANISATIONSEINHEITEN: 'organisationseinheiten', + UNAVAILABLE: 'unavailable', + STATISTIK: 'statistik', }; diff --git a/alfa-client/libs/admin/statistik/.eslintrc.json b/alfa-client/libs/admin/statistik/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..7474579d583c598ae092a906b3e6cf1ad3c08a50 --- /dev/null +++ b/alfa-client/libs/admin/statistik/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "admin", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "admin", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/alfa-client/libs/admin/statistik/README.md b/alfa-client/libs/admin/statistik/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ad651ba5c6b2577aae0af1c1ac56ca839db65022 --- /dev/null +++ b/alfa-client/libs/admin/statistik/README.md @@ -0,0 +1,7 @@ +# 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/jest.config.ts b/alfa-client/libs/admin/statistik/jest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc41bd8816868cdd6f860b9908d0f06dbc9defc9 --- /dev/null +++ b/alfa-client/libs/admin/statistik/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'admin-statistik', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/admin/statistik', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '<rootDir>/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/alfa-client/libs/admin/statistik/project.json b/alfa-client/libs/admin/statistik/project.json new file mode 100644 index 0000000000000000000000000000000000000000..a5c36fc013da6fcc3504172dad9627628b42cfd9 --- /dev/null +++ b/alfa-client/libs/admin/statistik/project.json @@ -0,0 +1,22 @@ +{ + "name": "admin-statistik", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/admin/statistik/src", + "prefix": "admin", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "tsConfig": "libs/admin/statistik/tsconfig.lib.json", + "jestConfig": "libs/admin/statistik/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + } + } +} diff --git a/alfa-client/libs/admin/statistik/src/index.ts b/alfa-client/libs/admin/statistik/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..cdc5d7b64f24d50920b721f3ab01c5b69cb41b99 --- /dev/null +++ b/alfa-client/libs/admin/statistik/src/index.ts @@ -0,0 +1 @@ +export * from './lib/statistik-container/statistik-container.component'; diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.html b/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.html new file mode 100644 index 0000000000000000000000000000000000000000..eb8c0eaa7fd1b08d5028ff9abbd5001ba0f89ea4 --- /dev/null +++ b/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.html @@ -0,0 +1,26 @@ +<!-- + + 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. + +--> +<h1 class="heading-1" data-test-id="statistik-header-text">Statistik</h1> diff --git a/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.spec.ts b/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..84b58ee0de584e0b5445a79865b4154cc60bb4c7 --- /dev/null +++ b/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.spec.ts @@ -0,0 +1,47 @@ +/* + * 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 { ComponentFixture, TestBed } from '@angular/core/testing'; +import { StatistikContainerComponent } from './statistik-container.component'; + +describe('StatistikContainerComponent', () => { + let component: StatistikContainerComponent; + let fixture: ComponentFixture<StatistikContainerComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StatistikContainerComponent], + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(StatistikContainerComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/ui/src/lib/ui/mattooltip/mattooltip.default.ts b/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.ts similarity index 71% rename from alfa-client/libs/ui/src/lib/ui/mattooltip/mattooltip.default.ts rename to alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.ts index af14a745eb9b5d1fe8556ab9ea07ccac3b14ded4..05e7e9db912474d8f6991acdf8f20d480b309fe2 100644 --- a/alfa-client/libs/ui/src/lib/ui/mattooltip/mattooltip.default.ts +++ b/alfa-client/libs/admin/statistik/src/lib/statistik-container/statistik-container.component.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * 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 @@ -21,12 +21,13 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { MatTooltipDefaultOptions } from '@angular/material/tooltip'; +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; -export const matTooltipDefaultOptions: MatTooltipDefaultOptions = { - showDelay: 1500, - hideDelay: 0, - touchendHideDelay: 1500, - positionAtOrigin: true, - disableTooltipInteractivity: true, -}; +@Component({ + selector: 'admin-statistik-container', + templateUrl: './statistik-container.component.html', + standalone: true, + imports: [CommonModule], +}) +export class StatistikContainerComponent {} diff --git a/alfa-client/libs/admin/statistik/src/test-setup.ts b/alfa-client/libs/admin/statistik/src/test-setup.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef358fbdf02a6bfc0ca9a7c2f5c73766b01ef584 --- /dev/null +++ b/alfa-client/libs/admin/statistik/src/test-setup.ts @@ -0,0 +1,7 @@ +globalThis.ngJest = { + testEnvironmentOptions: { + errorOnUnknownElements: true, + errorOnUnknownProperties: true, + }, +}; +import 'jest-preset-angular/setup-jest'; diff --git a/alfa-client/libs/admin/statistik/tsconfig.json b/alfa-client/libs/admin/statistik/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..8ca9ad312c2bd4dc364383853ddd91a2ed8f86fd --- /dev/null +++ b/alfa-client/libs/admin/statistik/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "target": "es2022" + } +} diff --git a/alfa-client/libs/admin/statistik/tsconfig.lib.json b/alfa-client/libs/admin/statistik/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..8441346f6e5858b2ef4235cb3c3160eda256f94a --- /dev/null +++ b/alfa-client/libs/admin/statistik/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": ["src/**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/alfa-client/libs/admin/statistik/tsconfig.spec.json b/alfa-client/libs/admin/statistik/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..723782fbd367969806c5992aea882773ab65af8b --- /dev/null +++ b/alfa-client/libs/admin/statistik/tsconfig.spec.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"], + "target": "ES2022", + "useDefineForClassFields": false + }, + "files": ["src/test-setup.ts"], + "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts", "jest.config.ts"] +} diff --git a/alfa-client/libs/admin/user-shared/src/lib/user.model.ts b/alfa-client/libs/admin/user-shared/src/lib/user.model.ts index 2e933072b14ea8f5bd080cfb143a05a54389e27b..f6dcac00ce2ddc7b7e50c0f908271e4140eaadde 100644 --- a/alfa-client/libs/admin/user-shared/src/lib/user.model.ts +++ b/alfa-client/libs/admin/user-shared/src/lib/user.model.ts @@ -21,12 +21,24 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import RoleRepresentation from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; + export interface User { id: string; username: string; email: string; firstName: string; lastName: string; + enabled: boolean; groups?: string[]; - roles?: string[]; + clientRoles: ClientRoles; +} + +export interface ClientRoles { + alfa: string[]; + admin: string[]; +} + +export interface ClientMapping { + mappings: RoleRepresentation[]; } diff --git a/alfa-client/libs/admin/user-shared/src/lib/user.repository.service.spec.ts b/alfa-client/libs/admin/user-shared/src/lib/user.repository.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ea7250024c77cbb16ce65dc76e499ce02c137c2c --- /dev/null +++ b/alfa-client/libs/admin/user-shared/src/lib/user.repository.service.spec.ts @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2024 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 { Mock, mock } from '@alfa-client/test-utils'; +import { TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { faker } from '@faker-js/faker'; +import KcAdminClient from '@keycloak/keycloak-admin-client'; +import { TokenProvider } from '@keycloak/keycloak-admin-client/lib/client'; +import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation'; +import MappingsRepresentation from '@keycloak/keycloak-admin-client/lib/defs/mappingsRepresentation'; +import { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { cold } from 'jest-marbles'; +import { throwError } from 'rxjs'; +import { UserAddFormservice } from '../../../user/src/lib/users-roles/user-add-form/user-add.formservice'; +import { createUser } from '../../test/user'; +import { User } from './user.model'; +import { UserRepository } from './user.repository.service'; + +describe('UserRepository', () => { + const accessToken: string = faker.string.alphanumeric(40); + + let repository: UserRepository; + + let kcAdminClient: Mock<KcAdminClient>; + let oAuthService: Mock<OAuthService>; + + beforeEach(() => { + kcAdminClient = mock(KcAdminClient); + oAuthService = mock(OAuthService); + TestBed.configureTestingModule({ + providers: [ + { provide: OAuthService, useValue: oAuthService }, + { provide: KcAdminClient, useValue: kcAdminClient }, + ], + }); + oAuthService.getAccessToken.mockReturnValue(accessToken); + repository = TestBed.inject(UserRepository); + }); + + it('should be created', () => { + expect(repository).toBeTruthy(); + }); + + describe('registerTokenProvider', () => { + it('should register token provider from oauth service', async () => { + const tokenProvider: TokenProvider = kcAdminClient.registerTokenProvider.mock.calls[0][0]; + const token: string = await tokenProvider.getAccessToken(); + expect(token).toEqual(accessToken); + }); + }); + + describe('createInKeycloak', () => { + const user: User = createUser(); + + beforeEach(() => { + kcAdminClient.users = <any>{ + create: jest.fn().mockReturnValue(Promise.resolve({ id: user.id })), + findOne: jest.fn().mockReturnValue(Promise.resolve(user)), + addClientRoleMappings: jest.fn().mockReturnValue(Promise.resolve()), + executeActionsEmail: jest.fn().mockReturnValue(Promise.resolve()), + }; + kcAdminClient.clients = <any>{ + find: jest.fn().mockReturnValue(Promise.resolve([{ id: faker.string.uuid() }])), + listRoles: jest.fn().mockReturnValue(Promise.resolve([{ id: faker.string.uuid(), name: faker.word.sample() }])), + }; + }); + + it('should call kcAdminClient users create', () => { + repository.createInKeycloak(user); + + expect(kcAdminClient.users['create']).toBeCalledWith(user); + }); + + it('should call addUserRoles', fakeAsync(() => { + const addUserRolesSpy: jest.SpyInstance = jest.spyOn(repository, 'addUserRoles'); + + repository.createInKeycloak(user).subscribe(); + tick(); + + expect(addUserRolesSpy).toBeCalledWith(user.id, user.clientRoles); + })); + + it('should call sendActivationMail', (done) => { + const sendActivationMailSpy: jest.SpyInstance = jest.spyOn(repository, 'sendActivationMail'); + jest.spyOn(repository, 'addUserRoles').mockReturnValue(Promise.resolve()); + + repository.createInKeycloak(user).subscribe(() => { + expect(sendActivationMailSpy).toBeCalledWith(user.id); + done(); + }); + }); + + it('should call getUserById', (done) => { + const getUserByIdSpy: jest.SpyInstance = jest.spyOn(repository, 'getUserById'); + + repository.createInKeycloak(user).subscribe(() => { + expect(getUserByIdSpy).toBeCalledWith(user.id); + done(); + }); + }); + + it('should call handleError', fakeAsync(() => { + const handleErrorSpy: jest.SpyInstance = jest.spyOn(repository, 'handleError'); + kcAdminClient.users['create'] = jest.fn().mockReturnValue(throwError(() => new Error('error'))); + + repository.createInKeycloak(user).subscribe({ error: () => {} }); + tick(); + + expect(handleErrorSpy).toHaveBeenCalled(); + })); + + describe('addUserRoles', () => { + it('should call addUserRolesForClient for admin', async () => { + const addUserRolesForClientSpy: jest.SpyInstance = jest.spyOn(repository, 'addUserRolesForClient'); + + await repository.addUserRoles(user.id, { admin: [UserAddFormservice.ADMIN], alfa: [] }); + + expect(addUserRolesForClientSpy).toBeCalledWith(user.id, [UserAddFormservice.ADMIN], UserRepository.ADMIN_CLIENT_NAME); + }); + + it('should call addUserRolesForClient for admin', async () => { + const addUserRolesForClientSpy: jest.SpyInstance = jest.spyOn(repository, 'addUserRolesForClient'); + + await repository.addUserRoles(user.id, { alfa: [UserAddFormservice.POSTSTELLE], admin: [] }); + + expect(addUserRolesForClientSpy).toBeCalledWith( + user.id, + [UserAddFormservice.POSTSTELLE], + UserRepository.ALFA_CLIENT_NAME, + ); + }); + + it('should not call addUserRolesForClient if clientRoles alfa and admin are empty', async () => { + const addUserRolesForClientSpy: jest.SpyInstance = jest.spyOn(repository, 'addUserRolesForClient'); + + await repository.addUserRoles(user.id, { admin: [], alfa: [] }); + + expect(addUserRolesForClientSpy).not.toHaveBeenCalled(); + }); + }); + + describe('addUserRolesForClient', () => { + const clientId: string = faker.string.uuid(); + const roleMapping: RoleMappingPayload[] = [{ id: faker.string.uuid(), name: faker.word.sample() }]; + let getClientIdSpy: jest.SpyInstance; + let mapUserRolesSpy: jest.SpyInstance; + let addUserRolesInKeycloak: jest.SpyInstance; + + beforeEach(() => { + getClientIdSpy = jest.spyOn(repository, 'getClientId').mockReturnValue(Promise.resolve(clientId)); + mapUserRolesSpy = jest.spyOn(repository, 'mapUserRoles').mockReturnValue(Promise.resolve(roleMapping)); + addUserRolesInKeycloak = jest.spyOn(repository, 'addUserRolesInKeycloak'); + }); + + it('should call getClientId', async () => { + await repository.addUserRolesForClient(user.id, [UserAddFormservice.ADMIN], UserRepository.ADMIN_CLIENT_NAME); + + expect(getClientIdSpy).toBeCalled(); + }); + + it('should call getAlfaClientId', async () => { + await repository.addUserRolesForClient(user.id, [UserAddFormservice.ADMIN], UserRepository.ADMIN_CLIENT_NAME); + + expect(mapUserRolesSpy).toHaveBeenCalledWith(clientId, [UserAddFormservice.ADMIN]); + }); + + it('should call addUserRolesInKeycloak', async () => { + await repository.addUserRolesForClient(user.id, [UserAddFormservice.ADMIN], UserRepository.ADMIN_CLIENT_NAME); + + expect(addUserRolesInKeycloak).toHaveBeenCalledWith(user.id, clientId, roleMapping); + }); + }); + + describe('sendActivationMail', () => { + it('should call kcAdminClient users executeActionsEmail', () => { + const userId: string = faker.string.uuid(); + + repository.sendActivationMail(userId); + + expect(kcAdminClient.users['executeActionsEmail']).toBeCalledWith({ + id: userId, + actions: ['VERIFY_EMAIL'], + lifespan: 3600 * 24 * 7, + }); + }); + }); + + describe('getUserById', () => { + it('should call kcAdminClient users findOne', () => { + const userId: string = faker.string.uuid(); + + repository.getUserById(userId); + + expect(kcAdminClient.users['findOne']).toBeCalledWith({ id: userId }); + }); + }); + + describe('handleError', () => { + it('should throw error', () => { + const error: Error = new Error('error'); + const result = repository.handleError(error); + + expect(result).toBeObservable(cold('#', null, new Error('An error occurred while creating the user.'))); + }); + }); + }); + + describe('getUsers', () => { + const userRep: User = createUser(); + const userRepArray: User[] = [userRep, userRep, userRep]; + const group: string = faker.word.sample(); + const alfaRole: string = faker.word.sample(); + const adminRole: string = faker.word.sample(); + const groupRep: GroupRepresentation[] = [{ name: group }]; + const roleRep: MappingsRepresentation = { + clientMappings: { + alfa: { mappings: [{ name: alfaRole }] }, + admin: { mappings: [{ name: adminRole }] }, + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + kcAdminClient.users = <any>{ + find: jest.fn().mockReturnValue(Promise.resolve(userRepArray)), + listGroups: jest.fn().mockReturnValue(Promise.resolve(groupRep)), + listRoleMappings: jest.fn().mockReturnValue(Promise.resolve(roleRep)), + }; + }); + + it('should call kcAdminClient users find', () => { + repository.getUsers(); + + expect(kcAdminClient.users['find']).toHaveBeenCalled(); + }); + + it('should call mapToUser', fakeAsync(() => { + const mapToUser: jest.SpyInstance<User> = jest.spyOn(repository, 'mapToUser'); + + repository.getUsers().subscribe(); + tick(); + + expect(mapToUser).toBeCalledTimes(userRepArray.length); + })); + + it('should call kcadminClient listGroups for every user', fakeAsync(() => { + repository.getUsers().subscribe(); + tick(); + + expect(kcAdminClient.users['listGroups']).toBeCalledTimes(userRepArray.length); + })); + + it('should call kcadminClient listRoleMappings for every user', fakeAsync(() => { + repository.getUsers().subscribe(); + tick(); + + expect(kcAdminClient.users['listRoleMappings']).toBeCalledTimes(userRepArray.length); + })); + + it('should return users with groups and roles', (done) => { + repository.getUsers().subscribe((users: User[]) => { + users.forEach((user) => + expect(user).toEqual({ ...userRep, groups: [group], clientRoles: { alfa: [alfaRole], admin: [adminRole] } }), + ); + done(); + }); + }); + + it('should return users with empty groups and roles if they have none', (done) => { + kcAdminClient.users['listGroups'] = jest.fn().mockReturnValue(Promise.resolve([])); + kcAdminClient.users['listRoleMappings'] = jest.fn().mockReturnValue(Promise.resolve({})); + + repository.getUsers().subscribe((users: User[]) => { + users.forEach((user) => expect(user).toEqual({ ...userRep, groups: [], clientRoles: { alfa: [], admin: [] } })); + done(); + }); + }); + }); +}); diff --git a/alfa-client/libs/admin/user-shared/src/lib/user.repository.service.ts b/alfa-client/libs/admin/user-shared/src/lib/user.repository.service.ts index 06fb8e0a8a8f76657d0dbdd6d1d85a11db443f5b..2b0ded61b000f7c95eaa6fe3ebbb0437636d3b95 100644 --- a/alfa-client/libs/admin/user-shared/src/lib/user.repository.service.ts +++ b/alfa-client/libs/admin/user-shared/src/lib/user.repository.service.ts @@ -24,14 +24,15 @@ import { Injectable } from '@angular/core'; import KcAdminClient from '@keycloak/keycloak-admin-client'; import { TokenProvider } from '@keycloak/keycloak-admin-client/lib/client'; +import ClientRepresentation from '@keycloak/keycloak-admin-client/lib/defs/clientRepresentation'; import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation'; import MappingsRepresentation from '@keycloak/keycloak-admin-client/lib/defs/mappingsRepresentation'; -import RoleRepresentation from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; +import RoleRepresentation, { RoleMappingPayload } from '@keycloak/keycloak-admin-client/lib/defs/roleRepresentation'; import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; import { OAuthService } from 'angular-oauth2-oidc'; import { isNil } from 'lodash-es'; -import { Observable, forkJoin, from, map, mergeMap } from 'rxjs'; -import { User } from './user.model'; +import { Observable, catchError, concatMap, forkJoin, from, map, mergeMap, switchMap, tap, throwError } from 'rxjs'; +import { ClientMapping, ClientRoles, User } from './user.model'; @Injectable({ providedIn: 'root', @@ -44,6 +45,9 @@ export class UserRepository { this.registerAccessTokenProvider(); } + public static readonly ALFA_CLIENT_NAME: string = 'alfa'; + public static readonly ADMIN_CLIENT_NAME: string = 'admin'; + private registerAccessTokenProvider(): void { this.kcAdminClient.registerTokenProvider(this.getTokenProvider()); } @@ -54,6 +58,69 @@ export class UserRepository { }; } + public createInKeycloak(user: Partial<User>): Observable<User> { + return from(this.kcAdminClient.users.create(user)).pipe( + concatMap(async (response: { id: string }) => { + await this.addUserRoles(response.id, user.clientRoles); + return response; + }), + tap((response: { id: string }) => this.sendActivationMail(response.id)), + switchMap((response: { id: string }): Observable<User> => this.getUserById(response.id)), + catchError((err) => this.handleError(err)), + ); + } + + async addUserRoles(userId: string, clientRoles: ClientRoles): Promise<void> { + if (clientRoles.admin.length > 0) { + await this.addUserRolesForClient(userId, clientRoles.admin, UserRepository.ADMIN_CLIENT_NAME); + } + + if (clientRoles.alfa.length > 0) { + await this.addUserRolesForClient(userId, clientRoles.alfa, UserRepository.ALFA_CLIENT_NAME); + } + } + + async addUserRolesForClient(userId: string, userRoles: string[], client: string): Promise<void> { + const clientId: string = await this.getClientId(client); + const roles: RoleMappingPayload[] = await this.mapUserRoles(clientId, userRoles); + await this.addUserRolesInKeycloak(userId, clientId, roles); + } + + async getClientId(client: string): Promise<string | undefined> { + const clients: ClientRepresentation[] = await this.kcAdminClient.clients.find({ clientId: client }); + return clients?.[0].id; + } + + async mapUserRoles(clientId: string, userRoles: string[]): Promise<RoleMappingPayload[]> { + const roles: RoleRepresentation[] = await this.kcAdminClient.clients.listRoles({ id: clientId }); + return roles.filter((role) => userRoles.includes(role.name)).map((role) => ({ id: role.id, name: role.name })); + } + + async addUserRolesInKeycloak(userId: string, clientId: string, roles: RoleMappingPayload[]): Promise<void> { + await this.kcAdminClient.users.addClientRoleMappings({ + id: userId, + clientUniqueId: clientId, + roles, + }); + } + + sendActivationMail(userId: string): void { + this.kcAdminClient.users.executeActionsEmail({ + id: userId, + actions: ['VERIFY_EMAIL'], + lifespan: 3600 * 24 * 7, + }); + } + + getUserById(userId: string): Observable<User> { + return from(this.kcAdminClient.users.findOne({ id: userId })) as Observable<User>; + } + + handleError(err: any): Observable<never> { + console.error('Error creating user in Keycloak:', err); + return throwError(() => new Error('An error occurred while creating the user.')); + } + public getUsers(): Observable<User[]> { return from(this.kcAdminClient.users.find()).pipe( map((userReps: UserRepresentation[]) => userReps.map((userReps) => this.mapToUser(userReps))), @@ -68,17 +135,21 @@ export class UserRepository { username: userRepresentation.username, firstName: userRepresentation.firstName, lastName: userRepresentation.lastName, + enabled: userRepresentation.enabled, groups: null, - roles: null, + clientRoles: { + alfa: [], + admin: [], + }, }; } private addInformationToUser(user: User): Observable<User> { - return forkJoin([this.getUserGroups(user), this.getAlfaClientRoles(user)]).pipe( - map(([groups, roles]) => ({ + return forkJoin([this.getUserGroups(user), this.getClientRoles(user)]).pipe( + map(([groups, clientRoles]) => ({ ...user, groups, - roles, + clientRoles, })), ); } @@ -89,17 +160,24 @@ export class UserRepository { ); } - private getAlfaClientRoles(user: User): Observable<string[]> { + private getClientRoles(user: User): Observable<ClientRoles> { return from(this.kcAdminClient.users.listRoleMappings({ id: user.id })).pipe( - map((clientMappings: MappingsRepresentation) => this.mapToAlfaClientRoleNames(clientMappings)), + map((roleMappings: MappingsRepresentation) => { + return { + alfa: this.mapToClientRoleNames(roleMappings, UserRepository.ALFA_CLIENT_NAME), + admin: this.mapToClientRoleNames(roleMappings, UserRepository.ADMIN_CLIENT_NAME), + }; + }), ); } - private mapToAlfaClientRoleNames(roleMappings: MappingsRepresentation): string[] { - if (isNil(roleMappings?.clientMappings?.['alfa'])) { + private mapToClientRoleNames(roleMappings: MappingsRepresentation, client: string): string[] { + const clientMappingsAlfa: ClientMapping | undefined = roleMappings?.clientMappings?.[client]; + + if (isNil(clientMappingsAlfa)) { return []; } - return roleMappings.clientMappings['alfa'].mappings.map((role: RoleRepresentation) => role.name); + return clientMappingsAlfa.mappings.map((role: RoleRepresentation) => role.name); } } diff --git a/alfa-client/libs/admin/user-shared/src/lib/user.repository.spec.ts b/alfa-client/libs/admin/user-shared/src/lib/user.repository.spec.ts deleted file mode 100644 index 35173f88cd008f41212d12306dcf3ac1f834a27d..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/user-shared/src/lib/user.repository.spec.ts +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2024 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 { Mock, mock } from '@alfa-client/test-utils'; -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { faker } from '@faker-js/faker'; -import KcAdminClient from '@keycloak/keycloak-admin-client'; -import { TokenProvider } from '@keycloak/keycloak-admin-client/lib/client'; -import GroupRepresentation from '@keycloak/keycloak-admin-client/lib/defs/groupRepresentation'; -import MappingsRepresentation from '@keycloak/keycloak-admin-client/lib/defs/mappingsRepresentation'; -import { OAuthService } from 'angular-oauth2-oidc'; -import { createUser } from '../../test/user'; -import { User } from './user.model'; -import { UserRepository } from './user.repository.service'; - -describe('UserRepository', () => { - const accessToken: string = faker.string.alphanumeric(40); - - let repository: UserRepository; - - let kcAdminClient: Mock<KcAdminClient>; - let oAuthService: Mock<OAuthService>; - - beforeEach(() => { - kcAdminClient = mock(KcAdminClient); - oAuthService = mock(OAuthService); - TestBed.configureTestingModule({ - providers: [ - { provide: OAuthService, useValue: oAuthService }, - { provide: KcAdminClient, useValue: kcAdminClient }, - ], - }); - oAuthService.getAccessToken.mockReturnValue(accessToken); - repository = TestBed.inject(UserRepository); - }); - - it('should be created', () => { - expect(repository).toBeTruthy(); - }); - - describe('registerTokenProvider', () => { - it('should register token provider from oauth service', async () => { - const tokenProvider: TokenProvider = kcAdminClient.registerTokenProvider.mock.calls[0][0]; - const token: string = await tokenProvider.getAccessToken(); - expect(token).toEqual(accessToken); - }); - }); - - describe('getUsers', () => { - const userRep: User = createUser(); - const userRepArray: User[] = [userRep, userRep, userRep]; - const group: string = faker.word.sample(); - const role: string = faker.word.sample(); - const groupRep: GroupRepresentation[] = [{ name: group }]; - const roleRep: MappingsRepresentation = { - clientMappings: { alfa: { mappings: [{ name: role }] } }, - }; - - beforeEach(() => { - jest.clearAllMocks(); - kcAdminClient.users = <any>{ - find: jest.fn().mockReturnValue(Promise.resolve(userRepArray)), - listGroups: jest.fn().mockReturnValue(Promise.resolve(groupRep)), - listRoleMappings: jest.fn().mockReturnValue(Promise.resolve(roleRep)), - }; - }); - - it('should call kcAdminClient users find', () => { - repository.getUsers(); - - expect(kcAdminClient.users['find']).toHaveBeenCalled(); - }); - - it('should call mapToUser', fakeAsync(() => { - const mapToUser: jest.SpyInstance<User> = jest.spyOn(repository, 'mapToUser'); - - repository.getUsers().subscribe(); - tick(); - - expect(mapToUser).toBeCalledTimes(userRepArray.length); - })); - - it('should call kcadminClient listGroups for every user', fakeAsync(() => { - repository.getUsers().subscribe(); - tick(); - - expect(kcAdminClient.users['listGroups']).toBeCalledTimes(userRepArray.length); - })); - - it('should call kcadminClient listRoleMappings for every user', fakeAsync(() => { - repository.getUsers().subscribe(); - tick(); - - expect(kcAdminClient.users['listRoleMappings']).toBeCalledTimes(userRepArray.length); - })); - - it('should return users with groups and roles', (done) => { - repository.getUsers().subscribe((users: User[]) => { - users.forEach((user) => expect(user).toEqual({ ...userRep, groups: [group], roles: [role] })); - done(); - }); - }); - - it('should return users with empty groups and roles if they have none', (done) => { - kcAdminClient.users['listGroups'] = jest.fn().mockReturnValue(Promise.resolve([])); - kcAdminClient.users['listRoleMappings'] = jest.fn().mockReturnValue(Promise.resolve({})); - - repository.getUsers().subscribe((users: User[]) => { - users.forEach((user) => expect(user).toEqual({ ...userRep, groups: [], roles: [] })); - done(); - }); - }); - }); -}); diff --git a/alfa-client/libs/admin/user-shared/src/lib/user.service.spec.ts b/alfa-client/libs/admin/user-shared/src/lib/user.service.spec.ts index f0067611db8651f2b00ecdd8faec239f37807086..c1148ee01e76d476ddf204eafee43060a936ad38 100644 --- a/alfa-client/libs/admin/user-shared/src/lib/user.service.spec.ts +++ b/alfa-client/libs/admin/user-shared/src/lib/user.service.spec.ts @@ -57,4 +57,14 @@ describe('UserService', () => { expect(sortUsersByLastNameSpy).toHaveBeenCalled(); })); }); + + describe('createInKeycloak', () => { + const user: User = createUser(); + + it('should call createInKeycloak from userRepository', () => { + service.createInKeycloak(user); + + expect(repository.createInKeycloak).toHaveBeenCalledWith(user); + }); + }); }); diff --git a/alfa-client/libs/admin/user-shared/src/lib/user.service.ts b/alfa-client/libs/admin/user-shared/src/lib/user.service.ts index 8b4b5dbfba75332786ad7bf395b972f54a9c3af0..af05eea872bcb10f9383ba790efd3e89e14fae34 100644 --- a/alfa-client/libs/admin/user-shared/src/lib/user.service.ts +++ b/alfa-client/libs/admin/user-shared/src/lib/user.service.ts @@ -40,11 +40,11 @@ export class UserService extends KeycloakResourceService<User> { return this.userRepository.getUsers().pipe(map(sortUsersByLastName)); } - createInKeycloak(item: Partial<User>): Observable<User> { - throw new Error('Method not implemented.'); + createInKeycloak(user: Partial<User>): Observable<User> { + return this.userRepository.createInKeycloak(user); } - saveInKeycloak(item: User): Observable<void> { + saveInKeycloak(user: User): Observable<void> { throw new Error('Method not implemented.'); } diff --git a/alfa-client/libs/admin/user-shared/test/user.ts b/alfa-client/libs/admin/user-shared/test/user.ts index 511f24510c38a12b07cd73b74a4a4a895df023c7..680d60824d2cebb018ca1c26b096f8c368d4bbbe 100644 --- a/alfa-client/libs/admin/user-shared/test/user.ts +++ b/alfa-client/libs/admin/user-shared/test/user.ts @@ -22,6 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { faker } from '@faker-js/faker'; +import { UserAddFormservice } from '../../user/src/lib/users-roles/user-add-form/user-add.formservice'; import { User } from '../src/lib/user.model'; export function createUser(): User { @@ -31,7 +32,11 @@ export function createUser(): User { firstName: faker.person.firstName(), lastName: faker.person.lastName(), email: faker.internet.email(), - roles: null, + enabled: true, + clientRoles: { + alfa: [UserAddFormservice.POSTSTELLE], + admin: [UserAddFormservice.ADMIN], + }, groups: null, }; } diff --git a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.html b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.html index 915c537c21ae378ea7638bb3891f6fbfaca8eb9a..e95ce249045910cd9c9951d9f4851e905832a883 100644 --- a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.html +++ b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.html @@ -26,28 +26,30 @@ <div class="max-w-[960px]" [formGroup]="formService.form"> <h1 class="heading-1 mb-4">Benutzer anlegen</h1> <div class="mb-4 grid gap-4 xl:grid-cols-2"> - <ods-text-editor [formControlName]="UserAddFormService.VORNAME" label="Vorname" required="true" /> - <ods-text-editor [formControlName]="UserAddFormService.NACHNAME" label="Nachname" required="true" /> - <ods-text-editor [formControlName]="UserAddFormService.BENUTZERNAME" label="Benutzername" required="true" /> - <ods-text-editor [formControlName]="UserAddFormService.EMAIL" label="E-Mail" required="true" /> + <ods-text-editor [formControlName]="UserAddFormService.VORNAME" [isRequired]="true" label="Vorname"/> + <ods-text-editor [formControlName]="UserAddFormService.NACHNAME" [isRequired]="true" label="Nachname"/> + <ods-text-editor [formControlName]="UserAddFormService.BENUTZERNAME" [isRequired]="true" label="Benutzername"/> + <ods-text-editor [formControlName]="UserAddFormService.EMAIL" [isRequired]="true" label="E-Mail"/> </div> <h3 class="text-md mb-4 block font-medium text-text">Organisationseinheiten</h3> - <ods-button-with-spinner text="Organisationseinheit hinzufügen" variant="outline" dataTestId="Add-organisationseinheit-button" /> + <ods-button-with-spinner text="Organisationseinheit hinzufügen" variant="outline" + dataTestId="Add-organisationseinheit-button"/> <h2 class="heading-2 mt-4">Rollen für OZG-Cloud</h2> <div [formGroupName]="UserAddFormService.ROLLEN_GROUP" class="mb-8 flex gap-56"> <div [formGroupName]="UserAddFormService.ADMINISTRATION_GROUP" class="flex flex-col gap-2"> <h3 class="text-md block font-medium text-text">Administration</h3> - <ods-checkbox-editor [formControlName]="UserAddFormService.ADMIN" label="Admin" inputId="admin" /> + <ods-checkbox-editor [formControlName]="UserAddFormService.ADMIN" label="Admin" inputId="admin"/> </div> <div [formGroupName]="UserAddFormService.ALFA_GROUP" class="flex flex-col gap-2"> <h3 class="text-md block font-medium text-text">Alfa</h3> - <ods-checkbox-editor [formControlName]="UserAddFormService.LOESCHEN" label="Löschen" inputId="delete" /> - <ods-checkbox-editor [formControlName]="UserAddFormService.USER" label="User" inputId="user" /> - <ods-checkbox-editor [formControlName]="UserAddFormService.POSTSTELLE" label="Poststelle" inputId="post_office" /> + <ods-checkbox-editor [formControlName]="UserAddFormService.LOESCHEN" label="Löschen" inputId="delete"/> + <ods-checkbox-editor [formControlName]="UserAddFormService.USER" label="User" inputId="user"/> + <ods-checkbox-editor [formControlName]="UserAddFormService.POSTSTELLE" label="Poststelle" inputId="post_office"/> </div> </div> - <ods-button-with-spinner text="Speichern" dataTestId="save-button" /> + <ods-button-with-spinner [stateResource]="submitStateResource$ | async" (clickEmitter)="submit()" text="Speichern" + dataTestId="save-button"/> </div> diff --git a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.spec.ts b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.spec.ts index 9e6abab7af6c7ad7633633967240034746c226cb..085cc24dc7a7e2e2bdd84a63287424511d428146 100644 --- a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.spec.ts @@ -21,35 +21,122 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { User, UserService } from '@admin-client/user-shared'; +import { NavigationService } from '@alfa-client/navigation-shared'; +import { createStateResource, StateResource } from '@alfa-client/tech-shared'; +import { dispatchEventFromFixture, getDebugElementFromFixtureByCss, mock, Mock } from '@alfa-client/test-utils'; +import { SnackBarService } from '@alfa-client/ui'; import { CommonModule } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; import { ButtonWithSpinnerComponent, CheckboxEditorComponent, TextEditorComponent } from '@ods/component'; +import { cold } from 'jest-marbles'; +import { createUser } from 'libs/admin/user-shared/test/user'; import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { getDataTestIdAttributeOf } from '../../../../../../tech-shared/test/data-test'; import { UserAddFormComponent } from './user-add-form.component'; +import { UserAddFormservice } from './user-add.formservice'; describe('UserAddFormComponent', () => { let component: UserAddFormComponent; let fixture: ComponentFixture<UserAddFormComponent>; + let formService: UserAddFormservice; + + let userService: Mock<UserService>; + let navigationService: Mock<NavigationService>; + let snackBarService: Mock<SnackBarService>; + + const saveButton: string = getDataTestIdAttributeOf('save-button'); + beforeEach(async () => { + userService = mock(UserService); + navigationService = mock(NavigationService); + snackBarService = mock(SnackBarService); + await TestBed.configureTestingModule({ - imports: [ + imports: [CommonModule, ReactiveFormsModule], + declarations: [ UserAddFormComponent, - CommonModule, - ReactiveFormsModule, MockComponent(ButtonWithSpinnerComponent), MockComponent(TextEditorComponent), MockComponent(CheckboxEditorComponent), ], + providers: [ + UserAddFormservice, + { provide: UserService, useValue: userService }, + { provide: NavigationService, useValue: navigationService }, + { provide: SnackBarService, useValue: snackBarService }, + ], }).compileComponents(); fixture = TestBed.createComponent(UserAddFormComponent); component = fixture.componentInstance; + formService = component.formService; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + describe('component', () => { + describe('submit', () => { + const userStateResource: StateResource<User> = createStateResource(createUser()); + + beforeEach(() => { + formService.submit = jest.fn().mockReturnValue(of(userStateResource)); + }); + + it('should call formService submit if form is valid', () => { + jest.spyOn(formService.form, 'invalid', 'get').mockReturnValue(false); + + component.submit(); + + expect(formService.submit).toHaveBeenCalled(); + }); + + it('should not call formService submit if form is not valid', () => { + jest.spyOn(formService.form, 'invalid', 'get').mockReturnValue(true); + + component.submit(); + + expect(formService.submit).not.toHaveBeenCalled(); + }); + + it('should set submitState$ if form is valid', () => { + jest.spyOn(formService.form, 'invalid', 'get').mockReturnValue(false); + + component.submit(); + + expect(component.submitStateResource$).toBeObservable(cold('(a|)', { a: userStateResource })); + }); + }); + }); + + describe('template', () => { + describe('ods-button-with-spinner save', () => { + describe('input', () => { + it('should be set', () => { + const stateResource: StateResource<User> = createStateResource(createUser()); + component.submitStateResource$ = of(stateResource); + + fixture.detectChanges(); + + expect(getDebugElementFromFixtureByCss(fixture, saveButton).componentInstance.stateResource).toEqual(stateResource); + }); + }); + + describe('output', () => { + it('should call submit', () => { + const submitSpy: jest.SpyInstance = jest.spyOn(component, 'submit'); + + dispatchEventFromFixture(fixture, saveButton, 'clickEmitter'); + + expect(submitSpy).toHaveBeenCalled(); + }); + }); + }); + }); }); diff --git a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.ts b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.ts index e62729b8f391032dc1e2cc27675251702a794d91..e5df17d9fb3bbf0f95105085e1943d58a13dbab7 100644 --- a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.ts +++ b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.component.ts @@ -21,20 +21,39 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { User } from '@admin-client/user-shared'; +import { StateResource } from '@alfa-client/tech-shared'; +import { AsyncPipe } from '@angular/common'; import { Component, inject } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ButtonWithSpinnerComponent, CheckboxEditorComponent, TextEditorComponent } from '@ods/component'; -import { UserAddFormService } from './user-add-form.service'; +import { Observable } from 'rxjs'; +import { UserAddFormservice } from './user-add.formservice'; @Component({ selector: 'admin-user-add-form', - providers: [UserAddFormService], + providers: [UserAddFormservice], templateUrl: './user-add-form.component.html', standalone: true, - imports: [FormsModule, ReactiveFormsModule, TextEditorComponent, ButtonWithSpinnerComponent, CheckboxEditorComponent], + imports: [ + FormsModule, + ReactiveFormsModule, + TextEditorComponent, + ButtonWithSpinnerComponent, + CheckboxEditorComponent, + AsyncPipe, + ], }) export class UserAddFormComponent { - formService = inject(UserAddFormService); + public readonly formService = inject(UserAddFormservice); - protected readonly UserAddFormService = UserAddFormService; + submitStateResource$: Observable<StateResource<User>>; + + protected readonly UserAddFormService = UserAddFormservice; + + public submit(): void { + if (!this.formService.form.invalid) { + this.submitStateResource$ = this.formService.submit(); + } + } } diff --git a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.service.ts b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.service.ts deleted file mode 100644 index 708b66353058db36bc8b913942cf9d50553e6f67..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-form.service.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2024 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 { AbstractFormService, EMPTY_STRING, StateResource } from '@alfa-client/tech-shared'; -import { Injectable } from '@angular/core'; -import { AbstractControl, FormControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { Resource } from '@ngxp/rest'; -import { Observable } from 'rxjs'; - -@Injectable() -export class UserAddFormService extends AbstractFormService { - public static readonly VORNAME: string = 'vorname'; - public static readonly NACHNAME: string = 'nachname'; - public static readonly BENUTZERNAME: string = 'benutzername'; - public static readonly EMAIL: string = 'e-mail'; - - public static readonly ROLLEN_GROUP: string = 'rollen'; - public static readonly ADMINISTRATION_GROUP: string = 'administration'; - public static readonly ADMIN: string = 'admin'; - public static readonly ALFA_GROUP: string = 'alfa'; - public static readonly LOESCHEN: string = 'loeschen'; - public static readonly USER: string = 'user'; - public static readonly POSTSTELLE: string = 'poststelle'; - - public static readonly USER_ADD_PREFIX: string = 'userAdd'; - - constructor(public formBuilder: UntypedFormBuilder) { - super(formBuilder); - this.initAlfaGroupLogic(); - } - - protected initForm(): UntypedFormGroup { - return this.formBuilder.group({ - [UserAddFormService.VORNAME]: new FormControl(EMPTY_STRING), - [UserAddFormService.NACHNAME]: new FormControl(EMPTY_STRING), - [UserAddFormService.BENUTZERNAME]: new FormControl(EMPTY_STRING), - [UserAddFormService.EMAIL]: new FormControl(EMPTY_STRING), - [UserAddFormService.ROLLEN_GROUP]: this.formBuilder.group({ - [UserAddFormService.ADMINISTRATION_GROUP]: this.formBuilder.group({ - [UserAddFormService.ADMIN]: new FormControl(false), - }), - [UserAddFormService.ALFA_GROUP]: this.formBuilder.group({ - [UserAddFormService.LOESCHEN]: new FormControl(false), - [UserAddFormService.USER]: new FormControl(false), - [UserAddFormService.POSTSTELLE]: new FormControl(false), - }), - }), - }); - } - - protected initAlfaGroupLogic(): void { - const alfaGroup: UntypedFormGroup = this.getAlfaGroup(); - alfaGroup.valueChanges.subscribe(() => { - this.handleAlfaGroupChange(alfaGroup); - }); - } - - private getAlfaGroup(): UntypedFormGroup { - return <UntypedFormGroup>this.form.get(UserAddFormService.ROLLEN_GROUP).get(UserAddFormService.ALFA_GROUP); - } - - handleAlfaGroupChange(group: UntypedFormGroup): void { - const anyChecked: boolean = this.isAnyChecked(group); - if (anyChecked) { - this.disableUncheckedCheckboxes(group); - } else { - this.enableAllCheckboxes(group); - } - } - - isAnyChecked(group: UntypedFormGroup): boolean { - return Object.keys(group.controls).some((key) => group.controls[key].value); - } - - disableUncheckedCheckboxes(alfaGroup: UntypedFormGroup): void { - for (const control of Object.values<AbstractControl>(alfaGroup.controls)) { - if (control.value === false) control.disable({ emitEvent: false }); - } - } - - enableAllCheckboxes(group: UntypedFormGroup): void { - for (const control of Object.values<AbstractControl>(group.controls)) { - control.enable({ emitEvent: false }); - } - } - - protected doSubmit(): Observable<StateResource<Resource>> { - throw new Error('Method not implemented.'); - } - - protected getPathPrefix(): string { - return UserAddFormService.USER_ADD_PREFIX; - } -} diff --git a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-formservice.spec.ts deleted file mode 100644 index fef180f7f196639c25f2ab265c343a14d7d64d2e..0000000000000000000000000000000000000000 --- a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add-formservice.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (C) 2024 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 { fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; -import { UserAddFormService } from './user-add-form.service'; -import SpyInstance = jest.SpyInstance; - -describe('UserAddFormService', () => { - let formService: UserAddFormService; - let alfaGroup: UntypedFormGroup; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [UserAddFormService, UntypedFormBuilder], - }); - - formService = TestBed.inject(UserAddFormService); - alfaGroup = <UntypedFormGroup>formService.form.get(UserAddFormService.ROLLEN_GROUP).get(UserAddFormService.ALFA_GROUP); - }); - - it('should create', () => { - expect(formService).toBeTruthy(); - }); - - describe('initAlfaGroupLogic', () => { - it('should call handleAlfaGroupChange when value of form element changes', fakeAsync(() => { - const handleAlfaGroupChangeSpy: SpyInstance = jest.spyOn(formService as any, 'handleAlfaGroupChange'); - - alfaGroup.get(UserAddFormService.LOESCHEN).setValue(true); - - tick(); - - expect(handleAlfaGroupChangeSpy).toHaveBeenCalled(); - })); - }); - - describe('handleAlfaGroupChange', () => { - it('should call disableUncheckedCheckboxes if any checkbox is checked', () => { - jest.spyOn(formService as any, 'isAnyChecked').mockReturnValue(true); - const disableUncheckedCheckboxesSpy: SpyInstance = jest.spyOn(formService as any, 'disableUncheckedCheckboxes'); - - formService.handleAlfaGroupChange(alfaGroup); - - expect(disableUncheckedCheckboxesSpy).toHaveBeenCalled(); - }); - - it('should call enableAllCheckboxes if not any checkbox is checked', () => { - jest.spyOn(formService as any, 'isAnyChecked').mockReturnValue(false); - const enableAllCheckboxesSpy: SpyInstance = jest.spyOn(formService as any, 'enableAllCheckboxes'); - - formService.handleAlfaGroupChange(alfaGroup); - - expect(enableAllCheckboxesSpy).toHaveBeenCalled(); - }); - }); - - describe('isAnyChecked', () => { - it('should return false if no checkbox is checked', () => { - const result = formService.isAnyChecked(alfaGroup); - - expect(result).toBe(false); - }); - - it('should return true if any checkbox is checked', () => { - alfaGroup.get(UserAddFormService.LOESCHEN).setValue(true); - - const result = formService.isAnyChecked(alfaGroup); - - expect(result).toBe(true); - }); - }); - - describe('disableUncheckedCheckboxes', () => { - it('if control value is false then control should be disabled', () => { - const control: AbstractControl = alfaGroup.get(UserAddFormService.LOESCHEN); - control.setValue(false); - - formService.disableUncheckedCheckboxes(alfaGroup); - - expect(control.disabled).toBe(true); - }); - - it('if control value is true then control should NOT be disabled', () => { - const control: AbstractControl = alfaGroup.get(UserAddFormService.LOESCHEN); - control.setValue(true); - - formService.disableUncheckedCheckboxes(alfaGroup); - - expect(control.disabled).toBe(false); - }); - }); - - describe('updateCheckboxStates', () => { - it('if control value is false then control should be disabled', () => { - const control: AbstractControl = alfaGroup.get(UserAddFormService.LOESCHEN); - control.setValue(false); - - formService.disableUncheckedCheckboxes(alfaGroup); - - expect(control.disabled).toBe(true); - }); - }); - - describe('enableAllCheckboxes', () => { - it('if control value is true then control should be enabled', () => { - const control: AbstractControl = alfaGroup.get(UserAddFormService.LOESCHEN); - const enableSpy = jest.spyOn(control, 'enable'); - - formService.enableAllCheckboxes(alfaGroup); - - expect(enableSpy).toHaveBeenCalled(); - }); - }); -}); diff --git a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add.formservice.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..93b68bb79ea62249b7864cd63bc3f5d706e0dfcf --- /dev/null +++ b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add.formservice.spec.ts @@ -0,0 +1,284 @@ +/* + * Copyright (C) 2024 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 { ROUTES } from '@admin-client/shared'; +import { User, UserService } from '@admin-client/user-shared'; +import { NavigationService } from '@alfa-client/navigation-shared'; +import { createEmptyStateResource, createStateResource } from '@alfa-client/tech-shared'; +import { Mock, mock } from '@alfa-client/test-utils'; +import { SnackBarService } from '@alfa-client/ui'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { AbstractControl, UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { cold } from 'jest-marbles'; +import { createUser } from 'libs/admin/user-shared/test/user'; +import { singleCold } from 'libs/tech-shared/test/marbles'; +import { of, throwError } from 'rxjs'; +import { UserAddFormservice } from './user-add.formservice'; +import SpyInstance = jest.SpyInstance; + +describe('UserAddFormService', () => { + let formService: UserAddFormservice; + let roleGroup: UntypedFormGroup; + let alfaGroup: UntypedFormGroup; + let administrationGroup: UntypedFormGroup; + + let userService: Mock<UserService>; + let navigationService: Mock<NavigationService>; + let snackBarService: Mock<SnackBarService>; + + beforeEach(() => { + userService = { ...mock(UserService), refresh: jest.fn() }; + navigationService = mock(NavigationService); + snackBarService = mock(SnackBarService); + + TestBed.configureTestingModule({ + providers: [ + UserAddFormservice, + UntypedFormBuilder, + { provide: UserService, useValue: userService }, + { provide: NavigationService, useValue: navigationService }, + { provide: SnackBarService, useValue: snackBarService }, + ], + }); + + formService = TestBed.inject(UserAddFormservice); + roleGroup = <UntypedFormGroup>formService.form.get(UserAddFormservice.ROLLEN_GROUP); + alfaGroup = <UntypedFormGroup>roleGroup.get(UserAddFormservice.ALFA_GROUP); + administrationGroup = <UntypedFormGroup>roleGroup.get(UserAddFormservice.ADMINISTRATION_GROUP); + }); + + it('should create', () => { + expect(formService).toBeTruthy(); + }); + + describe('initAlfaGroupLogic', () => { + it('should call handleAlfaGroupChange when value of form element changes', fakeAsync(() => { + const handleAlfaGroupChangeSpy: SpyInstance = jest.spyOn(formService as any, 'handleAlfaGroupChange'); + + alfaGroup.get(UserAddFormservice.LOESCHEN).setValue(true); + + tick(); + + expect(handleAlfaGroupChangeSpy).toHaveBeenCalled(); + })); + }); + + describe('roleValidator', () => { + it('should return error if no role is selected', () => { + const result = formService.roleValidator()(roleGroup); + + expect(result).toEqual({ atLeastOneRoleSelected: true }); + }); + + it('should return null if at least one role is selected', () => { + alfaGroup.get(UserAddFormservice.LOESCHEN).setValue(true); + + const result = formService.roleValidator()(roleGroup); + + expect(result).toBeNull(); + }); + }); + + describe('handleAlfaGroupChange', () => { + it('should call disableUncheckedCheckboxes if any checkbox is checked', () => { + jest.spyOn(formService as any, 'isAnyChecked').mockReturnValue(true); + const disableUncheckedCheckboxesSpy: SpyInstance = jest.spyOn(formService as any, 'disableUncheckedCheckboxes'); + + formService.handleAlfaGroupChange(alfaGroup); + + expect(disableUncheckedCheckboxesSpy).toHaveBeenCalled(); + }); + + it('should call enableAllCheckboxes if not any checkbox is checked', () => { + jest.spyOn(formService as any, 'isAnyChecked').mockReturnValue(false); + const enableAllCheckboxesSpy: SpyInstance = jest.spyOn(formService as any, 'enableAllCheckboxes'); + + formService.handleAlfaGroupChange(alfaGroup); + + expect(enableAllCheckboxesSpy).toHaveBeenCalled(); + }); + }); + + describe('isAnyChecked', () => { + it('should return false if no checkbox is checked', () => { + const result = formService.isAnyChecked(alfaGroup); + + expect(result).toBe(false); + }); + + it('should return true if any checkbox is checked', () => { + alfaGroup.get(UserAddFormservice.LOESCHEN).setValue(true); + + const result = formService.isAnyChecked(alfaGroup); + + expect(result).toBe(true); + }); + }); + + describe('disableUncheckedCheckboxes', () => { + it('if control value is false then control should be disabled', () => { + const control: AbstractControl = alfaGroup.get(UserAddFormservice.LOESCHEN); + control.setValue(false); + + formService.disableUncheckedCheckboxes(alfaGroup); + + expect(control.disabled).toBe(true); + }); + + it('if control value is true then control should NOT be disabled', () => { + const control: AbstractControl = alfaGroup.get(UserAddFormservice.LOESCHEN); + control.setValue(true); + + formService.disableUncheckedCheckboxes(alfaGroup); + + expect(control.disabled).toBe(false); + }); + }); + + describe('updateCheckboxStates', () => { + it('if control value is false then control should be disabled', () => { + const control: AbstractControl = alfaGroup.get(UserAddFormservice.LOESCHEN); + control.setValue(false); + + formService.disableUncheckedCheckboxes(alfaGroup); + + expect(control.disabled).toBe(true); + }); + }); + + describe('enableAllCheckboxes', () => { + it('if control value is true then control should be enabled', () => { + const control: AbstractControl = alfaGroup.get(UserAddFormservice.LOESCHEN); + const enableSpy = jest.spyOn(control, 'enable'); + + formService.enableAllCheckboxes(alfaGroup); + + expect(enableSpy).toHaveBeenCalled(); + }); + }); + + describe('doSubmit', () => { + const user: User = createUser(); + + beforeEach(() => { + userService.createInKeycloak.mockReturnValue(of(user)); + }); + + it('should call createUser', () => { + const createUserSpy: SpyInstance = jest.spyOn(formService as any, 'createUser'); + + formService.submit(); + + expect(createUserSpy).toHaveBeenCalled(); + }); + + it('should call userService createInKeycloak', () => { + formService.submit(); + + expect(userService.createInKeycloak).toHaveBeenCalled(); + }); + + it('should return user as stateResource', fakeAsync(() => { + userService.createInKeycloak.mockReturnValue(singleCold(user, '-a')); + + const result = formService.submit(); + + expect(result).toBeObservable( + cold('ab', { + a: createEmptyStateResource<User>(true), + b: createStateResource(user), + }), + ); + })); + + it('should call handleOnCreateUserSuccess', fakeAsync(() => { + const handleOnCreateUserSuccessSpy: SpyInstance = jest.spyOn(formService, 'handleOnCreateUserSuccess'); + + formService.submit().subscribe(); + tick(); + + expect(handleOnCreateUserSuccessSpy).toHaveBeenCalled(); + })); + + it('should call handleSubmitError on error', fakeAsync(() => { + userService.createInKeycloak.mockReturnValue(throwError(() => new Error())); + const handleSubmitErrorSpy: SpyInstance = jest.spyOn(formService, 'handleSubmitError'); + + formService.submit().subscribe(); + tick(); + + expect(handleSubmitErrorSpy).toHaveBeenCalled(); + })); + }); + + describe('handleOnCreateUserSuccess', () => { + it('should show success message', () => { + formService.handleOnCreateUserSuccess(); + + expect(snackBarService.showInfo).toHaveBeenCalledWith('Der Benutzer wurde hinzugefügt.'); + }); + + it('should navigate back to user list', () => { + formService.handleOnCreateUserSuccess(); + + expect(navigationService.navigate).toHaveBeenCalledWith(ROUTES.BENUTZER_UND_ROLLEN); + }); + + it('should refresh userList', () => { + formService.handleOnCreateUserSuccess(); + + expect(userService.refresh).toHaveBeenCalled(); + }); + }); + + describe('handleSubmitError', () => { + it('should return empty stateResource', () => { + const result = formService.handleSubmitError(); + + expect(result).toBeObservable(cold('(a|)', { a: createEmptyStateResource<User>() })); + }); + + it('should show error message', fakeAsync(() => { + formService.handleSubmitError().subscribe(); + tick(); + + expect(snackBarService.showError).toHaveBeenCalledWith('Der Benutzer konnte nicht hinzugefügt werden.'); + })); + }); + + describe('getRoles', () => { + it('should return no roles when none are active', () => { + const result: string[] = formService.getRoles(UserAddFormservice.ALFA_GROUP); + + expect(result).toEqual([]); + }); + + it('should return poststelle role when active', () => { + alfaGroup.get(UserAddFormservice.POSTSTELLE).setValue(true); + + const result: string[] = formService.getRoles(UserAddFormservice.ALFA_GROUP); + + expect(result).toEqual([UserAddFormservice.POSTSTELLE]); + }); + }); +}); diff --git a/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add.formservice.ts b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add.formservice.ts new file mode 100644 index 0000000000000000000000000000000000000000..5a6769f495ca2e04048c715bd878959a94486395 --- /dev/null +++ b/alfa-client/libs/admin/user/src/lib/users-roles/user-add-form/user-add.formservice.ts @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2024 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 { ROUTES } from '@admin-client/shared'; +import { User, UserService } from '@admin-client/user-shared'; +import { NavigationService } from '@alfa-client/navigation-shared'; +import { + AbstractFormService, + createEmptyStateResource, + createStateResource, + EMPTY_STRING, + StateResource, +} from '@alfa-client/tech-shared'; +import { SnackBarService } from '@alfa-client/ui'; +import { Injectable } from '@angular/core'; +import { + AbstractControl, + FormControl, + UntypedFormBuilder, + UntypedFormGroup, + ValidationErrors, + ValidatorFn, + Validators, +} from '@angular/forms'; +import { catchError, map, Observable, of, startWith, tap } from 'rxjs'; + +@Injectable() +export class UserAddFormservice extends AbstractFormService<User> { + public static readonly VORNAME: string = 'vorname'; + public static readonly NACHNAME: string = 'nachname'; + public static readonly BENUTZERNAME: string = 'benutzername'; + public static readonly EMAIL: string = 'e-mail'; + + public static readonly ROLLEN_GROUP: string = 'rollen'; + public static readonly ADMINISTRATION_GROUP: string = 'administration'; + public static readonly ADMIN: string = 'ADMIN_ADMIN'; + public static readonly ALFA_GROUP: string = 'alfa'; + public static readonly LOESCHEN: string = 'VERWALTUNG_LOESCHEN'; + public static readonly USER: string = 'VERWALTUNG_USER'; + public static readonly POSTSTELLE: string = 'VERWALTUNG_POSTSTELLE'; + + public static readonly USER_ADD_PREFIX: string = 'userAdd'; + + constructor( + public formBuilder: UntypedFormBuilder, + private userService: UserService, + private navigationService: NavigationService, + private snackBarService: SnackBarService, + ) { + super(formBuilder); + this.initAlfaGroupLogic(); + } + + protected initForm(): UntypedFormGroup { + return this.formBuilder.group({ + [UserAddFormservice.VORNAME]: new FormControl(EMPTY_STRING, Validators.required), + [UserAddFormservice.NACHNAME]: new FormControl(EMPTY_STRING, Validators.required), + [UserAddFormservice.BENUTZERNAME]: new FormControl(EMPTY_STRING, Validators.required), + [UserAddFormservice.EMAIL]: new FormControl(EMPTY_STRING, [Validators.required, Validators.email]), + [UserAddFormservice.ROLLEN_GROUP]: this.formBuilder.group( + { + [UserAddFormservice.ADMINISTRATION_GROUP]: this.formBuilder.group({ + [UserAddFormservice.ADMIN]: new FormControl(false), + }), + [UserAddFormservice.ALFA_GROUP]: this.formBuilder.group({ + [UserAddFormservice.LOESCHEN]: new FormControl(false), + [UserAddFormservice.USER]: new FormControl(false), + [UserAddFormservice.POSTSTELLE]: new FormControl(false), + }), + }, + { validators: this.roleValidator() }, + ), + }); + } + + roleValidator(): ValidatorFn { + return (control: AbstractControl<UntypedFormGroup>): ValidationErrors | null => { + const rolesGroups: UntypedFormGroup = control.value; + + if (this.anyRoleSelected(rolesGroups)) { + return null; + } + + return { atLeastOneRoleSelected: true }; + }; + } + + private anyRoleSelected(rolesGroups: UntypedFormGroup): boolean { + for (const subGroup of Object.values(rolesGroups)) { + if (Object.values(subGroup).includes(true)) { + return true; + } + } + return false; + } + + protected initAlfaGroupLogic(): void { + const alfaGroup: UntypedFormGroup = this.getRoleGroup(UserAddFormservice.ALFA_GROUP); + alfaGroup.valueChanges.subscribe(() => { + this.handleAlfaGroupChange(alfaGroup); + }); + } + + handleAlfaGroupChange(group: UntypedFormGroup): void { + const anyChecked: boolean = this.isAnyChecked(group); + if (anyChecked) { + this.disableUncheckedCheckboxes(group); + } else { + this.enableAllCheckboxes(group); + } + } + + isAnyChecked(group: UntypedFormGroup): boolean { + return Object.keys(group.controls).some((key) => group.controls[key].value); + } + + disableUncheckedCheckboxes(alfaGroup: UntypedFormGroup): void { + for (const control of Object.values<AbstractControl>(alfaGroup.controls)) { + if (control.value === false) control.disable({ emitEvent: false }); + } + } + + enableAllCheckboxes(group: UntypedFormGroup): void { + for (const control of Object.values<AbstractControl>(group.controls)) { + control.enable({ emitEvent: false }); + } + } + + protected doSubmit(): Observable<StateResource<User>> { + const user: Partial<User> = this.createUser(); + return this.userService.createInKeycloak(user).pipe( + map((createdUser: User): StateResource<User> => createStateResource(createdUser)), + tap(() => this.handleOnCreateUserSuccess()), + catchError((): Observable<StateResource<User>> => this.handleSubmitError()), + startWith(createEmptyStateResource<User>(true)), + ); + } + + handleOnCreateUserSuccess(): void { + this.snackBarService.showInfo('Der Benutzer wurde hinzugefügt.'); + this.navigationService.navigate(ROUTES.BENUTZER_UND_ROLLEN); + this.userService.refresh(); + } + + handleSubmitError(): Observable<StateResource<User>> { + this.snackBarService.showError('Der Benutzer konnte nicht hinzugefügt werden.'); + return of(createEmptyStateResource<User>()); + } + + createUser(): Partial<User> { + return { + email: this.form.get(UserAddFormservice.EMAIL).value, + username: this.form.get(UserAddFormservice.BENUTZERNAME).value, + firstName: this.form.get(UserAddFormservice.VORNAME).value, + lastName: this.form.get(UserAddFormservice.NACHNAME).value, + enabled: true, + clientRoles: { + alfa: this.getRoles(UserAddFormservice.ALFA_GROUP), + admin: this.getRoles(UserAddFormservice.ADMINISTRATION_GROUP), + }, + }; + } + + getRoles(roleGroup: string): string[] { + return this.getActiveRoles(this.getRoleGroup(roleGroup)); + } + + private getActiveRoles(formGroup: UntypedFormGroup): string[] { + return Object.keys(formGroup.controls).filter((key) => formGroup.get(key).value); + } + + private getRoleGroup(roleGroup: string): UntypedFormGroup { + return <UntypedFormGroup>this.form.get(UserAddFormservice.ROLLEN_GROUP).get(roleGroup); + } + + protected getPathPrefix(): string { + return UserAddFormservice.USER_ADD_PREFIX; + } +} diff --git a/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.html b/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.html index a6c6bab29fd949acc9456e737c89e9288ebadff4..4fb48e8cbc912f251454cf28d5e7a05109b93384 100644 --- a/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.html +++ b/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.html @@ -38,7 +38,7 @@ <dl class="flex flex-wrap gap-2"> <dt class="sr-only">Rollen:</dt> <dd - *ngFor="let role of user.roles" + *ngFor="let role of getRoles(user)" class="inline-flex flex-shrink-0 items-center rounded-full bg-green-50 px-1.5 py-0.5 text-sm font-medium text-green-700 ring-1 ring-inset ring-green-600/20" > {{ role }} diff --git a/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.spec.ts b/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.spec.ts index 4f8219c95ede01238172d6191c53b42a587a78c5..8532424baa0925e4b9fa9b71525382b98edb8e0c 100644 --- a/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.spec.ts @@ -85,6 +85,15 @@ describe('UsersRolesComponent', () => { expect(routerSpy).toHaveBeenCalledWith([ROUTES.BENUTZER_UND_ROLLEN_NEU]); }); }); + + describe('getRoles', () => { + it('should return roles', () => { + const user = createUser(); + const roles = component.getRoles(user); + + expect(roles).toEqual([...user.clientRoles.alfa, ...user.clientRoles.admin]); + }); + }); }); describe('template', () => { diff --git a/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.ts b/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.ts index 16e5dad34bd2ab5fb2af1dfa5d0bae9ddff7cc04..027e57aec2f26b7d10a359a306678c6fc36000c6 100644 --- a/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.ts +++ b/alfa-client/libs/admin/user/src/lib/users-roles/users-roles.component.ts @@ -50,13 +50,20 @@ export class UsersRolesComponent implements OnInit { private userService = inject(UserService); public users$: Observable<StateResource<User[]>>; - public readonly GROUPS_TO_DISPLAY = 3; + public readonly GROUPS_TO_DISPLAY: number = 3; ngOnInit() { this.users$ = this.userService.get(); } - navigateToAddUser(): void { + public navigateToAddUser(): void { this.router.navigate([ROUTES.BENUTZER_UND_ROLLEN_NEU]); } + + getRoles(user: User): string[] { + return Object.keys(user.clientRoles).reduce((acc: string[], client: string): string[] => { + acc.push(...user.clientRoles[client]); + return acc; + }, []); + } } diff --git a/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts b/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts index 4e39dc6551fae399b2557a29e47c88b329fc6a39..8628c7d5a2ad48b0e493e132d66c77ff3d8970b9 100644 --- a/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts +++ b/alfa-client/libs/api-root-shared/src/lib/api-root.linkrel.ts @@ -62,4 +62,5 @@ export enum ApiRootLinkRel { HINTS = 'hints', RESOURCE = 'resource', ORGANISATIONS_EINHEIT = 'organisationsEinheiten', + USERS = 'users', } diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts b/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts index bd176bb9141d31dce01662b6431a1222be6a2506..486415f425de82475e9733ec3f5800cb47128c92 100644 --- a/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts +++ b/alfa-client/libs/authentication/src/lib/authentication.service.spec.ts @@ -23,30 +23,39 @@ */ import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { UserProfileResource } from '@alfa-client/user-profile-shared'; +import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc'; +import { Environment } from 'libs/environment-shared/src/lib/environment.model'; import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile'; -import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; -import { AuthenticationService } from './authentication.service'; -import { createAuthConfig } from '../../test/authentication'; +import { Subject } from 'rxjs'; import { createEnvironment } from '../../../environment-shared/test/environment'; -import { Environment } from 'libs/environment-shared/src/lib/environment.model'; +import { createAuthConfig, createOAuthEvent } from '../../test/authentication'; +import { AuthenticationService } from './authentication.service'; describe('AuthenticationService', () => { let service: AuthenticationService; let oAuthService: Mock<OAuthService>; let environmentConfig: Environment; + let eventsSubject: Subject<OAuthEvent>; + beforeEach(() => { + eventsSubject = new Subject<OAuthEvent>(); + oAuthService = <any>{ ...mock(OAuthService), loadDiscoveryDocumentAndLogin: jest.fn().mockResolvedValue(() => Promise.resolve()), + hasValidAccessToken: jest.fn(), + hasValidIdToken: jest.fn(), }; + Object.defineProperty(oAuthService, 'events', { get: () => eventsSubject }); + environmentConfig = createEnvironment(); service = new AuthenticationService(useFromMock(oAuthService), environmentConfig); }); describe('login', () => { beforeEach(() => { - service.setCurrentUser = jest.fn(); + service.buildAuthEventPromise = jest.fn(); }); it('should configure service with authConfig', async () => { @@ -72,16 +81,227 @@ describe('AuthenticationService', () => { expect(oAuthService.tokenValidationHandler).not.toBeNull(); }); + it('should build auth event promise', async () => { + service.buildAuthEventPromise = jest.fn().mockResolvedValue(() => Promise.resolve()); + + await service.login(); + + expect(service.buildAuthEventPromise).toHaveBeenCalled(); + }); + it('should load discovery document and login', () => { service.login(); expect(oAuthService.loadDiscoveryDocumentAndLogin).toHaveBeenCalled(); }); - it('should set current user', async () => { - await service.login(); + it('should return eventPromise', async () => { + const promise: Promise<void> = Promise.resolve(); + service.buildAuthEventPromise = jest.fn().mockResolvedValue(promise); + + const returnPromise: Promise<void> = service.login(); + + await expect(returnPromise).resolves.toBeUndefined(); + }); + }); + + describe('build auth event promise', () => { + const event: OAuthEvent = createOAuthEvent(); + + beforeEach(() => { + service.shouldProceedByAuthEvent = jest.fn().mockReturnValue(true); + service.setCurrentUser = jest.fn(); + service.unsubscribeEvents = jest.fn(); + }); + + it('should call shouldProceedByAuthEvent on event trigger', () => { + service.buildAuthEventPromise(); + eventsSubject.next(event); + + expect(service.shouldProceedByAuthEvent).toHaveBeenCalledWith(event); + }); + + describe('on next', () => { + it('should set current user', () => { + service.buildAuthEventPromise(); + eventsSubject.next(event); + + expect(service.setCurrentUser).toHaveBeenCalled(); + }); + + it('should unsubscribe event', () => { + service.buildAuthEventPromise(); + eventsSubject.next(event); + + expect(service.unsubscribeEvents).toHaveBeenCalled(); + }); + + it('should resolved promise with a valid event', async () => { + const promise: Promise<void> = service.buildAuthEventPromise(); + eventsSubject.next(event); + + await expect(promise).resolves.toBeUndefined(); + }); + }); + + describe('on error', () => { + const errorMessage: string = 'Test Error'; + const error: Error = new Error(errorMessage); + + it('should unsubscribe event', () => { + service.buildAuthEventPromise(); + eventsSubject.error(error); + + expect(service.unsubscribeEvents).toHaveBeenCalled(); + }); + + it('should reject the promise with an error', async () => { + const promise: Promise<void> = service.buildAuthEventPromise(); + + eventsSubject.error(error); + + await expect(promise).rejects.toThrow(errorMessage); + }); + }); + }); + + describe('should proceed by auth event', () => { + const event: OAuthEvent = createOAuthEvent(); + + it('should call considered as login event', () => { + service.consideredAsLoginEvent = jest.fn(); + + service.shouldProceedByAuthEvent(event); + + expect(service.consideredAsLoginEvent).toHaveBeenCalledWith(event.type); + }); + + it('should return true on login event', () => { + service.consideredAsLoginEvent = jest.fn().mockReturnValue(true); + + const proceed: boolean = service.shouldProceedByAuthEvent(event); + + expect(proceed).toBeTruthy(); + }); + + it('should call considered as page reload event', () => { + service.consideredAsLoginEvent = jest.fn().mockReturnValue(false); + service.consideredAsPageReloadEvent = jest.fn(); + + service.shouldProceedByAuthEvent(event); + + expect(service.consideredAsPageReloadEvent).toHaveBeenCalledWith(event.type); + }); + + it('should return true on page reload event', () => { + service.consideredAsLoginEvent = jest.fn().mockReturnValue(false); + service.consideredAsPageReloadEvent = jest.fn().mockReturnValue(true); + + const proceed: boolean = service.shouldProceedByAuthEvent(event); + + expect(proceed).toBeTruthy(); + }); + + it('should return false on non login or page reload event', () => { + service.consideredAsLoginEvent = jest.fn().mockReturnValue(false); + service.consideredAsPageReloadEvent = jest.fn().mockReturnValue(false); + + const proceed: boolean = service.shouldProceedByAuthEvent(event); + + expect(proceed).toBeFalsy(); + }); + }); + + describe('consideredAsLoginEvent', () => { + it('should return true if event is "token_received"', () => { + const event: string = 'token_received'; + + const result: boolean = service.consideredAsLoginEvent(event); + + expect(result).toBeTruthy(); + }); + + it('should return false if event is not "token_received"', () => { + const event: string = 'something_else'; + + const result: boolean = service.consideredAsLoginEvent(event); + + expect(result).toBeFalsy(); + }); + }); + + describe('consideredAsPageReloadEvent', () => { + it('should return true if event is "discovery_document_loaded" and tokens are valid', () => { + service.hasValidToken = jest.fn().mockReturnValue(true); + const event: string = 'discovery_document_loaded'; + + const result: boolean = service.consideredAsPageReloadEvent(event); + + expect(result).toBeTruthy(); + }); + + it('should return false if event is "discovery_document_loaded" and tokens are invalid', () => { + service.hasValidToken = jest.fn().mockReturnValue(false); + const event: string = 'discovery_document_loaded'; + + const result: boolean = service.consideredAsPageReloadEvent(event); + + expect(result).toBeFalsy(); + }); + + it('should return false if event is not "discovery_document_loaded" and tokens are valid', () => { + service.hasValidToken = jest.fn().mockReturnValue(true); + const event: string = 'something_else'; + + const result: boolean = service.consideredAsPageReloadEvent(event); + + expect(result).toBeFalsy(); + }); + + it('should return false if event is not "discovery_document_loaded" and tokens are invalid', () => { + service.hasValidToken = jest.fn().mockReturnValue(false); + const event: string = 'something_else'; + + const result: boolean = service.consideredAsPageReloadEvent(event); + + expect(result).toBeFalsy(); + }); + }); + + describe('hasValidToken', () => { + it('should return true if both tokens are valid', () => { + oAuthService.hasValidAccessToken.mockReturnValue(true); + oAuthService.hasValidIdToken.mockReturnValue(true); + + const result: boolean = service.hasValidToken(); + + expect(result).toBeTruthy(); + }); + + it('should return false if access token is invalid', () => { + oAuthService.hasValidAccessToken.mockReturnValue(false); + + const result: boolean = service.hasValidToken(); + + expect(result).toBeFalsy(); + }); + + it('should return false if id token is invalid', () => { + oAuthService.hasValidAccessToken.mockReturnValue(true); + oAuthService.hasValidIdToken.mockReturnValue(false); + + const result: boolean = service.hasValidToken(); + + expect(result).toBeFalsy(); + }); + + it('should return false if both tokens are invalid', () => { + oAuthService.hasValidAccessToken.mockReturnValue(false); + oAuthService.hasValidIdToken.mockReturnValue(false); + + const result: boolean = service.hasValidToken(); - expect(service.setCurrentUser).toHaveBeenCalled(); + expect(result).toBeFalsy(); }); }); diff --git a/alfa-client/libs/authentication/src/lib/authentication.service.ts b/alfa-client/libs/authentication/src/lib/authentication.service.ts index 9b094a2ba2b953f715c8389735f6f40e21196ffd..e350dcafe7cb9312799db97db5eb50c0d25c8319 100644 --- a/alfa-client/libs/authentication/src/lib/authentication.service.ts +++ b/alfa-client/libs/authentication/src/lib/authentication.service.ts @@ -23,15 +23,18 @@ */ import { Environment, ENVIRONMENT_CONFIG } from '@alfa-client/environment-shared'; import { Inject, Injectable } from '@angular/core'; -import { AuthConfig, OAuthService } from 'angular-oauth2-oidc'; +import { AuthConfig, OAuthEvent, OAuthService } from 'angular-oauth2-oidc'; import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks'; import { UserProfileResource } from 'libs/user-profile-shared/src/lib/user-profile.model'; import { getUserNameInitials } from 'libs/user-profile-shared/src/lib/user-profile.util'; +import { filter, Subscription } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AuthenticationService { currentUserResource: UserProfileResource; + private eventSubscription: Subscription; + constructor( private oAuthService: OAuthService, @Inject(ENVIRONMENT_CONFIG) private envConfig: Environment, @@ -41,18 +44,52 @@ export class AuthenticationService { this.oAuthService.configure(this.buildConfiguration()); this.oAuthService.setupAutomaticSilentRefresh(); this.oAuthService.tokenValidationHandler = new JwksValidationHandler(); + + const eventPromise: Promise<void> = this.buildAuthEventPromise(); await this.oAuthService.loadDiscoveryDocumentAndLogin(); - this.setCurrentUser(); + return eventPromise; + } + + buildAuthEventPromise(): Promise<void> { + return new Promise<void>((resolve, reject) => this.handleAuthEventsForPromise(resolve, reject)); + } + + private handleAuthEventsForPromise(resolve: (value: void | PromiseLike<void>) => void, reject: (reason?: any) => void): void { + this.eventSubscription = this.oAuthService.events + .pipe(filter((event: OAuthEvent) => this.shouldProceedByAuthEvent(event))) + .subscribe({ + next: () => { + this.setCurrentUser(); + this.unsubscribeEvents(); + resolve(); + }, + error: (error: any) => { + this.unsubscribeEvents(); + reject(error); + }, + }); + } + + shouldProceedByAuthEvent(event: OAuthEvent): boolean { + return this.consideredAsLoginEvent(event.type) || this.consideredAsPageReloadEvent(event.type); + } + + consideredAsLoginEvent(eventType: string): boolean { + return eventType === 'token_received'; + } + + consideredAsPageReloadEvent(eventType: string): boolean { + return eventType === 'discovery_document_loaded' && this.hasValidToken(); + } + + hasValidToken(): boolean { + return this.oAuthService.hasValidAccessToken() && this.oAuthService.hasValidIdToken(); } buildConfiguration(): AuthConfig { return { issuer: this.envConfig.authServer + '/realms/' + this.envConfig.realm, - tokenEndpoint: - this.envConfig.authServer + - '/realms/' + - this.envConfig.realm + - '/protocol/openid-connect/token', + tokenEndpoint: this.envConfig.authServer + '/realms/' + this.envConfig.realm + '/protocol/openid-connect/token', redirectUri: window.location.origin + '/', clientId: this.envConfig.clientId, scope: 'openid profile', @@ -71,6 +108,10 @@ export class AuthenticationService { this.currentUserResource = userResource; } + unsubscribeEvents(): void { + this.eventSubscription.unsubscribe(); + } + public getCurrentUserInitials(): string { return getUserNameInitials(this.currentUserResource); } diff --git a/alfa-client/libs/authentication/test/authentication.ts b/alfa-client/libs/authentication/test/authentication.ts index 34223905913df6f0a941a02f0b8a63723a959b50..3cf143c030717a12d98d8d6c75e0188e586b34b7 100644 --- a/alfa-client/libs/authentication/test/authentication.ts +++ b/alfa-client/libs/authentication/test/authentication.ts @@ -21,8 +21,13 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { AuthConfig } from 'angular-oauth2-oidc'; +import { faker } from '@faker-js/faker'; +import { AuthConfig, OAuthEvent } from 'angular-oauth2-oidc'; export function createAuthConfig(): AuthConfig { return {}; } + +export function createOAuthEvent(): OAuthEvent { + return { type: <any>faker.lorem.word() }; +} diff --git a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts index 9c15cc2b3f6ad8c021b0367f88dc7c37d41597dc..0f2d961a4b48fae71acf2ed14685e34697da1b22 100644 --- a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts +++ b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts @@ -21,12 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { - BlobWithFileName, - StateResource, - createEmptyStateResource, - createStateResource, -} from '@alfa-client/tech-shared'; +import { BlobWithFileName, StateResource, createEmptyStateResource, createStateResource } from '@alfa-client/tech-shared'; import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { SnackBarService } from '@alfa-client/ui'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; @@ -35,13 +30,10 @@ import { faker } from '@faker-js/faker'; import { Resource, ResourceUri } from '@ngxp/rest'; import { cold, hot } from 'jest-marbles'; import { createBinaryFileResource, createBlob } from 'libs/binary-file-shared/test/binary-file'; -import { - VALIDATION_MESSAGES, - ValidationMessageCode, -} from 'libs/tech-shared/src/lib/validation/tech.validation.messages'; +import { VALIDATION_MESSAGES, ValidationMessageCode } from 'libs/tech-shared/src/lib/validation/tech.validation.messages'; import { DummyLinkRel } from 'libs/tech-shared/test/dummy'; import { createDummyResource } from 'libs/tech-shared/test/resource'; -import { Observable, of } from 'rxjs'; +import { Observable, of, throwError } from 'rxjs'; import { createHttpErrorResponse } from '../../../tech-shared/test/http'; import { singleHot } from '../../../tech-shared/test/marbles'; import { BinaryFileResource } from './binary-file.model'; @@ -85,10 +77,7 @@ describe('BinaryFileService', () => { it('should return value', () => { repository.download.mockReturnValue(singleHot(blob, '-a')); - const returnValue: Observable<StateResource<Blob>> = service.downloadFile( - binaryFileResource, - downloadNamePrefix, - ); + const returnValue: Observable<StateResource<Blob>> = service.downloadFile(binaryFileResource, downloadNamePrefix); expect(returnValue).toBeObservable( cold('ab', { @@ -109,18 +98,11 @@ describe('BinaryFileService', () => { it('should save file with prefix', () => { service.saveBinaryFile(blob, binaryFileResource, downloadNamePrefix); - expect(service.save).toHaveBeenCalledWith( - blob, - downloadNamePrefix + '_' + binaryFileResource.name, - ); + expect(service.save).toHaveBeenCalledWith(blob, downloadNamePrefix + '_' + binaryFileResource.name); }); it('should return loaded stateResource', () => { - const result: StateResource<Blob> = service.saveBinaryFile( - blob, - binaryFileResource, - downloadNamePrefix, - ); + const result: StateResource<Blob> = service.saveBinaryFile(blob, binaryFileResource, downloadNamePrefix); expect(result).toEqual(createStateResource(blob)); }); @@ -128,16 +110,36 @@ describe('BinaryFileService', () => { describe('on non existing response', () => { it('should return loading stateResource', () => { - const result: StateResource<Blob> = service.saveBinaryFile( - undefined, - binaryFileResource, - downloadNamePrefix, - ); + const result: StateResource<Blob> = service.saveBinaryFile(undefined, binaryFileResource, downloadNamePrefix); expect(result).toEqual(createEmptyStateResource(true)); }); }); }); + + it('should call handleDownloadError on error', fakeAsync(() => { + service.handleDownloadError = jest.fn().mockReturnValue(of(createEmptyStateResource())); + repository.download.mockReturnValue(throwError(() => new Error('Download error'))); + + service.downloadFile(binaryFileResource, downloadNamePrefix).subscribe(); + tick(); + + expect(service.handleDownloadError).toHaveBeenCalled(); + })); + + describe('handle download error', () => { + it('should call snackbarService showError', () => { + service.handleDownloadError(); + + expect(snackBarService.showError).toHaveBeenCalledWith('Die Datei konnte nicht heruntergeladen werden.'); + }); + + it('should return empty state resource', () => { + const result: Observable<StateResource<Blob>> = service.handleDownloadError(); + + expect(result).toBeObservable(cold('(a|)', { a: createEmptyStateResource() })); + }); + }); }); describe('download archive', () => { @@ -273,10 +275,7 @@ describe('BinaryFileService', () => { }); it('should not call snackbarService if not file size exceeded error', () => { - service.handleSnackBar( - buildUnprocessableEntityErrorResponse(ValidationMessageCode.FIELD_EMPTY), - true, - ); + service.handleSnackBar(buildUnprocessableEntityErrorResponse(ValidationMessageCode.FIELD_EMPTY), true); expect(snackBarService.showError).not.toHaveBeenCalled(); }); diff --git a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts index ff76416759fc55e2e8c20fc75e1a9077061a34b0..e06c03dff2f58d8e33467a52c08fc6a5f5e2f047 100644 --- a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts +++ b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts @@ -60,27 +60,17 @@ export class BinaryFileService { showValidationErrorSnackBar: boolean = true, ): Observable<StateResource<BinaryFileResource>> { return this.repository.uploadFile(resource, linkRel, file).pipe( - mergeMap((response: HttpResponse<Object>) => - this.getFile(response.headers.get(HttpHeader.LOCATION)), - ), - catchError((errorResponse) => - this.handleError(errorResponse.error, showValidationErrorSnackBar), - ), + mergeMap((response: HttpResponse<Object>) => this.getFile(response.headers.get(HttpHeader.LOCATION))), + catchError((errorResponse) => this.handleError(errorResponse.error, showValidationErrorSnackBar)), startWith(createEmptyStateResource<BinaryFileResource>(true)), ); } - private handleError( - errorResponse: HttpErrorResponse, - showValidationErrorSnackBar: boolean, - ): Observable<StateResource<any>> { + private handleError(errorResponse: HttpErrorResponse, showValidationErrorSnackBar: boolean): Observable<StateResource<any>> { return of(this.handleErrorByStatus(errorResponse, showValidationErrorSnackBar)); } - handleErrorByStatus( - error: HttpErrorResponse, - showValidationErrorSnackBar: boolean, - ): StateResource<any> { + handleErrorByStatus(error: HttpErrorResponse, showValidationErrorSnackBar: boolean): StateResource<any> { if (isUnprocessableEntity(error.status)) { this.handleSnackBar(error, showValidationErrorSnackBar); return createErrorStateResource(error.error); @@ -90,22 +80,23 @@ export class BinaryFileService { handleSnackBar(error: HttpErrorResponse, showValidationErrorSnackBar: boolean) { if (showValidationErrorSnackBar && isValidationFieldFileSizeExceedError(error.error)) { - this.snackbarService.showError( - getMessageForInvalidParam(EMPTY_STRING, error.error.invalidParams[0]), - ); + this.snackbarService.showError(getMessageForInvalidParam(EMPTY_STRING, error.error.invalidParams[0])); } } - public downloadFile( - file: BinaryFileResource, - fileNamePrefix: string, - ): Observable<StateResource<any>> { + public downloadFile(file: BinaryFileResource, fileNamePrefix: string): Observable<StateResource<Blob>> { return this.repository.download(file).pipe( map((data) => this.saveBinaryFile(data, file, fileNamePrefix)), - startWith(createEmptyStateResource(true)), + startWith(createEmptyStateResource<Blob>(true)), + catchError(() => this.handleDownloadError()), ); } + handleDownloadError(): Observable<StateResource<Blob>> { + this.snackbarService.showError('Die Datei konnte nicht heruntergeladen werden.'); + return of(createEmptyStateResource<Blob>()); + } + saveBinaryFile(data: any, file: BinaryFileResource, fileNamePrefix: string): StateResource<Blob> { if (isNil(data)) { return createEmptyStateResource(true); @@ -148,10 +139,7 @@ export class BinaryFileService { ); } - public getFiles( - resource: Resource, - linkRel: string, - ): Observable<StateResource<BinaryFileListResource>> { + public getFiles(resource: Resource, linkRel: string): Observable<StateResource<BinaryFileListResource>> { return this.repository.getFiles(resource, linkRel).pipe( map((fileList: BinaryFileListResource) => createStateResource(fileList)), startWith(createEmptyStateResource<BinaryFileListResource>(true)), diff --git a/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file-container.component.ts b/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file-container.component.ts index 900442515338e6700e012dc31e4f7f69b0fe7290..70c92ec582ab178bdef09a16ba7634540491b4dd 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file-container.component.ts +++ b/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file-container.component.ts @@ -21,10 +21,10 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Component, EventEmitter, Input, Output } from '@angular/core'; import { ApiDownloadToken, ApiRootService } from '@alfa-client/api-root-shared'; import { BinaryFileResource, BinaryFileService } from '@alfa-client/binary-file-shared'; import { StateResource } from '@alfa-client/tech-shared'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Observable } from 'rxjs'; @Component({ @@ -39,7 +39,7 @@ export class BinaryFileContainerComponent { @Output() startDelete: EventEmitter<BinaryFileResource> = new EventEmitter(); - fileStateResource$: Observable<StateResource<any>>; + fileStateResource$: Observable<StateResource<Blob>>; downloadToken$: Observable<ApiDownloadToken>; constructor( @@ -48,10 +48,7 @@ export class BinaryFileContainerComponent { ) {} startDownload(file: BinaryFileResource): void { - this.fileStateResource$ = this.binaryFileService.downloadFile( - file, - this.downloadFileNamePrefix, - ); + this.fileStateResource$ = this.binaryFileService.downloadFile(file, this.downloadFileNamePrefix); } getDownloadToken(): void { diff --git a/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file/binary-file.component.html b/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file/binary-file.component.html index bef7fe52816ac23dc5c4fc7dff6e1f7e25a99f1b..ad0a9c152b8652d95b4a737339645dd30283cfff 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file/binary-file.component.html +++ b/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file/binary-file.component.html @@ -23,12 +23,7 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<div - class="container" - [class.deletable]="deletable" - [matTooltip]="file.name" - matTooltipClass="word-break" -> +<div class="container" [class.deletable]="deletable" [tooltip]="file.name"> <div class="download-container" [class.downloadable]="file | hasLink: fileLinkRel.DOWNLOAD" diff --git a/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file/binary-file.component.spec.ts b/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file/binary-file.component.spec.ts index 8aaaf66f276b3cadf34437f02d4fcff5e48eb0e3..975c2bf58293f58f8a9f02ad52a7478d25051f13 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file/binary-file.component.spec.ts +++ b/alfa-client/libs/binary-file/src/lib/binary-file-container/binary-file/binary-file.component.spec.ts @@ -28,11 +28,11 @@ import { getElementFromFixture } from '@alfa-client/test-utils'; import { IconButtonWithSpinnerComponent, SpinnerComponent } from '@alfa-client/ui'; import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; import { MatIcon } from '@angular/material/icon'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { faker } from '@faker-js/faker'; +import { TooltipDirective } from '@ods/system'; import { createBinaryFileResource } from 'libs/binary-file-shared/test/binary-file'; import { getDataTestClassOf } from 'libs/tech-shared/test/data-test'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { BinaryFileComponent } from './binary-file.component'; describe('BinaryFileComponent', () => { @@ -49,9 +49,9 @@ describe('BinaryFileComponent', () => { MatIcon, FileSizePipe, HasLinkPipe, - MockModule(MatTooltipModule), MockComponent(SpinnerComponent), MockComponent(IconButtonWithSpinnerComponent), + MockDirective(TooltipDirective), ], }); }); diff --git a/alfa-client/libs/binary-file/src/lib/binary-file.module.ts b/alfa-client/libs/binary-file/src/lib/binary-file.module.ts index 1617356e95e80d98fbc07625f3b8fb876b930fbf..ee4cacdb5f276222620c9a4297808dc6e836c007 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file.module.ts +++ b/alfa-client/libs/binary-file/src/lib/binary-file.module.ts @@ -32,6 +32,7 @@ import { AttachmentWrapperComponent, CloseIconComponent, SpinnerIconComponent, + TooltipDirective, } from '@ods/system'; import { BinaryFileAttachmentContainerComponent } from './binary-file-attachment-container/binary-file-attachment-container.component'; import { BinaryFileContainerComponent } from './binary-file-container/binary-file-container.component'; @@ -56,6 +57,7 @@ import { VerticalBinaryFileListComponent } from './vertical-binary-file-list/ver SpinnerIconComponent, CloseIconComponent, DownloadButtonComponent, + TooltipDirective, ], declarations: [ BinaryFileAttachmentContainerComponent, diff --git a/alfa-client/libs/binary-file/src/lib/binary-file2-container/binary-file2-container.component.ts b/alfa-client/libs/binary-file/src/lib/binary-file2-container/binary-file2-container.component.ts index fb9892699cc3a2eda14be01752457906e4d59f26..a0d53c398dc37d5f06af744b9fed8c89487c1519 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file2-container/binary-file2-container.component.ts +++ b/alfa-client/libs/binary-file/src/lib/binary-file2-container/binary-file2-container.component.ts @@ -38,7 +38,7 @@ export class BinaryFile2ContainerComponent { @Output() startDelete: EventEmitter<BinaryFileResource> = new EventEmitter(); - fileStateResource$: Observable<StateResource<any>>; + fileStateResource$: Observable<StateResource<Blob>>; downloadToken$: Observable<ApiDownloadToken>; constructor( @@ -47,10 +47,7 @@ export class BinaryFile2ContainerComponent { ) {} startDownload(file: BinaryFileResource): void { - this.fileStateResource$ = this.binaryFileService.downloadFile( - file, - this.downloadFileNamePrefix, - ); + this.fileStateResource$ = this.binaryFileService.downloadFile(file, this.downloadFileNamePrefix); } getDownloadToken(): void { diff --git a/alfa-client/libs/design-system/src/index.ts b/alfa-client/libs/design-system/src/index.ts index 886dbc3377ed2be0887f68565cf76d1c6af61413..4c577c9087024d16b200f280a1ab6b90d3a8a32d 100644 --- a/alfa-client/libs/design-system/src/index.ts +++ b/alfa-client/libs/design-system/src/index.ts @@ -68,6 +68,7 @@ export * from './lib/icons/search-icon/search-icon.component'; export * from './lib/icons/send-icon/send-icon.component'; export * from './lib/icons/spinner-icon/spinner-icon.component'; export * from './lib/icons/stamp-icon/stamp-icon.component'; +export * from './lib/icons/statistic-icon/statistic-icon.component'; export * from './lib/icons/user-icon/user-icon.component'; export * from './lib/icons/users-icon/users-icon.component'; export * from './lib/instant-search/instant-search/instant-search.component'; diff --git a/alfa-client/libs/design-system/src/lib/dropdown-menu/dropdown-menu-button-item/dropdown-menu-button-item.component.ts b/alfa-client/libs/design-system/src/lib/dropdown-menu/dropdown-menu-button-item/dropdown-menu-button-item.component.ts index 90badf1ab01696f8203a8af8caee1af377c9cbae..2c1b5427927141d6b7ca40515ee64432d9d50e2e 100644 --- a/alfa-client/libs/design-system/src/lib/dropdown-menu/dropdown-menu-button-item/dropdown-menu-button-item.component.ts +++ b/alfa-client/libs/design-system/src/lib/dropdown-menu/dropdown-menu-button-item/dropdown-menu-button-item.component.ts @@ -32,6 +32,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; class="flex min-h-12 w-full items-center gap-4 border-2 border-transparent px-4 py-3 text-start outline-none hover:border-primary focus-visible:border-focus" role="menuitem" (click)="itemClicked.emit()" + [attr.data-test-id]="dataTestId" > <ng-content select="[icon]" /> <p class="text-text">{{ caption }}</p> @@ -39,6 +40,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; }) export class DropdownMenuButtonItemComponent { @Input({ required: true }) caption!: string; + @Input() dataTestId: string; @Output() itemClicked: EventEmitter<MouseEvent> = new EventEmitter(); } diff --git a/alfa-client/libs/design-system/src/lib/dropdown-menu/dropdown-menu/dropdown-menu.component.ts b/alfa-client/libs/design-system/src/lib/dropdown-menu/dropdown-menu/dropdown-menu.component.ts index 20d8fd4a4742512a13322ef6ffbcb274fd6867c2..5a7a7bba99728581d60fa65fd9fda66dbf093391 100644 --- a/alfa-client/libs/design-system/src/lib/dropdown-menu/dropdown-menu/dropdown-menu.component.ts +++ b/alfa-client/libs/design-system/src/lib/dropdown-menu/dropdown-menu/dropdown-menu.component.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * 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 @@ -33,7 +33,7 @@ import { twMerge } from 'tailwind-merge'; imports: [CommonModule, CdkTrapFocus], template: ` <div class="relative w-fit"> <button - [ngClass]="[twMerge('block w-fit outline-2 outline-offset-2 outline-focus', buttonClass)]" + [ngClass]="[twMerge('block w-fit outline-2 outline-offset-2 outline-focus empty:hidden', buttonClass)]" (click)="handleButtonClick()" [attr.aria-expanded]="isPopupOpen" aria-haspopup="true" diff --git a/alfa-client/libs/design-system/src/lib/icons/settings-icon/settengs-icon.stories.ts b/alfa-client/libs/design-system/src/lib/icons/settings-icon/settengs-icon.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..790a794f4807fcc24841e66aac74a84846d9ec86 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/settings-icon/settengs-icon.stories.ts @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 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 type { Meta, StoryObj } from '@storybook/angular'; + +import { SettingsIconComponent } from './settings-icon.component'; + +const meta: Meta<SettingsIconComponent> = { + title: 'Icons/Settings icon', + component: SettingsIconComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<SettingsIconComponent>; + +export const Default: Story = { + args: { size: 'large' }, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium', 'large', 'extra-large', 'full'], + description: 'Size of icon. Property "full" means 100%', + table: { + defaultValue: { summary: 'medium' }, + }, + }, + }, +}; diff --git a/alfa-client/libs/design-system/src/lib/icons/statistic-icon/statistic-icon.component.spec.ts b/alfa-client/libs/design-system/src/lib/icons/statistic-icon/statistic-icon.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..9f6bddf811de36b7005098584a3c436010fc9bb3 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/statistic-icon/statistic-icon.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { StatisticIconComponent } from './statistic-icon.component'; + +describe('StatisticIconComponent', () => { + let component: StatisticIconComponent; + let fixture: ComponentFixture<StatisticIconComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StatisticIconComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(StatisticIconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/icons/statistic-icon/statistic-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/statistic-icon/statistic-icon.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..df5d2b5bdcba9549b60cea9ee4b04d7e8bc4d8c4 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/statistic-icon/statistic-icon.component.ts @@ -0,0 +1,24 @@ +import { NgClass } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { twMerge } from 'tailwind-merge'; +import { iconVariants, IconVariants } from '../iconVariants'; + +@Component({ + selector: 'ods-statistic-icon', + standalone: true, + imports: [NgClass], + template: `<svg + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + [ngClass]="twMerge(iconVariants({ size }), 'fill-black', class)" + > + <path d="M2 21V19H22V21H2ZM3 18V11H6V18H3ZM8 18V6H11V18H8ZM13 18V9H16V18H13ZM18 18V3H21V18H18Z" /> + </svg>`, +}) +export class StatisticIconComponent { + @Input() size: IconVariants['size'] = 'medium'; + @Input() class: string = undefined; + + readonly iconVariants = iconVariants; + readonly twMerge = twMerge; +} diff --git a/alfa-client/libs/design-system/src/lib/icons/statistic-icon/statistic-icon.stories.ts b/alfa-client/libs/design-system/src/lib/icons/statistic-icon/statistic-icon.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..272f6fe850c3862c1a7c8212bf2807369a41064b --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/statistic-icon/statistic-icon.stories.ts @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 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 type { Meta, StoryObj } from '@storybook/angular'; + +import { StatisticIconComponent } from './statistic-icon.component'; + +const meta: Meta<StatisticIconComponent> = { + title: 'Icons/Statistic icon', + component: StatisticIconComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<StatisticIconComponent>; + +export const Default: Story = { + args: { size: 'large' }, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium', 'large', 'extra-large', 'full'], + description: 'Size of icon. Property "full" means 100%', + table: { + defaultValue: { summary: 'medium' }, + }, + }, + }, +}; diff --git a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.component.ts b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.component.ts index 85e9b48d104df61a2139cdf829bc78f9728bc914..1f813a9e5ea79cb5e30111a132dca50be8ebcc2f 100644 --- a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.component.ts +++ b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.component.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * 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 @@ -29,7 +29,7 @@ import { TooltipPosition } from './tooltip.directive'; selector: 'ods-tooltip', imports: [NgClass], template: `<span - class="tooltip fixed z-[100] max-w-xs animate-fadeIn cursor-default break-words rounded bg-ozggray-900 px-3 py-2 text-sm text-whitetext before:absolute before:border-l-[0.5rem] before:border-r-[0.5rem] before:border-l-transparent before:border-r-transparent dark:bg-white md:max-w-[calc(90vw)]" + class="tooltip fixed z-[100] max-w-md animate-fadeIn cursor-default whitespace-pre rounded bg-ozggray-900 px-3 py-2 text-sm text-whitetext before:absolute before:border-l-[0.5rem] before:border-r-[0.5rem] before:border-l-transparent before:border-r-transparent dark:bg-white md:max-w-[calc(90vw)]" [ngClass]="class" [class.visible]="show" [class.invisible]="!show" diff --git a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.spec.ts b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.spec.ts index 9265a021a9ce2e988257793c5474970b79d06fc3..e41c410f0efb001d9c7896eaec93fb388fb9d23a 100644 --- a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.spec.ts +++ b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.spec.ts @@ -21,7 +21,6 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { EMPTY_STRING } from '@alfa-client/tech-shared'; import { InteractivityChecker } from '@angular/cdk/a11y'; import { ComponentRef, ElementRef, Renderer2, ViewContainerRef } from '@angular/core'; import { TestBed } from '@angular/core/testing'; @@ -64,11 +63,28 @@ describe('TooltipDirective', () => { expect(directive).toBeTruthy(); }); - describe('ngAfterViewInit', () => { - it('should create tooltip', () => { + describe('set tooltip', () => { + beforeEach(() => { directive.createTooltip = jest.fn(); + directive.destroy = jest.fn(); + }); + + it('should destroy tooltip if it exists already', () => { + directive.componentRef = mockComponentRef; - directive.ngAfterViewInit(); + directive.tooltip = 'test'; + + expect(directive.destroy).toHaveBeenCalled(); + }); + + it('should not create tooltip if text is empty', () => { + directive.tooltip = ''; + + expect(directive.createTooltip).not.toHaveBeenCalled(); + }); + + it('should create tooltip', () => { + directive.tooltip = 'test'; expect(directive.createTooltip).toHaveBeenCalled(); }); @@ -85,49 +101,48 @@ describe('TooltipDirective', () => { }); describe('createTooltip', () => { + const tooltipText: string = faker.lorem.sentence(); + beforeEach(() => { directive.viewContainerRef.createComponent = jest.fn().mockReturnValue({ location: { nativeElement: {} } }); directive.setAriaAttribute = jest.fn(); directive.setInitialTooltipProperties = jest.fn(); directive.getParentElement = jest.fn().mockReturnValue({ appendChild: jest.fn() }); directive.interactivityChecker.isFocusable = jest.fn(); - directive.tooltip = faker.lorem.sentence(); }); it('should create tooltip component', () => { - directive.createTooltip(); + directive.createTooltip(tooltipText); expect(directive.viewContainerRef.createComponent).toHaveBeenCalled(); }); it('should get parent element', () => { - directive.createTooltip(); + directive.createTooltip(tooltipText); expect(directive.getParentElement).toHaveBeenCalled(); }); it('should insert tooltip component to parent element', () => { - directive.createTooltip(); + directive.createTooltip(tooltipText); expect(directive.parentElement.appendChild).toHaveBeenCalled(); }); it('should set initial tooltip properties', () => { - directive.createTooltip(); + directive.createTooltip(tooltipText); expect(directive.setInitialTooltipProperties).toHaveBeenCalled(); }); it('should set aria attribute to parent', () => { - directive.createTooltip(); + directive.createTooltip(tooltipText); expect(directive.setAriaAttribute).toHaveBeenCalled(); }); it('should not create tooltip', () => { - directive.tooltip = EMPTY_STRING; - - directive.createTooltip(); + directive.createTooltip(''); expect(directive.getParentElement).not.toHaveBeenCalled(); }); @@ -135,10 +150,19 @@ describe('TooltipDirective', () => { describe('showTooltip', () => { beforeEach(() => { + directive.componentRef = mockComponentRef; directive.setTooltipProperties = jest.fn(); directive.elementRef.nativeElement.contains = jest.fn().mockReturnValue(true); }); + it('should not set tooltip properties if component ref is null', () => { + directive.componentRef = null; + + directive.showTooltip(); + + expect(directive.setTooltipProperties).not.toHaveBeenCalled(); + }); + it('should check if element focused', () => { directive.showTooltip(); @@ -218,9 +242,6 @@ describe('TooltipDirective', () => { }); it('should set tooltip instance properties', () => { - directive.tooltip = 'I am tooltip'; - directive.tooltipId = 'tooltip-1'; - directive.setTooltipProperties(); expect(directive.componentRef.instance).toMatchObject({ diff --git a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.ts b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.ts index cdc414d0e3f580d085037276f26f03697f98decc..ce1f687ce7a0df7411b5d3169f42d9c152f9c88e 100644 --- a/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.ts +++ b/alfa-client/libs/design-system/src/lib/tooltip/tooltip.directive.ts @@ -21,10 +21,9 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { isEscapeKey } from '@alfa-client/tech-shared'; +import { isEscapeKey, isNotNull } from '@alfa-client/tech-shared'; import { InteractivityChecker } from '@angular/cdk/a11y'; import { - AfterViewInit, ComponentRef, Directive, ElementRef, @@ -49,8 +48,18 @@ type TooltipAriaType = 'aria-describedby' | 'aria-labelledby'; selector: '[tooltip]', standalone: true, }) -export class TooltipDirective implements AfterViewInit, OnDestroy { - @Input() tooltip: string = ''; +export class TooltipDirective implements OnDestroy { + @Input() set tooltip(value: string) { + if (isNotNull(this.componentRef)) { + this.destroy(); + } + + if (isEmpty(value)) { + return; + } + + this.createTooltip(value); + } @Input() tooltipPosition: TooltipPosition = TooltipPosition.BELOW; @Input() tooltipAriaType: TooltipAriaType = 'aria-describedby'; @@ -61,14 +70,10 @@ export class TooltipDirective implements AfterViewInit, OnDestroy { position: TooltipPosition; leftOffset: number = 0; - public viewContainerRef: ViewContainerRef = inject(ViewContainerRef); - public elementRef: ElementRef<HTMLElement> = inject(ElementRef); - public renderer: Renderer2 = inject(Renderer2); - public interactivityChecker: InteractivityChecker = inject(InteractivityChecker); - - ngAfterViewInit(): void { - this.createTooltip(); - } + public readonly viewContainerRef: ViewContainerRef = inject(ViewContainerRef); + public readonly elementRef: ElementRef<HTMLElement> = inject(ElementRef); + public readonly renderer: Renderer2 = inject(Renderer2); + public readonly interactivityChecker: InteractivityChecker = inject(InteractivityChecker); ngOnDestroy(): void { this.destroy(); @@ -77,6 +82,10 @@ export class TooltipDirective implements AfterViewInit, OnDestroy { @HostListener('mouseenter') @HostListener('focusin') showTooltip(): void { + if (isNull(this.componentRef)) { + return; + } + const nativeElement: HTMLElement = this.elementRef.nativeElement; this.attachedToFocused = nativeElement.contains(document.activeElement); this.setTooltipProperties(); @@ -97,8 +106,8 @@ export class TooltipDirective implements AfterViewInit, OnDestroy { } } - createTooltip(): void { - if (isEmpty(this.tooltip)) { + createTooltip(tooltipText: string): void { + if (isEmpty(tooltipText)) { return; } @@ -107,7 +116,7 @@ export class TooltipDirective implements AfterViewInit, OnDestroy { this.parentElement = this.getParentElement(nativeElement); this.parentElement.appendChild(this.componentRef.location.nativeElement); this.tooltipId = uniqueId('tooltip'); - this.setInitialTooltipProperties(this.tooltip, this.tooltipId); + this.setInitialTooltipProperties(tooltipText, this.tooltipId); this.setAriaAttribute(this.tooltipAriaType); } @@ -199,6 +208,10 @@ export class TooltipDirective implements AfterViewInit, OnDestroy { } hide(): void { + if (isNull(this.componentRef)) { + return; + } + this.componentRef.instance.show = false; } diff --git a/alfa-client/libs/environment-shared/src/lib/environment.model.ts b/alfa-client/libs/environment-shared/src/lib/environment.model.ts index 751085929a4e9756a07fd3b7615197b75dd77553..83e2674c9e48c189746d57f5751496a00d655f5f 100644 --- a/alfa-client/libs/environment-shared/src/lib/environment.model.ts +++ b/alfa-client/libs/environment-shared/src/lib/environment.model.ts @@ -30,8 +30,4 @@ export interface Environment { realm: string; clientId: string; processorNames: string[]; - features: { - postfach: boolean; - benutzerRollen: boolean; - }; } diff --git a/alfa-client/libs/environment-shared/src/lib/environment.service.ts b/alfa-client/libs/environment-shared/src/lib/environment.service.ts index df9f879478212ef196191edaba9de6a134ae0277..51d2b60795077165455bfc1c78ef31b3e3890a08 100644 --- a/alfa-client/libs/environment-shared/src/lib/environment.service.ts +++ b/alfa-client/libs/environment-shared/src/lib/environment.service.ts @@ -25,7 +25,7 @@ import { getBaseUrl } from '@alfa-client/tech-shared'; import { InjectionToken } from '@angular/core'; import { Environment } from './environment.model'; -export const ENVIRONMENT_CONFIG = new InjectionToken('environmentConfig'); +export const ENVIRONMENT_CONFIG = new InjectionToken<Environment>('environmentConfig'); export function getEnvironmentFactory(): Environment { // @ts-ignore diff --git a/alfa-client/libs/environment-shared/test/environment.ts b/alfa-client/libs/environment-shared/test/environment.ts index a0b3bf79e50da3351ab61283e7903d4747956942..ef29099703a89b5bb9d0fc989a1de91a13070bc4 100644 --- a/alfa-client/libs/environment-shared/test/environment.ts +++ b/alfa-client/libs/environment-shared/test/environment.ts @@ -32,10 +32,6 @@ const environment: Environment = { realm: faker.word.sample(), clientId: faker.string.uuid(), processorNames: [faker.person.fullName()], - features: { - postfach: true, - benutzerRollen: true, - }, }; export function createEnvironment(): Environment { diff --git a/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.ts b/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.ts index faabb47b7a8addbc06449f48ab9b4ecb1f68de77..59f82aaf5ade160fccaaebb794f117926b91e148 100644 --- a/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.ts +++ b/alfa-client/libs/tech-shared/src/lib/service/formservice.abstract.ts @@ -34,7 +34,7 @@ import { isNotUndefined } from '../tech.util'; import { setInvalidParamValidationError } from '../validation/tech.validation.util'; @Injectable() -export abstract class AbstractFormService<T extends Resource = Resource> { +export abstract class AbstractFormService<T = Resource> { form: UntypedFormGroup; pathPrefix: string; source: any; diff --git a/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.html b/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.html index 15b7525df35717921a5d10f25db575a969fa6070..29c6b91fcdefa4629cae62422ee8b56fe62346b2 100644 --- a/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.html +++ b/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.html @@ -1,6 +1,6 @@ <!-- - Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den + 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 @@ -28,8 +28,8 @@ mat-icon-button data-test-id="back-button" class="back-button" - [attr.aria-label]="label" - [matTooltip]="label" + [tooltip]="label" + tooltipAriaType="aria-labelledby" > <mat-icon>arrow_back</mat-icon> </a> diff --git a/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.spec.ts b/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.spec.ts index c27ac7f78d4ef8f71075e09a7e872f11e8d15398..9ca87b6fea4263137d68e5451ad05639cff3666d 100644 --- a/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.spec.ts +++ b/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.spec.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den + * 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 @@ -24,11 +24,11 @@ import { getElementFromFixture } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatIcon } from '@angular/material/icon'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterTestingModule } from '@angular/router/testing'; import { faker } from '@faker-js/faker'; +import { TooltipDirective } from '@ods/system'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; -import { MockModule } from 'ng-mocks'; +import { MockDirective } from 'ng-mocks'; import { BackButtonComponent } from './back-button.component'; describe('BackButtonComponent', () => { @@ -41,7 +41,7 @@ describe('BackButtonComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [BackButtonComponent, MockModule(MatTooltipModule)], + declarations: [BackButtonComponent, MockDirective(TooltipDirective)], imports: [MatIcon, RouterTestingModule], }).compileComponents(); @@ -54,19 +54,6 @@ describe('BackButtonComponent', () => { expect(component).toBeTruthy(); }); - describe('label', () => { - beforeEach(() => { - component.label = faker.lorem.sentence(); - fixture.detectChanges(); - }); - - it('should set aria-label attribute', () => { - const backButtonElement: HTMLAnchorElement = getElementFromFixture(fixture, backButton); - - expect(backButtonElement).toHaveAttribute('aria-label', component.label); - }); - }); - describe('linkTo', () => { it('string path should set href attribute', () => { component.linkTo = linkToAsString; diff --git a/alfa-client/libs/ui/src/lib/ui/download-button/download-button.component.html b/alfa-client/libs/ui/src/lib/ui/download-button/download-button.component.html index 837eb1f03cfbac8cffcdb498528ac68a87927009..5c65fd900ce1b07a5ae957e00295792655481bee 100644 --- a/alfa-client/libs/ui/src/lib/ui/download-button/download-button.component.html +++ b/alfa-client/libs/ui/src/lib/ui/download-button/download-button.component.html @@ -28,7 +28,7 @@ data-test-id="open-url-in-new-window" [href]="url" [attr.aria-label]="text" - [matTooltip]="text" + [tooltip]="text" [download]="targetName" [color]="'primary'" > diff --git a/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.html b/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.html index 17f6df625694ad4a805506963c8278fe055a3d48..1bb8fb6c9424bb9fb420a5f0007d61a88ffe6beb 100644 --- a/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.html +++ b/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.html @@ -1,6 +1,6 @@ <!-- - Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den + 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 @@ -26,36 +26,18 @@ <button mat-icon-button data-test-class="icon-button" - [attr.aria-label]=" - toolTip ? toolTip : 'Icon Button mit einem ' + (icon ? icon : svgIcon) + ' Icon' - " [disabled]="isDisabled" - [matTooltip]="toolTip" + [tooltip]="toolTip" + tooltipAriaType="aria-labelledby" [matMenuTriggerFor]="matMenuTriggerFor" (click)="clickEmitter.emit($event)" type="button" > - <mat-icon - *ngIf="icon" - data-test-class="icon" - [style.visibility]="isDisabled ? 'hidden' : 'visible'" - > + <mat-icon *ngIf="icon" data-test-class="icon" [style.visibility]="isDisabled ? 'hidden' : 'visible'"> {{ icon }} </mat-icon> - <mat-icon - *ngIf="svgIcon" - data-test-class="icon" - [svgIcon]="svgIcon" - [style.visibility]="isDisabled ? 'hidden' : 'visible'" - > - </mat-icon> + <mat-icon *ngIf="svgIcon" data-test-class="icon" [svgIcon]="svgIcon" [style.visibility]="isDisabled ? 'hidden' : 'visible'" /> - <ozgcloud-spinner - [stateResource]="getStateResource()" - [diameter]="22" - [show]="showSpinner" - padding="0" - > - </ozgcloud-spinner> + <ozgcloud-spinner [stateResource]="getStateResource()" [diameter]="22" [show]="showSpinner" padding="0" /> </button> diff --git a/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.spec.ts b/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.spec.ts index e303e74c6fd61eef05ccc6cef15bff6f60607866..7cf06e5752ca8ebf4c3b0b797b97631bc3e04c25 100644 --- a/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.spec.ts +++ b/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.spec.ts @@ -25,8 +25,8 @@ import { createEmptyStateResource } from '@alfa-client/tech-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatIcon } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { TooltipDirective } from '@ods/system'; +import { MockComponent, MockDirective, MockModule } from 'ng-mocks'; import { SpinnerComponent } from '../spinner/spinner.component'; import { IconButtonWithSpinnerComponent } from './icon-button-with-spinner.component'; @@ -43,7 +43,7 @@ describe('IconButtonWithSpinnerComponent', () => { IconButtonWithSpinnerComponent, MatIcon, MockComponent(SpinnerComponent), - MockModule(MatTooltipModule), + MockDirective(TooltipDirective), MockModule(MatMenuModule), ], }); diff --git a/alfa-client/libs/ui/src/lib/ui/open-url-button/open-url-button.component.html b/alfa-client/libs/ui/src/lib/ui/open-url-button/open-url-button.component.html index 9c27adeed48b413aba66c4816fc1f993b8567a70..39ef61cb6485f2d1a8704f729e75f4c13b57fa8d 100644 --- a/alfa-client/libs/ui/src/lib/ui/open-url-button/open-url-button.component.html +++ b/alfa-client/libs/ui/src/lib/ui/open-url-button/open-url-button.component.html @@ -29,7 +29,7 @@ [href]="url" [target]="targetName" [attr.aria-label]="text" - [matTooltip]="tooltip" + [tooltip]="tooltip" [color]="'primary'" [title]="tooltip" class="button" diff --git a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-button-with-spinner/ozgcloud-button-with-spinner.component.html b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-button-with-spinner/ozgcloud-button-with-spinner.component.html index 93bcb7d18bb803fb08dedf587b10a3261d0099a5..374a22364102cade464a3fad961bbffe1a7b330a 100644 --- a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-button-with-spinner/ozgcloud-button-with-spinner.component.html +++ b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-button-with-spinner/ozgcloud-button-with-spinner.component.html @@ -30,7 +30,7 @@ [color]="color" [type]="type" [disabled]="isDisabled" - [matTooltip]="toolTip" + [tooltip]="toolTip" [class.with-text]="text" (click)="clickEmitter.emit($event)" > diff --git a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-button-with-spinner/ozgcloud-button-with-spinner.component.spec.ts b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-button-with-spinner/ozgcloud-button-with-spinner.component.spec.ts index cb73ead5c66d13042bc5ddb7476b59c7aed81703..ee97fa55891a3b5ea04917c80ea255458f58d68b 100644 --- a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-button-with-spinner/ozgcloud-button-with-spinner.component.spec.ts +++ b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-button-with-spinner/ozgcloud-button-with-spinner.component.spec.ts @@ -24,13 +24,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatButton } from '@angular/material/button'; import { MatRipple } from '@angular/material/core'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { createCommandResource } from 'libs/command-shared/test/command'; import { getDataTestClassOf } from 'libs/tech-shared/test/data-test'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { OzgcloudButtonContentComponent } from '../shared/ozgcloud-button-content/ozgcloud-button-content.component'; import { OzgcloudButtonWithSpinnerComponent } from './ozgcloud-button-with-spinner.component'; +import { TooltipDirective } from '@ods/system'; import * as ResourceUtils from 'libs/tech-shared/src/lib/resource/resource.util'; describe('OzgcloudButtonWithSpinnerComponent', () => { @@ -45,7 +45,7 @@ describe('OzgcloudButtonWithSpinnerComponent', () => { MatButton, MatRipple, OzgcloudButtonWithSpinnerComponent, - MockModule(MatTooltipModule), + MockDirective(TooltipDirective), MockComponent(OzgcloudButtonContentComponent), ], }).compileComponents(); diff --git a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.html b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.html index b0ecf6c5de127cddea84861601dde60fb1e2efb1..21035dd8675511d51040c8f6aa627b995bab54ed 100644 --- a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.html +++ b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.html @@ -27,7 +27,7 @@ mat-icon-button data-test-class="icon-button-primary" [attr.aria-label]="ariaLabel" - [matTooltip]="tooltip" + [tooltip]="tooltip" (click)="clickEmitter.emit($event)" color="primary" type="button" diff --git a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.scss b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.scss index af82db8c6ff99b292d748f41a135b0e811f8a879..a01b48881d53d2d9f8ee17768a885a76c7d902c5 100644 --- a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.scss +++ b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.scss @@ -1,5 +1,5 @@ /** - * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den + * 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 @@ -23,4 +23,5 @@ */ :host { position: relative; + z-index: 1; } diff --git a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.spec.ts b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.spec.ts index ad86b2f5b418a6a2fc6c8ef74c69dc32063f165a..976c0b0c396b7dfc68b3f7e53148d1d75cf1baa8 100644 --- a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.spec.ts +++ b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-icon-button-primary/ozgcloud-icon-button-primary.component.spec.ts @@ -26,9 +26,9 @@ import { getElementFromFixture } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatIcon } from '@angular/material/icon'; import { MatIconTestingModule } from '@angular/material/icon/testing'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import { TooltipDirective } from '@ods/system'; import { getDataTestClassOf } from 'libs/tech-shared/test/data-test'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { OzgcloudIconButtonPrimaryComponent } from './ozgcloud-icon-button-primary.component'; jest.mock('@alfa-client/tech-shared'); @@ -41,11 +41,7 @@ describe('IconButtonPrimaryWithSpinnerComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [MatIconTestingModule], - declarations: [ - OzgcloudIconButtonPrimaryComponent, - MockComponent(MatIcon), - MockModule(MatTooltipModule), - ], + declarations: [OzgcloudIconButtonPrimaryComponent, MockComponent(MatIcon), MockDirective(TooltipDirective)], }); }); diff --git a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-stroked-button-with-spinner/ozgcloud-stroked-button-with-spinner.component.html b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-stroked-button-with-spinner/ozgcloud-stroked-button-with-spinner.component.html index 58afc8842e49c8afe650594a94757aca75f23b3f..1c6c7d85154956f4d5ae0a1c0c09225accde76f9 100644 --- a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-stroked-button-with-spinner/ozgcloud-stroked-button-with-spinner.component.html +++ b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-stroked-button-with-spinner/ozgcloud-stroked-button-with-spinner.component.html @@ -30,7 +30,7 @@ [color]="color" [type]="type" [disabled]="isDisabled" - [matTooltip]="toolTip" + [tooltip]="toolTip" [class.with-text]="text" (click)="clickEmitter.emit($event)" > diff --git a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-stroked-button-with-spinner/ozgcloud-stroked-button-with-spinner.component.spec.ts b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-stroked-button-with-spinner/ozgcloud-stroked-button-with-spinner.component.spec.ts index 8703dcf19b3978964792b7874155cca2aec699eb..208868555e600c7fd81f6edc4bd1e8fd541827e6 100644 --- a/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-stroked-button-with-spinner/ozgcloud-stroked-button-with-spinner.component.spec.ts +++ b/alfa-client/libs/ui/src/lib/ui/ozgcloud-button/ozgcloud-stroked-button-with-spinner/ozgcloud-stroked-button-with-spinner.component.spec.ts @@ -24,13 +24,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatButton } from '@angular/material/button'; import { MatRipple } from '@angular/material/core'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { createCommandResource } from 'libs/command-shared/test/command'; import { getDataTestClassOf } from 'libs/tech-shared/test/data-test'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { OzgcloudButtonContentComponent } from '../shared/ozgcloud-button-content/ozgcloud-button-content.component'; import { OzgcloudStrokedButtonWithSpinnerComponent } from './ozgcloud-stroked-button-with-spinner.component'; +import { TooltipDirective } from '@ods/system'; import * as ResourceUtils from 'libs/tech-shared/src/lib/resource/resource.util'; describe('OzgcloudStrokedButtonWithSpinnerComponent', () => { @@ -45,7 +45,7 @@ describe('OzgcloudStrokedButtonWithSpinnerComponent', () => { MatButton, MatRipple, OzgcloudStrokedButtonWithSpinnerComponent, - MockModule(MatTooltipModule), + MockDirective(TooltipDirective), MockComponent(OzgcloudButtonContentComponent), ], }).compileComponents(); diff --git a/alfa-client/libs/ui/src/lib/ui/ozgcloud-paste-text-button/ozgcloud-paste-text-button.component.spec.ts b/alfa-client/libs/ui/src/lib/ui/ozgcloud-paste-text-button/ozgcloud-paste-text-button.component.spec.ts index 22a817c9b19c19c5c6a5981e94bc533d62759b36..e91b91840bb1f855e5821d6eaac5949bc815181c 100644 --- a/alfa-client/libs/ui/src/lib/ui/ozgcloud-paste-text-button/ozgcloud-paste-text-button.component.spec.ts +++ b/alfa-client/libs/ui/src/lib/ui/ozgcloud-paste-text-button/ozgcloud-paste-text-button.component.spec.ts @@ -28,9 +28,9 @@ import { EventEmitter } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatIcon } from '@angular/material/icon'; import { MatIconTestingModule } from '@angular/material/icon/testing'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import { TooltipDirective } from '@ods/system'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { OzgcloudPasteTextButtonComponent } from './ozgcloud-paste-text-button.component'; jest.mock('@alfa-client/tech-shared'); @@ -55,7 +55,7 @@ describe('OzgcloudPasteTextButtonComponent', () => { OzgcloudPasteTextButtonComponent, MockComponent(MatIcon), MockComponent(OzgcloudIconButtonPrimaryComponent), - MockModule(MatTooltipModule), + MockDirective(TooltipDirective), ], }).compileComponents(); diff --git a/alfa-client/libs/ui/src/lib/ui/slide-toggle/slide-toggle.component.html b/alfa-client/libs/ui/src/lib/ui/slide-toggle/slide-toggle.component.html index 366854df1b2cb313dc452750c536d10a208bdbb3..8d302a06932bc9a1adfad5834af5bbf3ec69d88f 100644 --- a/alfa-client/libs/ui/src/lib/ui/slide-toggle/slide-toggle.component.html +++ b/alfa-client/libs/ui/src/lib/ui/slide-toggle/slide-toggle.component.html @@ -26,7 +26,7 @@ <mat-slide-toggle color="primary" [disabled]="disabled" - [matTooltip]="toolTip" + [tooltip]="toolTip" [checked]="checked" (change)="valueChanged.emit($event.checked)" ><span class="text-sm">{{ label }}</span> diff --git a/alfa-client/libs/ui/src/lib/ui/slide-toggle/slide-toggle.component.spec.ts b/alfa-client/libs/ui/src/lib/ui/slide-toggle/slide-toggle.component.spec.ts index 35d63c5fa5c386e8907a09f8a985c36ba555a4dd..39488cb32fad62fe43a90bd77a5a409cbfb41541 100644 --- a/alfa-client/libs/ui/src/lib/ui/slide-toggle/slide-toggle.component.spec.ts +++ b/alfa-client/libs/ui/src/lib/ui/slide-toggle/slide-toggle.component.spec.ts @@ -24,8 +24,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatRippleModule } from '@angular/material/core'; import { MatSlideToggle } from '@angular/material/slide-toggle'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { MockModule } from 'ng-mocks'; +import { TooltipDirective } from '@ods/system'; +import { MockDirective } from 'ng-mocks'; import { SlideToggleComponent } from './slide-toggle.component'; describe('SlideToggleComponent', () => { @@ -35,7 +35,7 @@ describe('SlideToggleComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [MatRippleModule, MatSlideToggle], - declarations: [SlideToggleComponent, MockModule(MatTooltipModule)], + declarations: [SlideToggleComponent, MockDirective(TooltipDirective)], }).compileComponents(); fixture = TestBed.createComponent(SlideToggleComponent); diff --git a/alfa-client/libs/ui/src/lib/ui/ui.module.ts b/alfa-client/libs/ui/src/lib/ui/ui.module.ts index 1738c23448649c9653efaa1f8f266b4426136286..eeff7417056ee49e49cbe8c4206a2e72aa5dd9a5 100644 --- a/alfa-client/libs/ui/src/lib/ui/ui.module.ts +++ b/alfa-client/libs/ui/src/lib/ui/ui.module.ts @@ -46,9 +46,8 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatTabsModule } from '@angular/material/tabs'; -import { MAT_TOOLTIP_DEFAULT_OPTIONS, MatTooltipModule } from '@angular/material/tooltip'; import { RouterModule } from '@angular/router'; -import { FileUploadButtonComponent, SpinnerIconComponent } from '@ods/system'; +import { FileUploadButtonComponent, SpinnerIconComponent, TooltipDirective } from '@ods/system'; import { de } from 'date-fns/locale'; import { AppIconComponent } from '../icon/app-icon/app-icon.component'; import { PostfachIconComponent } from '../icon/postfach-icon/postfach-icon.component'; @@ -73,7 +72,6 @@ import { FixedDialogComponent } from './fixed-dialog/fixed-dialog.component'; import { ConnectionTimeoutRetryDialogComponent } from './http-error-dialog/connection-timeout-retry-dialog/connection-timeout-retry-dialog.component'; import { ConnectionTimeoutRetryFailDialogComponent } from './http-error-dialog/connection-timeout-retry-fail-dialog/connection-timeout-retry-fail-dialog.component'; import { IconButtonWithSpinnerComponent } from './icon-button-with-spinner/icon-button-with-spinner.component'; -import { matTooltipDefaultOptions } from './mattooltip/mattooltip.default'; import { InternalServerErrorDialogComponent } from './notification/internal-server-error-dialog/internal-server-error-dialog.component'; import { OpenUrlButtonComponent } from './open-url-button/open-url-button.component'; import { OzgcloudButtonWithSpinnerComponent } from './ozgcloud-button/ozgcloud-button-with-spinner/ozgcloud-button-with-spinner.component'; @@ -156,7 +154,6 @@ import { ValidationErrorComponent } from './validation-error/validation-error.co MatAutocompleteModule, MatDialogModule, MatTabsModule, - MatTooltipModule, MatBadgeModule, CommonModule, TechSharedModule, @@ -164,6 +161,7 @@ import { ValidationErrorComponent } from './validation-error/validation-error.co MatButtonToggleModule, FileUploadButtonComponent, SpinnerIconComponent, + TooltipDirective, ], exports: [ MatButtonModule, @@ -186,7 +184,6 @@ import { ValidationErrorComponent } from './validation-error/validation-error.co MatAutocompleteModule, MatDialogModule, MatTabsModule, - MatTooltipModule, MatBadgeModule, CommonModule, TechSharedModule, @@ -253,7 +250,6 @@ import { ValidationErrorComponent } from './validation-error/validation-error.co useClass: DateFnsAdapter, deps: [MAT_DATE_LOCALE], }, - { provide: MAT_TOOLTIP_DEFAULT_OPTIONS, useValue: matTooltipDefaultOptions }, ], }) export class UiModule {} diff --git a/alfa-client/libs/user-profile/src/lib/link-with-user-name-tooltip-container/link-with-user-name-tooltip/link-with-user-name-tooltip.component.html b/alfa-client/libs/user-profile/src/lib/link-with-user-name-tooltip-container/link-with-user-name-tooltip/link-with-user-name-tooltip.component.html index df527af17224d0db0a84f10f81052589f4b0485b..8789fcac61ca88a8a0d8b96204aa967c1b7fc606 100644 --- a/alfa-client/libs/user-profile/src/lib/link-with-user-name-tooltip-container/link-with-user-name-tooltip/link-with-user-name-tooltip.component.html +++ b/alfa-client/libs/user-profile/src/lib/link-with-user-name-tooltip-container/link-with-user-name-tooltip/link-with-user-name-tooltip.component.html @@ -23,6 +23,6 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<a routerLink="{{ routerLinkString }}" [matTooltip]="tooltip" class="link"> +<a routerLink="{{ routerLinkString }}" [tooltip]="tooltip" class="link"> <ng-content></ng-content> </a> diff --git a/alfa-client/libs/user-profile/src/lib/link-with-user-name-tooltip-container/link-with-user-name-tooltip/link-with-user-name-tooltip.component.spec.ts b/alfa-client/libs/user-profile/src/lib/link-with-user-name-tooltip-container/link-with-user-name-tooltip/link-with-user-name-tooltip.component.spec.ts index ce240696e45f53cab52a250b9222facdd53094fb..6907e8674b1417d075237a4975c2ce0f1dd4295c 100644 --- a/alfa-client/libs/user-profile/src/lib/link-with-user-name-tooltip-container/link-with-user-name-tooltip/link-with-user-name-tooltip.component.spec.ts +++ b/alfa-client/libs/user-profile/src/lib/link-with-user-name-tooltip-container/link-with-user-name-tooltip/link-with-user-name-tooltip.component.spec.ts @@ -24,10 +24,10 @@ import { createStateResource } from '@alfa-client/tech-shared'; import { UserProfileResource } from '@alfa-client/user-profile-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterTestingModule } from '@angular/router/testing'; +import { TooltipDirective } from '@ods/system'; import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile'; -import { MockModule } from 'ng-mocks'; +import { MockDirective } from 'ng-mocks'; import { LinkWithUserNameTooltipComponent } from './link-with-user-name-tooltip.component'; describe('LinkWithUserNameTooltipComponent', () => { @@ -40,7 +40,7 @@ describe('LinkWithUserNameTooltipComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [RouterTestingModule], - declarations: [LinkWithUserNameTooltipComponent, MockModule(MatTooltipModule)], + declarations: [LinkWithUserNameTooltipComponent, MockDirective(TooltipDirective)], }).compileComponents(); }); diff --git a/alfa-client/libs/user-profile/src/lib/text-with-user-name-tooltip-container/text-with-user-name-tooltip/text-with-user-name-tooltip.component.html b/alfa-client/libs/user-profile/src/lib/text-with-user-name-tooltip-container/text-with-user-name-tooltip/text-with-user-name-tooltip.component.html index 27535f64fdc19260a6eae325f3e1f4f2334a9b83..d659fff0fa6e49f8332bef42998e9da5fd6ae992 100644 --- a/alfa-client/libs/user-profile/src/lib/text-with-user-name-tooltip-container/text-with-user-name-tooltip/text-with-user-name-tooltip.component.html +++ b/alfa-client/libs/user-profile/src/lib/text-with-user-name-tooltip-container/text-with-user-name-tooltip/text-with-user-name-tooltip.component.html @@ -23,4 +23,4 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<p [matTooltip]="tooltip">{{ text }}</p> +<p [tooltip]="tooltip">{{ text }}</p> diff --git a/alfa-client/libs/user-profile/src/lib/text-with-user-name-tooltip-container/text-with-user-name-tooltip/text-with-user-name-tooltip.component.spec.ts b/alfa-client/libs/user-profile/src/lib/text-with-user-name-tooltip-container/text-with-user-name-tooltip/text-with-user-name-tooltip.component.spec.ts index 9fd7e0083708d0957f4d55df8fad4bf7a6257c6e..23163b82f8921f720872808dc86348c65b83dad8 100644 --- a/alfa-client/libs/user-profile/src/lib/text-with-user-name-tooltip-container/text-with-user-name-tooltip/text-with-user-name-tooltip.component.spec.ts +++ b/alfa-client/libs/user-profile/src/lib/text-with-user-name-tooltip-container/text-with-user-name-tooltip/text-with-user-name-tooltip.component.spec.ts @@ -24,9 +24,9 @@ import { createStateResource } from '@alfa-client/tech-shared'; import { UserProfileResource } from '@alfa-client/user-profile-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import { TooltipDirective } from '@ods/system'; import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile'; -import { MockModule } from 'ng-mocks'; +import { MockDirective } from 'ng-mocks'; import { TextWithUserNameTooltipComponent } from './text-with-user-name-tooltip.component'; describe('TextWithUserNameTooltipComponent', () => { @@ -38,7 +38,7 @@ describe('TextWithUserNameTooltipComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [TextWithUserNameTooltipComponent, MockModule(MatTooltipModule)], + declarations: [TextWithUserNameTooltipComponent, MockDirective(TooltipDirective)], }).compileComponents(); }); diff --git a/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.html b/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.html index d5b5d476d5720e7005f3c70871d57b9a1d376f47..a6866ab9d0a536f9c4af983e893e4408600efb69 100644 --- a/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.html +++ b/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.html @@ -25,11 +25,10 @@ --> <ozgcloud-spinner [stateResource]="userProfileStateResource" diameter="30" padding="3"> <div - [matTooltipDisabled]="disableTooltip" data-test-class="user-profile-icon" class="relative flex size-9 items-center justify-center overflow-hidden rounded-full text-lg text-white" [class.bg-ozggray-900]="userProfileStateResource.resource || errorMessageCode === messageCode.RESOURCE_NOT_FOUND" - [matTooltip]="tooltip" + [tooltip]="tooltip" > <ng-container *ngIf="userProfileStateResource.resource; else noUser"> <span data-test-class="user-profile-assigned">{{ initials }}</span> diff --git a/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.spec.ts b/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.spec.ts index c4a359cdc07d4b7e08c992414ba82bf33c191665..5a26a51d01121a4d0c41f18a75d49b03df411b00 100644 --- a/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.spec.ts +++ b/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.spec.ts @@ -34,12 +34,11 @@ import { SpinnerComponent } from '@alfa-client/ui'; import { NO_NAME_MESSAGE, UserProfileResource, userProfileMessage } from '@alfa-client/user-profile-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatIcon } from '@angular/material/icon'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { faker } from '@faker-js/faker'; -import { ErrorIconComponent, UserIconComponent as OdsUserIconComponent } from '@ods/system'; +import { ErrorIconComponent, UserIconComponent as OdsUserIconComponent, TooltipDirective } from '@ods/system'; import { getDataTestClassOf } from 'libs/tech-shared/test/data-test'; import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { createApiError, createIssue } from '../../../../tech-shared/test/error'; import { UserIconComponent } from './user-icon.component'; @@ -60,7 +59,7 @@ describe('UserIconComponent', () => { UserIconComponent, MatIcon, MockComponent(SpinnerComponent), - MockModule(MatTooltipModule), + MockDirective(TooltipDirective), MockComponent(ErrorIconComponent), MockComponent(OdsUserIconComponent), ], diff --git a/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.ts b/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.ts index a691fdefc542600e33cdac2b9a3b0864fd59c3f8..787e1e16eabcd6eedb875ed7e203f229bfcc9df1 100644 --- a/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.ts +++ b/alfa-client/libs/user-profile/src/lib/user-icon/user-icon.component.ts @@ -32,7 +32,6 @@ import { isUndefined } from 'lodash-es'; }) export class UserIconComponent { @Input() userProfileStateResource: StateResource<UserProfileResource> = createEmptyStateResource<UserProfileResource>(); - @Input() disableTooltip: boolean = false; readonly messageCode = MessageCode; diff --git a/alfa-client/libs/user-profile/src/lib/user-profile-in-header-container/user-profile-in-header/user-profile-in-header.component.html b/alfa-client/libs/user-profile/src/lib/user-profile-in-header-container/user-profile-in-header/user-profile-in-header.component.html index 8808114310f78fe0a7addeb9417dc889456a6e21..a2aac078aaf9d5fc6783864da5b363cf460d9bee 100644 --- a/alfa-client/libs/user-profile/src/lib/user-profile-in-header-container/user-profile-in-header/user-profile-in-header.component.html +++ b/alfa-client/libs/user-profile/src/lib/user-profile-in-header-container/user-profile-in-header/user-profile-in-header.component.html @@ -26,16 +26,17 @@ <ods-dropdown-menu buttonClass="rounded-full p-1 hover:bg-neutral-100 focus:bg-neutral-200 focus:outline-none dark:hover:bg-neutral-700 dark:focus:bg-neutral-600" [label]="buttonLabel" + buttonTestId="user-icon-button" data-test-id="user-icon-dropdown-menu" > <alfa-user-icon button-content - data-test-id="user-icon-button" + data-test-id="user-icon" [userProfileStateResource]="currentUserResource" class="user-profile-icon" > </alfa-user-icon> - <ods-dropdown-menu-button-item caption="Abmelden" (itemClicked)="logoutEmitter.emit()" data-test-id="logout-button"> + <ods-dropdown-menu-button-item caption="Abmelden" (itemClicked)="logoutEmitter.emit()" dataTestId="logout-button"> <ods-logout-icon icon /> </ods-dropdown-menu-button-item> </ods-dropdown-menu> diff --git a/alfa-client/libs/user-profile/src/lib/user-profile-in-header-container/user-profile-in-header/user-profile-in-header.component.spec.ts b/alfa-client/libs/user-profile/src/lib/user-profile-in-header-container/user-profile-in-header/user-profile-in-header.component.spec.ts index 536a5722d27b6afced3eb3d91b8c1bfc0fc163db..1f91ba4533ee9c3ee1b09102a7df8deb9de9b849 100644 --- a/alfa-client/libs/user-profile/src/lib/user-profile-in-header-container/user-profile-in-header/user-profile-in-header.component.spec.ts +++ b/alfa-client/libs/user-profile/src/lib/user-profile-in-header-container/user-profile-in-header/user-profile-in-header.component.spec.ts @@ -30,7 +30,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatIcon } from '@angular/material/icon'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { DropdownMenuButtonItemComponent, DropdownMenuComponent, LogoutIconComponent } from '@ods/system'; -import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { getDataTestIdAttributeOf, getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile'; import { MockComponent } from 'ng-mocks'; import { UserProfileInHeaderComponent } from './user-profile-in-header.component'; @@ -39,7 +39,7 @@ describe('UserProfileInHeaderComponent', () => { let component: UserProfileInHeaderComponent; let fixture: ComponentFixture<UserProfileInHeaderComponent>; - const logoutButton: string = getDataTestIdOf('logout-button'); + const logoutButton: string = getDataTestIdAttributeOf('logout-button'); const userIconDropDownMenu: string = getDataTestIdOf('user-icon-dropdown-menu'); const userProfile: UserProfileResource = createUserProfileResource(); diff --git a/alfa-client/libs/user-profile/src/lib/user-profile.module.ts b/alfa-client/libs/user-profile/src/lib/user-profile.module.ts index 6bd1588ea1c21f419e2e78828ab9a466f0add14f..11ab2e9213281a32febbbd4664fa79d59d4969f4 100644 --- a/alfa-client/libs/user-profile/src/lib/user-profile.module.ts +++ b/alfa-client/libs/user-profile/src/lib/user-profile.module.ts @@ -34,6 +34,7 @@ import { ErrorIconComponent, LogoutIconComponent, UserIconComponent as OdsUserIconComponent, + TooltipDirective, } from '@ods/system'; import { AssignUserProfileButtonContainerComponent } from './assign-user-profile-button-container/assign-user-profile-button-container.component'; import { LinkWithUserNameTooltipContainerComponent } from './link-with-user-name-tooltip-container/link-with-user-name-tooltip-container.component'; @@ -68,6 +69,7 @@ import { UserProfileComponent } from './user-profile/user-profile.component'; DropdownMenuComponent, DropdownMenuItemComponent, DropdownMenuButtonItemComponent, + TooltipDirective, ], declarations: [ UserIconComponent, diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-action-buttons/vorgang-detail-action-buttons.component.scss b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-action-buttons/vorgang-detail-action-buttons.component.scss index cbc6bcde3a27788c790ba3e0e879ee27b28881b7..073c7c4392d2b1dd921d4ac949942297033a68c4 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-action-buttons/vorgang-detail-action-buttons.component.scss +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-action-buttons/vorgang-detail-action-buttons.component.scss @@ -1,5 +1,5 @@ /** - * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den + * 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 @@ -24,6 +24,7 @@ :host { display: flex; flex-grow: 1; + z-index: 1; ::ng-deep { a { diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-back-button-container/vorgang-detail-back-button-container.component.scss b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-back-button-container/vorgang-detail-back-button-container.component.scss index 54c4f3eb8c92af93694c03cdf577fed23cf9f86b..d3c1422dbfc51198d27b9a9abf05525c6c3b3196 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-back-button-container/vorgang-detail-back-button-container.component.scss +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-back-button-container/vorgang-detail-back-button-container.component.scss @@ -1,5 +1,5 @@ /** - * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den + * 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 @@ -21,3 +21,6 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +:host { + z-index: 1; +} diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/aktenzeichen/aktenzeichen.component.html b/alfa-client/libs/vorgang-shared-ui/src/lib/aktenzeichen/aktenzeichen.component.html index a68da5c1302fdc7e652ecfdd960550162eb795c1..8c0250145675de160686ed311fd4ee34fc76b181 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/aktenzeichen/aktenzeichen.component.html +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/aktenzeichen/aktenzeichen.component.html @@ -26,7 +26,7 @@ <div class="line-clamp-1 flex-shrink overflow-hidden break-all text-base font-normal lg:line-clamp-none lg:flex" data-test-id="aktenzeichen" - [matTooltip]="aktenzeichen" + [tooltip]="aktenzeichen" > {{ aktenzeichen }} </div> diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/aktenzeichen/aktenzeichen.component.spec.ts b/alfa-client/libs/vorgang-shared-ui/src/lib/aktenzeichen/aktenzeichen.component.spec.ts index be991fb41f4c0b6b9282959b28478160a33a56aa..d2fa4010f6b41217c0e0a3c16467012c83f7d7af 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/aktenzeichen/aktenzeichen.component.spec.ts +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/aktenzeichen/aktenzeichen.component.spec.ts @@ -23,9 +23,9 @@ */ import { VorgangResource } from '@alfa-client/vorgang-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import { TooltipDirective } from '@ods/system'; import { createVorgangResource } from 'libs/vorgang-shared/test/vorgang'; -import { MockModule } from 'ng-mocks'; +import { MockDirective } from 'ng-mocks'; import { VORGANG_KEIN_AKTENZEICHEN_ZUGEWIESEN } from '../vorgang-util'; import { AktenzeichenComponent } from './aktenzeichen.component'; @@ -39,7 +39,7 @@ describe('AktenzeichenComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [AktenzeichenComponent, MockModule(MatTooltipModule)], + declarations: [AktenzeichenComponent, MockDirective(TooltipDirective)], }).compileComponents(); }); diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-nummer/vorgang-nummer.component.html b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-nummer/vorgang-nummer.component.html index b6c4a666d805ca9138db7480ad8b35aef0f64bc3..3a5eabc30c87191c1257dc4952ce8d5ba65b2ab9 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-nummer/vorgang-nummer.component.html +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-nummer/vorgang-nummer.component.html @@ -29,7 +29,7 @@ <div class="line-clamp-1 flex-shrink overflow-hidden break-all text-base font-normal lg:line-clamp-none lg:flex" data-test-id="vorgang-nummer" - [matTooltip]="vorgang.nummer" + [tooltip]="vorgang.nummer" > {{ vorgang.nummer }} </div> diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-nummer/vorgang-nummer.component.spec.ts b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-nummer/vorgang-nummer.component.spec.ts index c92ef713ea14d618417ee0e3d52b92597d105f48..c6b0813916c7f1dacd623532a7bbc78a31a7ffa6 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-nummer/vorgang-nummer.component.spec.ts +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-nummer/vorgang-nummer.component.spec.ts @@ -26,10 +26,10 @@ import { VorgangResource } from '@alfa-client/vorgang-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatIcon } from '@angular/material/icon'; import { MatIconTestingModule } from '@angular/material/icon/testing'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import { TooltipDirective } from '@ods/system'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { createVorgangResource } from 'libs/vorgang-shared/test/vorgang'; -import { MockModule } from 'ng-mocks'; +import { MockDirective } from 'ng-mocks'; import { VorgangNummerComponent } from './vorgang-nummer.component'; describe('VorgangNummerComponent', () => { @@ -41,7 +41,7 @@ describe('VorgangNummerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [MockModule(MatTooltipModule), VorgangNummerComponent], + declarations: [MockDirective(TooltipDirective), VorgangNummerComponent], imports: [MatIcon, MatIconTestingModule], }).compileComponents(); }); diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-shared-ui.module.ts b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-shared-ui.module.ts index 3ae0643465d4492367f4787a699453b7ebe1a799..a6549aff7c59fa5979d55f7fbb7bb130b9c068fc 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-shared-ui.module.ts +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-shared-ui.module.ts @@ -27,6 +27,7 @@ import { VorgangSharedModule } from '@alfa-client/vorgang-shared'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { TooltipDirective } from '@ods/system'; import { AktenzeichenComponent } from './aktenzeichen/aktenzeichen.component'; import { VorgangNummerComponent } from './vorgang-nummer/vorgang-nummer.component'; import { VorgangSearchContainerComponent } from './vorgang-search-container/vorgang-search-container.component'; @@ -38,7 +39,7 @@ import { VorgangStatusTextComponent } from './vorgang-status-text/vorgang-status import { WiedervorlageIconComponent } from './wiedervorlage-icon/wiedervorlage-icon.component'; @NgModule({ - imports: [CommonModule, VorgangSharedModule, UiModule, RouterModule, TechSharedModule], + imports: [CommonModule, VorgangSharedModule, UiModule, RouterModule, TechSharedModule, TooltipDirective], declarations: [ VorgangSearchContainerComponent, VorgangSearchComponent, diff --git a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-created-at/vorgang-created-at.component.html b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-created-at/vorgang-created-at.component.html index bbf247957fa5a6a1be70c6ec1af5e88c75ccad33..edfe41eb178ce5a0f3dddefc33585fe2e465c96b 100644 --- a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-created-at/vorgang-created-at.component.html +++ b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-created-at/vorgang-created-at.component.html @@ -23,7 +23,7 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<div matTooltip="Eingang: {{ vorgang.createdAt | formatDateWithTimePipe }}"> +<div tooltip="Eingang: {{ vorgang.createdAt | formatDateWithTimePipe }}"> <mat-icon svgIcon="incoming"></mat-icon> <span data-test-id="created-at">{{ vorgang.createdAt | formatDateWithoutYearWithTime }}</span> </div> diff --git a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-created-at/vorgang-created-at.component.spec.ts b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-created-at/vorgang-created-at.component.spec.ts index 01dce45c92b20a216875e28e00370fcd0a5bf490..b551e0550b2ce804d1a28a22416428c704b1cbc2 100644 --- a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-created-at/vorgang-created-at.component.spec.ts +++ b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-created-at/vorgang-created-at.component.spec.ts @@ -33,10 +33,10 @@ import localeDe from '@angular/common/locales/de'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatIcon } from '@angular/material/icon'; import { MatIconTestingModule } from '@angular/material/icon/testing'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import { TooltipDirective } from '@ods/system'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { createVorgangResource } from 'libs/vorgang-shared/test/vorgang'; -import { MockModule } from 'ng-mocks'; +import { MockDirective } from 'ng-mocks'; import { VorgangCreatedAtComponent } from './vorgang-created-at.component'; registerLocaleData(localeDe); @@ -54,7 +54,7 @@ describe('VorgangCreatedAtComponent', () => { FormatDateWithoutYearWithTimePipe, FormatDateWithTimePipe, VorgangCreatedAtComponent, - MockModule(MatTooltipModule), + MockDirective(TooltipDirective), ], imports: [MatIcon, MatIconTestingModule], }).compileComponents(); diff --git a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.spec.ts b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.spec.ts index 19cf045603cf83efe2acddffaabc484dd7c63f2c..cfab40f848188662f2d0f2ea1db7074d60ff7ef7 100644 --- a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.spec.ts +++ b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.spec.ts @@ -48,12 +48,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DATE_LOCALE } from '@angular/material/core'; import { MatIcon } from '@angular/material/icon'; import { MatIconTestingModule } from '@angular/material/icon/testing'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterTestingModule } from '@angular/router/testing'; import { getDataTestClassOf, getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { createUserProfileResource } from 'libs/user-profile-shared/test/user-profile'; import { createVorgangResource } from 'libs/vorgang-shared/test/vorgang'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; import { VorgangArchiveStatusComponent } from './vorgang-archive-status/vorgang-archive-status.component'; import { VorgangBescheidStatusComponent } from './vorgang-bescheid-status/vorgang-bescheid-status.component'; @@ -97,7 +96,6 @@ describe('VorgangListItemComponent', () => { MockComponent(VorgangCreatedAtComponent), MockComponent(VorgangBescheidStatusComponent), MockComponent(VorgangArchiveStatusComponent), - MockModule(MatTooltipModule), ], providers: [ { provide: UserProfileService, useValue: userProfileService }, diff --git a/alfa-client/libs/vorgang/src/lib/vorgang.module.ts b/alfa-client/libs/vorgang/src/lib/vorgang.module.ts index 4a58042a60125194bd7fd2b2b4953188c1f4aa28..cf3421cb9ed957b151e3cf8948828bde38b7f6b1 100644 --- a/alfa-client/libs/vorgang/src/lib/vorgang.module.ts +++ b/alfa-client/libs/vorgang/src/lib/vorgang.module.ts @@ -34,7 +34,7 @@ import { NgModule } from '@angular/core'; import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { RouterModule, Routes } from '@angular/router'; import { ButtonToggleGroupComponent } from '@ods/component'; -import { ArchiveIconComponent, ButtonToggleComponent } from '@ods/system'; +import { ArchiveIconComponent, ButtonToggleComponent, TooltipDirective } from '@ods/system'; import { vorgangFilterViewGuard } from './vorgang-filter-view.guard'; import { VorgangListContainerComponent } from './vorgang-list-container/vorgang-list-container.component'; import { EmptyListComponent } from './vorgang-list-container/vorgang-list/empty-list/empty-list.component'; @@ -182,6 +182,7 @@ const routes: Routes = [ ButtonToggleComponent, ButtonToggleGroupComponent, ArchiveIconComponent, + TooltipDirective, ], declarations: [ VorgangListComponent, diff --git a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-container/wiedervorlage-list-in-vorgang/wiedervorlage-in-vorgang/wiedervorlage-in-vorgang.component.html b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-container/wiedervorlage-list-in-vorgang/wiedervorlage-in-vorgang/wiedervorlage-in-vorgang.component.html index 04ee9bbaeebc4f00c72b095b49a514b4f652aa16..bdef88d210bb2d4b5e240b70f638a0efeb10c9e3 100644 --- a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-container/wiedervorlage-list-in-vorgang/wiedervorlage-in-vorgang/wiedervorlage-in-vorgang.component.html +++ b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-container/wiedervorlage-list-in-vorgang/wiedervorlage-in-vorgang/wiedervorlage-in-vorgang.component.html @@ -30,7 +30,7 @@ <div class="row"> <alfa-wiedervorlage-status data-test-class="status" - [matTooltip]="wiedervorlageResource.frist | toTrafficLightTooltip" + [tooltip]="wiedervorlageResource.frist | toTrafficLightTooltip" [wiedervorlageResource]="wiedervorlageResource" > </alfa-wiedervorlage-status> diff --git a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-container/wiedervorlage-list-in-vorgang/wiedervorlage-in-vorgang/wiedervorlage-in-vorgang.component.spec.ts b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-container/wiedervorlage-list-in-vorgang/wiedervorlage-in-vorgang/wiedervorlage-in-vorgang.component.spec.ts index 5d7273682e8e067353ec1a1ff5ca47d9f9fc393a..6673dc39b36123bb4d9bdd8bbb3e06e1e1dc2dc1 100644 --- a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-container/wiedervorlage-list-in-vorgang/wiedervorlage-in-vorgang/wiedervorlage-in-vorgang.component.spec.ts +++ b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-container/wiedervorlage-list-in-vorgang/wiedervorlage-in-vorgang/wiedervorlage-in-vorgang.component.spec.ts @@ -43,10 +43,10 @@ import { LOCALE_ID } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MAT_DATE_LOCALE } from '@angular/material/core'; import { MatIcon } from '@angular/material/icon'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { RouterTestingModule } from '@angular/router/testing'; +import { TooltipDirective } from '@ods/system'; import { createWiedervorlageResource } from 'libs/wiedervorlage-shared/test/wiedervorlage'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { WiedervorlageStatusComponent } from '../../../wiedervorlage-status/wiedervorlage-status.component'; import { WiedervorlageAttachmentListContainerComponent } from './wiedervorlage-attachment-list-container/wiedervorlage-attachment-list-container.component'; import { WiedervorlageInVorgangExpandButtonComponent } from './wiedervorlage-in-vorgang-expand-button/wiedervorlage-in-vorgang-expand-button.component'; @@ -78,7 +78,7 @@ describe('WiedervorlageInVorgangComponent', () => { MockComponent(WiedervorlageInVorgangExpandButtonComponent), MockComponent(LinkWithUserNameTooltipContainerComponent), MockComponent(TextWithUserNameTooltipContainerComponent), - MockModule(MatTooltipModule), + MockDirective(TooltipDirective), ], imports: [RouterTestingModule], providers: [ diff --git a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-list-container/wiedervorlage-list-in-vorgang-list-container.component.html b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-list-container/wiedervorlage-list-in-vorgang-list-container.component.html index 30f5ee2bb76434da75135c98eea8eff8141f52ef..d70d62eca2546b3e4e322a16040e18b403e70b7e 100644 --- a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-list-container/wiedervorlage-list-in-vorgang-list-container.component.html +++ b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-list-container/wiedervorlage-list-in-vorgang-list-container.component.html @@ -37,7 +37,7 @@ <td> <div class="status"> <alfa-wiedervorlage-status - [matTooltip]="wiedervorlageResource.frist | toTrafficLightTooltip" + [tooltip]="wiedervorlageResource.frist | toTrafficLightTooltip" [wiedervorlageResource]="wiedervorlageResource" data-test-class="status" > diff --git a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-list-container/wiedervorlage-list-in-vorgang-list-container.component.spec.ts b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-list-container/wiedervorlage-list-in-vorgang-list-container.component.spec.ts index e3a91dc026d83280f12fdc55a05d220a01e85a47..9ae9dd5c0812065834a9802f17fcc27883f757f0 100644 --- a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-list-container/wiedervorlage-list-in-vorgang-list-container.component.spec.ts +++ b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-list-in-vorgang-list-container/wiedervorlage-list-in-vorgang-list-container.component.spec.ts @@ -29,9 +29,9 @@ import { import { mock } from '@alfa-client/test-utils'; import { WiedervorlageService } from '@alfa-client/wiedervorlage-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatTooltipModule } from '@angular/material/tooltip'; +import { TooltipDirective } from '@ods/system'; import { createWiedervorlageListResource } from 'libs/wiedervorlage-shared/test/wiedervorlage'; -import { MockComponent, MockModule } from 'ng-mocks'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { of } from 'rxjs'; import { WiedervorlageStatusComponent } from '../wiedervorlage-status/wiedervorlage-status.component'; import { WiedervorlageListInVorgangListContainerComponent } from './wiedervorlage-list-in-vorgang-list-container.component'; @@ -52,7 +52,7 @@ describe('WiedervorlageListInVorgangListContainerComponent', () => { FormatToPrettyDatePipe, ToTrafficLightTooltipPipe, MockComponent(WiedervorlageStatusComponent), - MockModule(MatTooltipModule), + MockDirective(TooltipDirective), ], providers: [ { diff --git a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-status/wiedervorlage-status.component.spec.ts b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-status/wiedervorlage-status.component.spec.ts index 92614350fe078c2e66fb3a659050a551eedc9daf..ab2dd54f9740cb097bff0679157bc920bf8e762e 100644 --- a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-status/wiedervorlage-status.component.spec.ts +++ b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage-status/wiedervorlage-status.component.spec.ts @@ -24,9 +24,7 @@ import { ToTrafficLightPipe } from '@alfa-client/tech-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatIcon } from '@angular/material/icon'; -import { MatTooltipModule } from '@angular/material/tooltip'; import { createWiedervorlageResource } from 'libs/wiedervorlage-shared/test/wiedervorlage'; -import { MockModule } from 'ng-mocks'; import { WiedervorlageStatusComponent } from './wiedervorlage-status.component'; const doneIcon: string = '[data-test-class="done-icon"]'; @@ -37,12 +35,8 @@ describe('WiedervorlageStatusComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - WiedervorlageStatusComponent, - ToTrafficLightPipe, - MatIcon, - MockModule(MatTooltipModule), - ], + declarations: [WiedervorlageStatusComponent, ToTrafficLightPipe], + imports: [MatIcon], }).compileComponents(); }); diff --git a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage.module.ts b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage.module.ts index 73ccc935276138242f2e14f9d14aadf427bdf54b..fec88f242f4f485bf156ef60e5fe897c210b0f59 100644 --- a/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage.module.ts +++ b/alfa-client/libs/wiedervorlage/src/lib/wiedervorlage.module.ts @@ -21,15 +21,16 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { CommonModule, DatePipe } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { RouterModule, Routes } from '@angular/router'; import { BinaryFileModule } from '@alfa-client/binary-file'; import { TechSharedModule } from '@alfa-client/tech-shared'; import { UiModule } from '@alfa-client/ui'; import { UserProfileModule } from '@alfa-client/user-profile'; import { VorgangSharedModule } from '@alfa-client/vorgang-shared'; import { VorgangSharedUiModule } from '@alfa-client/vorgang-shared-ui'; +import { CommonModule, DatePipe } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { TooltipDirective } from '@ods/system'; import { CreateWiedervorlageButtonContainerComponent } from './create-wiedervorlage-button-container/create-wiedervorlage-button-container.component'; import { ErledigenButtonContainerComponent } from './erledigen-button-container/erledigen-button-container.component'; import { SubmitWiedervorlageButtonComponent } from './submit-wiedervorlage-button/submit-wiedervorlage-button.component'; @@ -69,6 +70,7 @@ const routes: Routes = [ VorgangSharedUiModule, BinaryFileModule, UserProfileModule, + TooltipDirective, ], declarations: [ WiedervorlagePageComponent, diff --git a/alfa-client/tsconfig.base.json b/alfa-client/tsconfig.base.json index f6e9e575c23782986c485ac2912154cf9ed224a7..deeabaafe3641c159f6a14ebdf3db81c51e267a0 100644 --- a/alfa-client/tsconfig.base.json +++ b/alfa-client/tsconfig.base.json @@ -17,12 +17,14 @@ "baseUrl": ".", "paths": { "@admin-client/configuration-shared": ["libs/admin/configuration-shared/src/index.ts"], + "@admin-client/configuration": ["libs/admin/configuration/src/index.ts"], "@admin-client/organisations-einheit": ["libs/admin/organisations-einheit/src/index.ts"], "@admin-client/organisations-einheit-shared": ["libs/admin/organisations-einheit-shared/src/index.ts"], "@admin-client/postfach": ["libs/admin/postfach/src/index.ts"], "@admin-client/postfach-shared": ["libs/admin/postfach-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/user": ["libs/admin/user/src/index.ts"], "@admin-client/user-shared": ["libs/admin/user-shared/src/index.ts"], "@alfa-client/api-root-shared": ["libs/api-root-shared/src/index.ts"], @@ -72,9 +74,9 @@ "@alfa-client/wiedervorlage-shared": ["libs/wiedervorlage-shared/src/index.ts"], "@alfa-client/zustaendige-stelle": ["libs/zustaendige-stelle/src/index.ts"], "@alfa-client/zustaendige-stelle-shared": ["libs/zustaendige-stelle-shared/src/index.ts"], + "@authentication": ["libs/authentication/src/index.ts"], "@ods/component": ["libs/design-component/src/index.ts"], - "@ods/system": ["libs/design-system/src/index.ts"], - "authentication": ["libs/authentication/src/index.ts"] + "@ods/system": ["libs/design-system/src/index.ts"] } }, "exclude": ["node_modules", "tmp"]