diff --git a/alfa-client/apps/alfa-e2e/docker-compose.yml b/alfa-client/apps/alfa-e2e/docker-compose.yml index c1084ee33bd694659129af4a90e59dc5d1b0c288..e541a7eef1c009fcd890726f87fb35cb11b50868 100644 --- a/alfa-client/apps/alfa-e2e/docker-compose.yml +++ b/alfa-client/apps/alfa-e2e/docker-compose.yml @@ -211,10 +211,7 @@ services: - 7080:8080 - 7081:8081 healthcheck: - test: [ - 'CMD-SHELL', - "wget --spider localhost:8081/version", - ] + test: ['CMD-SHELL', 'wget --spider localhost:8081/version'] interval: 5s timeout: 5s retries: 5 @@ -228,4 +225,4 @@ services: curl -X POST http://smocker:8081/mocks -H 'Content-Type: application/x-yaml' --data-binary @/mocks/mocks.yaml" depends_on: smocker: - condition: service_healthy \ No newline at end of file + condition: service_healthy 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 9433275b79fafd874af6f7fbef835e5ad2deb76f..9197f6bc89b181b3cfb5851d03d86083b60704ed 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 @@ -1,4 +1,4 @@ -import { enterWith } from '../../support/cypress.util'; +import { contains, enterWith, haveValue } from '../../support/cypress.util'; export class VorgangZusammenarbeitE2EComponent { private readonly anfrageButton: string = 'anfrage-erstellen-button'; @@ -7,6 +7,12 @@ export class VorgangZusammenarbeitE2EComponent { private readonly messageText: string = 'Nachricht-textarea'; private readonly sendButton: string = 'collaboration-request-submit-button'; private readonly cancelButton: string = 'collaboration-request-cancel-button'; + private readonly searchText: string = 'instant_search-text-input'; + private readonly closeSearchButton: string = 'close-search-dialog'; + private readonly clearSearchButton: string = 'clear-instant-search'; + private readonly searchEntry: string = 'item-button'; + private readonly orgaAddress: string = 'organisations-einheit-in-collaboration'; + private readonly anfrageResult: string = 'collaboration-request-result'; public getAnfrageButton(): Cypress.Chainable<JQuery<HTMLElement>> { return cy.getTestElement(this.anfrageButton); @@ -64,4 +70,60 @@ export class VorgangZusammenarbeitE2EComponent { public cancelAnfrage(): void { this.getCancelButton().click(); } + + public getCloseSearchButton(): Cypress.Chainable<JQuery<HTMLElement>> { + return cy.getTestElement(this.closeSearchButton); + } + + public closeSearch(): void { + this.getCloseSearchButton().click(); + } + + public getSearchText(): Cypress.Chainable<JQuery<HTMLElement>> { + return cy.getTestElement(this.searchText); + } + + public hasSearchText(text: string): void { + haveValue(this.getSearchText(), text); + } + + public enterSearchInput(text: string): void { + this.getSearchText().clear().type(text); + } + + public getClearSearchButton(): Cypress.Chainable<JQuery<HTMLElement>> { + return cy.getTestElement(this.clearSearchButton); + } + + public clearSearch(): void { + this.getClearSearchButton().click(); + } + + public countSearchEntries(): Cypress.Chainable<number> { + return cy.getTestElement(this.searchEntry).then((entries) => { + return Cypress.$(entries).length; + }); + } + + public expectNumberOfEntriesToBe(entries: number): void { + this.countSearchEntries().then((count) => { + expect(count).to.equal(entries); + }); + } + + public clickSearchEntry(index: number): void { + cy.getTestElement(this.searchEntry).eq(index).click(); + } + + public addressContains(address: string): void { + contains(cy.getTestElement(this.orgaAddress), address); + } + + public getAnfrageResult(): Cypress.Chainable<JQuery<HTMLElement>> { + return cy.getTestElement(this.anfrageResult); + } + + public anfrageResultContains(anfrage: string): void { + contains(this.getAnfrageResult(), anfrage); + } } diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-nachricht-reply-button.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-nachricht-reply-button.cy.ts index eca73b26f756e3a3610b82f62299ddbde612e5a3..10a58ba9be2022445c7f3bbae826a7388a88ca3b 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-nachricht-reply-button.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-nachricht-reply-button.cy.ts @@ -44,7 +44,7 @@ describe('Postfach Nachricht reply button', () => { header: { collaborationLevel: 0, serviceKonto: { - type: 'BayernId', + type: 'BAYERN_ID', postfachAddress: [ { type: 1, 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 index 323803e5693ea60d476d70aff45717143b624cc4..f81145752792b75c1f19e7169f42dbc0a9c5e54c 100644 --- 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 @@ -32,7 +32,7 @@ import { objectIds, } from '../../../support/vorgang-util'; -describe('Ungelesene Nachrichten', () => { +describe('VorgangList Ungelesene Nachrichten', () => { const mainPage: MainPage = new MainPage(); const vorgangList: VorgangListE2EComponent = mainPage.getVorgangList(); const vorgangPage: VorgangPage = new VorgangPage(); 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 e02c7bed2c4080d6b8fa7d227f826722c67699ce..ff47d4f57c44947ca091adfd2b7075fa92013942 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 @@ -1,14 +1,15 @@ import { registerLocaleData } from '@angular/common'; import localeDe from '@angular/common/locales/de'; import localeDeExtra from '@angular/common/locales/extra/de'; +import { SnackBarE2EComponent } from 'apps/alfa-e2e/src/components/ui/snackbar.e2e.component'; import { VorgangZusammenarbeitE2EComponent } from 'apps/alfa-e2e/src/components/vorgang/vorgang-zusammenarbeit.e2e.component'; -import { VorgangE2E, VorgangStatusE2E } from 'apps/alfa-e2e/src/model/vorgang'; +import { VorgangE2E, VorgangMessagesE2E, VorgangStatusE2E } from 'apps/alfa-e2e/src/model/vorgang'; import 'cypress-real-events/support'; import { VorgangListE2EComponent } from '../../../components/vorgang/vorgang-list.e2e.component'; import { MainPage, waitForSpinnerToDisappear } from '../../../page-objects/main.po'; import { VorgangPage } from '../../../page-objects/vorgang.po'; import { dropCollections } from '../../../support/cypress-helper'; -import { exist, notExist } from '../../../support/cypress.util'; +import { contains, exist, notExist } from '../../../support/cypress.util'; import { initUsermanagerUsers, loginAsSabine } from '../../../support/user-util'; import { buildVorgang, initVorgaenge, objectIds } from '../../../support/vorgang-util'; @@ -30,6 +31,9 @@ describe('Vorgang Zusammenarbeit anfragen', () => { 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!'; + const stelleSearch1: string = 'k'; + const stelleSearch2: string = 'Kiel'; + const snackBar: SnackBarE2EComponent = mainPage.getSnackBar(); before(() => { initVorgaenge([zusammenarbeitVorgang]); @@ -76,48 +80,66 @@ describe('Vorgang Zusammenarbeit anfragen', () => { it('should open new search label for Zustaendige Stelle', () => { zusammenarbeitContainer.createAnfrage(); - //button click - //Layer wird angezeigt + zusammenarbeitContainer.searchZustaendigeStelle(); + + exist(zusammenarbeitContainer.getCloseSearchButton()); + exist(zusammenarbeitContainer.getSearchText()); }); it('should close layer on Cancel click', () => { - //click Abbrechen + zusammenarbeitContainer.closeSearch(); + + notExist(zusammenarbeitContainer.getCloseSearchButton()); + notExist(zusammenarbeitContainer.getSearchText()); + exist(zusammenarbeitContainer.getZustaendigeStelleButton()); }); - it('should show no search on entering 1 element', () => { - //1 Zeichen in Suche eingeben - //keine Vorschau + it('should delete search term on clicking X', () => { + zusammenarbeitContainer.searchZustaendigeStelle(); + zusammenarbeitContainer.enterSearchInput(stelleSearch1); + zusammenarbeitContainer.clearSearch(); + + zusammenarbeitContainer.hasSearchText(''); }); - it('should show results on entering 2 elements', () => { - //2 Zeichen in Suche eingeben - //Vorschau kontrollieren + it.skip('TODO: Setup Jenkins --- should find 2 results after entering kiel', () => { + zusammenarbeitContainer.enterSearchInput(stelleSearch2); + + zusammenarbeitContainer.expectNumberOfEntriesToBe(2); }); - it('should delete search term on clicking X', () => { - //X in Suche klicken - //Suche ist leer - //keine Vorschau + it.skip('should close layer after click on search entry', () => { + zusammenarbeitContainer.clickSearchEntry(0); + + exist(zusammenarbeitContainer.getSendButton()); + notExist(zusammenarbeitContainer.getSearchText()); }); - it('should copy and paste Zustaendige Stelle after selection', () => { - //click Suchergebnis - //Adresse und Name wird übernommen - //Layer ist geschlossen + it.skip('should show part of address in Zufi header', () => { + zusammenarbeitContainer.addressContains(stelleSearch2); }); - it('should be able to enter title and message, and show scrollbar on long text', () => { + it.skip('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', () => { - //Button klicken - //Titel und Datum werden angezeigt - //Nachricht wird angezeigt - //Buttons werden ausgeblendet + it.skip('should show title and message, show snackbar, and remove buttons after sending', () => { + zusammenarbeitContainer.sendAnfrage(); + + notExist(zusammenarbeitContainer.getSendButton()); + notExist(zusammenarbeitContainer.getCancelButton()); + + zusammenarbeitContainer.anfrageResultContains(titleText); + + //TODO: Zeilenumbrüche (OZG-6682) + //zusammenarbeitContainer.anfrageResultContains(messageText); + + //TODO: Datum wird angezeigt (OZG-6675) + + contains(snackBar.getMessage(), VorgangMessagesE2E.ZUARBEIT_ANGEFRAGT); }); }); }); diff --git a/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-ea-dev.yaml b/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-ea-dev.yaml index 0c58eb6aae1455fb57339185833c48b0e3c1eee9..58e0660e0f37cba42cc6ab8ed1b0c824d5e3902d 100644 --- a/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-ea-dev.yaml +++ b/alfa-client/apps/alfa-e2e/src/fixtures/argocd/by-ea-dev.yaml @@ -20,6 +20,8 @@ alfa: first_name: Emil last_name: Ansprechpartner password: 'Y9nk43yrQ_zzIPpfFU-I' + update_user: true + email: emil.ansprechpartner@ozg-sh.de client_roles: - name: alfa role: EINHEITLICHER_ANSPRECHPARTNER diff --git a/alfa-client/apps/alfa-e2e/src/model/vorgang.ts b/alfa-client/apps/alfa-e2e/src/model/vorgang.ts index 7fdbcfd62b93935aef7f35cb4a4f7fc1858c4422..472bf861d161a81067038844cd3adbcf27c02f71 100644 --- a/alfa-client/apps/alfa-e2e/src/model/vorgang.ts +++ b/alfa-client/apps/alfa-e2e/src/model/vorgang.ts @@ -157,6 +157,7 @@ export enum VorgangMessagesE2E { LOESCHEN_ANFORDERN = 'Für den Vorgang wurde eine Löschanforderung gestellt.', ENDGUELTIG_LOESCHEN = 'Der Vorgang wurde gelöscht.', WIEDERVORLAGE_BEARBEITEN_NICHT_MOEGLICH = 'Im Status "Zu löschen" ist die Bearbeitung von Wiedervorlagen nicht möglich.', + ZUARBEIT_ANGEFRAGT = 'Die Zuarbeit wurde angefragt.', } export const NO_AKTENZEICHEN: string = 'kein Aktenzeichen zugewiesen'; diff --git a/alfa-client/apps/demo/src/app/app.component.html b/alfa-client/apps/demo/src/app/app.component.html index 7dbdd36d8134a9a38fdd859b1371c5a0362820df..4c187c3b809d118c8bc9bb2a3fed9b29534338ff 100644 --- a/alfa-client/apps/demo/src/app/app.component.html +++ b/alfa-client/apps/demo/src/app/app.component.html @@ -54,6 +54,17 @@ </div> <form id="antrag_bescheiden_form" [formGroup]="exampleForm"> + <div class="my-4"> + <ods-fieldset legend="Checkboxes!" name="Checkboxes group"> + <ods-checkbox label="Brand new checkbox" inputId="chckbx1" /> + <ods-checkbox label="Disabled checkbox" inputId="chckbx2" [disabled]="true" /> + <ods-checkbox + label="Checkbox with error" + inputId="chckbx3" + [fieldControl]="checkboxControl" + /> + </ods-fieldset> + </div> <div class="my-4"> <ods-text-input id="test-input-id1" label="Betreff" placeholder="Betreff hier eingeben" /> </div> diff --git a/alfa-client/apps/demo/src/app/app.component.ts b/alfa-client/apps/demo/src/app/app.component.ts index fbd58a87093ed57b19c83b1e1c78151e75ca1b66..fe9bea7b9e284ee8c47c36bb051a6e682f991b84 100644 --- a/alfa-client/apps/demo/src/app/app.component.ts +++ b/alfa-client/apps/demo/src/app/app.component.ts @@ -11,8 +11,10 @@ import { BescheidUploadIconComponent, ButtonCardComponent, ButtonComponent, + CheckboxComponent, CloseIconComponent, ErrorMessageComponent, + FieldsetComponent, FileIconComponent, FileUploadButtonComponent, InstantSearchComponent, @@ -40,6 +42,8 @@ import { CustomStepperComponent } from './components/cdk-demo/custom-stepper.com @Component({ standalone: true, imports: [ + CheckboxComponent, + FieldsetComponent, CommonModule, AttachmentComponent, AttachmentWrapperComponent, @@ -101,6 +105,7 @@ export class AppComponent { }, ]; instantSearchFormControl = new FormControl(EMPTY_STRING); + checkboxControl = new FormControl(false); getInstantSearchResults() { if (this.instantSearchFormControl.value.length < 2) return []; @@ -127,6 +132,7 @@ export class AppComponent { effect(() => { window.localStorage.setItem('darkMode', JSON.stringify(this.darkMode())); }); + this.checkboxControl.setErrors({ 1: 'error' }); } public onSearchQueryChanged(searchQuery: InstantSearchQuery) { diff --git a/alfa-client/libs/admin/settings/src/lib/postfach/postfach-container/postfach-form/postfach-signatur/postfach-signatur.component.html b/alfa-client/libs/admin/settings/src/lib/postfach/postfach-container/postfach-form/postfach-signatur/postfach-signatur.component.html index a0d251c9e393b6755b2705705da3e049a2271e55..91f5982d6a2e93f317b7a301d4177ec9c59482df 100644 --- a/alfa-client/libs/admin/settings/src/lib/postfach/postfach-container/postfach-form/postfach-signatur/postfach-signatur.component.html +++ b/alfa-client/libs/admin/settings/src/lib/postfach/postfach-container/postfach-form/postfach-signatur/postfach-signatur.component.html @@ -1,4 +1,4 @@ -<form [formGroup]="formService.form"> +<form [formGroup]="formService.form" class="max-w-[960px]"> <h2 class="heading-2">Signatur</h2> <p id="signatur-desc"> Diese Signatur wird bei allen Nachrichten an den Antragsteller angezeigt. @@ -6,8 +6,9 @@ <ods-textarea-editor [formControlName]="formServiceClass.SIGNATUR_FIELD" [isResizable]="false" + [showLabel]="false" data-test-id="signatur-text" - label="" + label="signature" rows="6" class="w-full" aria-describedby="signatur-desc" diff --git a/alfa-client/libs/admin/settings/src/lib/postfach/postfach.service.spec.ts b/alfa-client/libs/admin/settings/src/lib/postfach/postfach.service.spec.ts index 0d8fd6b5eb2d724ff31293252d1b3d10a2c8937b..d787449be4be3621a443ff9cd4a9824613fbc167 100644 --- a/alfa-client/libs/admin/settings/src/lib/postfach/postfach.service.spec.ts +++ b/alfa-client/libs/admin/settings/src/lib/postfach/postfach.service.spec.ts @@ -91,7 +91,7 @@ describe('PostfachService', () => { tick(); expect(snackbarService.showInfo).toHaveBeenCalledWith( - 'Die Signatur wird erfolgreich gespeichert.', + 'Die Signatur wurde erfolgreich gespeichert.', ); })); }); diff --git a/alfa-client/libs/admin/settings/src/lib/postfach/postfach.service.ts b/alfa-client/libs/admin/settings/src/lib/postfach/postfach.service.ts index 2fc6ec706f41b554a0a9812034f45cb596edf9f8..1ff867073051235829114ca5cba89d9c227128ad 100644 --- a/alfa-client/libs/admin/settings/src/lib/postfach/postfach.service.ts +++ b/alfa-client/libs/admin/settings/src/lib/postfach/postfach.service.ts @@ -28,7 +28,7 @@ export class PostfachService { private showInfoAfterSave(stateResource: StateResource<PostfachResource>) { if (!stateResource.loading) { - this.snackbarService.showInfo('Die Signatur wird erfolgreich gespeichert.'); + this.snackbarService.showInfo('Die Signatur wurde erfolgreich gespeichert.'); } } diff --git a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html index 9252326a5bdcc9d9c25ec164981bd0e24915bafa..f8858992626b2be8a4dc43e07f7947e3b0a84735 100644 --- a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html +++ b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.html @@ -7,6 +7,7 @@ [attr.data-test-id]="(label | convertForDataTest) + '-text-editor'" [required]="isRequired" [focus]="focus" + [showLabel]="showLabel" > <ods-validation-error error diff --git a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts index c40849cf31e2279a3727527898213567bab07c4c..d6a19c8b70cac1f11b73038b321b1fc79fa2af58 100644 --- a/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/text-editor/text-editor.component.ts @@ -24,6 +24,7 @@ export class TextEditorComponent extends FormControlEditorAbstractComponent { @Input() placeholder: string = ''; @Input() isRequired: boolean = false; @Input() focus: boolean = false; + @Input() showLabel: boolean = true; get variant(): string { return this.invalidParams.length > 0 ? 'error' : 'default'; diff --git a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html index baa159622243b8c03783314ee0f27e17cf6c78a4..22a5c192844d111ff69b4ed90f1be4b7108fcee8 100644 --- a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html +++ b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.html @@ -8,6 +8,7 @@ [required]="isRequired" [focus]="focus" [isResizable]="isResizable" + [showLabel]="showLabel" > <ods-validation-error error diff --git a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts index 5dea452149b2facd76f03b209835786e7af4b970..ef615ae42c6b31689a63ce85d22613537190588e 100644 --- a/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/textarea-editor/textarea-editor.component.ts @@ -25,6 +25,7 @@ export class TextareaEditorComponent extends FormControlEditorAbstractComponent @Input() isRequired: boolean = false; @Input() focus: boolean = false; @Input() isResizable: boolean = true; + @Input() showLabel: boolean = true; get variant(): string { return this.invalidParams.length > 0 ? 'error' : 'default'; diff --git a/alfa-client/libs/design-system/src/index.ts b/alfa-client/libs/design-system/src/index.ts index 8adc00f2d8215c5f947d879874ed17e6f190184a..117e400441f35a9a21750521457c1c7537a808d0 100644 --- a/alfa-client/libs/design-system/src/index.ts +++ b/alfa-client/libs/design-system/src/index.ts @@ -5,6 +5,8 @@ export * from './lib/bescheid-status-text/bescheid-status-text.component'; export * from './lib/bescheid-wrapper/bescheid-wrapper.component'; export * from './lib/button-card/button-card.component'; export * from './lib/button/button.component'; +export * from './lib/checkbox/checkbox.component'; +export * from './lib/fieldset/fieldset.component'; export * from './lib/form/error-message/error-message.component'; export * from './lib/form/file-upload-button/file-upload-button.component'; export * from './lib/form/radio-button-card/radio-button-card.component'; diff --git a/alfa-client/libs/design-system/src/lib/checkbox/checbox.stories.ts b/alfa-client/libs/design-system/src/lib/checkbox/checbox.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b3954527bc86c75615e36839194754fce3466ef --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/checkbox/checbox.stories.ts @@ -0,0 +1,67 @@ +import { FormControl } from '@angular/forms'; +import { argsToTemplate, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; +import { CheckboxComponent } from './checkbox.component'; + +const formControlWithError = new FormControl(false); +formControlWithError.setErrors({ error: 404 }); + +const meta: Meta<CheckboxComponent> = { + title: 'Checkbox', + component: CheckboxComponent, + decorators: [ + moduleMetadata({ + imports: [CheckboxComponent], + }), + ], + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<CheckboxComponent>; + +export const Default: Story = { + args: { + value: 'Checkbox value', + label: 'Basic checkbox', + inputId: '1', + disabled: false, + }, + argTypes: { + label: { description: 'Checkbox label' }, + disabled: { description: 'Disabled state of checkbox' }, + inputId: { description: 'Id of checkbox input' }, + value: { description: 'Value of checkbox' }, + fieldControl: { + description: 'Form control object', + table: { type: { summary: 'FormControl' } }, + }, + }, +}; + +export const Error: Story = { + args: { + label: 'Checkbox with error state', + inputId: '2', + fieldControl: formControlWithError, + }, +}; + +export const Disabled: Story = { + args: { + label: 'Disabled checkbox', + inputId: '3', + disabled: true, + }, +}; + +export const MultiLineLabel: Story = { + args: { + label: 'Very long label that should break', + inputId: '4', + }, + render: (args) => ({ + props: args, + template: `<div class="w-40"><ods-checkbox ${argsToTemplate(args)}/></div>`, + }), +}; diff --git a/alfa-client/libs/design-system/src/lib/checkbox/checkbox.component.spec.ts b/alfa-client/libs/design-system/src/lib/checkbox/checkbox.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c25c5efdcce199e3c83dfd6adfceed567f2ea5d3 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/checkbox/checkbox.component.spec.ts @@ -0,0 +1,45 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl } from '@angular/forms'; +import { CheckboxComponent } from './checkbox.component'; + +describe('CheckboxComponent', () => { + let component: CheckboxComponent; + let fixture: ComponentFixture<CheckboxComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CheckboxComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CheckboxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('component', () => { + describe('hasError', () => { + it('should return true', () => { + const formControlWithError = new FormControl(false); + formControlWithError.setErrors({ 1: 'error' }); + component.fieldControl = formControlWithError; + + const result: boolean = component.hasError(); + + expect(result).toBe(true); + }); + + it('should return false', () => { + const formControl = new FormControl(false); + component.fieldControl = formControl; + + const result: boolean = component.hasError(); + + expect(result).toBe(false); + }); + }); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/checkbox/checkbox.component.ts b/alfa-client/libs/design-system/src/lib/checkbox/checkbox.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a6ebfc41ac9fa2903387a0474510f080a3ec306 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/checkbox/checkbox.component.ts @@ -0,0 +1,52 @@ +import { isNotEmpty } from '@alfa-client/tech-shared'; +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; + +@Component({ + selector: 'ods-checkbox', + standalone: true, + imports: [CommonModule, ReactiveFormsModule], + template: ` + <div class="flex items-start gap-3 text-start"> + <input + type="checkbox" + class="peer relative box-border size-5 shrink-0 appearance-none rounded-sm border bg-whitetext + outline outline-2 outline-offset-2 outline-transparent hover:border-2 focus-visible:border-background-200 + disabled:border-disabled-dark disabled:bg-disabled disabled:hover:border" + [ngClass]=" + hasError() ? + 'border-error hover:border-error focus-visible:outline-error' + : 'border-primary hover:border-primary-hover focus-visible:outline-focus' + " + [value]="value" + [checked]="fieldControl.value" + [attr.id]="inputId" + [disabled]="disabled" + /> + <label class="leading-5 text-text" [attr.for]="inputId">{{ label }}</label> + <svg + viewBox="0 0 12 9" + xmlns="http://www.w3.org/2000/svg" + class="pointer-events-none absolute hidden size-5 p-1 outline-none peer-checked:block" + [ngClass]="hasError() ? 'fill-error' : 'fill-primary focus-visible:fill-focus'" + > + <path + d="M3.81353 7.10067L0.968732 4.30201L0 5.24832L3.81353 + 9L12 0.946309L11.0381 0L3.81353 7.10067Z" + /> + </svg> + </div> + `, +}) +export class CheckboxComponent { + @Input() fieldControl: FormControl = new FormControl(false); + @Input() value: string; + @Input() inputId: string; + @Input() label: string; + @Input() disabled: boolean = false; + + hasError(): boolean { + return isNotEmpty(this.fieldControl.errors); + } +} diff --git a/alfa-client/libs/design-system/src/lib/fieldset/fieldset.component.spec.ts b/alfa-client/libs/design-system/src/lib/fieldset/fieldset.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3653c1ac3fe9086724750c67dd9a91cb071921f7 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/fieldset/fieldset.component.spec.ts @@ -0,0 +1,42 @@ +import { existsAsHtmlElement, notExistsAsHtmlElement } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { FieldsetComponent } from './fieldset.component'; + +describe('FieldsetComponent', () => { + let component: FieldsetComponent; + let fixture: ComponentFixture<FieldsetComponent>; + + const legend = getDataTestIdOf('fieldset-legend'); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FieldsetComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(FieldsetComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('template', () => { + describe('legend element', () => { + it('should show', () => { + component.legend = 'Test'; + fixture.detectChanges(); + + existsAsHtmlElement(fixture, legend); + }); + it('should hide', () => { + component.legend = ''; + fixture.detectChanges(); + + notExistsAsHtmlElement(fixture, legend); + }); + }); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/fieldset/fieldset.component.ts b/alfa-client/libs/design-system/src/lib/fieldset/fieldset.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..a0f5d77a8ae8ab65ea97cfd1a9b1f84177ee92b2 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/fieldset/fieldset.component.ts @@ -0,0 +1,18 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ods-fieldset', + standalone: true, + imports: [CommonModule], + template: `<fieldset class="flex flex-col gap-2" [name]="name"> + <legend *ngIf="legend" class="mb-2 font-medium text-text" data-test-id="fieldset-legend"> + {{ legend }} + </legend> + <ng-content /> + </fieldset>`, +}) +export class FieldsetComponent { + @Input() name: string; + @Input() legend: string; +} diff --git a/alfa-client/libs/design-system/src/lib/fieldset/fieldset.stories.ts b/alfa-client/libs/design-system/src/lib/fieldset/fieldset.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..616372a4f5f22eba38b1b1995934f5d677c73e63 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/fieldset/fieldset.stories.ts @@ -0,0 +1,36 @@ +import { argsToTemplate, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; +import { CheckboxComponent } from '../checkbox/checkbox.component'; +import { FieldsetComponent } from './fieldset.component'; + +const meta: Meta<FieldsetComponent> = { + title: 'Fieldset', + component: FieldsetComponent, + decorators: [ + moduleMetadata({ + imports: [FieldsetComponent, CheckboxComponent], + }), + ], + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<FieldsetComponent>; + +export const Default: Story = { + args: { + name: 'checkboxGroup', + legend: 'A group of checkboxes', + }, + argTypes: { + name: { description: 'The name associated with the group' }, + legend: { description: 'Title for the fieldset' }, + }, + render: (args) => ({ + props: args, + template: `<ods-fieldset ${argsToTemplate(args)}> + <ods-checkbox inputId="1" label="First checkbox" /> + <ods-checkbox inputId="2" label="Second checkbox" /> + </ods-fieldset>`, + }), +}; diff --git a/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts b/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts index 5c418556063b84c751fd274fbd901f18cd3eb4a3..cc91e16b2345a9f1a578001c60b7b20655ed5598 100644 --- a/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/textarea/textarea.component.ts @@ -31,7 +31,7 @@ type TextareaVariants = VariantProps<typeof textareaVariants>; imports: [CommonModule, ReactiveFormsModule, TechSharedModule], template: ` <div class="mt-2"> - <label [for]="id" class="text-md mb-2 block font-medium text-text"> + <label *ngIf="showLabel" [for]="id" class="text-md mb-2 block font-medium text-text"> {{ inputLabel }}<ng-container *ngIf="required"><i aria-hidden="true">*</i></ng-container> </label> <textarea @@ -65,6 +65,7 @@ export class TextareaComponent { @Input() fieldControl: FormControl = new FormControl(EMPTY_STRING); @Input() required: boolean = false; @Input() isResizable: boolean = true; + @Input() showLabel: boolean = true; @Input() set focus(value: boolean) { if (value && this.textAreaElement) { diff --git a/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.spec.ts b/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.spec.ts index f33524b795e39fff7585c3829d8094659429afa2..71b3e7c98f8e23d30f056ef4ad65544794294196 100644 --- a/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.spec.ts +++ b/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.spec.ts @@ -61,4 +61,12 @@ describe('NavItemComponent', () => { }); }); }); + + describe('template', () => { + describe('host element role attribute', () => { + it('should be "menuitem"', () => { + expect(fixture.nativeElement.getAttribute('role')).toBe('menuitem'); + }); + }); + }); }); diff --git a/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.ts b/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.ts index d5f54907fe995fa082505d3449a236d89b6f4ecb..14acc1a1b2c6f3c6ca800bc30710d5a7adf48cdc 100644 --- a/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.ts +++ b/alfa-client/libs/design-system/src/lib/navbar/nav-item/nav-item.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, Input } from '@angular/core'; +import { Component, HostBinding, Input } from '@angular/core'; import { RouterLink, RouterLinkActive } from '@angular/router'; @Component({ @@ -14,7 +14,6 @@ import { RouterLink, RouterLinkActive } from '@angular/router'; 'border border-transparent hover:border-primary', 'outline-2 outline-offset-4 outline-focus focus-visible:border-background-200', ]" - role="menuitem" [attr.data-test-id]="'link-to-' + to" > <ng-content select="[icon]" /> @@ -24,4 +23,6 @@ import { RouterLink, RouterLinkActive } from '@angular/router'; export class NavItemComponent { @Input({ required: true }) caption!: string; @Input() to: string; + + @HostBinding('attr.role') role = 'menuitem'; } 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 index 171397997f1bccc57c87c4a59b014dbd2aa6ed6a..60adf093640b07510296fab35c0a6e4fbb3650e9 100644 --- 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 @@ -23,7 +23,7 @@ import { twMerge } from 'tailwind-merge'; *ngIf="isPopupOpen" class="absolute max-h-120 min-w-44 max-w-80 animate-fadeIn overflow-y-auto rounded shadow-lg shadow-grayborder focus:outline-none" [ngClass]="alignTo === 'left' ? 'right-0' : 'left-0'" - role="dialog" + role="menu" aria-modal="true" tabIndex="-1" cdkTrapFocus diff --git a/alfa-client/libs/design-system/src/lib/tailwind-preset/root.css b/alfa-client/libs/design-system/src/lib/tailwind-preset/root.css index 10ac4b918b2c3e41d10f5a1a0e41036a3cfdfc03..49ba05caeaf723693a01b5dbb9be991b4196afec 100644 --- a/alfa-client/libs/design-system/src/lib/tailwind-preset/root.css +++ b/alfa-client/libs/design-system/src/lib/tailwind-preset/root.css @@ -9,6 +9,9 @@ --color-error: 0 71% 49%; --color-focus: 212 64% 47%; + --color-disabled: 206 14% 95%; + --color-disabled-dark: 208 12% 65%; + --color-background-secondary: 0 0% 98%; --color-mainbg: 0 0% 100%; --text: 0 0% 0%; @@ -39,6 +42,9 @@ --color-error: 0 99% 72%; --color-focus: 43 100% 48%; + --color-disabled: 206 14% 15%; + --color-disabled-dark: 208 12% 33%; + --color-background-secondary: 0 0% 16%; --color-mainbg: 0 0% 14%; --text: 0 0% 100%; 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 cb8ed07e1d8c25e88620f6a60df143c9e7da8dfb..588bdac521f0d439cd3df6cd7d3ec1228156298a 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 @@ -119,6 +119,10 @@ module.exports = { warning: 'hsl(var(--warning))', error: 'hsl(var(--color-error))', focus: 'hsl(var(--color-focus))', + disabled: { + dark: 'hsl(var(--color-disabled-dark) / <alpha-value>)', + DEFAULT: 'hsl(var(--color-disabled) / <alpha-value>)', + }, }, backgroundColor: { greybackdrop: 'rgb(229, 229, 229, 0.95)', diff --git a/alfa-client/libs/postfach/src/lib/postfach-mail-form/postfach-mail-form.component.spec.ts b/alfa-client/libs/postfach/src/lib/postfach-mail-form/postfach-mail-form.component.spec.ts index 40d4bc90cda99e87336563376dd405d63298d575..3f25e40be126c663b985d8591d829789743a0b99 100644 --- a/alfa-client/libs/postfach/src/lib/postfach-mail-form/postfach-mail-form.component.spec.ts +++ b/alfa-client/libs/postfach/src/lib/postfach-mail-form/postfach-mail-form.component.spec.ts @@ -21,12 +21,6 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; -import { MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatInputModule } from '@angular/material/input'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { PostfachService } from '@alfa-client/postfach-shared'; import { createStateResource } from '@alfa-client/tech-shared'; import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; @@ -39,8 +33,14 @@ import { TextAreaEditorComponent, TextEditorComponent, } from '@alfa-client/ui'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { createCommandResource } from 'libs/command-shared/test/command'; -import { PostfachTestFactory } from 'libs/postfach-shared/test/postfach'; +import { PostfachTestFactory, createPostfachSettings } from 'libs/postfach-shared/test/postfach'; import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; import { PostfachMailFormComponent } from './postfach-mail-form.component'; @@ -54,7 +54,10 @@ describe('PostfachMailFormComponent', () => { let component: PostfachMailFormComponent; let fixture: ComponentFixture<PostfachMailFormComponent>; - const postfachService: Mock<PostfachService> = mock(PostfachService); + const postfachService: Mock<PostfachService> = { + ...mock(PostfachService), + getSettings: jest.fn().mockReturnValue(of(createPostfachSettings())), + }; const formService: PostfachMailFormservice = new PostfachMailFormservice( new FormBuilder(), useFromMock(postfachService), 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 d07cea93308d33ecfb5af0d917ad1fdfd84629d6..ec5c2b1f2a356d87ce90983eb5206fe24245c0c4 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 @@ -31,6 +31,7 @@ " [disabled]="isDisabled" [matTooltip]="toolTip" + [matMenuTriggerFor]="matMenuTriggerFor" (click)="clickEmitter.emit($event)" type="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 cc423f9a8086d18af5fdc13a0db41ab2fae861df..47b9ce82546885a666ae26c46fcff54a7acca36c 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 @@ -24,6 +24,7 @@ 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 { SpinnerComponent } from '../spinner/spinner.component'; @@ -43,6 +44,7 @@ describe('IconButtonWithSpinnerComponent', () => { MatIcon, MockComponent(SpinnerComponent), MockModule(MatTooltipModule), + MockModule(MatMenuModule), ], }); }); diff --git a/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.ts b/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.ts index aac12cd1ba42b1995fc8c188498c08f5ab0c7211..aac1f896a23e916e76a4f59e07d6e9294d0f6dc5 100644 --- a/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.ts +++ b/alfa-client/libs/ui/src/lib/ui/icon-button-with-spinner/icon-button-with-spinner.component.ts @@ -21,8 +21,8 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { StateResource, createEmptyStateResource } from '@alfa-client/tech-shared'; import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { createEmptyStateResource, StateResource } from '@alfa-client/tech-shared'; import { Resource } from '@ngxp/rest'; import { isNil } from 'lodash-es'; @@ -34,6 +34,7 @@ import { isNil } from 'lodash-es'; export class IconButtonWithSpinnerComponent { @Input() icon: string; @Input() svgIcon: string; + @Input() matMenuTriggerFor: string; @Input() stateResource: StateResource<Resource>; @Input() toolTip: string = ''; @Input() showSpinner: boolean = false; diff --git a/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.html b/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.html index 5fa5cdf3ab2ef420c6aba8fdafb9fe6b64f8dd17..f455cb99443c2e6a9be7146de5e4ed7e15022704 100644 --- a/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.html +++ b/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.html @@ -28,7 +28,7 @@ [matMenuTriggerFor]="menu.matMenu" (menuOpened)="showUserProfileSearch()" (menuClosed)="hideUserProfileSearch()" - aria-label="Bearbeiter ändern" + [attr.aria-label]="userButtonLabel" class="user-profile-button" > <alfa-user-icon diff --git a/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.spec.ts b/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.spec.ts index 4464f2f109503b0424b404266a04c7bd80bee70b..6c12e1395e939a926ad075df0c40159f9ae04cb4 100644 --- a/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.spec.ts +++ b/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.spec.ts @@ -21,10 +21,11 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createEmptyStateResource, StateResource } from '@alfa-client/tech-shared'; import { mock } from '@alfa-client/test-utils'; import { OzgcloudMenuComponent, UiModule } from '@alfa-client/ui'; -import { UserProfileService } from '@alfa-client/user-profile-shared'; +import { UserProfileResource, UserProfileService } from '@alfa-client/user-profile-shared'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MockComponent } from 'ng-mocks'; import { BehaviorSubject } from 'rxjs'; import { UserIconComponent } from '../../../user-icon/user-icon.component'; @@ -114,4 +115,31 @@ describe('UserProfileButtonContainerComponent', () => { expect(userProfileService.hideUserProfileSearch).toHaveBeenCalled(); }); }); + + describe('getUserButtonLabel', () => { + it('should return label', () => { + component.userProfile = createEmptyStateResource(); + + const result = component.getUserButtonLabel(); + + expect(result).toBe('Bearbeiter ändern. Aktueller Bearbeiter: Unbekannter Benutzer'); + }); + }); + + describe('set userProfile', () => { + const userProfileStateResource: StateResource<UserProfileResource> = createEmptyStateResource(); + it('should set userProfile', () => { + component.userProfile = userProfileStateResource; + + expect(component.userProfile).toBe(userProfileStateResource); + }); + + it('should get label for user button', () => { + component.getUserButtonLabel = jest.fn(); + + component.userProfile = userProfileStateResource; + + expect(component.getUserButtonLabel).toHaveBeenCalled(); + }); + }); }); diff --git a/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.ts b/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.ts index 1c7451882382e8bdd72e1b6a66b44c48a3b1dc01..e559f74639d522733a7506a840c689091f6460fe 100644 --- a/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.ts +++ b/alfa-client/libs/user-profile/src/lib/user-profile-in-vorgang-container/user-profile-in-vorgang/user-profile-button-container/user-profile-button-container.component.ts @@ -21,10 +21,14 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { StateResource } from '@alfa-client/tech-shared'; +import { + getUserName, + UserProfileResource, + UserProfileService, +} from '@alfa-client/user-profile-shared'; import { Component, Input, OnInit, ViewChild } from '@angular/core'; import { MatMenuTrigger } from '@angular/material/menu'; -import { StateResource } from '@alfa-client/tech-shared'; -import { UserProfileResource, UserProfileService } from '@alfa-client/user-profile-shared'; import { Observable, tap } from 'rxjs'; @Component({ @@ -33,11 +37,21 @@ import { Observable, tap } from 'rxjs'; styleUrls: ['./user-profile-button-container.component.scss'], }) export class UserProfileButtonContainerComponent implements OnInit { - @Input() userProfile: StateResource<UserProfileResource>; + @Input() + set userProfile(value: StateResource<UserProfileResource>) { + this._userProfile = value; + this.userButtonLabel = this.getUserButtonLabel(); + } + + get userProfile() { + return this._userProfile; + } @ViewChild(MatMenuTrigger) menuTrigger: MatMenuTrigger; showUserProfileSearch$: Observable<boolean>; + private _userProfile: StateResource<UserProfileResource>; + userButtonLabel: string; constructor(public userProfileService: UserProfileService) {} @@ -64,4 +78,8 @@ export class UserProfileButtonContainerComponent implements OnInit { public hideUserProfileSearch(): void { this.userProfileService.hideUserProfileSearch(); } + + public getUserButtonLabel(): string { + return `Bearbeiter ändern. Aktueller Bearbeiter: ${getUserName(this.userProfile.resource)}`; + } } diff --git a/alfa-client/libs/user-settings/src/lib/user-settings-container/user-settings/user-settings-menu-button/user-settings-menu-button.component.html b/alfa-client/libs/user-settings/src/lib/user-settings-container/user-settings/user-settings-menu-button/user-settings-menu-button.component.html index b5c75e65e763c68cd0300190f8c1ff422738c3fc..b13bac6befba485ab93860d409d3f1b79492ebec 100644 --- a/alfa-client/libs/user-settings/src/lib/user-settings-container/user-settings/user-settings-menu-button/user-settings-menu-button.component.html +++ b/alfa-client/libs/user-settings/src/lib/user-settings-container/user-settings/user-settings-menu-button/user-settings-menu-button.component.html @@ -27,4 +27,5 @@ icon="settings" toolTip="Einstellungen" data-test-id="menu-button" + [matMenuTriggerFor]="matMenuTriggerFor" ></ozgcloud-icon-button-with-spinner> diff --git a/alfa-client/libs/user-settings/src/lib/user-settings-container/user-settings/user-settings-menu-button/user-settings-menu-button.component.ts b/alfa-client/libs/user-settings/src/lib/user-settings-container/user-settings/user-settings-menu-button/user-settings-menu-button.component.ts index 5151de3ca312af3db73cc04385d649a4d3c7ee92..e952c3b73d18d4ee756c56cabfc23f2a3d2cb5d5 100644 --- a/alfa-client/libs/user-settings/src/lib/user-settings-container/user-settings/user-settings-menu-button/user-settings-menu-button.component.ts +++ b/alfa-client/libs/user-settings/src/lib/user-settings-container/user-settings/user-settings-menu-button/user-settings-menu-button.component.ts @@ -21,11 +21,13 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Component } from '@angular/core'; +import { Component, Input } from '@angular/core'; @Component({ selector: 'alfa-user-settings-menu-button', templateUrl: './user-settings-menu-button.component.html', styleUrls: ['./user-settings-menu-button.component.scss'], }) -export class UserSettingsMenuButtonComponent {} +export class UserSettingsMenuButtonComponent { + @Input() matMenuTriggerFor: string; +} diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.spec.ts b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.spec.ts index 4c85845d2178b7a2faadd81cc18a0ee8371f092a..31b6f8e0de9bc3cfa18c45e1ad5860f747589982 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.spec.ts +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.spec.ts @@ -21,9 +21,9 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { EnumToLabelPipe } from '@alfa-client/tech-shared'; import { VorgangResource, VorgangStatusLabel } from '@alfa-client/vorgang-shared'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { createVorgangResource } from 'libs/vorgang-shared/test/vorgang'; import { VorgangStatusTextComponent } from './vorgang-status-text.component'; @@ -51,7 +51,7 @@ describe('VorgangStatusTextComponent', () => { expect(component).toBeTruthy(); }); - it('should show status dot', () => { + it('should show status text', () => { component.status = vorgang.status; const statusText: string = VorgangStatusLabel[vorgang.status]; diff --git a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.ts b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.ts index 4a46a08223176794182bf46222f0f8fe829de4fb..a1c90fd8afdb3114a17086f359b36ee5110a4f42 100644 --- a/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.ts +++ b/alfa-client/libs/vorgang-shared-ui/src/lib/vorgang-status-text/vorgang-status-text.component.ts @@ -21,8 +21,8 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Component, Input } from '@angular/core'; import { VorgangStatus, VorgangStatusLabel } from '@alfa-client/vorgang-shared'; +import { Component, Input } from '@angular/core'; @Component({ selector: 'alfa-vorgang-status-text', 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 13b574ef5c2fcdc6d993803208437cb01bb5ce3c..6015354498a4860c7c4bfabd7feb57ce2f9c7a93 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 @@ -21,12 +21,24 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { convertForDataTest, ConvertForDataTestPipe, EnumToLabelPipe, HasLinkPipe, ToResourceUriPipe } from '@alfa-client/tech-shared'; -import { getElementFromFixture } from '@alfa-client/test-utils'; +import { + ConvertForDataTestPipe, + createStateResource, + EnumToLabelPipe, + HasLinkPipe, + ToResourceUriPipe, +} from '@alfa-client/tech-shared'; +import { getElementFromFixture, mock } from '@alfa-client/test-utils'; import { PostfachIconComponent } from '@alfa-client/ui'; import { UserProfileInVorgangListItemContainerComponent } from '@alfa-client/user-profile'; -import { VorgangHeaderLinkRel } from '@alfa-client/vorgang-shared'; -import { AktenzeichenComponent, VorgangNummerComponent, VorgangStatusDotComponent, VorgangStatusTextComponent } from '@alfa-client/vorgang-shared-ui'; +import { UserProfileResource, UserProfileService } from '@alfa-client/user-profile-shared'; +import { VorgangHeaderLinkRel, VorgangStatus } from '@alfa-client/vorgang-shared'; +import { + AktenzeichenComponent, + VorgangNummerComponent, + VorgangStatusDotComponent, + VorgangStatusTextComponent, +} from '@alfa-client/vorgang-shared-ui'; import { WiedervorlageListInVorgangListContainerComponent } from '@alfa-client/wiedervorlage'; import { registerLocaleData } from '@angular/common'; import localeDe from '@angular/common/locales/de'; @@ -39,8 +51,10 @@ 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 { of } from 'rxjs'; import { VorgangBescheidStatusComponent } from './vorgang-bescheid-status/vorgang-bescheid-status.component'; import { VorgangCreatedAtComponent } from './vorgang-created-at/vorgang-created-at.component'; import { VorgangListItemComponent } from './vorgang-list-item.component'; @@ -52,10 +66,14 @@ describe('VorgangListItemComponent', () => { let component: VorgangListItemComponent; let fixture: ComponentFixture<VorgangListItemComponent>; + const userProfileService = mock(UserProfileService); + const user: string = getDataTestIdOf('vorgang-user-icon'); const postfachStatus: string = getDataTestClassOf('postfach-icon'); const bescheidStatus: string = getDataTestIdOf('vorgang-list-item-bescheid-status'); + const userProfile: UserProfileResource = createUserProfileResource(); + beforeEach(async () => { await TestBed.configureTestingModule({ imports: [RouterTestingModule, MatIconTestingModule], @@ -79,6 +97,7 @@ describe('VorgangListItemComponent', () => { MockModule(MatTooltipModule), ], providers: [ + { provide: UserProfileService, useValue: userProfileService }, { provide: LOCALE_ID, useValue: 'de' }, { provide: MAT_DATE_LOCALE, useValue: 'de-DE' }, ], @@ -96,6 +115,52 @@ describe('VorgangListItemComponent', () => { expect(component).toBeTruthy(); }); + describe('ngOnInit', () => { + describe('user profile', () => { + beforeEach(() => { + component.buildAriaLabel = jest.fn(); + component.userProfileService.getAssignedUserProfile = jest + .fn() + .mockReturnValue(of(createStateResource(userProfile))); + }); + + describe('vorgang has no "assigned to" link', () => { + beforeEach(() => { + component.vorgang = createVorgangResource(); + }); + it('should not get profile', () => { + component.ngOnInit(); + + expect(component.userProfileService.getAssignedUserProfile).not.toHaveBeenCalled(); + }); + + it('should build aria label without user profile resource', () => { + component.ngOnInit(); + + expect(component.buildAriaLabel).toHaveBeenCalledWith(); + }); + }); + + describe('vorgang has "assigned to" link', () => { + beforeEach(() => { + component.vorgang = createVorgangResource([VorgangHeaderLinkRel.ASSIGNED_TO]); + }); + + it('should get profile', () => { + component.ngOnInit(); + + expect(component.userProfileService.getAssignedUserProfile).toHaveBeenCalled(); + }); + + it('should build aria label with user profile resource', () => { + component.ngOnInit(); + + expect(component.buildAriaLabel).toHaveBeenCalledWith(userProfile); + }); + }); + }); + }); + describe('bearbeiter/user-icon', () => { it('should be visible', () => { component.vorgang = createVorgangResource([ @@ -119,142 +184,218 @@ describe('VorgangListItemComponent', () => { }); }); - describe('Aria label', () => { - it('should contain Wiedervorlage', () => { + describe('mail icon', () => { + beforeEach(() => { component.vorgang = createVorgangResource([ VorgangHeaderLinkRel.VORGANG_WITH_EINGANG, - VorgangHeaderLinkRel.WIEDERVORLAGEN, + VorgangHeaderLinkRel.POSTFACH_MAILS, ]); - const listItem: string = getDataTestIdOf( - `vorgang-list-item-${convertForDataTest(component.vorgang.name)}`, - ); - component.ngOnInit(); + }); + + it('should show mail icon if Vorgang has new Postfachnachricht', () => { + component.vorgang.hasPostfachNachricht = true; fixture.detectChanges(); - const element: HTMLDivElement = fixture.nativeElement.querySelector(listItem); - const ariaLabel: string = element.getAttribute('aria-label'); + const statusElement = getElementFromFixture(fixture, postfachStatus); - expect(ariaLabel).toContain('Wiedervorlage'); + expect(statusElement).toBeInstanceOf(HTMLElement); }); - it('should not contain Wiedervorlage if no nextFrist but LinkRel.WIEDERVORLAGEN', () => { - component.vorgang = { - ...createVorgangResource([ - VorgangHeaderLinkRel.VORGANG_WITH_EINGANG, - VorgangHeaderLinkRel.WIEDERVORLAGEN, - ]), - nextFrist: null, - }; - const listItem: string = getDataTestIdOf( - `vorgang-list-item-${convertForDataTest(component.vorgang.name)}`, - ); - component.ngOnInit(); + it('should not show mail icon if Vorgang has no new Postfachnachricht', () => { + component.vorgang.hasPostfachNachricht = false; fixture.detectChanges(); - const element: HTMLDivElement = fixture.nativeElement.querySelector(listItem); - const ariaLabel: string = element.getAttribute('aria-label'); + const statusElement = getElementFromFixture(fixture, postfachStatus); - expect(ariaLabel).not.toContain('Wiedervorlage'); + expect(statusElement).not.toBeInstanceOf(HTMLElement); }); + }); - it('should not contain Wiedervorlage if no LinkRel.WIEDERVORLAGEN', () => { - component.vorgang = createVorgangResource([VorgangHeaderLinkRel.VORGANG_WITH_EINGANG]); - const listItem: string = getDataTestIdOf( - `vorgang-list-item-${convertForDataTest(component.vorgang.name)}`, - ); - component.ngOnInit(); + describe('Bescheid-Status', () => { + it('should show bescheid status if Vorgang has antragBewilligt true', () => { + component.vorgang.antragBewilligt = true; fixture.detectChanges(); - const element: HTMLDivElement = fixture.nativeElement.querySelector(listItem); - const ariaLabel: string = element.getAttribute('aria-label'); + const element = getElementFromFixture(fixture, bescheidStatus); - expect(ariaLabel).not.toContain('Wiedervorlage'); + expect(element).toBeInstanceOf(HTMLElement); }); - it('should contain hasPostfachnachricht text if vorgang has Postfachnachricht', () => { - component.vorgang.hasPostfachNachricht = true; - component.vorgang.hasNewPostfachNachricht = false; - const listItem: string = getDataTestIdOf( - `vorgang-list-item-${convertForDataTest(component.vorgang.name)}`, - ); - component.ngOnInit(); + it('should show bescheid status if Vorgang has antragBewilligt false', () => { + component.vorgang.antragBewilligt = false; fixture.detectChanges(); - const element: HTMLDivElement = fixture.nativeElement.querySelector(listItem); - const ariaLabel: string = element.getAttribute('aria-label'); + const element = getElementFromFixture(fixture, bescheidStatus); - expect(ariaLabel).toContain('enthält Postfachnachrichten'); + expect(element).toBeInstanceOf(HTMLElement); }); - it('should contain hasNewPostfachnachricht text if vorgang has new Postfachnachricht', () => { - component.vorgang.hasPostfachNachricht = true; - component.vorgang.hasNewPostfachNachricht = true; - const listItem: string = getDataTestIdOf( - `vorgang-list-item-${convertForDataTest(component.vorgang.name)}`, - ); - component.ngOnInit(); + it('should not show bescheid status if Vorgang has no antragBewilligt', () => { + component.vorgang.antragBewilligt = null; fixture.detectChanges(); - const element: HTMLDivElement = fixture.nativeElement.querySelector(listItem); - const ariaLabel: string = element.getAttribute('aria-label'); + const element = getElementFromFixture(fixture, bescheidStatus); - expect(ariaLabel).toContain('enthält neue Postfachnachrichten'); + expect(element).not.toBeInstanceOf(HTMLElement); }); }); - describe('mail icon', () => { + describe('buildAriaLabel', () => { beforeEach(() => { + component.getWiedervorlageText = jest.fn(); + component.getPostfachNachricht = jest.fn(); + }); + it('should get status', () => { + component.getStatus = jest.fn(); + + component.buildAriaLabel(); + + expect(component.getStatus).toHaveBeenCalled(); + }); + + it('should get approval text', () => { + component.getApprovalText = jest.fn(); + + component.buildAriaLabel(); + + expect(component.getApprovalText).toHaveBeenCalled(); + }); + + it('should get user text', () => { + component.getUserText = jest.fn(); + + component.buildAriaLabel(userProfile); + + expect(component.getUserText).toHaveBeenCalledWith(userProfile); + }); + + it('should get Wiedervorlage', () => { component.vorgang = createVorgangResource([ VorgangHeaderLinkRel.VORGANG_WITH_EINGANG, - VorgangHeaderLinkRel.POSTFACH_MAILS, + VorgangHeaderLinkRel.WIEDERVORLAGEN, ]); + + component.buildAriaLabel(); + + expect(component.getWiedervorlageText).toHaveBeenCalled(); }); - it('should show mail icon if Vorgang has new Postfachnachricht', () => { + it('should not get Wiedervorlage if no nextFrist but LinkRel.WIEDERVORLAGEN', () => { + component.vorgang = { + ...createVorgangResource([ + VorgangHeaderLinkRel.VORGANG_WITH_EINGANG, + VorgangHeaderLinkRel.WIEDERVORLAGEN, + ]), + nextFrist: null, + }; + + component.buildAriaLabel(); + + expect(component.getWiedervorlageText).not.toHaveBeenCalled(); + }); + + it('should not get Wiedervorlage if no LinkRel.WIEDERVORLAGEN', () => { + component.vorgang = createVorgangResource([VorgangHeaderLinkRel.VORGANG_WITH_EINGANG]); + + component.buildAriaLabel(); + + expect(component.getWiedervorlageText).not.toHaveBeenCalled(); + }); + + it('should get message text if vorgang has message', () => { component.vorgang.hasPostfachNachricht = true; - fixture.detectChanges(); - const statusElement = getElementFromFixture(fixture, postfachStatus); + component.buildAriaLabel(); - expect(statusElement).toBeInstanceOf(HTMLElement); + expect(component.getPostfachNachricht).toHaveBeenCalled(); }); - it('should not show mail icon if Vorgang has no new Postfachnachricht', () => { + it('should not get message text if vorgang has no messages', () => { component.vorgang.hasPostfachNachricht = false; - fixture.detectChanges(); - const statusElement = getElementFromFixture(fixture, postfachStatus); + component.buildAriaLabel(); - expect(statusElement).not.toBeInstanceOf(HTMLElement); + expect(component.getPostfachNachricht).not.toHaveBeenCalled(); }); }); - describe('Bescheid-Status', () => { - it('should show bescheid status if Vorgang has antragBewilligt true', () => { + describe('getWiedervorlageText', () => { + it('should return text for next resubmission', () => { + component.vorgang.nextFrist = new Date('07.02.1977'); + + const result: string = component.getWiedervorlageText(); + + expect(result).toBe(', Nächste Wiedervorlage am 02.07.1977'); + }); + }); + + describe('getPostfachNachricht', () => { + it('should return "contains new messages"', () => { + component.vorgang.hasNewPostfachNachricht = true; + + const result: string = component.getPostfachNachricht(); + + expect(result).toBe(', enthält neue Postfachnachrichten'); + }); + + it('should return "contains messages"', () => { + component.vorgang.hasNewPostfachNachricht = false; + + const result: string = component.getPostfachNachricht(); + + expect(result).toBe(', enthält Postfachnachrichten'); + }); + }); + + describe('getStatus', () => { + it('should return status', () => { + component.vorgang.status = VorgangStatus.NEU; + + const result: string = component.getStatus(); + + expect(result).toBe('Neu'); + }); + }); + + describe('getApprovalText', () => { + it('should return empty string', () => { + component.vorgang.antragBewilligt = undefined; + + const result: string = component.getApprovalText(); + + expect(result).toBe(''); + }); + + it('should return "approved" text', () => { component.vorgang.antragBewilligt = true; - fixture.detectChanges(); - const element = getElementFromFixture(fixture, bescheidStatus); + const result: string = component.getApprovalText(); - expect(element).toBeInstanceOf(HTMLElement); + expect(result).toBe('bewilligt'); }); - it('should show bescheid status if Vorgang has antragBewilligt false', () => { + it('should return "not approved" text', () => { component.vorgang.antragBewilligt = false; - fixture.detectChanges(); - const element = getElementFromFixture(fixture, bescheidStatus); + const result: string = component.getApprovalText(); - expect(element).toBeInstanceOf(HTMLElement); + expect(result).toBe('abgelehnt'); }); + }); - it('should not show bescheid status if Vorgang has no antragBewilligt', () => { - component.vorgang.antragBewilligt = null; - fixture.detectChanges(); + describe('getUserText', () => { + it('should return no assigned user text', () => { + const result: string = component.getUserText(undefined); - const element = getElementFromFixture(fixture, bescheidStatus); + expect(result).toBe('Kein Bearbeiter zugewiesen'); + }); - expect(element).not.toBeInstanceOf(HTMLElement); + it('should return assigned user text', () => { + const result: string = component.getUserText(userProfile); + + expect(result).toBe( + `Aktuell zugewiesener Nutzer: ${userProfile.firstName} ${userProfile.lastName}`, + ); }); }); }); diff --git a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.ts b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.ts index bdf00833cb720c671cf9bc6ec4b6a0026ea64e1d..b8cbab9cabd6479cc85550faf03fd35dc9550a9c 100644 --- a/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.ts +++ b/alfa-client/libs/vorgang/src/lib/vorgang-list-container/vorgang-list/vorgang-list-item/vorgang-list-item.component.ts @@ -21,11 +21,28 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { formatToPrettyDate } from '@alfa-client/tech-shared'; -import { VorgangHeaderLinkRel, VorgangResource } from '@alfa-client/vorgang-shared'; +import { + EnumToLabelPipe, + formatFullDateWithTimeWithoutSeconds, + formatToPrettyDate, + isNotNull, + StateResource, +} from '@alfa-client/tech-shared'; +import { + getUserName, + UserProfileResource, + UserProfileService, +} from '@alfa-client/user-profile-shared'; +import { + VorgangHeaderLinkRel, + VorgangResource, + VorgangStatusLabel, +} from '@alfa-client/vorgang-shared'; import { getAktenzeichenText } from '@alfa-client/vorgang-shared-ui'; import { Component, Input, OnInit } from '@angular/core'; import { hasLink } from '@ngxp/rest'; +import { isNil } from 'lodash-es'; +import { first } from 'rxjs'; @Component({ selector: 'alfa-vorgang-list-item', @@ -38,18 +55,33 @@ export class VorgangListItemComponent implements OnInit { public ariaLabel: string = ''; + constructor(public userProfileService: UserProfileService) {} + ngOnInit(): void { - this.buildAriaLabel(); + if (hasLink(this.vorgang, VorgangHeaderLinkRel.ASSIGNED_TO)) { + this.userProfileService + .getAssignedUserProfile(this.vorgang, VorgangHeaderLinkRel.ASSIGNED_TO) + .pipe( + first((userProfile: StateResource<UserProfileResource>) => + isNotNull(userProfile.resource), + ), + ) + .subscribe((userProfileStateResource: StateResource<UserProfileResource>) => { + this.buildAriaLabel(userProfileStateResource.resource); + }); + } else this.buildAriaLabel(); } - buildAriaLabel() { + buildAriaLabel(userProfileResource: UserProfileResource = undefined) { const name: string = this.vorgang.name; const aktenzeichen: string = getAktenzeichenText(this.vorgang); const nummer: string = this.vorgang.nummer; - const status: string = this.vorgang.status; - const createdAt: string = formatToPrettyDate(this.vorgang.createdAt); + const status: string = this.getStatus(); + const approvalStatus: string = this.getApprovalText(); + const createdAt: string = formatFullDateWithTimeWithoutSeconds(this.vorgang.createdAt); + const userText: string = this.getUserText(userProfileResource); - this.ariaLabel = `Vorgang: ${name}, Aktenzeichen: ${aktenzeichen}, Nummer: ${nummer} Status: ${status}, Eingang: ${createdAt}`; + this.ariaLabel = `Vorgang: ${name}, Aktenzeichen: ${aktenzeichen}, Nummer: ${nummer} Status: ${status} ${approvalStatus}, Eingang: ${createdAt}, ${userText}`; if ( hasLink(this.vorgang, VorgangHeaderLinkRel.WIEDERVORLAGEN) && @@ -68,11 +100,28 @@ export class VorgangListItemComponent implements OnInit { return `, Nächste Wiedervorlage am ${nextFrist}`; } - getPostfachNachricht() { + getPostfachNachricht(): string { if (this.vorgang.hasNewPostfachNachricht) { return ', enthält neue Postfachnachrichten'; } return ', enthält Postfachnachrichten'; } + + getStatus(): string { + const enumToLabel = new EnumToLabelPipe(); + return enumToLabel.transform(this.vorgang.status, VorgangStatusLabel); + } + + getApprovalText(): string { + if (isNil(this.vorgang.antragBewilligt)) return ''; + + return this.vorgang.antragBewilligt ? 'bewilligt' : 'abgelehnt'; + } + + getUserText(userProfileResource: UserProfileResource): string { + return userProfileResource ? + `Aktuell zugewiesener Nutzer: ${getUserName(userProfileResource)}` + : 'Kein Bearbeiter zugewiesen'; + } } diff --git a/alfa-client/package-lock.json b/alfa-client/package-lock.json index b551b3e5a341ab04c060b67cf3b84a262f0e74d4..50747ff0743d33211bbf4255b761c6b5f013f61d 100644 --- a/alfa-client/package-lock.json +++ b/alfa-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "alfa", - "version": "1.0.0-SNAPSHOT", + "version": "1.1.0-SNAPSHOT", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "alfa", - "version": "1.0.0-SNAPSHOT", + "version": "1.1.0-SNAPSHOT", "license": "MIT", "dependencies": { "@angular/animations": "17.3.10", diff --git a/alfa-client/package.json b/alfa-client/package.json index e1523dd25e7b7786a77adf6d0fe52760d69516fe..4c807787822c57a9f17c83def17fd5bc69a31235 100644 --- a/alfa-client/package.json +++ b/alfa-client/package.json @@ -1,6 +1,6 @@ { "name": "alfa", - "version": "1.0.0-SNAPSHOT", + "version": "1.1.0-SNAPSHOT", "license": "MIT", "scripts": { "start": "nx run alfa:serve --port 4300 --disable-host-check", diff --git a/alfa-client/pom.xml b/alfa-client/pom.xml index 576dd79ddd4c0ab6fb8adbf86fcb0e631b3aab55..8e8395494a1b55f9399a9d7c997e6697e19ebb14 100644 --- a/alfa-client/pom.xml +++ b/alfa-client/pom.xml @@ -29,7 +29,7 @@ <parent> <groupId>de.ozgcloud.alfa</groupId> <artifactId>alfa</artifactId> - <version>2.13.0-SNAPSHOT</version> + <version>2.14.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> diff --git a/alfa-server/pom.xml b/alfa-server/pom.xml index 8310c3d2875b5bd9052d1a8704a958ecb3957786..2e9313095672888c2e1a95fedb74f716eb0dd417 100644 --- a/alfa-server/pom.xml +++ b/alfa-server/pom.xml @@ -5,7 +5,7 @@ <parent> <groupId>de.ozgcloud.alfa</groupId> <artifactId>alfa</artifactId> - <version>2.13.0-SNAPSHOT</version> + <version>2.14.0-SNAPSHOT</version> </parent> <artifactId>alfa-server</artifactId> @@ -131,4 +131,4 @@ </plugins> </build> -</project> \ No newline at end of file +</project> diff --git a/alfa-service/pom.xml b/alfa-service/pom.xml index 86daeeea0530386f5a9b708e593c5b46362e622d..e7ae7b5424b45b6d10d3e854e64ad139859a03ef 100644 --- a/alfa-service/pom.xml +++ b/alfa-service/pom.xml @@ -24,16 +24,14 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<project xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>de.ozgcloud.alfa</groupId> <artifactId>alfa</artifactId> - <version>2.13.0-SNAPSHOT</version> + <version>2.14.0-SNAPSHOT</version> </parent> <artifactId>alfa-service</artifactId> @@ -239,4 +237,4 @@ </plugins> </build> -</project> \ No newline at end of file +</project> diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResource.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResource.java new file mode 100644 index 0000000000000000000000000000000000000000..3a5d37676d95578418ebba3385ddb8f5f7287e99 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResource.java @@ -0,0 +1,8 @@ +package de.ozgcloud.alfa.resource; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown=true) +public class OzgcloudResource { + +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceController.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceController.java new file mode 100644 index 0000000000000000000000000000000000000000..a0ccfcd416ac66767589bc373197b2238d4e99c5 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceController.java @@ -0,0 +1,47 @@ +package de.ozgcloud.alfa.resource; + +import java.util.Collection; +import java.util.Optional; + +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import de.ozgcloud.alfa.common.errorhandling.ResourceNotFoundException; +import de.ozgcloud.common.errorhandling.TechnicalException; +import jakarta.validation.constraints.NotBlank; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping(OzgcloudResourceController.PATH) +@RequiredArgsConstructor +public class OzgcloudResourceController { + + static final String PATH = "/api/resources"; + static final String PARAM_URI = "uri"; + + private final Collection<OzgcloudResourceURIResolver> resolvers; + private final OzgcloudResourceModelAssembler assembler; + + @GetMapping(params = PARAM_URI) + public RepresentationModel<EntityModel<OzgcloudResource>> getOzgcloudResource(@RequestParam @NotBlank String uri) { + var resourceLink = resolveUri(uri); + if (resourceLink.isEmpty()) { + throw new ResourceNotFoundException(OzgcloudResource.class, uri); + } + return assembler.toModel(new OzgcloudResourceURIResolveResult(uri, resourceLink.get())); + } + + private Optional<Link> resolveUri(String uri) { + return resolvers.stream() + .map(resolver -> resolver.resolve(uri)) + .flatMap(Optional::stream) + .reduce((r1, r2) -> { + throw new TechnicalException("Multiple resolvers accepted uri " + uri); + }); + } +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssembler.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssembler.java new file mode 100644 index 0000000000000000000000000000000000000000..e3e83cf3a97ba6615974c72667fb32b976b6a733 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssembler.java @@ -0,0 +1,24 @@ +package de.ozgcloud.alfa.resource; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; + +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.server.RepresentationModelAssembler; +import org.springframework.stereotype.Component; + +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +class OzgcloudResourceModelAssembler implements RepresentationModelAssembler<OzgcloudResourceURIResolveResult, EntityModel<OzgcloudResource>> { + + @Override + public EntityModel<OzgcloudResource> toModel(OzgcloudResourceURIResolveResult uriResolveResult) { + return EntityModel.of(new OzgcloudResource()).add(getSelfLink(uriResolveResult.getResourceURI())).add(uriResolveResult.getResourceLink()); + } + + private Link getSelfLink(String uri) { + return linkTo(methodOn(OzgcloudResourceController.class).getOzgcloudResource(uri)).withSelfRel(); + } +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessor.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..ebe46beb467ae69720dafe31da6121fcd4b4a956 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessor.java @@ -0,0 +1,20 @@ +package de.ozgcloud.alfa.resource; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; + +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.server.RepresentationModelProcessor; +import org.springframework.stereotype.Component; + +import de.ozgcloud.alfa.Root; + +@Component +class OzgcloudResourceRootProcessor implements RepresentationModelProcessor<EntityModel<Root>> { + + static final String REL_RESOURCE = "resource"; + + @Override + public EntityModel<Root> process(EntityModel<Root> model) { + return model.add(linkTo(methodOn(OzgcloudResourceController.class).getOzgcloudResource(null)).withRel(REL_RESOURCE)); + } +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResult.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResult.java new file mode 100644 index 0000000000000000000000000000000000000000..edcbd9ef072905e7a30930396e1afa0d68a90aa3 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResult.java @@ -0,0 +1,15 @@ +package de.ozgcloud.alfa.resource; + +import org.springframework.hateoas.Link; + +import lombok.Builder; +import lombok.Getter; +import lombok.ToString; + +@Builder +@Getter +@ToString +public class OzgcloudResourceURIResolveResult { + private final String resourceURI; + private final Link resourceLink; +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolver.java b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..de38d882ecb8d41c198915eafc20b830798f4fbc --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolver.java @@ -0,0 +1,10 @@ +package de.ozgcloud.alfa.resource; + +import java.util.Optional; + +import org.springframework.hateoas.Link; + +public interface OzgcloudResourceURIResolver { + + Optional<Link> resolve(String uri); +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/vorgang/VorgangURIResolver.java b/alfa-service/src/main/java/de/ozgcloud/alfa/vorgang/VorgangURIResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..c3b6a185dce5c3e3189fabe78e6301c23ccce5a0 --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/vorgang/VorgangURIResolver.java @@ -0,0 +1,29 @@ +package de.ozgcloud.alfa.vorgang; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; + +import java.util.Optional; +import java.util.regex.Pattern; + +import org.springframework.hateoas.Link; +import org.springframework.stereotype.Component; + +import de.ozgcloud.alfa.resource.OzgcloudResourceURIResolver; + +@Component +class VorgangURIResolver implements OzgcloudResourceURIResolver { + + static final String REL_NAME = "vorgang"; + + private final Pattern pattern = Pattern.compile("ozgcloud://[^/]+/vorgangs/([0-9a-fA-F]+)"); + + @Override + public Optional<Link> resolve(String uri) { + var matcher = pattern.matcher(uri); + if (!matcher.matches()) { + return Optional.empty(); + } + var vorgangId = matcher.group(1); + return Optional.of(linkTo(VorgangController.class).slash(vorgangId).withRel(REL_NAME)); + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/RootControllerITCase.java b/alfa-service/src/test/java/de/ozgcloud/alfa/RootControllerITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..c4bc47bb1aba700a268c32f282fb569ecb3f909e --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/RootControllerITCase.java @@ -0,0 +1,52 @@ +package de.ozgcloud.alfa; + +import static org.mockito.Mockito.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.hamcrest.core.StringEndsWith; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import de.ozgcloud.alfa.common.user.CurrentUserService; +import de.ozgcloud.alfa.common.user.UserProfileTestFactory; + +@AutoConfigureMockMvc +@SpringBootTest +@WithMockUser +class RootControllerITCase { + + @MockBean + private CurrentUserService currentUserService; + + @Autowired + private MockMvc mockMvc; + + @BeforeEach + void init() { + when(currentUserService.getUser()).thenReturn(UserProfileTestFactory.create()); + } + + @Nested + class TestProcess { + + @Test + void shouldAddResourceLink() throws Exception { + var response = doRequest(); + + response.andExpect(jsonPath("$._links.resource.href").value(StringEndsWith.endsWith("/api/resources?uri={uri}"))); + } + } + + private ResultActions doRequest() throws Exception { + return mockMvc.perform(get(RootController.PATH)).andExpect(status().isOk()); + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerITCase.java b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..a545d2ee05325037544e883b529b7c39d4d9c991 --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerITCase.java @@ -0,0 +1,90 @@ +package de.ozgcloud.alfa.resource; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import org.hamcrest.core.StringEndsWith; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; + +import de.ozgcloud.alfa.common.AlfaTestUtils; +import lombok.SneakyThrows; + +@AutoConfigureMockMvc +@SpringBootTest +@WithMockUser +class OzgcloudResourceControllerITCase { + + @Autowired + private MockMvc mockMvc; + + @Nested + class TestGetOzgcloudResource { + + private static final String VORGANG_ID = AlfaTestUtils.createMongoDbObjectId(); + + @Test + void shouldReturnStatusOk() throws Exception { + var response = doRequest("ozgcloud://test.de/vorgangs/" + VORGANG_ID); + + response.andExpect(status().isOk()); + } + + @Test + void shouldHaveVorgangLink() throws Exception { + var response = doRequest("ozgcloud://test.de/vorgangs/" + VORGANG_ID); + + response.andExpect(jsonPath("$._links.vorgang.href").value(StringEndsWith.endsWith("/api/vorgangs/" + VORGANG_ID))); + } + + @Test + void shouldHaveSelfLink() throws Exception { + var uri = "ozgcloud://test.de/vorgangs/" + VORGANG_ID; + var encodedUri = URLEncoder.encode(uri, StandardCharsets.UTF_8); + + var response = doRequest(uri); + + response.andExpect(jsonPath("$._links.self.href").value(StringEndsWith.endsWith(OzgcloudResourceController.PATH + "?uri=" + encodedUri))); + } + + @Test + void shouldReturnStatusNotFound() throws Exception { + var response = doRequest("dummy://test.de"); + + response.andExpect(status().isNotFound()); + } + + @Test + void shouldReturnBadRequestOnNoRequestParam() throws Exception { + var response = doRequestWithQueryString(""); + + response.andExpect(status().isBadRequest()); + } + + @Test + void shouldReturnBadRequestOnEmptyUri() throws Exception { + var response = doRequestWithQueryString("?uri="); + + response.andExpect(status().isBadRequest()); + } + + @SneakyThrows + private ResultActions doRequest(String uri) { + return doRequestWithQueryString("?uri=" + uri); + } + + @SneakyThrows + private ResultActions doRequestWithQueryString(String queryString) { + return mockMvc.perform(get(OzgcloudResourceController.PATH + queryString)); + } + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..9ea5e3d62933188bc4a839a69b7b3cb4d2c63cb1 --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceControllerTest.java @@ -0,0 +1,130 @@ +package de.ozgcloud.alfa.resource; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.RepresentationModel; + +import de.ozgcloud.alfa.common.errorhandling.ResourceNotFoundException; +import de.ozgcloud.common.errorhandling.TechnicalException; +import lombok.SneakyThrows; + +class OzgcloudResourceControllerTest { + + @Mock + private OzgcloudResourceURIResolver resourceAResolver; + @Mock + private OzgcloudResourceURIResolver resourceBResolver; + @Spy + private Collection<OzgcloudResourceURIResolver> resourceResolvers = new ArrayList<>(); + @Mock + private OzgcloudResourceModelAssembler assembler; + @InjectMocks + private OzgcloudResourceController controller; + + @BeforeEach + void init() { + resourceResolvers.add(resourceAResolver); + resourceResolvers.add(resourceBResolver); + } + + @Nested + class TestGetOzgcloudResource { + + @Nested + class OnUriCouldBeResolved { + + private final ArgumentMatcher<OzgcloudResourceURIResolveResult> HAS_MATCHING_RESOURCE_URI = mapping -> mapping.getResourceURI().equals( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI); + private final ArgumentMatcher<OzgcloudResourceURIResolveResult> HAS_MATCHING_RESOURCE_LINK = mapping -> mapping.getResourceLink().equals( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK); + + @BeforeEach + void init() { + when(resourceBResolver.resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI)).thenReturn(Optional.of( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK)); + } + + @Test + void shouldCallResolver() { + callController(); + + verify(resourceBResolver).resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI); + } + + @Test + void shouldCallAssemblerWithUri() { + callController(); + + verify(assembler).toModel(argThat(HAS_MATCHING_RESOURCE_URI)); + } + + @Test + void shouldCallAssemblerWithResourceLink() { + callController(); + + verify(assembler).toModel(argThat(HAS_MATCHING_RESOURCE_LINK)); + } + + @Test + void shouldReturnModelFromAssembler() { + EntityModel<OzgcloudResource> modelFromAssembler = EntityModel.of(new OzgcloudResource()); + when(assembler.toModel(argThat(mapping -> HAS_MATCHING_RESOURCE_URI.matches(mapping) && HAS_MATCHING_RESOURCE_LINK.matches(mapping)))) + .thenReturn(modelFromAssembler); + + var model = callController(); + + assertThat(model).isEqualTo(modelFromAssembler); + } + } + + @Nested + class OnUriCouldNotBeResolved { + + @BeforeEach + void init() { + when(resourceAResolver.resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI)).thenReturn(Optional.empty()); + when(resourceBResolver.resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI)).thenReturn(Optional.empty()); + } + + @Test + void shouldThrowResourceNotFoundException() { + assertThatThrownBy(TestGetOzgcloudResource.this::callController).isInstanceOf(ResourceNotFoundException.class); + } + } + + @Nested + class OnMultipleResolveResults { + + @BeforeEach + void init() { + when(resourceAResolver.resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI)).thenReturn(Optional.of( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK)); + when(resourceBResolver.resolve(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI)).thenReturn(Optional.of( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK)); + } + + @Test + void shouldThrowTechnicalException() { + assertThatThrownBy(TestGetOzgcloudResource.this::callController).isInstanceOf(TechnicalException.class); + } + } + + @SneakyThrows + private RepresentationModel<EntityModel<OzgcloudResource>> callController() { + return controller.getOzgcloudResource(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_URI); + } + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssemblerTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssemblerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ed144040a39978e418d2248ca496007c4c4f05ac --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceModelAssemblerTest.java @@ -0,0 +1,43 @@ +package de.ozgcloud.alfa.resource; + +import static de.ozgcloud.alfa.resource.OzgcloudResourceController.*; +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.hateoas.Link; + +class OzgcloudResourceModelAssemblerTest { + + @InjectMocks + private OzgcloudResourceModelAssembler assembler; + + @Nested + class TestToModel { + + private final OzgcloudResourceURIResolveResult uriResolveResult = OzgcloudResourceURIResolveResultTestFactory.create(); + + @Test + void shouldHaveSelfLink() { + var model = callAssembler(); + + assertThat(model.getLink(IanaLinkRelations.SELF_VALUE)).isPresent().get().extracting(Link::getHref) + .isEqualTo(OzgcloudResourceController.PATH + "?" + PARAM_URI + "=" + OzgcloudResourceURIResolveResultTestFactory.ENCODED_RESOURCE_URI); + } + + @Test + void shouldHaveResourceLink() { + var model = callAssembler(); + + assertThat(model.getLink(OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK.getRel())).isPresent().get().isEqualTo( + OzgcloudResourceURIResolveResultTestFactory.RESOURCE_LINK); + } + + private EntityModel<OzgcloudResource> callAssembler() { + return assembler.toModel(uriResolveResult); + } + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessorTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..71d60de1e746dd5f801b5d0a4f87b0d93493bcc7 --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceRootProcessorTest.java @@ -0,0 +1,39 @@ +package de.ozgcloud.alfa.resource; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; + +import de.ozgcloud.alfa.Root; +import de.ozgcloud.alfa.RootTestFactory; + +class OzgcloudResourceRootProcessorTest { + + private final OzgcloudResourceRootProcessor processor = new OzgcloudResourceRootProcessor(); + + @Nested + class TestProcess { + + private final EntityModel<Root> model = EntityModel.of(RootTestFactory.create()); + + @Test + void shouldReturnOriginalModel() { + var result = processor.process(model); + + assertThat(result).isEqualTo(model); + } + + @Test + void shouldAddResourceLink() { + processor.process(model); + + assertThat(model.getLink(OzgcloudResourceRootProcessor.REL_RESOURCE)).isPresent().get().extracting(Link::getHref) + .isEqualTo(OzgcloudResourceController.PATH + "?uri={uri}"); + } + } +} + + diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResultTestFactory.java b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResultTestFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..dd0ea565f584133ece1265c2c2a9782135a65a74 --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/resource/OzgcloudResourceURIResolveResultTestFactory.java @@ -0,0 +1,23 @@ +package de.ozgcloud.alfa.resource; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import org.springframework.hateoas.Link; + +import com.thedeanda.lorem.LoremIpsum; + +class OzgcloudResourceURIResolveResultTestFactory { + + public static final String RESOURCE_URI = String.format("%s://%s.%s/%s", (Object[]) LoremIpsum.getInstance().getWords(4).split("\\s")); + public static final String ENCODED_RESOURCE_URI = URLEncoder.encode(RESOURCE_URI, StandardCharsets.UTF_8); + public static final Link RESOURCE_LINK = Link.of(LoremIpsum.getInstance().getUrl()).withRel(LoremIpsum.getInstance().getWords(1)); + + public static OzgcloudResourceURIResolveResult create() { + return createBuilder().build(); + } + + public static OzgcloudResourceURIResolveResult.OzgcloudResourceURIResolveResultBuilder createBuilder() { + return OzgcloudResourceURIResolveResult.builder().resourceURI(RESOURCE_URI).resourceLink(RESOURCE_LINK); + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/vorgang/VorgangURIResolverTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/vorgang/VorgangURIResolverTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1d691c621cc7e42d207861ccb6503bd04026a74c --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/vorgang/VorgangURIResolverTest.java @@ -0,0 +1,61 @@ +package de.ozgcloud.alfa.vorgang; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.LinkRelation; + +import de.ozgcloud.alfa.common.AlfaTestUtils; + +class VorgangURIResolverTest { + + private VorgangURIResolver resolver = new VorgangURIResolver(); + + @Nested + class TestResolve { + + private final String VORGANG_ID = AlfaTestUtils.createMongoDbObjectId(); + private final String VORGANG_URI = "ozgcloud://dummy.de/vorgangs/" + VORGANG_ID; + + @ParameterizedTest + @ValueSource(strings = { + "http://dummy.de/vorgangs/123", + "ozgcloud://dummy.de/res-a/123", + "ozgcloud://dummy.de/xyz/vorgangs/123", + "ozgcloud://dummy.de/vorgangs/xyz/123" }) + void shouldReturnEmptyIfUriIsNotRecognized(String uri) { + var result = resolver.resolve(uri); + + assertThat(result).isEmpty(); + } + + @Test + void shouldReturnLink() { + var result = resolver.resolve(VORGANG_URI); + + assertThat(result).isPresent(); + } + + @Nested + class TestVorgangLink { + + @Test + void shouldHaveCorrectRelValue() { + var result = resolver.resolve(VORGANG_URI); + + assertThat(result).get().extracting(Link::getRel).extracting(LinkRelation::value).isEqualTo(VorgangURIResolver.REL_NAME); + } + + @Test + void shouldHaveHrefOfVorgang() { + var result = resolver.resolve(VORGANG_URI); + + assertThat(result).get().extracting(Link::getHref).isEqualTo("/api/vorgangs/" + VORGANG_ID); + } + } + } +} diff --git a/alfa-xdomea/pom.xml b/alfa-xdomea/pom.xml index 4bd9cb9cc3356d4ff4484bfd957150d964526142..ce932acc53706fb904954f17bc19ea7c52c29b22 100644 --- a/alfa-xdomea/pom.xml +++ b/alfa-xdomea/pom.xml @@ -31,7 +31,7 @@ <parent> <groupId>de.ozgcloud.alfa</groupId> <artifactId>alfa</artifactId> - <version>2.13.0-SNAPSHOT</version> + <version>2.14.0-SNAPSHOT</version> </parent> <artifactId>alfa-xdomea</artifactId> diff --git a/pom.xml b/pom.xml index 5c3018e62bf6370472aba1576215d06d0e43067d..e05df380d5db04e9031bf6d4d0c59d49253bc3d9 100644 --- a/pom.xml +++ b/pom.xml @@ -24,9 +24,7 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<project xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> @@ -37,7 +35,7 @@ <groupId>de.ozgcloud.alfa</groupId> <artifactId>alfa</artifactId> - <version>2.13.0-SNAPSHOT</version> + <version>2.14.0-SNAPSHOT</version> <name>Alfa Parent</name> <packaging>pom</packaging> @@ -52,11 +50,11 @@ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> - <vorgang-manager.version>2.8.0</vorgang-manager.version> - <nachrichten-manager.version>2.7.0</nachrichten-manager.version> + <vorgang-manager.version>2.13.0</vorgang-manager.version> + <nachrichten-manager.version>2.11.0</nachrichten-manager.version> <ozgcloud-common-pdf.version>3.0.1</ozgcloud-common-pdf.version> - <user-manager.version>2.2.0</user-manager.version> - <zufi-manager.version>1.2.0</zufi-manager.version> + <user-manager.version>2.8.0</user-manager.version> + <zufi-manager.version>1.3.0</zufi-manager.version> <spring-cloud-config-client.version>4.1.3</spring-cloud-config-client.version> <!-- TODO: die Version über ozgcloud-common ziehen --> @@ -179,4 +177,4 @@ <url>https://nexus.ozg-sh.de/repository/ozg-snapshots/</url> </snapshotRepository> </distributionManagement> -</project> \ No newline at end of file +</project> diff --git a/src/main/helm/templates/_helpers.tpl b/src/main/helm/templates/_helpers.tpl index a20881dd0de555971004516d03a0a3767809c394..fde4a4e14938c745ee8972e5a8c9781a7f5c0669 100644 --- a/src/main/helm/templates/_helpers.tpl +++ b/src/main/helm/templates/_helpers.tpl @@ -50,7 +50,7 @@ app.kubernetes.io/namespace: {{ include "app.namespace" . }} {{- end -}} {{- define "app.grpc_client_vorgang_manager_address" -}} -{{ printf "%s.%s:9090" ( coalesce .Values.vorgangManagerName "vorgang-manager" ) .Release.Namespace | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{ printf "dns:///%s.%s:9090" ( coalesce .Values.vorgangManagerName "vorgang-manager" ) .Release.Namespace | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end -}} {{- define "app.grpc_client_user-manager_address" -}} diff --git a/src/test/helm/deployment_defaults_env_test.yaml b/src/test/helm/deployment_defaults_env_test.yaml index 0bf3a7adbf32e293791d0af1a2804bf7c8c4d6df..f02a3b9989743688a6baa2527e7376ca7c9945f4 100644 --- a/src/test/helm/deployment_defaults_env_test.yaml +++ b/src/test/helm/deployment_defaults_env_test.yaml @@ -48,7 +48,7 @@ tests: path: spec.template.spec.containers[0].env content: name: grpc_client_vorgang-manager_address - value: vorgang-manager.sh-helm-test:9090 + value: dns:///vorgang-manager.sh-helm-test:9090 - contains: path: spec.template.spec.containers[0].env content: diff --git a/src/test/helm/deployment_vorgang_manager_address_env_test.yaml b/src/test/helm/deployment_vorgang_manager_address_env_test.yaml index 794e933ec92b521e7664244bae38f8b5b05b6469..9c0e685c4e9256e0053ebe3b4203e392f816e271 100644 --- a/src/test/helm/deployment_vorgang_manager_address_env_test.yaml +++ b/src/test/helm/deployment_vorgang_manager_address_env_test.yaml @@ -46,4 +46,4 @@ tests: path: spec.template.spec.containers[0].env content: name: grpc_client_vorgang-manager_address - value: my-test-vorgang-manager-name.sh-helm-test:9090 + value: dns:///my-test-vorgang-manager-name.sh-helm-test:9090