diff --git a/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-subnavigation.ts b/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-subnavigation.ts index 826cb6a09e8321d97578db6ddc61f3706874ba1c..40dc7120e89587eca1214c5b2eb673df1450a220 100644 --- a/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-subnavigation.ts +++ b/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-subnavigation.ts @@ -45,6 +45,10 @@ export class VorgangSubnavigationE2EComponent { return cy.getTestElement(this.backIconButton); } + public back(): void { + this.getBackButton().click(); + } + public getAnnehmenIconButton() { return cy.getTestElement(this.annehmenIconButton); } diff --git a/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-views.e2e.component.ts b/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-views.e2e.component.ts index 446c287de76ee0c62fd746f6df9241c95ed0ce92..719825fd08a222a32c30f8bafd7c70e260d403b7 100644 --- a/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-views.e2e.component.ts +++ b/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-views.e2e.component.ts @@ -18,6 +18,9 @@ export class VorgangViewsE2EComponent { private readonly zuLoeschenViewItem: VorgangViewE2EComponent = new VorgangViewE2EComponent( 'Zu_Loschen', ); + private readonly ungelesenViewItem: VorgangViewE2EComponent = new VorgangViewE2EComponent( + 'Ungelesen', + ); private readonly wiedervorlagenViewItem: VorgangViewE2EComponent = new VorgangViewE2EComponent( 'Wiedervorlagen', ); @@ -54,6 +57,10 @@ export class VorgangViewsE2EComponent { return this.zuLoeschenViewItem; } + public getUngelesen(): VorgangViewE2EComponent { + return this.ungelesenViewItem; + } + public getWiedervorlagen(): VorgangViewE2EComponent { return this.wiedervorlagenViewItem; } diff --git a/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-zusammenarbeit.e2e.component.ts b/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-zusammenarbeit.e2e.component.ts index e8bafb9462c834d331dec872c19a0d2da747c23b..dbd5760dbef9a22d5739ee2e05a7b1c7df37c73b 100644 --- a/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-zusammenarbeit.e2e.component.ts +++ b/alfa-client/apps/alfa-e2e/src/components/vorgang/vorgang-zusammenarbeit.e2e.component.ts @@ -2,9 +2,9 @@ import { enterWith } from '../../support/cypress.util'; export class VorgangZusammenarbeitE2EComponent { private readonly anfrageButton: string = 'anfrage-erstellen-button'; - private readonly zustaendigeStelleButton: string = 'zustaendige-stelle-search-button'; + private readonly zustaendigeStelleButton: string = 'organisations-einheit-search-button'; private readonly titelText: string = 'Titel-text-input'; - private readonly nachrichtText: string = 'Nachricht-textarea'; + private readonly messageText: string = 'Nachricht-textarea'; private readonly sendButton: string = 'collaboration-request-send-button'; private readonly cancelButton: string = 'collaboration-request-cancel-button'; @@ -32,12 +32,21 @@ export class VorgangZusammenarbeitE2EComponent { enterWith(this.getStelleTitel(), text); } - public getStelleNachricht(): Cypress.Chainable<JQuery<HTMLElement>> { - return cy.getTestElement(this.nachrichtText); + public getMessageText(): Cypress.Chainable<JQuery<HTMLElement>> { + return cy.getTestElement(this.messageText); } - public enterNachricht(text: string): void { - enterWith(this.getStelleNachricht(), text); + public enterMessage(text: string): void { + enterWith(this.getMessageText(), text); + } + + public messageScrollbarIsPresent(): void { + this.getMessageText().then(($textarea) => { + const scrollHeight = $textarea[0].scrollHeight; + const clientHeight = $textarea[0].clientHeight; + + expect(scrollHeight).to.be.greaterThan(clientHeight); + }); } public getSendButton(): Cypress.Chainable<JQuery<HTMLElement>> { diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-bescheid/vorgang-bescheid-automatisch-erstellen.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-bescheid/vorgang-bescheid-automatisch-erstellen.cy.ts index c2a6b888ce2b2bad7055322952440965512d27b5..8f465721f0b67a4a971702d125d5f4bba9df64a9 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-bescheid/vorgang-bescheid-automatisch-erstellen.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-bescheid/vorgang-bescheid-automatisch-erstellen.cy.ts @@ -28,7 +28,7 @@ import { registerLocaleData(localeDe, 'de', localeDeExtra); //TODO: Jenkins konfigurieren -describe.skip('Upload automatic Bescheid', () => { +describe('Upload automatic Bescheid', () => { const mainPage: MainPage = new MainPage(); const vorgangList: VorgangListE2EComponent = mainPage.getVorgangList(); @@ -77,7 +77,7 @@ describe.skip('Upload automatic Bescheid', () => { dropCollections(); }); - describe.skip('Upload automatic Bescheid document', () => { + describe('Upload automatic Bescheid document', () => { it('should show automatic Bescheid button', () => { vorgangList.getListItem(bescheidAutomatik.name).getRoot().click(); waitForSpinnerToDisappear(); diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-list/vorgang-list-ungelesen.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-list/vorgang-list-ungelesen.cy.ts new file mode 100644 index 0000000000000000000000000000000000000000..323803e5693ea60d476d70aff45717143b624cc4 --- /dev/null +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-list/vorgang-list-ungelesen.cy.ts @@ -0,0 +1,127 @@ +import { + PostfachMailE2EComponent, + PostfachMailListItem, +} from 'apps/alfa-e2e/src/components/postfach/postfach-mail.e2e.component'; +import { VorgangSubnavigationE2EComponent } from 'apps/alfa-e2e/src/components/vorgang/vorgang-subnavigation'; +import { + VorgangViewE2EComponent, + VorgangViewsE2EComponent, +} from 'apps/alfa-e2e/src/components/vorgang/vorgang-views.e2e.component'; +import { + PostfachMailItemE2E, + VorgangAttachedItemClientE2E, + VorgangAttachedItemE2E, +} from 'apps/alfa-e2e/src/model/vorgang-attached-item'; +import { VorgangPage } from 'apps/alfa-e2e/src/page-objects/vorgang.po'; +import { exist, haveLength } from 'apps/alfa-e2e/src/support/cypress.util'; +import { + createPostfachNachrichtAttachedItem, + createPostfachNachrichtReplyItem, + initVorgangAttachedItem, +} from 'apps/alfa-e2e/src/support/vorgang-attached-item-util'; +import { VorgangListE2EComponent } from '../../../components/vorgang/vorgang-list.e2e.component'; +import { ClientAttributeNameE2E, ClientAttributesE2E, VorgangE2E } from '../../../model/vorgang'; +import { MainPage, waitForSpinnerToDisappear } from '../../../page-objects/main.po'; +import { dropCollections } from '../../../support/cypress-helper'; +import { initUsermanagerUsers, loginAsSabine } from '../../../support/user-util'; +import { + buildVorgang, + createHasNewPostfachNachrichtClientAttribute, + createHasPostfachNachrichtClientAttribute, + initVorgaenge, + objectIds, +} from '../../../support/vorgang-util'; + +describe('Ungelesene Nachrichten', () => { + const mainPage: MainPage = new MainPage(); + const vorgangList: VorgangListE2EComponent = mainPage.getVorgangList(); + const vorgangPage: VorgangPage = new VorgangPage(); + const subnavigation: VorgangSubnavigationE2EComponent = vorgangPage.getSubnavigation(); + const views: VorgangViewsE2EComponent = mainPage.getViews(); + const ungelesenView: VorgangViewE2EComponent = views.getUngelesen(); + const postfachMailContainer: PostfachMailE2EComponent = vorgangPage.getPostfachMailcontainer(); + + const clientAttributes: ClientAttributesE2E = { + [VorgangAttachedItemClientE2E.OZGCLOUD_NACHRICHTEN_MANAGER]: { + [ClientAttributeNameE2E.HAS_NEW_POSTFACH_NACHRICHT]: + createHasNewPostfachNachrichtClientAttribute(true), + [ClientAttributeNameE2E.HAS_POSTFACH_NACHRICHT]: + createHasPostfachNachrichtClientAttribute(true), + }, + }; + + const vorgangWithReply1: VorgangE2E = { + ...buildVorgang(objectIds[0], 'VorgangWithReply'), + clientAttributes, + }; + + const vorgangWithReply2: VorgangE2E = { + ...buildVorgang(objectIds[1], 'VorgangWithReply 2'), + clientAttributes, + }; + + const postfachMailReply1: PostfachMailItemE2E = createPostfachNachrichtReplyItem(); + const postfachMailReply2: PostfachMailItemE2E = createPostfachNachrichtReplyItem(); + + const postfachNachrichtAttachedItem1: VorgangAttachedItemE2E = { + ...createPostfachNachrichtAttachedItem(objectIds[0], vorgangWithReply1._id.$oid), + item: postfachMailReply1, + }; + const postfachNachrichtAttachedItem2: VorgangAttachedItemE2E = { + ...createPostfachNachrichtAttachedItem(objectIds[1], vorgangWithReply2._id.$oid), + item: postfachMailReply2, + }; + + before(() => { + initVorgaenge([vorgangWithReply1, vorgangWithReply2]); + initVorgangAttachedItem([postfachNachrichtAttachedItem1]); + initVorgangAttachedItem([postfachNachrichtAttachedItem2]); + initUsermanagerUsers(); + + loginAsSabine(); + + waitForSpinnerToDisappear(); + exist(vorgangList.getRoot()); + }); + + after(() => { + dropCollections(); + }); + + describe('Show number of unread messages', () => { + it('should show 2 unread messages in filter', () => { + ungelesenView.getRoot().click(); + waitForSpinnerToDisappear(); + + haveLength(vorgangList.getItems(), 2); + }); + + it('should show 1 unread message after viewing first message', () => { + vorgangList.getListItem(vorgangWithReply1.name).getRoot().click(); + waitForSpinnerToDisappear(); + + const postfachMailItem: PostfachMailListItem = postfachMailContainer.getListItem('Subject'); + postfachMailItem.getRoot().click(); + waitForSpinnerToDisappear(); + + subnavigation.back(); + subnavigation.back(); + + haveLength(vorgangList.getItems(), 1); + }); + + it('should show 0 unread messages after viewing second message', () => { + vorgangList.getListItem(vorgangWithReply2.name).getRoot().click(); + waitForSpinnerToDisappear(); + + const postfachMailItem: PostfachMailListItem = postfachMailContainer.getListItem('Subject'); + postfachMailItem.getRoot().click(); + waitForSpinnerToDisappear(); + + subnavigation.back(); + subnavigation.back(); + + haveLength(vorgangList.getItems(), 0); + }); + }); +}); diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-zusammenarbeit/vorgang-zusammenarbeit-anfragen.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-zusammenarbeit/vorgang-zusammenarbeit-anfragen.cy.ts index ce31b02a2e37104d05b65ec5d3f917969c5ab305..e02c7bed2c4080d6b8fa7d227f826722c67699ce 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-zusammenarbeit/vorgang-zusammenarbeit-anfragen.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/vorgang-zusammenarbeit/vorgang-zusammenarbeit-anfragen.cy.ts @@ -27,6 +27,10 @@ describe('Vorgang Zusammenarbeit anfragen', () => { status: VorgangStatusE2E.IN_BEARBEITUNG, }; + const titleText: string = 'Dies ist ein Test-Titel !"§$%&'; + const messageText: string = + 'Sehr geehrter Tester\n\n Dies ist ein !"§$%& Test\n zum Testen der Nachricht.\n\n\n\nhier sollte eine \nScrollbar\nangezeigt\nwerden!\n\nMfG!'; + before(() => { initVorgaenge([zusammenarbeitVorgang]); initUsermanagerUsers(); @@ -54,7 +58,7 @@ describe('Vorgang Zusammenarbeit anfragen', () => { exist(zusammenarbeitContainer.getZustaendigeStelleButton()); exist(zusammenarbeitContainer.getStelleTitel()); - exist(zusammenarbeitContainer.getStelleNachricht()); + exist(zusammenarbeitContainer.getMessageText()); exist(zusammenarbeitContainer.getSendButton()); exist(zusammenarbeitContainer.getCancelButton()); }); @@ -64,13 +68,14 @@ describe('Vorgang Zusammenarbeit anfragen', () => { notExist(zusammenarbeitContainer.getZustaendigeStelleButton()); notExist(zusammenarbeitContainer.getStelleTitel()); - notExist(zusammenarbeitContainer.getStelleNachricht()); + notExist(zusammenarbeitContainer.getMessageText()); notExist(zusammenarbeitContainer.getSendButton()); notExist(zusammenarbeitContainer.getCancelButton()); exist(zusammenarbeitContainer.getAnfrageButton()); }); it('should open new search label for Zustaendige Stelle', () => { + zusammenarbeitContainer.createAnfrage(); //button click //Layer wird angezeigt }); @@ -101,9 +106,14 @@ describe('Vorgang Zusammenarbeit anfragen', () => { //Layer ist geschlossen }); + it('should be able to enter title and message, and show scrollbar on long text', () => { + zusammenarbeitContainer.enterTitel(titleText); + zusammenarbeitContainer.enterMessage(messageText); + + zusammenarbeitContainer.messageScrollbarIsPresent(); + }); + it('should show title and message read-only and remove buttons after sending', () => { - //Titel eingeben - //Nachricht eingeben //Button klicken //Titel und Datum werden angezeigt //Nachricht wird angezeigt diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/wiedervorlage/wiedervorlage.erledigen.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/wiedervorlage/wiedervorlage.erledigen.cy.ts index b20cea9b1d2b31d36db3535b7526a8032f1b88ed..c9ea833a1b6086cabc8286b7821531d0b8aac9bb 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/wiedervorlage/wiedervorlage.erledigen.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/wiedervorlage/wiedervorlage.erledigen.cy.ts @@ -158,6 +158,8 @@ describe('Wiedervorlage erledigen/wiedereroeffnen', () => { waitforSpinnerToAppear(); waitForSpinnerToDisappear(); + wait(1000, 'Wait for async BE to complete'); + containClass(wiedervorlageContainer.getStatusDot(), 'erledigt'); contains( snackBar.getMessage(), @@ -199,6 +201,8 @@ describe('Wiedervorlage erledigen/wiedereroeffnen', () => { waitforSpinnerToAppear(); waitForSpinnerToDisappear(); + wait(1000, 'Wait for async BE to complete'); + notContainClass(wiedervorlageContainer.getStatusDot(), 'erledigt'); contains( snackBar.getMessage(), @@ -229,6 +233,7 @@ describe('Wiedervorlage erledigen/wiedereroeffnen', () => { const locatorIconDefault: string = 'wiedervorlage-icon-default'; it('back to vorgang list', () => { + wait(1000, 'Wait for async BE to complete'); vorgangPage.getSubnavigation().getBackButton().click(); waitForSpinnerToDisappear(); }); @@ -249,6 +254,8 @@ describe('Wiedervorlage erledigen/wiedereroeffnen', () => { subnavigation.erledigen(); waitForSpinnerToDisappear(); + wait(1000, 'Wait for async BE to complete'); + subnavigation.navigateBack(); waitForSpinnerToDisappear(); }); diff --git a/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-main-dev.yaml b/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-main-dev.yaml index c73587a0b07ff1fb36e484323bd8f5ea31a19300..00e4ac5efb4577ca7cec3289756e6a85b464340d 100644 --- a/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-main-dev.yaml +++ b/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-main-dev.yaml @@ -9,6 +9,8 @@ project: alfa: env: overrideSpringProfiles: "oc,e2e,dev" + customList: + ozgcloud_feature_bescheid-wizard: "true" ingress: use_staging_cert: true ozgcloud: @@ -20,11 +22,12 @@ alfa: vorgang_manager: env: overrideSpringProfiles: "oc,e2e,dev" - ozgcloud_bescheid_smart_documents_url: http://smocker:8080/smartdocuments - ozgcloud_bescheid_smart_documents_basic_auth_username: MGM - ozgcloud_bescheid_smart_documents_basic_auth_password: MGM - ozgcloud_bescheid_smart_documents_template_group: OzgCloudTest - ozgcloud_bescheid_smart_documents_template: Halteverbot + customList: + ozgcloud_bescheid_smart_documents_url: http://smocker:8080/smartdocuments + ozgcloud_bescheid_smart_documents_basic_auth_username: MGM + ozgcloud_bescheid_smart_documents_basic_auth_password: MGM + ozgcloud_bescheid_smart_documents_template_group: OzgCloudTest + ozgcloud_bescheid_smart_documents_template: Halteverbot elasticsearch: enabled: true replicaCount: 1 diff --git a/alfa-client/apps/alfa-e2e/src/fixtures/user-main/user_zelda.json b/alfa-client/apps/alfa-e2e/src/fixtures/user-main/user_zelda.json new file mode 100644 index 0000000000000000000000000000000000000000..a283f68b0934c9f7b991b4ca08ba36caefa20853 --- /dev/null +++ b/alfa-client/apps/alfa-e2e/src/fixtures/user-main/user_zelda.json @@ -0,0 +1,12 @@ +{ + "name": "zelda", + "password": "Y9nk43yrQ_zzIPpfFU-I", + "firstName": "Zelda", + "lastName": "Zusammen", + "fullName": "Zelda Zusammen", + "email": "zelda.z@ozg-sh.de", + "initials": "ZZ", + "dataTestId": "Zelda_Zusammen", + "clientRoles": ["VERWALTUNG_USER"], + "groups": ["E2E Tests"] +} diff --git a/alfa-client/apps/alfa-e2e/src/fixtures/usermanager/usermanager_user_zelda.json b/alfa-client/apps/alfa-e2e/src/fixtures/usermanager/usermanager_user_zelda.json new file mode 100644 index 0000000000000000000000000000000000000000..f41f879cb6fdddb220a07c880ebc5fba946a7da0 --- /dev/null +++ b/alfa-client/apps/alfa-e2e/src/fixtures/usermanager/usermanager_user_zelda.json @@ -0,0 +1,18 @@ +{ + "_id": { + "$oid": "63284e55c39b316b2ad02e2z" + }, + "createdAt": { + "$date": "2024-08-14T13:11:56.489Z" + }, + "deleted": false, + "keycloakUserId": "2ccf0c13-da74-4516-ae3d-f46d30e8ec0c", + "firstName": "Zelda", + "fullName": "Zelda Zusammen", + "lastName": "Zusammen", + "email": "zelda-z@ozg-sh.de", + "lastSyncTimestamp": 1663585874687, + "organisationsEinheitIds": ["9797773", "9093371"], + "roles": ["VERWALTUNG_USER"], + "username": "zelda" +} diff --git a/alfa-client/apps/alfa-e2e/src/support/user-util.ts b/alfa-client/apps/alfa-e2e/src/support/user-util.ts index cf437c63163898a88410f7bbb47143774a312967..a89ac2641cc41863bbce0534b308f3c6cc24ad85 100644 --- a/alfa-client/apps/alfa-e2e/src/support/user-util.ts +++ b/alfa-client/apps/alfa-e2e/src/support/user-util.ts @@ -27,11 +27,13 @@ import { initUsermanagerData, login } from './cypress-helper'; const sabineFixture: UserE2E = require('../fixtures/user-main/user_sabine.json'); const dorotheaFixture: UserE2E = require('../fixtures/user-main/user_dorothea.json'); +const zeldaFixture: UserE2E = require('../fixtures/user-main/user_zelda.json'); const userManagerSabineFixture: UsermanagerUserE2E = require('../fixtures/usermanager/usermanager_user_sabine.json'); const userManagerPeterFixture: UsermanagerUserE2E = require('../fixtures/usermanager/usermanager_user_peter.json'); const userManagerEmilFixture: UsermanagerUserE2E = require('../fixtures/usermanager/usermanager_user_emil.json'); const userManagerDorotheaFixture: UsermanagerUserE2E = require('../fixtures/usermanager/usermanager_user_dorothea.json'); +const userManagerZeldaFixture: UsermanagerUserE2E = require('../fixtures/usermanager/usermanager_user_zelda.json'); export function initUsermanagerUsers() { initUsermanagerData([ @@ -39,6 +41,7 @@ export function initUsermanagerUsers() { getUserManagerUserPeter(), getUserManagerUserEmil(), getUserManagerUserDorothea(), + //getUserManagerUserZelda(), ]); } @@ -50,6 +53,10 @@ export function getUserDorothea(): UserE2E { return dorotheaFixture; } +export function getUserZelda(): UserE2E { + return zeldaFixture; +} + export function getUserManagerUserSabine(): UsermanagerUserE2E { return userManagerSabineFixture; } @@ -66,6 +73,10 @@ export function getUserManagerUserDorothea(): UsermanagerUserE2E { return userManagerDorotheaFixture; } +export function getUserManagerUserZelda(): UsermanagerUserE2E { + return userManagerZeldaFixture; +} + export function getUserSabineId(): string { return getUserManagerUserSabine()._id.$oid; } @@ -77,6 +88,7 @@ enum DatabaseUser { PETER = 'user-main/user_peter.json', RICHARD = 'user-main/user_richard.json', SABINE = 'user-main/user_sabine.json', + ZELDA = 'user-main/user_zelda.json', ZONK = 'user-main/user_zonk.json', } @@ -96,6 +108,10 @@ export function loginAsRichard(): void { login(DatabaseUser.RICHARD); } +export function loginAsZelda(): void { + login(DatabaseUser.ZELDA); +} + export function loginAsSabine(): void { login(DatabaseUser.SABINE); } diff --git a/alfa-client/libs/design-system/src/index.ts b/alfa-client/libs/design-system/src/index.ts index 0c83b3d1ba9df2ab88f88227a9197f681970ca18..16c66be88076bf07e9c5bf7ec2b4052fa5a20d89 100644 --- a/alfa-client/libs/design-system/src/index.ts +++ b/alfa-client/libs/design-system/src/index.ts @@ -26,4 +26,6 @@ export * from './lib/icons/spinner-icon/spinner-icon.component'; export * from './lib/icons/stamp-icon/stamp-icon.component'; export * from './lib/instant-search/instant-search/instant-search.component'; export * from './lib/instant-search/instant-search/instant-search.model'; +export * from './lib/popup/popup-list-item/popup-list-item.component'; +export * from './lib/popup/popup/popup.component'; export * from './lib/testbtn/testbtn.component'; diff --git a/alfa-client/libs/design-system/src/lib/icons/iconVariants.ts b/alfa-client/libs/design-system/src/lib/icons/iconVariants.ts index 6ea2dff309ce50e62416e5dd898bcc4771563625..e13cd3b4f155db048c8aab75887fa4f8f13172da 100644 --- a/alfa-client/libs/design-system/src/lib/icons/iconVariants.ts +++ b/alfa-client/libs/design-system/src/lib/icons/iconVariants.ts @@ -8,6 +8,7 @@ export const iconVariants = cva('', { medium: 'size-6', large: 'size-8', 'extra-large': 'size-10', + xxl: 'size-12', }, }, }); diff --git a/alfa-client/libs/design-system/src/lib/icons/office-icon/office-icon.stories.ts b/alfa-client/libs/design-system/src/lib/icons/office-icon/office-icon.stories.ts index 8a54d90c40add3636c0f41c9748ab529f09fc253..46b9e413bf6ac9da265c12d440201cf12017da9e 100644 --- a/alfa-client/libs/design-system/src/lib/icons/office-icon/office-icon.stories.ts +++ b/alfa-client/libs/design-system/src/lib/icons/office-icon/office-icon.stories.ts @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/angular'; import { OfficeIconComponent } from './office-icon.component'; const meta: Meta<OfficeIconComponent> = { - title: 'Icons/Save icon', + title: 'Icons/Office icon', component: OfficeIconComponent, excludeStories: /.*Data$/, tags: ['autodocs'], diff --git a/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.spec.ts b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e80ce433f6db593962497c6e2c92386745a5045 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { UserIconComponent } from './user-icon.component'; + +describe('UserIconComponent', () => { + let component: UserIconComponent; + let fixture: ComponentFixture<UserIconComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [UserIconComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(UserIconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..90c01e353f6029d459584e6fcdafa425c6e6d9be --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.component.ts @@ -0,0 +1,31 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { twMerge } from 'tailwind-merge'; +import { ExclamationIconComponent } from '../exclamation-icon/exclamation-icon.component'; +import { IconVariants, iconVariants } from '../iconVariants'; + +@Component({ + selector: 'ods-user-icon', + standalone: true, + imports: [CommonModule, ExclamationIconComponent], + template: ` + <svg + viewBox="0 0 47 47" + fill="none" + xmlns="http://www.w3.org/2000/svg" + [ngClass]="[twMerge(iconVariants({ size }), 'fill-ozggray-300', class)]" + > + <path + d="M23.5 3.91663C12.69 3.91663 3.91669 12.69 3.91669 23.5C3.91669 34.31 12.69 43.0833 23.5 43.0833C34.31 43.0833 43.0834 34.31 43.0834 23.5C43.0834 12.69 34.31 3.91663 23.5 3.91663ZM23.5 9.79163C26.7509 9.79163 29.375 12.4158 29.375 15.6666C29.375 18.9175 26.7509 21.5416 23.5 21.5416C20.2492 21.5416 17.625 18.9175 17.625 15.6666C17.625 12.4158 20.2492 9.79163 23.5 9.79163ZM23.5 37.6C18.6042 37.6 14.2763 35.0933 11.75 31.2941C11.8088 27.397 19.5834 25.2625 23.5 25.2625C27.3971 25.2625 35.1913 27.397 35.25 31.2941C32.7238 35.0933 28.3959 37.6 23.5 37.6Z" + /> + </svg> + `, +}) +export class UserIconComponent { + @Input() variant: 'user' | 'initials' = 'user'; + @Input() size: IconVariants['size'] = 'xxl'; + @Input() class: string = undefined; + + iconVariants = iconVariants; + twMerge = twMerge; +} diff --git a/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.stories.ts b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..cf801c263a4b51d9dc560f937de4047b07da27a2 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/user-icon/user-icon.stories.ts @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/angular'; + +import { UserIconComponent } from './user-icon.component'; + +const meta: Meta<UserIconComponent> = { + title: 'Icons/User icon', + component: UserIconComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<UserIconComponent>; + +export const Default: Story = { + args: { size: 'xxl' }, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium', 'large', 'extra-large', 'xxl', 'full'], + description: 'Size of icon. Property "full" means 100%', + table: { + defaultValue: { summary: 'xxl' }, + }, + }, + }, +}; diff --git a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.stories.ts b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.stories.ts index bfe67d23232b417e6cddd3b33f53245a793b161e..522bc7f81a9e3f677d55c9ac8066632c02f15247 100644 --- a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.stories.ts +++ b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.stories.ts @@ -26,14 +26,12 @@ export const SearchResults: Story = { headerText: 'In der OZG-Cloud', searchResults: [ { - text: 'Landeshauptstadt Kiel - Ordnungsamt, Gewerbe- und Schornsteinfegeraufsicht', - subText: 'Fabrikstraße 8-10, 24103 Kiel', - onClick: () => undefined, + title: 'Landeshauptstadt Kiel - Ordnungsamt, Gewerbe- und Schornsteinfegeraufsicht', + description: 'Fabrikstraße 8-10, 24103 Kiel', }, { - text: 'Amt für Digitalisierung, Breitband und Vermessung Nürnberg Außenstelle Hersbruck', - subText: 'Rathausmarkt 7, Hersbruck', - onClick: () => undefined, + title: 'Amt für Digitalisierung, Breitband und Vermessung Nürnberg Außenstelle Hersbruck', + description: 'Rathausmarkt 7, Hersbruck', }, ], }, diff --git a/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.spec.ts b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3d624f9674b26a1f03a2e024f0139f9eb8230746 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.spec.ts @@ -0,0 +1,32 @@ +import { dispatchEventFromFixture } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PopupListItemComponent } from './popup-list-item.component'; + +describe('PopupListItemComponent', () => { + let component: PopupListItemComponent; + let fixture: ComponentFixture<PopupListItemComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PopupListItemComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PopupListItemComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('itemClicked emitter', () => { + it('should emit itemClicked', () => { + component.itemClicked.emit = jest.fn(); + + dispatchEventFromFixture(fixture, 'button', 'click'); + + expect(component.itemClicked.emit).toHaveBeenCalled(); + }); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.ts b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3bc88c4c30db6091ccf72a4e3658972fe4f117b6 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.component.ts @@ -0,0 +1,21 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'ods-popup-list-item', + standalone: true, + imports: [CommonModule], + template: `<button + class="flex min-h-12 w-full items-center gap-4 border-2 border-transparent bg-whitetext px-4 py-3 text-start outline-none hover:border-primary focus-visible:border-focus" + role="listitem" + (click)="itemClicked.emit()" + > + <ng-content select="[icon]" /> + <p class="text-text">{{ caption }}</p> + </button>`, +}) +export class PopupListItemComponent { + @Input({ required: true }) caption!: string; + + @Output() itemClicked: EventEmitter<MouseEvent> = new EventEmitter(); +} diff --git a/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.stories.ts b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..a28201314e94cc47b348fd9082f158ec06326174 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup-list-item/popup-list-item.stories.ts @@ -0,0 +1,24 @@ +import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; + +import { PopupListItemComponent } from './popup-list-item.component'; + +const meta: Meta<PopupListItemComponent> = { + title: 'Popup/Popup list item', + component: PopupListItemComponent, + decorators: [ + moduleMetadata({ + imports: [PopupListItemComponent], + }), + ], + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<PopupListItemComponent>; + +export const Default: Story = { + args: { + caption: 'List item', + }, +}; diff --git a/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.spec.ts b/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d03abd239d9a8054e0efed06259e41bffb8d516 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.spec.ts @@ -0,0 +1,212 @@ +import { getElementFromFixture } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { PopupComponent } from './popup.component'; + +describe('PopupComponent', () => { + let component: PopupComponent; + let fixture: ComponentFixture<PopupComponent>; + const popupButton: string = getDataTestIdOf('popup-button'); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PopupComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(PopupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('togglePopup', () => { + it('should change false to true', () => { + component.isPopupOpen = false; + + component.togglePopup(); + + expect(component.isPopupOpen).toBe(true); + }); + + it('should change true to false', () => { + component.isPopupOpen = true; + + component.togglePopup(); + + expect(component.isPopupOpen).toBe(false); + }); + }); + + describe('aria-expanded', () => { + it('should be true if popup is open', () => { + component.isPopupOpen = true; + fixture.detectChanges(); + + const buttonElement: HTMLElement = getElementFromFixture(fixture, popupButton); + + expect(buttonElement.getAttribute('aria-expanded')).toBe('true'); + }); + + it('should be false if popup is closed', () => { + component.isPopupOpen = false; + fixture.detectChanges(); + + const buttonElement: HTMLElement = getElementFromFixture(fixture, popupButton); + + expect(buttonElement.getAttribute('aria-expanded')).toBe('false'); + }); + }); + + describe('isEscapeKey', () => { + it('should return true', () => { + const escapeKeyEvent: KeyboardEvent = { + ...new KeyboardEvent('esc'), + key: 'Escape', + }; + + const result: boolean = component.isEscapeKey(escapeKeyEvent); + + expect(result).toBe(true); + }); + + it('should return false', () => { + const keyEvent: KeyboardEvent = new KeyboardEvent('whatever'); + + const result: boolean = component.isEscapeKey(keyEvent); + + expect(result).toBe(false); + }); + }); + + describe('closePopupAndFocusButton', () => { + beforeEach(() => { + component.isPopupOpen = true; + jest.spyOn(component.buttonRef.nativeElement, 'focus'); + }); + it('should close popup', () => { + component.closePopupAndFocusButton(); + + expect(component.isPopupOpen).toBe(false); + }); + + it('should focus button', () => { + component.closePopupAndFocusButton(); + + expect(component.buttonRef.nativeElement.focus).toHaveBeenCalled(); + }); + }); + + describe('isPopupClosed', () => { + it('should return true', () => { + component.isPopupOpen = false; + + const result: boolean = component.isPopupClosed(); + + expect(result).toBe(true); + }); + + it('should return false', () => { + component.isPopupOpen = true; + + const result: boolean = component.isPopupClosed(); + + expect(result).toBe(false); + }); + }); + + describe('onKeydownHandler', () => { + const e: KeyboardEvent = new KeyboardEvent('test'); + + beforeEach(() => { + component.closePopupAndFocusButton = jest.fn(); + component.isEscapeKey = jest.fn(); + }); + + describe('popup is closed', () => { + beforeEach(() => { + component.isPopupClosed = jest.fn().mockReturnValue(true); + }); + + it('should not check for escape key', () => { + component.onKeydownHandler(e); + + expect(component.isEscapeKey).not.toHaveBeenCalled(); + }); + }); + + describe('popup is open', () => { + beforeEach(() => { + component.isPopupClosed = jest.fn().mockReturnValue(false); + }); + + it('should check for escape key', () => { + component.onKeydownHandler(e); + + expect(component.isEscapeKey).toHaveBeenCalled(); + }); + + it('should handle escape key', () => { + component.isEscapeKey = jest.fn().mockReturnValue(true); + + component.onKeydownHandler(e); + + expect(component.closePopupAndFocusButton).toHaveBeenCalled(); + }); + + it('should not handle escape key', () => { + component.onKeydownHandler(e); + + expect(component.closePopupAndFocusButton).not.toHaveBeenCalled(); + }); + }); + }); + describe('onClickHandler', () => { + const e: MouseEvent = new MouseEvent('test'); + + beforeEach(() => { + component.closePopupAndFocusButton = jest.fn(); + component.buttonRef.nativeElement.contains = jest.fn(); + }); + + describe('popup is closed', () => { + beforeEach(() => { + component.isPopupClosed = jest.fn().mockReturnValue(true); + }); + + it('should not check for button containing event target', () => { + component.onClickHandler(e); + + expect(component.buttonRef.nativeElement.contains).not.toHaveBeenCalled(); + }); + }); + + describe('popup is open', () => { + beforeEach(() => { + component.isPopupClosed = jest.fn().mockReturnValue(false); + }); + + it('should check for button containing event target', () => { + component.onClickHandler(e); + + expect(component.buttonRef.nativeElement.contains).toHaveBeenCalled(); + }); + + it('should handle click', () => { + component.onClickHandler(e); + + expect(component.closePopupAndFocusButton).toHaveBeenCalled(); + }); + + it('should not handle click', () => { + component.buttonRef.nativeElement.contains = jest.fn().mockReturnValue(true); + + component.onClickHandler(e); + + expect(component.closePopupAndFocusButton).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.ts b/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a4901d657b0d4d7246dcff422553a17fa60a31df --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup/popup.component.ts @@ -0,0 +1,76 @@ +import { CdkTrapFocus } from '@angular/cdk/a11y'; +import { CommonModule } from '@angular/common'; +import { Component, ElementRef, HostListener, Input, ViewChild } from '@angular/core'; +import { twMerge } from 'tailwind-merge'; + +@Component({ + selector: 'ods-popup', + standalone: true, + imports: [CommonModule, CdkTrapFocus], + template: `<div class="relative w-fit"> + <button + class="w-fit outline-2 outline-offset-2 outline-focus" + [ngClass]="[twMerge('w-fit outline-2 outline-offset-2 outline-focus', buttonClass)]" + (click)="togglePopup()" + [attr.aria-expanded]="isPopupOpen" + aria-haspopup="true" + [attr.aria-label]="label" + data-test-id="popup-button" + #button + > + <ng-content select="[button]" /> + </button> + <ul + *ngIf="isPopupOpen" + class="max-h-120 animate-fadeIn absolute min-w-44 max-w-80 overflow-y-auto rounded shadow-lg shadow-grayborder focus:outline-none" + [ngClass]="alignTo === 'left' ? 'right-0' : 'left-0'" + role="dialog" + aria-modal="true" + cdkTrapFocus + [cdkTrapFocusAutoCapture]="true" + > + <ng-content /> + </ul> + </div>`, +}) +export class PopupComponent { + @Input() alignTo: 'left' | 'right' = 'left'; + @Input() label: string = ''; + @Input() buttonClass: string = ''; + + isPopupOpen: boolean = false; + twMerge = twMerge; + + @ViewChild('button') buttonRef: ElementRef<HTMLButtonElement>; + + @HostListener('document:keydown', ['$event']) + onKeydownHandler(e: KeyboardEvent): void { + if (this.isPopupClosed()) return; + if (this.isEscapeKey(e)) this.closePopupAndFocusButton(); + } + + @HostListener('document:click', ['$event']) + onClickHandler(e: MouseEvent): void { + if (this.isPopupClosed()) return; + if (!this.buttonRef.nativeElement.contains(e.target as HTMLElement)) { + this.closePopupAndFocusButton(); + } + } + + togglePopup(): void { + this.isPopupOpen = !this.isPopupOpen; + } + + closePopupAndFocusButton(): void { + this.isPopupOpen = false; + this.buttonRef.nativeElement.focus(); + } + + isEscapeKey(e: KeyboardEvent): boolean { + return e.key === 'Escape'; + } + + isPopupClosed(): boolean { + return !this.isPopupOpen; + } +} diff --git a/alfa-client/libs/design-system/src/lib/popup/popup/popup.stories.ts b/alfa-client/libs/design-system/src/lib/popup/popup/popup.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..ecde2c16145d2e95ddcd3327d51e003d27f721f6 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/popup/popup/popup.stories.ts @@ -0,0 +1,78 @@ +import { + argsToTemplate, + componentWrapperDecorator, + moduleMetadata, + type Meta, + type StoryObj, +} from '@storybook/angular'; + +import { SaveIconComponent } from '../../icons/save-icon/save-icon.component'; +import { UserIconComponent } from '../../icons/user-icon/user-icon.component'; +import { PopupListItemComponent } from '../popup-list-item/popup-list-item.component'; +import { PopupComponent } from './popup.component'; + +const meta: Meta<PopupComponent> = { + title: 'Popup/Popup', + component: PopupComponent, + decorators: [ + moduleMetadata({ + imports: [PopupComponent, PopupListItemComponent, SaveIconComponent, UserIconComponent], + }), + componentWrapperDecorator((story) => `<div class="flex justify-center mb-32">${story}</div>`), + ], + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<PopupComponent>; + +export const Default: Story = { + args: { alignTo: 'left', label: '', buttonClass: '' }, + argTypes: { + alignTo: { + control: 'select', + options: ['left', 'right'], + table: { + defaultValue: { summary: 'left' }, + }, + }, + buttonClass: { description: 'Tailwind class for button' }, + label: { description: 'Aria-label for button' }, + }, + render: (args) => ({ + props: args, + template: `<ods-popup ${argsToTemplate(args)}> + <ods-user-icon button /> + <ods-popup-list-item caption="Lorem" /> + <ods-popup-list-item caption="Ipsum" /> + <ods-popup-list-item caption="Dolor" /> + </ods-popup>`, + }), +}; + +export const LongText: Story = { + render: (args) => ({ + props: args, + template: `<ods-popup ${argsToTemplate(args)}> + <p button>Trigger popup</p> + <ods-popup-list-item caption="Lorem" /> + <ods-popup-list-item caption="Lorem ipsum dolor sit amet" /> + </ods-popup>`, + }), +}; + +export const ItemsWithIcons: Story = { + render: (args) => ({ + props: args, + template: `<ods-popup ${argsToTemplate(args)}> + <p button>Trigger popup</p> + <ods-popup-list-item caption="Lorem"> + <ods-save-icon icon size="small" /> + </ods-popup-list-item> + <ods-popup-list-item caption="Lorem ipsum dolor sit amet"> + <ods-save-icon icon size="small" /> + </ods-popup-list-item> + </ods-popup>`, + }), +}; diff --git a/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js b/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js index 2fe29cc4a320a6a10b8dec75884f5f423b549e1d..b17ef3c855216e3e735314a6e0b17984d4b3068f 100644 --- a/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js +++ b/alfa-client/libs/design-system/src/lib/tailwind-preset/tailwind.config.js @@ -13,7 +13,11 @@ module.exports = { darkMode: 'class', theme: { extend: { - animation: { dash: 'dash 1.5s ease-in-out infinite', 'spin-slow': 'spin 2s linear infinite' }, + animation: { + dash: 'dash 1.5s ease-in-out infinite', + 'spin-slow': 'spin 2s linear infinite', + fadeIn: 'fade-in 0.2s ease-in-out 1', + }, keyframes: { dash: { from: { @@ -29,10 +33,21 @@ module.exports = { 'stroke-dashoffset': '-49', }, }, + 'fade-in': { + '0%': { + opacity: 0, + }, + '100%': { + opacity: 1, + }, + }, }, borderWidth: { 3: '3px', }, + maxHeight: { + 120: '480px', + }, colors: { ozgblue: { 50: 'hsl(200, 100%, 96%)', diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.html b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.html index 2f18c946d79bd3a22d40f0a4a68e04fe9da8da11..2d16e8047196cf952d24fea9f905b058d7a3e9d9 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.html +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.html @@ -29,7 +29,7 @@ <div class="flex min-h-full grow flex-col divide-y divide-gray-200 border-l border-r border-gray-200 dark:divide-background-100 dark:border-transparent lg:flex-row lg:divide-x lg:divide-y-0" > - <div class="grow"> + <div class="w-full grow"> <alfa-vorgang-detail-header [vorgangWithEingang]="vorgangResource" data-test-id="vorgang-detail-header" @@ -92,7 +92,7 @@ </div> </div> </div> - <div class="flex h-full min-w-80 flex-1 flex-col px-4 py-3"> + <div class="flex w-[calc(100vw-2.5rem)] flex-col px-6 py-4 lg:w-80 lg:px-3 lg:py-4"> <alfa-vorgang-detail-antragsteller [antragsteller]="vorgangResource.eingang.antragsteller" data-test-id="vorgang-detail-antragsteller" diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.ts b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.ts index 84620549535e2efd0a84a992fd9f239f18093fa5..f6a3f401d927e1350cb0903df7cc30ef6a5fa84e 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.ts +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.ts @@ -33,7 +33,7 @@ import { Component, Input } from '@angular/core'; selector: 'alfa-vorgang-detail-area', templateUrl: './vorgang-detail-area.component.html', styleUrls: ['./vorgang-detail-area.component.scss'], - styles: [':host {@apply relative flex flex-row grow}'], + styles: [':host {@apply relative w-full}'], }) export class VorgangDetailAreaComponent { @Input() vorgangStateResource: StateResource<VorgangWithEingangResource>; diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/Command.java b/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/Command.java index 1a5d2ac5fda7dc714f9f2b13514dc22705159219..9e862c4756eb06bfa8aa47591b7d390de442d865 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/Command.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/Command.java @@ -26,8 +26,6 @@ package de.ozgcloud.alfa.common.command; import java.time.ZonedDateTime; import java.util.Map; -import org.apache.commons.lang3.StringUtils; - import com.fasterxml.jackson.annotation.JsonIgnore; import de.ozgcloud.alfa.common.LinkedUserProfileResource; @@ -76,14 +74,4 @@ public class Command { public CommandOrder getCommandOrder() { return CommandOrder.fromOrder(order); } - - @JsonIgnore - public boolean isFinishedSuccessfully() { - return status == CommandStatus.FINISHED && StringUtils.isEmpty(errorMessage); - } - - @JsonIgnore - public boolean isNotDone() { - return status.isNotDone(); - } } \ No newline at end of file diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/CommandService.java b/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/CommandService.java index da98c4e572fda1d5c7175040bd88eace508555f6..dc573993c67ac660e97502ae4087e66d43ed3928 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/CommandService.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/CommandService.java @@ -23,7 +23,6 @@ */ package de.ozgcloud.alfa.common.command; -import java.util.Calendar; import java.util.Optional; import java.util.stream.Stream; @@ -43,8 +42,6 @@ import lombok.RequiredArgsConstructor; public class CommandService { static final long NO_RELATION_VERSION = -1; - private static final int WAIT_TIME_MS = 500; - private static final int COMMAND_REQUEST_THRESHOLD_MILLIS = 10000; private final CommandRemoteService remoteService; @@ -114,25 +111,4 @@ public class CommandService { return remoteService.findCommands(vorgangId, Optional.of(CommandStatus.FINISHED), Optional.empty()); } - public Command waitUntilDone(Command commandToWaitFor) { - var command = commandToWaitFor; - var calendar = Calendar.getInstance(); - var timeout = calendar.getTimeInMillis() + COMMAND_REQUEST_THRESHOLD_MILLIS; - while (command.isNotDone() && calendar.getTimeInMillis() < timeout) { - synchronized (this) { - try { - wait(WAIT_TIME_MS); - command = reloadCommand(command.getId()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - } - return command; - } - - public Command reloadCommand(String commandId) { - return remoteService.getCommand(commandId); - } - } \ No newline at end of file diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/CommandStatus.java b/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/CommandStatus.java index 548107b825bca6aaed64a50f6f24611c9d7a8174..285ebdaae3817652aa5c80b1d9817628fd044e6c 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/CommandStatus.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/common/command/CommandStatus.java @@ -23,14 +23,6 @@ */ package de.ozgcloud.alfa.common.command; -import java.util.Set; - public enum CommandStatus { PENDING, FINISHED, ERROR, REVOKE_PENDING, REVOKED; - - private static final Set<CommandStatus> FINAL_STATES = Set.of(FINISHED, ERROR, REVOKED); - - public boolean isNotDone() { - return !FINAL_STATES.contains(this); - } } diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandController.java b/alfa-service/src/main/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandController.java index 2cc413979da3622e47fc9d0d5677b804f001cf23..7f068905ec5b2f0c464fe677dc4cfa10634847c2 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandController.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandController.java @@ -53,23 +53,26 @@ public class WiedervorlageCommandController { @PathVariable long wiedervorlageVersion) { var wiedervorlage = service.getById(wiedervorlageId); var createdCommand = createCommand(wiedervorlage, command); - var doneCommand = service.updateNextFrist(createdCommand, wiedervorlage.getVorgangId()); - return ResponseEntity.created(linkTo(CommandController.class).slash(doneCommand.getId()).toUri()).build(); + return ResponseEntity.created(linkTo(CommandController.class).slash(createdCommand.getId()).toUri()).build(); } Command createCommand(Wiedervorlage wiedervorlage, CreateCommand command) { switch (command.getOrder()) { case LegacyOrder.WIEDERVORLAGE_ERLEDIGEN: { - return service.erledigen(wiedervorlage); + var changed = wiedervorlage.toBuilder().done(true).build(); + service.updateNextFrist(wiedervorlage.getVorgangId(), changed); + return service.erledigen(changed); } case LegacyOrder.WIEDERVORLAGE_WIEDEREROEFFNEN: { - return service.wiedereroeffnen(wiedervorlage); + var changed = wiedervorlage.toBuilder().done(false).build(); + service.updateNextFrist(wiedervorlage.getVorgangId(), changed); + return service.wiedereroeffnen(changed); } case LegacyOrder.EDIT_WIEDERVORLAGE: { - return service.editWiedervorlage(updateWiedervorlageByCommand(wiedervorlage, (Wiedervorlage) command.getBody()), - wiedervorlage.getId(), - wiedervorlage.getVersion()); + var changed = updateWiedervorlageByCommand(wiedervorlage, (Wiedervorlage) command.getBody()); + service.updateNextFrist(wiedervorlage.getVorgangId(), changed); + return service.editWiedervorlage(changed, wiedervorlage.getId(), wiedervorlage.getVersion()); } default: throw new TechnicalException("Unsupported order " + command.getOrder()); @@ -95,10 +98,12 @@ public class WiedervorlageCommandController { @PostMapping public ResponseEntity<Void> createWiedervorlage(@RequestBody CreateCommand command, @PathVariable String vorgangId) { - var createdCommand = service.createWiedervorlage((Wiedervorlage) command.getBody(), vorgangId); - var doneCommand = service.updateNextFrist(createdCommand, createdCommand.getVorgangId()); + var wiedervorlage = (Wiedervorlage) command.getBody(); + var createdCommand = service.createWiedervorlage(wiedervorlage, vorgangId); - return ResponseEntity.created(linkTo(CommandController.class).slash(doneCommand.getId()).toUri()).build(); + service.updateNextFrist(vorgangId, wiedervorlage); + + return ResponseEntity.created(linkTo(CommandController.class).slash(createdCommand.getId()).toUri()).build(); } } } \ No newline at end of file diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageService.java b/alfa-service/src/main/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageService.java index 28f4795c3695c12aa76981ebcb01c2f807fbbbde..c61984151d2b6dd72de917a16391c28aedad325c 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageService.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageService.java @@ -33,12 +33,12 @@ import java.util.stream.Stream; import jakarta.validation.Valid; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import de.ozgcloud.alfa.common.attacheditem.VorgangAttachedItemService; import de.ozgcloud.alfa.common.command.Command; -import de.ozgcloud.alfa.common.command.CommandService; import de.ozgcloud.alfa.common.user.CurrentUserService; import lombok.RequiredArgsConstructor; @@ -52,7 +52,6 @@ class WiedervorlageService { private final WiedervorlageRemoteService remoteService; private final VorgangAttachedItemService vorgangAttachedItemService; private final CurrentUserService currentUserService; - private final CommandService commandService; public Command createWiedervorlage(@Valid Wiedervorlage wiedervorlage, String vorgangId) { return vorgangAttachedItemService.createNewWiedervorlage(addCreated(wiedervorlage), vorgangId); @@ -73,18 +72,14 @@ class WiedervorlageService { return remoteService.getById(wiedervorlageId); } - public Command updateNextFrist(Command command, String vorgangId) { - var doneCommand = commandService.waitUntilDone(command); - if (doneCommand.isFinishedSuccessfully()) { - doUpdateNextFrist(vorgangId); - } - return doneCommand; - } - - void doUpdateNextFrist(String vorgangId) { - var allWiedervorlagen = findByVorgangId(vorgangId); + @Async + public void updateNextFrist(String vorgangId, Wiedervorlage changedOrNewWiedervorlage) { + var persistedWiedervorlagen = findByVorgangId(vorgangId); + var persistedWiedervorlagenExcludingChanged = persistedWiedervorlagen.filter( + wiedervorlage -> !wiedervorlage.getId().equals(changedOrNewWiedervorlage.getId())); - remoteService.updateNextFrist(vorgangId, calculateNextFrist(allWiedervorlagen)); + var wiedervorlagen = Stream.concat(persistedWiedervorlagenExcludingChanged, Stream.of(changedOrNewWiedervorlage)); + remoteService.updateNextFrist(vorgangId, calculateNextFrist(wiedervorlagen)); } Optional<LocalDate> calculateNextFrist(Stream<Wiedervorlage> wiedervorlagen) { @@ -100,11 +95,11 @@ class WiedervorlageService { return remoteService.findByVorgangId(vorgangId); } - Command erledigen(Wiedervorlage wiedervorlage) { + public Command erledigen(Wiedervorlage wiedervorlage) { return vorgangAttachedItemService.setWiedervorlageDone(wiedervorlage, true); } - Command wiedereroeffnen(Wiedervorlage wiedervorlage) { + public Command wiedereroeffnen(Wiedervorlage wiedervorlage) { return vorgangAttachedItemService.setWiedervorlageDone(wiedervorlage, false); } diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandServiceTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandServiceTest.java index 2bf962ce6cb343babed90d058cbe4549f6acb1b5..95b37ba1901cbd62a954642ee61d4daf09fe4954 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandServiceTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandServiceTest.java @@ -27,7 +27,6 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import java.util.Calendar; import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -40,7 +39,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.MockedStatic; import org.mockito.Spy; import de.ozgcloud.alfa.loeschanforderung.DeleteLoeschAnforderung; @@ -299,99 +297,4 @@ class CommandServiceTest { verify(remoteService).findCommands(VorgangHeaderTestFactory.ID, Optional.of(CommandStatus.FINISHED), Optional.empty()); } } - - @Nested - class TestWaitUntilDone { - - private final Command pendingCommand = CommandTestFactory.createBuilder().status(CommandStatus.PENDING).build(); - private final Command finishedCommand = pendingCommand.toBuilder().status(CommandStatus.FINISHED).build(); - - @Nested - class OnFinishedCommand { - - @Test - void shouldReturnFinishedCommand() { - var resultCommand = service.waitUntilDone(finishedCommand); - - assertThat(resultCommand).isEqualTo(finishedCommand); - } - - @Test - void shouldNotReloadCommand() { - service.waitUntilDone(finishedCommand); - - verify(service, never()).reloadCommand(any()); - } - } - - @Nested - class OnPendingCommand { - - @BeforeEach - void setUp() { - doReturn(finishedCommand).when(service).reloadCommand(pendingCommand.getId()); - } - - @Test - void shouldReloadCommand() { - service.waitUntilDone(pendingCommand); - - verify(service).reloadCommand(pendingCommand.getId()); - } - - @Test - void shouldReturnDoneCommand() { - var resultCommand = service.waitUntilDone(pendingCommand); - - assertThat(resultCommand).isEqualTo(finishedCommand); - } - } - - @Nested - class OnTimeoutExceeded { - - @Mock - private Calendar calendar; - - @Test - void shouldReturnPendingCommand() { - try (MockedStatic<Calendar> calendarMockedStatic = mockStatic(Calendar.class)) { - calendarMockedStatic.when(Calendar::getInstance).thenReturn(calendar); - when(calendar.getTimeInMillis()).thenReturn(0L, 15000L); - - var resultCommand = service.waitUntilDone(pendingCommand); - - assertThat(resultCommand).isEqualTo(pendingCommand); - } - } - - @Test - void shouldReloadPendingCommand() { - doReturn(pendingCommand).when(service).reloadCommand(pendingCommand.getId()); - - try (MockedStatic<Calendar> calendarMockedStatic = mockStatic(Calendar.class)) { - calendarMockedStatic.when(Calendar::getInstance).thenReturn(calendar); - when(calendar.getTimeInMillis()).thenReturn(0L, 0L, 15000L); - - service.waitUntilDone(pendingCommand); - - verify(service).reloadCommand(pendingCommand.getId()); - } - } - - @Test - void shouldReturnPendingCommandAfterReload() { - doReturn(pendingCommand).when(service).reloadCommand(pendingCommand.getId()); - - try (MockedStatic<Calendar> calendarMockedStatic = mockStatic(Calendar.class)) { - calendarMockedStatic.when(Calendar::getInstance).thenReturn(calendar); - when(calendar.getTimeInMillis()).thenReturn(0L, 0L, 15000L); - - var resultCommand = service.waitUntilDone(pendingCommand); - - assertThat(resultCommand).isEqualTo(pendingCommand); - } - } - } - } } \ No newline at end of file diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandStatusTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandStatusTest.java deleted file mode 100644 index f91dc7028af4ca954d1dfc9fae1e70affbdcc066..0000000000000000000000000000000000000000 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandStatusTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.ozgcloud.alfa.common.command; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.EnumSource.Mode; - -class CommandStatusTest { - - @Nested - class TestIsNotDone { - - @ParameterizedTest - @EnumSource(names = { "PENDING", "REVOKE_PENDING" }) - void shouldReturnTrue(CommandStatus status) { - var istNotDone = status.isNotDone(); - - assertThat(istNotDone).isTrue(); - - } - - @ParameterizedTest - @EnumSource(names = { "PENDING", "REVOKE_PENDING" }, mode = Mode.EXCLUDE) - void shouldReturnFalse(CommandStatus status) { - var istNotDone = status.isNotDone(); - - assertThat(istNotDone).isFalse(); - } - - } - -} \ No newline at end of file diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandTest.java deleted file mode 100644 index c838d402226703a6133dc5f8a65b79e7d911373e..0000000000000000000000000000000000000000 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/command/CommandTest.java +++ /dev/null @@ -1,84 +0,0 @@ -package de.ozgcloud.alfa.common.command; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.EnumSource.Mode; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.Mock; - -import com.thedeanda.lorem.LoremIpsum; - -class CommandTest { - - @Nested - class TestIsFinishedSuccessfully { - - @Test - void shouldReturnTrue() { - var command = CommandTestFactory.createBuilder().status(CommandStatus.FINISHED).build(); - - var isDoneSuccessfully = command.isFinishedSuccessfully(); - - assertThat(isDoneSuccessfully).isTrue(); - } - - @ParameterizedTest - @EnumSource(names = "FINISHED", mode = Mode.EXCLUDE) - void shouldReturnFalseOnStatusNotFinished(CommandStatus commandStatus) { - var command = CommandTestFactory.createBuilder().status(commandStatus).build(); - - var isDoneSuccessfully = command.isFinishedSuccessfully(); - - assertThat(isDoneSuccessfully).isFalse(); - } - - @ParameterizedTest - @EnumSource - void shouldReturnFalseOnErrorMessage(CommandStatus commandStatus) { - var command = CommandTestFactory.createBuilder().status(commandStatus).errorMessage(LoremIpsum.getInstance().getWords(1)).build(); - - var isDoneSuccessfully = command.isFinishedSuccessfully(); - - assertThat(isDoneSuccessfully).isFalse(); - } - - } - - @Nested - class TestIsNotDone { - - @Mock - private CommandStatus commandStatus; - private Command command; - - @BeforeEach - void setUp() { - command = CommandTestFactory.createBuilder().status(commandStatus).build(); - } - - @Test - void shouldCallIsNotDone() { - command.isNotDone(); - - verify(commandStatus).isNotDone(); - } - - @ParameterizedTest - @ValueSource(booleans = { true, false }) - void shouldReturnIsNotDone(boolean isNotDone) { - when(commandStatus.isNotDone()).thenReturn(isNotDone); - - var result = command.isNotDone(); - - assertThat(result).isEqualTo(isNotDone); - } - - } - -} \ No newline at end of file diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandByVorgangControllerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandByVorgangControllerTest.java index ef4a43f95f4ed8458d9be2f37bb190e32bd87e84..f8b56e2cb33333ee2e01cd40d411f201632f79d4 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandByVorgangControllerTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandByVorgangControllerTest.java @@ -44,13 +44,8 @@ import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import de.ozgcloud.alfa.common.binaryfile.BinaryFileTestFactory; -import de.ozgcloud.alfa.common.command.Command; -import de.ozgcloud.alfa.common.command.CommandController.CommandByRelationController; -import de.ozgcloud.alfa.common.command.CommandStatus; import de.ozgcloud.alfa.common.command.CommandTestFactory; -import de.ozgcloud.alfa.common.command.CreateCommand; import de.ozgcloud.alfa.common.command.LegacyOrder; -import de.ozgcloud.alfa.common.user.CurrentUserService; import de.ozgcloud.alfa.vorgang.VorgangHeaderTestFactory; import de.ozgcloud.alfa.wiedervorlage.WiedervorlageCommandController.WiedervorlageCommandByVorgangController; import lombok.SneakyThrows; @@ -60,15 +55,10 @@ class WiedervorlageCommandByVorgangControllerTest { @Spy @InjectMocks private WiedervorlageCommandByVorgangController controller; - @Mock - private CommandByRelationController commandByRelationController; - @Mock - private CurrentUserService userService; + @Mock private WiedervorlageService service; - @Captor - private ArgumentCaptor<CreateCommand> createCommandCaptor; private MockMvc mockMvc; @BeforeEach @@ -82,15 +72,10 @@ class WiedervorlageCommandByVorgangControllerTest { @Captor private ArgumentCaptor<Wiedervorlage> wiedervorlageCaptor; - private Command createCommand; - private Command doneCommand; @BeforeEach void mockUserService() { - createCommand = CommandTestFactory.create(); - doneCommand = createCommand.toBuilder().status(CommandStatus.FINISHED).build(); - when(service.createWiedervorlage(any(), any())).thenReturn(createCommand); - when(service.updateNextFrist(createCommand, VorgangHeaderTestFactory.ID)).thenReturn(doneCommand); + when(service.createWiedervorlage(any(), any())).thenReturn(CommandTestFactory.create()); } @Nested @@ -105,10 +90,14 @@ class WiedervorlageCommandByVorgangControllerTest { } @Test - void shouldUpdateNextFristOnSuccessfullyDoneCommand() { + void shouldCallServiceToUpdateNextFrist() { doRequest(); - verify(service).updateNextFrist(createCommand, VorgangHeaderTestFactory.ID); + verify(service).updateNextFrist(eq(VorgangHeaderTestFactory.ID), wiedervorlageCaptor.capture()); + assertThat(wiedervorlageCaptor.getValue()) + .usingRecursiveComparison() + .comparingOnlyFields("betreff", "beschreibung", "frist", "attachments") + .isEqualTo(WiedervorlageTestFactory.create()); } @SneakyThrows diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandControllerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandControllerTest.java index 198f7a00535cfc7dbcc73c53fa65f3d826a2225d..4ab1dcc715ab7ea4234a59cf2cd79dc30f9d57bb 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandControllerTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageCommandControllerTest.java @@ -35,6 +35,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; @@ -46,7 +48,6 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import de.ozgcloud.alfa.common.binaryfile.FileId; import de.ozgcloud.alfa.common.command.Command; import de.ozgcloud.alfa.common.command.CommandOrder; -import de.ozgcloud.alfa.common.command.CommandStatus; import de.ozgcloud.alfa.common.command.CommandTestFactory; import de.ozgcloud.alfa.common.command.CreateCommand; import de.ozgcloud.alfa.common.command.LegacyOrder; @@ -78,18 +79,15 @@ class WiedervorlageCommandControllerTest { @Nested class ControllerMethods { - private Command createCommand; - private Command doneCommand; + @Captor + private ArgumentCaptor<Wiedervorlage> wiedervorlageCaptor; @BeforeEach void init() { when(service.getById(any())).thenReturn(WiedervorlageTestFactory.create()); - createCommand = CommandTestFactory.createBuilder() + when(service.editWiedervorlage(any(), any(), anyLong())).thenReturn(CommandTestFactory.createBuilder() .order(CommandOrder.UPDATE_ATTACHED_ITEM.name()) - .body(WiedervorlageTestFactory.createAsMap()).build(); - doneCommand = createCommand.toBuilder().status(CommandStatus.FINISHED).build(); - when(service.editWiedervorlage(any(), any(), anyLong())).thenReturn(createCommand); - when(service.updateNextFrist(createCommand, VorgangHeaderTestFactory.ID)).thenReturn(doneCommand); + .body(WiedervorlageTestFactory.createAsMap()).build()); } @SneakyThrows @@ -101,10 +99,14 @@ class WiedervorlageCommandControllerTest { } @Test - void shouldUpdateNextFristOnSuccessfullyDoneCommand() { + void shouldCallServiceUpdateNextFrist() { doRequest(); - verify(service).updateNextFrist(createCommand, VorgangHeaderTestFactory.ID); + verify(service).updateNextFrist(eq(VorgangHeaderTestFactory.ID), wiedervorlageCaptor.capture()); + assertThat(wiedervorlageCaptor.getValue()) + .usingRecursiveComparison() + .comparingOnlyFields("betreff", "beschreibung", "frist", "attachments") + .isEqualTo(WiedervorlageTestFactory.create()); } @SneakyThrows @@ -152,6 +154,9 @@ class WiedervorlageCommandControllerTest { @Nested class TestCreateCommand { + @Captor + private ArgumentCaptor<Wiedervorlage> wiedervorlageArgumentCaptor; + @DisplayName("for order 'erledigen'") @Nested class TestOnErledigenOrder { @@ -162,6 +167,23 @@ class WiedervorlageCommandControllerTest { verify(service).erledigen(any(Wiedervorlage.class)); } + + @Test + void shouldUpdateNextFrist() { + callCreateCommand(LegacyOrder.WIEDERVORLAGE_ERLEDIGEN); + + verify(service).updateNextFrist(eq(VorgangHeaderTestFactory.ID), any(Wiedervorlage.class)); + } + + @Test + void shouldSetWiedervorlageAsDone() { + callCreateCommand(LegacyOrder.WIEDERVORLAGE_ERLEDIGEN); + + verify(service).updateNextFrist(eq(VorgangHeaderTestFactory.ID), wiedervorlageArgumentCaptor.capture()); + assertThat(wiedervorlageArgumentCaptor.getValue()) + .usingRecursiveComparison() + .isEqualTo(WiedervorlageTestFactory.createBuilder().done(true).build()); + } } @DisplayName("for order 'wiedereroeffnen'") @@ -174,6 +196,23 @@ class WiedervorlageCommandControllerTest { verify(service).wiedereroeffnen(any(Wiedervorlage.class)); } + + @Test + void shouldUpdateNextFrist() { + callCreateCommand(LegacyOrder.WIEDERVORLAGE_WIEDEREROEFFNEN); + + verify(service).updateNextFrist(eq(VorgangHeaderTestFactory.ID), any(Wiedervorlage.class)); + } + + @Test + void shouldSetWiedervorlageAsOpen() { + callCreateCommand(LegacyOrder.WIEDERVORLAGE_WIEDEREROEFFNEN); + + verify(service).updateNextFrist(eq(VorgangHeaderTestFactory.ID), wiedervorlageArgumentCaptor.capture()); + assertThat(wiedervorlageArgumentCaptor.getValue()) + .usingRecursiveComparison() + .isEqualTo(WiedervorlageTestFactory.createBuilder().done(false).build()); + } } @DisplayName("for order 'edit'") @@ -188,6 +227,16 @@ class WiedervorlageCommandControllerTest { eq(WiedervorlageTestFactory.VERSION)); } + @Test + void shouldUpdateNextFrist() { + var wiedervorlage = WiedervorlageTestFactory.create(); + doReturn(wiedervorlage).when(controller).updateWiedervorlageByCommand(any(), any()); + + callCreateCommand(LegacyOrder.EDIT_WIEDERVORLAGE); + + verify(service).updateNextFrist(VorgangHeaderTestFactory.ID, wiedervorlage); + } + @DisplayName("update wiedervorlage by given command") @Nested class TestUpdateWiedervorlageByCommand { diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageServiceTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageServiceTest.java index ae7f2e55675bd0ee63bb76f7a1b175b3bec39534..b8ab4843ea90a7148c175f4349f6a23ba62a6d39 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageServiceTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/wiedervorlage/WiedervorlageServiceTest.java @@ -38,7 +38,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -47,7 +46,6 @@ import org.mockito.Spy; import de.ozgcloud.alfa.common.attacheditem.VorgangAttachedItemService; import de.ozgcloud.alfa.common.command.Command; import de.ozgcloud.alfa.common.command.CommandService; -import de.ozgcloud.alfa.common.command.CommandStatus; import de.ozgcloud.alfa.common.command.CommandTestFactory; import de.ozgcloud.alfa.common.user.CurrentUserService; import de.ozgcloud.alfa.common.user.UserProfileTestFactory; @@ -108,14 +106,14 @@ class WiedervorlageServiceTest { } @Test - void shouldSetCreatedAt() { + void shouldSetCreatedAt() throws Exception { var wiedervorlage = callAddCreated(); assertThat(wiedervorlage.getCreatedAt()).isNotNull().isCloseTo(ZonedDateTime.now(), within(2, ChronoUnit.SECONDS)); } @Test - void shouldSetCreatedBy() { + void shouldSetCreatedBy() throws Exception { var wiedervorlage = callAddCreated(); assertThat(wiedervorlage.getCreatedBy()).isEqualTo(UserProfileTestFactory.ID.toString()); @@ -173,150 +171,95 @@ class WiedervorlageServiceTest { } @Nested - class TestDoUpdateNextFrist { - - @Nested - class ServiceMethod { - - @BeforeEach - void mockService() { - when(remoteService.findByVorgangId(any())).thenReturn(Stream.of(WiedervorlageTestFactory.create())); - } - - @Test - void shoulDoCalculation() { - callUpdateNextFrist(); - - verify(service).calculateNextFrist(ArgumentMatchers.<Stream<Wiedervorlage>>any()); - } - - @Test - void shouldCallFindByVorgangId() { - callUpdateNextFrist(); - - verify(service).findByVorgangId(VorgangHeaderTestFactory.ID); - } - - @Test - void shouldCallRemoteService() { - doReturn(Optional.of(WiedervorlageTestFactory.FRIST)).when(service).calculateNextFrist(any()); + class TestUpdateNextFrist { - callUpdateNextFrist(); + private final Wiedervorlage wiedervorlage = WiedervorlageTestFactory.create(); + private final Wiedervorlage foundWiedervorlage = WiedervorlageTestFactory.create(); - verify(remoteService).updateNextFrist(VorgangHeaderTestFactory.ID, Optional.of(WiedervorlageTestFactory.FRIST)); - } + @Captor + private ArgumentCaptor<Stream<Wiedervorlage>> allWiedervorlagen; - private void callUpdateNextFrist() { - service.doUpdateNextFrist(VorgangHeaderTestFactory.ID); - } + @BeforeEach + void mockService() { + when(remoteService.findByVorgangId(VorgangHeaderTestFactory.ID)).thenReturn(Stream.of(foundWiedervorlage)); + doReturn(Optional.of(WiedervorlageTestFactory.FRIST)).when(service).calculateNextFrist(any()); } - @Nested - class Calculation { + @Test + void shouldDoCalculation() { + callUpdateNextFrist(); - @Test - void shouldReturnNullOnAllDone() { - var wiedervorlage = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().plus(1, ChronoUnit.DAYS)).done(true).build(); + verify(service).calculateNextFrist(any()); + } - var nextFrist = calculateNextFrist(Stream.of(wiedervorlage)); + @Test + void shouldAddWiedervorlageToFoundWiedervorlagen() { + callUpdateNextFrist(); - assertThat(nextFrist).isEmpty(); - } + verify(service).calculateNextFrist(allWiedervorlagen.capture()); + assertThat(allWiedervorlagen.getValue()).containsExactly(wiedervorlage); + } - @Test - void shouldReturnEarliestFrist() { - var fristPast2Days = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().minus(2, ChronoUnit.DAYS)).done(false) - .build(); - var fristPast1Day = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().minus(1, ChronoUnit.DAYS)).done(false).build(); - var fristFuture1Day = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().plus(1, ChronoUnit.DAYS)).done(false) - .build(); - var fristFuture2Days = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().plus(2, ChronoUnit.DAYS)).done(false) - .build(); - - var nextFrist = calculateNextFrist(Stream.of(fristPast2Days, fristPast1Day, fristFuture1Day, fristFuture2Days)); - - assertThat(nextFrist).contains(LocalDate.now().minus(2, ChronoUnit.DAYS)); - } + @Test + void shouldCallFindByVorgangId() { + callUpdateNextFrist(); - @Test - void shouldReturnFristIgnoringDone() { - var fristPast1DayNotDone = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().plus(1, ChronoUnit.DAYS)).done(false) - .build(); - var fristPast1DayDone = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().minus(1, ChronoUnit.DAYS)).done(true) - .build(); + verify(service).findByVorgangId(VorgangHeaderTestFactory.ID); + } - var nextFrist = calculateNextFrist(Stream.of(fristPast1DayNotDone, fristPast1DayDone)); + @Test + void shouldCallRemoteService() { + callUpdateNextFrist(); - assertThat(nextFrist).contains(LocalDate.now().plus(1, ChronoUnit.DAYS)); - } + verify(remoteService).updateNextFrist(VorgangHeaderTestFactory.ID, Optional.of(WiedervorlageTestFactory.FRIST)); + } - private Optional<LocalDate> calculateNextFrist(Stream<Wiedervorlage> wiedervorlagen) { - return service.calculateNextFrist(wiedervorlagen); - } + private void callUpdateNextFrist() { + service.updateNextFrist(VorgangHeaderTestFactory.ID, wiedervorlage); } } @Nested - class TestUpdateNextFrist { + class TestCalculateNextFrist { @Test - void shouldWaitUntilCommandDone() { - var pendingCommand = CommandTestFactory.createBuilder().status(CommandStatus.PENDING).build(); - var command = CommandTestFactory.create(); - when(commandService.waitUntilDone(command)).thenReturn(pendingCommand); + void shouldReturnNullOnAllDone() { + var wiedervorlage = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().plus(1, ChronoUnit.DAYS)).done(true).build(); - service.updateNextFrist(command, VorgangHeaderTestFactory.ID); + var nextFrist = calculateNextFrist(Stream.of(wiedervorlage)); - verify(commandService).waitUntilDone(command); + assertThat(nextFrist).isEmpty(); } @Test - void shouldReturnDoneCommand() { - var doneCommand = CommandTestFactory.createBuilder().status(CommandStatus.FINISHED).build(); - var command = CommandTestFactory.create(); - when(commandService.waitUntilDone(command)).thenReturn(doneCommand); - - var result = service.updateNextFrist(command, VorgangHeaderTestFactory.ID); - - assertThat(result).isEqualTo(doneCommand); + void shouldReturnEarliestFrist() { + var fristPast2Days = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().minus(2, ChronoUnit.DAYS)).done(false) + .build(); + var fristPast1Day = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().minus(1, ChronoUnit.DAYS)).done(false).build(); + var fristFuture1Day = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().plus(1, ChronoUnit.DAYS)).done(false) + .build(); + var fristFuture2Days = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().plus(2, ChronoUnit.DAYS)).done(false) + .build(); + + var nextFrist = calculateNextFrist(Stream.of(fristPast2Days, fristPast1Day, fristFuture1Day, fristFuture2Days)); + + assertThat(nextFrist).contains(LocalDate.now().minus(2, ChronoUnit.DAYS)); } - @Nested - class OnDoneSuccessfullyCommand { - private final Command command = CommandTestFactory.create(); - private final Command doneCommand = CommandTestFactory.createBuilder().status(CommandStatus.FINISHED).build(); - - @BeforeEach - void setUp() { - when(commandService.waitUntilDone(command)).thenReturn(doneCommand); - } + @Test + void shouldReturnFristIgnoringDone() { + var fristPast1DayNotDone = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().plus(1, ChronoUnit.DAYS)).done(false) + .build(); + var fristPast1DayDone = WiedervorlageTestFactory.createBuilder().frist(LocalDate.now().minus(1, ChronoUnit.DAYS)).done(true) + .build(); - @Test - void shouldUpdateNextFrist() { - service.updateNextFrist(command, VorgangHeaderTestFactory.ID); + var nextFrist = calculateNextFrist(Stream.of(fristPast1DayNotDone, fristPast1DayDone)); - verify(service).doUpdateNextFrist(VorgangHeaderTestFactory.ID); - } + assertThat(nextFrist).contains(LocalDate.now().plus(1, ChronoUnit.DAYS)); } - @Nested - class OnNotDoneSuccessfullyCommand { - private final Command command = CommandTestFactory.create(); - private final Command pendingCommand = CommandTestFactory.createBuilder().status(CommandStatus.PENDING).build(); - - @BeforeEach - void setUp() { - when(commandService.waitUntilDone(command)).thenReturn(pendingCommand); - } - - @Test - void shouldNotUpdateNextFrist() { - service.updateNextFrist(command, VorgangHeaderTestFactory.ID); - - verify(service, never()).doUpdateNextFrist(VorgangHeaderTestFactory.ID); - } + private Optional<LocalDate> calculateNextFrist(Stream<Wiedervorlage> wiedervorlagen) { + return service.calculateNextFrist(wiedervorlagen); } - } - } \ No newline at end of file diff --git a/src/main/helm/templates/deployment.yaml b/src/main/helm/templates/deployment.yaml index c21b9c17e45df3527a4550c51caffadb2295c26c..edfb381cc4c6e5a540411f140df4b359727c044d 100644 --- a/src/main/helm/templates/deployment.yaml +++ b/src/main/helm/templates/deployment.yaml @@ -109,6 +109,10 @@ spec: value: {{ ((.Values.ozgcloud).xdomea).behoerdenschluesselUri}} - name: ozgcloud_xdomea_behoerdenschluesselVersion value: {{ ((.Values.ozgcloud).xdomea).behoerdenschluesselVersion | quote }} + - name: grpc_client_zufi-manager_address + value: {{ .Values.zufiManager.address }} + - name: grpc_client_zufi-manager_negotiationType + value: {{ (.Values.zufiManager).grpcClientNegotiationType | default "TLS" }} {{- if ((.Values.ozgcloud).feature).collaborationEnabled }} - name: ozgcloud_feature_collaborationEnabled value: {{ ((.Values.ozgcloud).feature).collaborationEnabled | quote }} diff --git a/src/main/helm/values.yaml b/src/main/helm/values.yaml index 0e84c312bbfbcf0e243f8b32f2da91f815f353fe..1a13c1ebe2980adc97be000d735850ee5d59b900 100644 --- a/src/main/helm/values.yaml +++ b/src/main/helm/values.yaml @@ -31,7 +31,8 @@ replicaCount: 2 # [default: 2] usermanagerName: user-manager - +zufiManager: + address: zufi-server.zufi:9090 # env: # overrideSpringProfiles: "oc,prod" diff --git a/src/test/helm-linter-values.yaml b/src/test/helm-linter-values.yaml index 51a5840b198286e9e6f323503971c3e56981900f..18f72b2662390d78c5e547e1f7b90d403f8edf7d 100644 --- a/src/test/helm-linter-values.yaml +++ b/src/test/helm-linter-values.yaml @@ -37,3 +37,6 @@ sso: serverUrl: https://sso.company.local imagePullSecret: image-pull-secret + +zufiManager: + address: https://url.url \ No newline at end of file diff --git a/src/test/helm/deployment_collaboration_env_test.yaml b/src/test/helm/deployment_collaboration_env_test.yaml index 21ca8ba4550390d89e333fb62a3110e389615f81..4cb772d0ee55c2dc51d332ecf415bc1526e2b8b2 100644 --- a/src/test/helm/deployment_collaboration_env_test.yaml +++ b/src/test/helm/deployment_collaboration_env_test.yaml @@ -14,6 +14,16 @@ set: serverUrl: https://sso.company.local imagePullSecret: image-pull-secret tests: + - it: should have set zufi server address + set: + zufiManager: + address: url://url.url + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: grpc_client_zufi-manager_address + value: url://url.url - it: should enable collaboration set: ozgcloud: @@ -35,4 +45,22 @@ tests: path: spec.template.spec.containers[0].env content: name: ozgcloud_feature_collaborationEnabled - any: true \ No newline at end of file + any: true + - it: should set negotiation type + set: + zufiManager: + grpcClientNegotiationType: PLAINTEXT + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: grpc_client_zufi-manager_negotiationType + value: PLAINTEXT + - it: negotiation type is TLS in standard + set: + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: grpc_client_zufi-manager_negotiationType + value: TLS \ No newline at end of file