diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-mail.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-mail.cy.ts index 8f32eb567802ce78e439567c3cdc4bcfb5c0bfb8..292ad9ca566f8a29f7be1fcbf104757c712eb5cb 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-mail.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/postfach-mail/postfach-mail.cy.ts @@ -116,7 +116,7 @@ describe('PostfachMail', () => { return { ...buildVorgang(objectIds[2], 'VorgangWithoutPostfachId'), eingangs: [...vorgang.eingangs], - header: { serviceKonto: null }, + header: { serviceKonto: null, collaborationLevel: 0 }, }; } 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 ca29375a29c0d5ce113b82357dca681679efdad5..eca73b26f756e3a3610b82f62299ddbde612e5a3 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 @@ -42,6 +42,7 @@ describe('Postfach Nachricht reply button', () => { }, name: 'BayernID Vorgang', header: { + collaborationLevel: 0, serviceKonto: { type: 'BayernId', postfachAddress: [ diff --git a/alfa-client/apps/alfa-e2e/src/fixtures/vorgang/vorgang.json b/alfa-client/apps/alfa-e2e/src/fixtures/vorgang/vorgang.json index 7db3791c3468b1f263a22f255637f3153de69eee..fd4d23ab75d2d554467e4631abb1848acefd52f7 100644 --- a/alfa-client/apps/alfa-e2e/src/fixtures/vorgang/vorgang.json +++ b/alfa-client/apps/alfa-e2e/src/fixtures/vorgang/vorgang.json @@ -12,6 +12,7 @@ "status": "NEU", "inCreation": false, "header": { + "collaborationLevel": 0, "serviceKonto": { "type": "OSI", "postfachAddresses": [ diff --git a/alfa-client/apps/alfa-e2e/src/model/vorgang.ts b/alfa-client/apps/alfa-e2e/src/model/vorgang.ts index aca577c4cea54f5d30ca05ca61956be6cb82099b..7fdbcfd62b93935aef7f35cb4a4f7fc1858c4422 100644 --- a/alfa-client/apps/alfa-e2e/src/model/vorgang.ts +++ b/alfa-client/apps/alfa-e2e/src/model/vorgang.ts @@ -67,6 +67,7 @@ export class VorgangE2E { export class VorgangHeaderE2E { serviceKonto: ServiceKontoE2E; + collaborationLevel: number; } export class ServiceKontoE2E { diff --git a/alfa-client/apps/demo/src/app/app.component.html b/alfa-client/apps/demo/src/app/app.component.html index 81bc4dff284d5e739981af85b8d4e1cc0c4b3844..7dbdd36d8134a9a38fdd859b1371c5a0362820df 100644 --- a/alfa-client/apps/demo/src/app/app.component.html +++ b/alfa-client/apps/demo/src/app/app.component.html @@ -14,6 +14,16 @@ <nav>NAV</nav> </div> <main class="flex-auto bg-background-50 p-6"> + <div class="my-5"> + <ods-instant-search + headerText="In der OZG-Cloud" + placeholder="zuständige Stelle suchen" + [control]="instantSearchFormControl" + [searchResults]="getInstantSearchResults()" + (searchResultSelected)="selectSearchResult($event)" + (searchQueryChanged)="onSearchQueryChanged($event)" + ></ods-instant-search> + </div> <div class="w-96"> <ods-attachment-wrapper> <ods-attachment @@ -21,6 +31,7 @@ description="234 kB" fileType="pdf" isLoading="true" + loadingCaption="Mein_Bescheid.pdf wird heruntergeladen..." > </ods-attachment> <ods-attachment caption="Mein_Bescheid.xml" description="234 kB" fileType="xml"> @@ -94,7 +105,7 @@ value="abgelehnt" variant="bescheid_abgelehnt" > - <ods-close-icon class="fill-abgelehnt" /> + <ods-close-icon class="fill-abgelehnt" size="large" /> </ods-radio-button-card> </div> </form> @@ -209,14 +220,6 @@ <p text class="text-center">Bescheiddokument<br />hochladen</p></ods-file-upload-button > </div> - - <div class="mt-4"> - <ods-file-upload-button class="w-72" [isLoading]="false" id="upload129"> - <ods-bescheid-upload-icon /> - <ods-spinner-icon spinner size="medium" /> - <div text class="text-center">Anhang hochladen</div></ods-file-upload-button - > - </div> <div class="mt-4"> <ods-file-upload-button class="w-72" [isLoading]="true" id="upload130"> <ods-attachment-icon icon /> diff --git a/alfa-client/apps/demo/src/app/app.component.ts b/alfa-client/apps/demo/src/app/app.component.ts index 5d64b15a623f2330d29201455ec298907bcd94fa..40addd4c8966f682e403357a673cb578348d5213 100644 --- a/alfa-client/apps/demo/src/app/app.component.ts +++ b/alfa-client/apps/demo/src/app/app.component.ts @@ -15,6 +15,7 @@ import { ErrorMessageComponent, FileIconComponent, FileUploadButtonComponent, + InstantSearchComponent, RadioButtonCardComponent, SaveIconComponent, SendIconComponent, @@ -24,6 +25,11 @@ import { TextareaComponent, } from '@ods/system'; +import { EMPTY_STRING } from '@alfa-client/tech-shared'; +import { + InstantSearchQuery, + InstantSearchResult, +} from 'libs/design-system/src/lib/instant-search/instant-search/instant-search.model'; import { BescheidDialogExampleComponent } from './components/bescheid-dialog/bescheid-dialog.component'; import { BescheidPaperComponent } from './components/bescheid-paper/bescheid-paper.component'; import { BescheidStepperComponent } from './components/bescheid-stepper/bescheid-stepper.component'; @@ -46,6 +52,7 @@ import { CustomStepperComponent } from './components/cdk-demo/custom-stepper.com BescheidPaperComponent, RadioButtonCardComponent, ReactiveFormsModule, + InstantSearchComponent, SaveIconComponent, SendIconComponent, StampIconComponent, @@ -64,14 +71,49 @@ import { CustomStepperComponent } from './components/cdk-demo/custom-stepper.com templateUrl: './app.component.html', }) export class AppComponent { - title = 'demo'; - darkMode = signal<boolean>(JSON.parse(window.localStorage.getItem('darkMode') ?? 'false')); @HostBinding('class.dark') get mode() { return this.darkMode(); } + title = 'demo'; + + instantSearchItems: InstantSearchResult<unknown>[] = [ + { + title: 'Landeshauptstadt Kiel - Ordnungsamt, Gewerbe- und Schornsteinfegeraufsicht', + description: 'Fabrikstraße 8-10, 24103 Kiel', + data: { resource: 'dummy 1' }, + }, + { + title: 'Amt für Digitalisierung, Breitband und Vermessung Nürnberg Außenstelle Hersbruck', + description: 'Rathausmarkt 7, Hersbruck', + data: { resource: 'dummy 2' }, + }, + { + title: 'Amt für Digitalisierung, Breitband und Vermessung Stuttgart', + description: 'Rathausmarkt 7, Stuttgart', + data: { resource: 'dummy 3' }, + }, + { + title: 'Amt für Digitalisierung, Breitband und Vermessung Ulm', + description: 'Rathausmarkt 7, Ulm', + data: { resource: 'dummy 4' }, + }, + ]; + instantSearchFormControl = new FormControl(EMPTY_STRING); + + getInstantSearchResults() { + if (this.instantSearchFormControl.value.length < 2) return []; + return this.instantSearchItems.filter((item) => + item.title.toLowerCase().includes(this.instantSearchFormControl.value.toLowerCase()), + ); + } + + selectSearchResult(result: InstantSearchResult<unknown>) { + console.log(result); + } + exampleForm = new FormGroup({ exampleName: new FormControl('bewilligt'), }); @@ -87,4 +129,8 @@ export class AppComponent { window.localStorage.setItem('darkMode', JSON.stringify(this.darkMode())); }); } + + public onSearchQueryChanged(searchQuery: InstantSearchQuery) { + console.info('Search query: %o', searchQuery); + } } diff --git a/alfa-client/libs/bescheid/src/lib/beschieden-date-in-vorgang-container/beschieden-date-container/beschieden-date-container.component.html b/alfa-client/libs/bescheid/src/lib/beschieden-date-in-vorgang-container/beschieden-date-container/beschieden-date-container.component.html index 704a6ddd28f903eee9a9fb887c8ac40d4d56683d..61ae1d119505cb9e60d173dce420dd872cfa784e 100644 --- a/alfa-client/libs/bescheid/src/lib/beschieden-date-in-vorgang-container/beschieden-date-container/beschieden-date-container.component.html +++ b/alfa-client/libs/bescheid/src/lib/beschieden-date-in-vorgang-container/beschieden-date-container/beschieden-date-container.component.html @@ -9,7 +9,6 @@ <ods-close-icon *ngIf="!bescheid.bewilligt" data-test-id="abgelehnt-icon" - size="small" class="fill-abgelehnt" /> diff --git a/alfa-client/libs/collaboration/.eslintrc.json b/alfa-client/libs/collaboration/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..243c51741f65cc7afb3a7d85531c24afdcab5e56 --- /dev/null +++ b/alfa-client/libs/collaboration/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "extends": ["plugin:@nx/angular", "plugin:@angular-eslint/template/process-inline-templates"], + "rules": { + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "alfa", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "alfa", + "style": "kebab-case" + } + ] + } + }, + { + "files": ["*.html"], + "extends": ["plugin:@nx/angular-template"], + "rules": {} + } + ] +} diff --git a/alfa-client/libs/collaboration/README.md b/alfa-client/libs/collaboration/README.md new file mode 100644 index 0000000000000000000000000000000000000000..53577986259ad6d8403d1446c6c1426d24e6d06d --- /dev/null +++ b/alfa-client/libs/collaboration/README.md @@ -0,0 +1,7 @@ +# collaboration + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test collaboration` to execute the unit tests. diff --git a/alfa-client/libs/collaboration/jest.config.ts b/alfa-client/libs/collaboration/jest.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..fdccd307388de8b7fb2256a71ec07808ec5b05de --- /dev/null +++ b/alfa-client/libs/collaboration/jest.config.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +export default { + displayName: 'collaboration', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], + coverageDirectory: '../../coverage/libs/collaboration', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '<rootDir>/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/alfa-client/libs/collaboration/project.json b/alfa-client/libs/collaboration/project.json new file mode 100644 index 0000000000000000000000000000000000000000..39d9434a1f4c96550e4d71423eed803a6c397993 --- /dev/null +++ b/alfa-client/libs/collaboration/project.json @@ -0,0 +1,21 @@ +{ + "name": "collaboration", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/collaboration/src", + "prefix": "alfa", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/libs/collaboration"], + "options": { + "tsConfig": "libs//collaboration/tsconfig.spec.json", + "jestConfig": "libs/collaboration/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/alfa-client/libs/collaboration/src/index.ts b/alfa-client/libs/collaboration/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..fe7fae5e1dd38a384cd93d2ad8425d93ac37ae03 --- /dev/null +++ b/alfa-client/libs/collaboration/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/collaboration-in-vorgang-container/collaboration-in-vorgang-container.component'; +export * from './lib/collaboration.module'; diff --git a/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang-container.component.html b/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang-container.component.html new file mode 100644 index 0000000000000000000000000000000000000000..9b55f329b9326a6ebff530a431714165d7a89a0c --- /dev/null +++ b/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang-container.component.html @@ -0,0 +1,3 @@ +<ods-button variant="outline" text="Anfrage erstellen" dataTestId="anfrage-erstellen-button"> + <ods-collaboration-icon icon /> +</ods-button> diff --git a/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang-container.component.spec.ts b/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang-container.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..bbb2fbafa9d8d9513f0d8a54702dcbb83a0f244d --- /dev/null +++ b/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang-container.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ButtonComponent, CollaborationIconComponent, SaveIconComponent } from '@ods/system'; +import { MockComponent } from 'ng-mocks'; +import { CollaborationInVorgangContainerComponent } from './collaboration-in-vorgang-container.component'; + +describe('CollaborationInVorgangContainerComponent', () => { + let component: CollaborationInVorgangContainerComponent; + let fixture: ComponentFixture<CollaborationInVorgangContainerComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ButtonComponent, SaveIconComponent], + declarations: [ + CollaborationInVorgangContainerComponent, + MockComponent(CollaborationIconComponent), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(CollaborationInVorgangContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang-container.component.ts b/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang-container.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..816cd743cf9dbc2a992d115a7e3598c7a7590ee2 --- /dev/null +++ b/alfa-client/libs/collaboration/src/lib/collaboration-in-vorgang-container/collaboration-in-vorgang-container.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'alfa-collaboration-in-vorgang-container', + templateUrl: './collaboration-in-vorgang-container.component.html', +}) +export class CollaborationInVorgangContainerComponent {} diff --git a/alfa-client/libs/collaboration/src/lib/collaboration.module.spec.ts b/alfa-client/libs/collaboration/src/lib/collaboration.module.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..1fa74dca425307c2116f48587c5179882184daa1 --- /dev/null +++ b/alfa-client/libs/collaboration/src/lib/collaboration.module.spec.ts @@ -0,0 +1,14 @@ +import { TestBed } from '@angular/core/testing'; +import { CollaborationModule } from './collaboration.module'; + +describe('CollaborationModule', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CollaborationModule], + }).compileComponents(); + }); + + it('should create', () => { + expect(CollaborationModule).toBeDefined(); + }); +}); diff --git a/alfa-client/libs/collaboration/src/lib/collaboration.module.ts b/alfa-client/libs/collaboration/src/lib/collaboration.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..adcfd33ce0ff125a1dab9099fc4a8ffebe986055 --- /dev/null +++ b/alfa-client/libs/collaboration/src/lib/collaboration.module.ts @@ -0,0 +1,11 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ButtonComponent, CollaborationIconComponent } from '@ods/system'; +import { CollaborationInVorgangContainerComponent } from './collaboration-in-vorgang-container/collaboration-in-vorgang-container.component'; + +@NgModule({ + imports: [CommonModule, ButtonComponent, CollaborationIconComponent], + declarations: [CollaborationInVorgangContainerComponent], + exports: [CollaborationInVorgangContainerComponent], +}) +export class CollaborationModule {} diff --git a/alfa-client/libs/collaboration/src/test-setup.ts b/alfa-client/libs/collaboration/src/test-setup.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b07c0bac34c40aa6afeef02c18c8db08f79de48 --- /dev/null +++ b/alfa-client/libs/collaboration/src/test-setup.ts @@ -0,0 +1,15 @@ +import '@testing-library/jest-dom'; +import 'jest-preset-angular/setup-jest'; + +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing'; + +getTestBed().resetTestEnvironment(); +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { + teardown: { destroyAfterEach: false }, + errorOnUnknownProperties: true, + errorOnUnknownElements: true, +}); diff --git a/alfa-client/libs/collaboration/tsconfig.json b/alfa-client/libs/collaboration/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..7cc6baf2f58ed5ccfba098131996f579979e9f18 --- /dev/null +++ b/alfa-client/libs/collaboration/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "compilerOptions": { + "target": "es2022" + } +} diff --git a/alfa-client/libs/collaboration/tsconfig.lib.json b/alfa-client/libs/collaboration/tsconfig.lib.json new file mode 100644 index 0000000000000000000000000000000000000000..4cab05d46338c6e9d4dfe6512fd7eb7f60340d3d --- /dev/null +++ b/alfa-client/libs/collaboration/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": ["src/**/*.spec.ts", "src/test-setup.ts", "jest.config.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/alfa-client/libs/collaboration/tsconfig.spec.json b/alfa-client/libs/collaboration/tsconfig.spec.json new file mode 100644 index 0000000000000000000000000000000000000000..7870b7c011681fb77d6114001f44d3eeca69975b --- /dev/null +++ b/alfa-client/libs/collaboration/tsconfig.spec.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] +} diff --git a/alfa-client/libs/design-system/src/index.ts b/alfa-client/libs/design-system/src/index.ts index 57c220dc85cf06f66d1e23edd66bc7d9bff947ea..9a59b13cbc25c6174275daa3727bdd8388fb0dc4 100644 --- a/alfa-client/libs/design-system/src/index.ts +++ b/alfa-client/libs/design-system/src/index.ts @@ -14,6 +14,7 @@ export * from './lib/icons/attachment-icon/attachment-icon.component'; export * from './lib/icons/bescheid-generate-icon/bescheid-generate-icon.component'; export * from './lib/icons/bescheid-upload-icon/bescheid-upload-icon.component'; export * from './lib/icons/close-icon/close-icon.component'; +export * from './lib/icons/collaboration-icon/collaboration-icon.component'; export * from './lib/icons/exclamation-icon/exclamation-icon.component'; export * from './lib/icons/file-icon/file-icon.component'; export * from './lib/icons/iconVariants'; @@ -21,4 +22,5 @@ export * from './lib/icons/save-icon/save-icon.component'; export * from './lib/icons/send-icon/send-icon.component'; export * from './lib/icons/spinner-icon/spinner-icon.component'; export * from './lib/icons/stamp-icon/stamp-icon.component'; +export * from './lib/instant-search/instant-search/instant-search.component'; export * from './lib/testbtn/testbtn.component'; diff --git a/alfa-client/libs/design-system/src/lib/aria-live-region/aria-live-region.component.spec.ts b/alfa-client/libs/design-system/src/lib/aria-live-region/aria-live-region.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0848c08f798f58fd91f850597ce0d15f267e065 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/aria-live-region/aria-live-region.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AriaLiveRegionComponent } from './aria-live-region.component'; + +describe('AriaLiveRegionComponent', () => { + let component: AriaLiveRegionComponent; + let fixture: ComponentFixture<AriaLiveRegionComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AriaLiveRegionComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(AriaLiveRegionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/aria-live-region/aria-live-region.component.ts b/alfa-client/libs/design-system/src/lib/aria-live-region/aria-live-region.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..3cec9afff4c23502077704daea8b04f125158bdd --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/aria-live-region/aria-live-region.component.ts @@ -0,0 +1,12 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ods-aria-live-region', + standalone: true, + imports: [CommonModule], + template: `<span aria-live="polite" class="sr-only" role="status">{{ text }}</span>`, +}) +export class AriaLiveRegionComponent { + @Input() text: string = ''; +} diff --git a/alfa-client/libs/design-system/src/lib/aria-live-region/aria-live-region.stories.ts b/alfa-client/libs/design-system/src/lib/aria-live-region/aria-live-region.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..7af950ebf415e5e2a634be01a2edaa32a9d78a01 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/aria-live-region/aria-live-region.stories.ts @@ -0,0 +1,25 @@ +import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular'; +import { AriaLiveRegionComponent } from './aria-live-region.component'; + +const meta: Meta<AriaLiveRegionComponent> = { + title: 'Aria live region', + component: AriaLiveRegionComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<AriaLiveRegionComponent>; + +export const Default: Story = { + args: { + text: '', + }, + render: (args) => ({ + props: args, + template: ` + <h2>This component is suitable for screen reader. Fill text field to make screen reader read it aloud.</h2> + <ods-aria-live-region ${argsToTemplate(args)} /> + `, + }), +}; diff --git a/alfa-client/libs/design-system/src/lib/attachment-wrapper/attachment-wrapper.stories.ts b/alfa-client/libs/design-system/src/lib/attachment-wrapper/attachment-wrapper.stories.ts index 485acda2600b59166e331fe38bffba7392c1e783..7d8f0fbec59aeb00afc99d2377c1902abd20c73d 100644 --- a/alfa-client/libs/design-system/src/lib/attachment-wrapper/attachment-wrapper.stories.ts +++ b/alfa-client/libs/design-system/src/lib/attachment-wrapper/attachment-wrapper.stories.ts @@ -1,7 +1,8 @@ -import { argsToTemplate, moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; +import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; import { DownloadButtonComponent } from '../../../../design-component/src/lib/download-button/download-button.component'; +import { AttachmentHeaderComponent } from '../attachment-header/attachment-header.component'; import { AttachmentComponent } from '../attachment/attachment.component'; import { AttachmentWrapperComponent } from './attachment-wrapper.component'; @@ -17,7 +18,12 @@ const meta: Meta<AttachmentWrapperComponent> = { }, decorators: [ moduleMetadata({ - imports: [AttachmentWrapperComponent, AttachmentComponent, DownloadButtonComponent], + imports: [ + AttachmentWrapperComponent, + AttachmentComponent, + DownloadButtonComponent, + AttachmentHeaderComponent, + ], }), ], excludeStories: /.*Data$/, @@ -28,18 +34,11 @@ export default meta; type Story = StoryObj<AttachmentWrapperComponent>; export const Default: Story = { - args: { - title: 'Anhänge', - }, - argTypes: { - title: { - description: 'Title for group of files', - }, - }, - render: (args) => ({ - props: args, - template: `<ods-attachment-wrapper ${argsToTemplate(args)}> - <ods-download-button action-buttons /> + render: () => ({ + template: `<ods-attachment-wrapper> + <ods-attachment-header title="Anhänge"> + <ods-download-button action-buttons /> + </ods-attachment-header> <ods-attachment caption="Attachment" description="200 kB" fileType="pdf"></ods-attachment> <ods-attachment caption="Second attachment" description="432 kB" fileType="doc"></ods-attachment> </ods-attachment-wrapper>`, diff --git a/alfa-client/libs/design-system/src/lib/bescheid-status-text/bescheid-status-text.component.ts b/alfa-client/libs/design-system/src/lib/bescheid-status-text/bescheid-status-text.component.ts index 9ed55dd799191cfb77c2462d25a357425f60c2ab..586fcb8a140c698f4e90a5d8b0b53f0e97556ac8 100644 --- a/alfa-client/libs/design-system/src/lib/bescheid-status-text/bescheid-status-text.component.ts +++ b/alfa-client/libs/design-system/src/lib/bescheid-status-text/bescheid-status-text.component.ts @@ -13,7 +13,7 @@ import { StampIconComponent } from '../icons/stamp-icon/stamp-icon.component'; ><ods-stamp-icon size="medium" class="fill-bewilligt" />Bewilligt am {{ dateText }}</span > <span class="flex items-center gap-2" *ngIf="!bewilligt" - ><ods-close-icon size="medium" class="fill-abgelehnt" />Abgelehnt am + ><ods-close-icon class="fill-abgelehnt" />Abgelehnt am {{ dateText }} </span> <span diff --git a/alfa-client/libs/design-system/src/lib/button/button.component.ts b/alfa-client/libs/design-system/src/lib/button/button.component.ts index 58ba7ec0d96c8c0eb0516c9fbc367b885d686837..277dbfbf830d140408f411526c2e84f33748e379 100644 --- a/alfa-client/libs/design-system/src/lib/button/button.component.ts +++ b/alfa-client/libs/design-system/src/lib/button/button.component.ts @@ -6,7 +6,10 @@ import { IconVariants } from '../icons/iconVariants'; import { SpinnerIconComponent } from '../icons/spinner-icon/spinner-icon.component'; export const buttonVariants = cva( - 'flex cursor-pointer items-center gap-4 rounded-md font-medium disabled:cursor-wait text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary', + [ + 'flex items-center gap-4 rounded-md disabled:cursor-wait text-sm font-medium box-border', + 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus', + ], { variants: { variant: { diff --git a/alfa-client/libs/design-system/src/lib/form/radio-button-card/radio-button-card.stories.ts b/alfa-client/libs/design-system/src/lib/form/radio-button-card/radio-button-card.stories.ts index 96ec7406916b5f4773f6aa448ad12112c913ebfc..dd949aeef16caab452a0ee6c8aee76bb0624cd79 100644 --- a/alfa-client/libs/design-system/src/lib/form/radio-button-card/radio-button-card.stories.ts +++ b/alfa-client/libs/design-system/src/lib/form/radio-button-card/radio-button-card.stories.ts @@ -53,7 +53,7 @@ export const Default: Story = { value="abgelehnt" variant="bescheid_abgelehnt" > - <ods-close-icon class="fill-abgelehnt" /> + <ods-close-icon class="fill-abgelehnt" size="large" /> </ods-radio-button-card> </div>`, }), diff --git a/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts b/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts index e136a50bb991af6df9384d7ceca287879b3cdcc8..a374f53e940446a07e40774135bdb40d7578461d 100644 --- a/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/text-input/text-input.component.ts @@ -1,12 +1,12 @@ -import { convertForDataTest, TechSharedModule } from '@alfa-client/tech-shared'; +import { convertForDataTest, EMPTY_STRING, TechSharedModule } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; -import { Component, ElementRef, Input, ViewChild } from '@angular/core'; +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { cva, VariantProps } from 'class-variance-authority'; import { ErrorMessageComponent } from '../error-message/error-message.component'; const textInputVariants = cva( - 'block w-full rounded-lg border bg-background-50 px-3 py-2 text-base leading-5 text-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', + 'w-full h-10 rounded-lg border bg-background-50 px-3 py-2 text-base leading-5 text-text focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2', { variants: { variant: { @@ -28,23 +28,40 @@ type TextInputVariants = VariantProps<typeof textInputVariants>; standalone: true, imports: [CommonModule, ErrorMessageComponent, ReactiveFormsModule, TechSharedModule], template: ` - <div> + <div class="relative"> <label [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> <div class="mt-2"> + <div + *ngIf="withPrefix" + class="pointer-events-none absolute bottom-2 left-2 flex size-6 items-center justify-center" + > + <ng-content select="[prefix]" /> + </div> <input type="text" [id]="id" [formControl]="fieldControl" - [ngClass]="textInputVariants({ variant })" + [ngClass]="[ + textInputVariants({ variant }), + withPrefix ? 'pl-10' : '', + withSuffix ? 'pr-10' : '', + ]" [placeholder]="placeholder" [autocomplete]="autocomplete" [attr.aria-required]="required" [attr.aria-invalid]="variant === 'error'" [attr.data-test-id]="(inputLabel | convertForDataTest) + '-text-input'" + (click)="clickEmitter.emit()" #inputElement /> + <div + *ngIf="withSuffix" + class="absolute bottom-2 right-2 flex size-6 items-center justify-center" + > + <ng-content select="[suffix]" /> + </div> </div> <ng-content select="[error]"></ng-content> </div> @@ -60,8 +77,10 @@ export class TextInputComponent { @Input() placeholder: string = ''; @Input() autocomplete: string = 'off'; @Input() variant: TextInputVariants['variant']; - @Input() fieldControl: FormControl; + @Input() fieldControl: FormControl = new FormControl(EMPTY_STRING); @Input() required: boolean = false; + @Input() withPrefix: boolean = false; + @Input() withSuffix: boolean = false; @Input() set focus(value: boolean) { if (value && this.inputElement) { @@ -69,6 +88,8 @@ export class TextInputComponent { } } + @Output() clickEmitter: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>(); + inputLabel: string; id: string; textInputVariants = textInputVariants; 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 90a4fa85f7dcb0b5a55f6a926a0d08ab63e332a9..da75590e40ed4afedee224ff82f89a741c9d5bef 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 @@ -1,4 +1,4 @@ -import { TechSharedModule, convertForDataTest } from '@alfa-client/tech-shared'; +import { EMPTY_STRING, TechSharedModule, convertForDataTest } from '@alfa-client/tech-shared'; import { CommonModule } from '@angular/common'; import { Component, ElementRef, Input, ViewChild } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; @@ -59,7 +59,7 @@ export class TextareaComponent { @Input() rows: number = 3; @Input() autocomplete: string = 'off'; @Input() variant: TextareaVariants['variant']; - @Input() fieldControl: FormControl; + @Input() fieldControl: FormControl = new FormControl(EMPTY_STRING); @Input() required: boolean = false; @Input() set focus(value: boolean) { diff --git a/alfa-client/libs/design-system/src/lib/icons/close-icon/close-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/close-icon/close-icon.component.ts index 5de688d73e98d67b1296d1519332fd7e3b7c9660..48fe0de3b897881d8bb669774572ea8f68a6895e 100644 --- a/alfa-client/libs/design-system/src/lib/icons/close-icon/close-icon.component.ts +++ b/alfa-client/libs/design-system/src/lib/icons/close-icon/close-icon.component.ts @@ -12,16 +12,16 @@ import { IconVariants, iconVariants } from '../iconVariants'; xmlns="http://www.w3.org/2000/svg" [ngClass]="[twMerge(iconVariants({ size }), 'fill-black', class)]" aria-hidden="true" - viewBox="0 0 14 14" + viewBox="0 0 24 24" fill="inherit" > <path - d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z" + d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z" /> </svg>`, }) export class CloseIconComponent { - @Input() size: IconVariants['size'] = 'small'; + @Input() size: IconVariants['size'] = 'medium'; @Input() class: string = undefined; iconVariants = iconVariants; diff --git a/alfa-client/libs/design-system/src/lib/icons/close-icon/close-icon.stories.ts b/alfa-client/libs/design-system/src/lib/icons/close-icon/close-icon.stories.ts index b8a8eda0b8afab576ba5b7086d226aa2f0adcc96..9cd750050413bd481bcdc246810c8eeb24b53c8c 100644 --- a/alfa-client/libs/design-system/src/lib/icons/close-icon/close-icon.stories.ts +++ b/alfa-client/libs/design-system/src/lib/icons/close-icon/close-icon.stories.ts @@ -20,7 +20,7 @@ export const Default: Story = { options: ['small', 'medium', 'large', 'extra-large', 'full'], description: 'Size of icon. Property "full" means 100%', table: { - defaultValue: { summary: 'small' }, + defaultValue: { summary: 'medium' }, }, }, }, diff --git a/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.spec.ts b/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1a2136a653c2b9dfc52bc039fb0aa1d48ce0733 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { CollaborationIconComponent } from './collaboration-icon.component'; + +describe('CollaborationIconComponent', () => { + let component: CollaborationIconComponent; + let fixture: ComponentFixture<CollaborationIconComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [CollaborationIconComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(CollaborationIconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..8565574eac373499fcedaea5b514e7f616b344e1 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.component.ts @@ -0,0 +1,53 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { twMerge } from 'tailwind-merge'; +import { IconVariants, iconVariants } from '../iconVariants'; + +@Component({ + selector: 'ods-collaboration-icon', + standalone: true, + imports: [CommonModule], + template: `<svg + viewBox="0 0 24 24" + [ngClass]="[twMerge(iconVariants({ size }), 'stroke-primary', class)]" + aria-hidden="true" + fill="inherit" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M16 21V19C16 17.9391 15.5786 16.9217 14.8284 16.1716C14.0783 15.4214 13.0609 15 12 15H6C4.93913 15 3.92172 15.4214 3.17157 16.1716C2.42143 16.9217 2 17.9391 2 19V21" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + fill="none" + /> + <path + d="M9 11C11.2091 11 13 9.20914 13 7C13 4.79086 11.2091 3 9 3C6.79086 3 5 4.79086 5 7C5 9.20914 6.79086 11 9 11Z" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + fill="none" + /> + <path + d="M22 20.9999V18.9999C21.9993 18.1136 21.7044 17.2527 21.1614 16.5522C20.6184 15.8517 19.8581 15.3515 19 15.1299" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + fill="none" + /> + <path + d="M16 3.12988C16.8604 3.35018 17.623 3.85058 18.1676 4.55219C18.7122 5.2538 19.0078 6.11671 19.0078 7.00488C19.0078 7.89305 18.7122 8.75596 18.1676 9.45757C17.623 10.1592 16.8604 10.6596 16 10.8799" + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + fill="none" + /> + </svg>`, +}) +export class CollaborationIconComponent { + @Input() size: IconVariants['size'] = 'medium'; + @Input() class: string = undefined; + + iconVariants = iconVariants; + twMerge = twMerge; +} diff --git a/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.stories.ts b/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..dbc1440939f920bb66062062c12ac343a347ddce --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/collaboration-icon/collaboration-icon.stories.ts @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/angular'; + +import { CollaborationIconComponent } from './collaboration-icon.component'; + +const meta: Meta<CollaborationIconComponent> = { + title: 'Icons/Collaboration icon', + component: CollaborationIconComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<CollaborationIconComponent>; + +export const Default: Story = { + args: { size: 'medium' }, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium', 'large', 'extra-large', 'full'], + description: 'Size of icon. Property "full" means 100%', + table: { + defaultValue: { summary: 'medium' }, + }, + }, + }, +}; diff --git a/alfa-client/libs/design-system/src/lib/icons/search-icon/search-icon.component.spec.ts b/alfa-client/libs/design-system/src/lib/icons/search-icon/search-icon.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..5981e81db28b29d1dfe651b306e3f3f938ec0bfb --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/search-icon/search-icon.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SearchIconComponent } from './search-icon.component'; + +describe('SearchIconComponent', () => { + let component: SearchIconComponent; + let fixture: ComponentFixture<SearchIconComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SearchIconComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchIconComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/icons/search-icon/search-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/search-icon/search-icon.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..641713c3842c42cadb4d4075051fc51141d5f2e3 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/search-icon/search-icon.component.ts @@ -0,0 +1,28 @@ +import { NgClass } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { twMerge } from 'tailwind-merge'; + +import { IconVariants, iconVariants } from '../iconVariants'; + +@Component({ + selector: 'ods-search-icon', + standalone: true, + imports: [NgClass], + template: `<svg + [ngClass]="twMerge(iconVariants({ size }), 'fill-primary', class)" + viewBox="0 0 24 24" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M15.5 14H14.71L14.43 13.73C15.41 12.59 16 11.11 16 9.5C16 5.91 13.09 3 9.5 3C5.91 3 3 5.91 3 9.5C3 13.09 5.91 16 9.5 16C11.11 16 12.59 15.41 13.73 14.43L14 14.71V15.5L19 20.49L20.49 19L15.5 14ZM9.5 14C7.01 14 5 11.99 5 9.5C5 7.01 7.01 5 9.5 5C11.99 5 14 7.01 14 9.5C14 11.99 11.99 14 9.5 14Z" + /> + </svg>`, +}) +export class SearchIconComponent { + @Input() size: IconVariants['size'] = 'medium'; + @Input() class: string = ''; + + iconVariants = iconVariants; + twMerge = twMerge; +} diff --git a/alfa-client/libs/design-system/src/lib/icons/search-icon/search-icon.stories.ts b/alfa-client/libs/design-system/src/lib/icons/search-icon/search-icon.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..1952260187ba8fc7f462803ad2e2674f6d071856 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/icons/search-icon/search-icon.stories.ts @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/angular'; + +import { SearchIconComponent } from './search-icon.component'; + +const meta: Meta<SearchIconComponent> = { + title: 'Icons/Search icon', + component: SearchIconComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<SearchIconComponent>; + +export const Default: Story = { + args: { size: 'large' }, + argTypes: { + size: { + control: 'select', + options: ['small', 'medium', 'large', 'extra-large', 'full'], + description: 'Size of icon. Property "full" means 100%', + table: { + defaultValue: { summary: 'medium' }, + }, + }, + }, +}; diff --git a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.spec.ts b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f900a81ba73a568c8be41ba225930acfc7d687a0 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.spec.ts @@ -0,0 +1,609 @@ +import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; +import { EventEmitter } from '@angular/core'; +import { + ComponentFixture, + TestBed, + discardPeriodicTasks, + fakeAsync, + tick, +} from '@angular/core/testing'; +import { Subscription } from 'rxjs'; +import { InstantSearchComponent } from './instant-search.component'; +import { InstantSearchQuery, InstantSearchResult } from './instant-search.model'; + +describe('InstantSearchComponent', () => { + let component: InstantSearchComponent; + let fixture: ComponentFixture<InstantSearchComponent>; + + const searchResults: InstantSearchResult<unknown>[] = [ + { title: 'test', description: 'test' }, + { title: 'caption', description: 'desc' }, + ]; + const searchBy: string = 'query'; + + let searchQueryChanged: Mock<EventEmitter<any>>; + let searchResultSelected: Mock<EventEmitter<any>>; + + beforeEach(async () => { + searchQueryChanged = <any>mock(EventEmitter); + searchResultSelected = <any>mock(EventEmitter); + + await TestBed.configureTestingModule({ + imports: [InstantSearchComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(InstantSearchComponent); + component = fixture.componentInstance; + component.searchQueryChanged = useFromMock(searchQueryChanged); + component.searchResultSelected = useFromMock(searchResultSelected); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('ngOnInit', () => { + it('should handle value changes', () => { + component.handleValueChanges = jest.fn(); + + component.ngOnInit(); + + expect(component.handleValueChanges).toHaveBeenCalled(); + }); + }); + + describe('handleValueChanges', () => { + beforeEach(() => { + component.showResults = jest.fn(); + }); + + it('should subscribe to value changes', () => { + component.control.valueChanges.subscribe = jest.fn(); + + component.handleValueChanges(); + + expect(component.control.valueChanges.subscribe).toHaveBeenCalled(); + }); + + it('should emit query', fakeAsync(() => { + component.handleValueChanges(); + + component.control.setValue(searchBy); + + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS); + component.control.valueChanges.subscribe(); + + expect(searchQueryChanged.emit).toHaveBeenCalledWith({ searchBy } as InstantSearchQuery); + })); + + it('should not emit query', fakeAsync(() => { + component.handleValueChanges(); + + const searchBy: string = 'q'; + component.control.setValue(searchBy); + + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS); + component.control.valueChanges.subscribe(); + + expect(searchQueryChanged.emit).not.toHaveBeenCalled(); + })); + + describe('result are already visible', () => { + beforeEach(() => { + component.areResultsVisible = true; + }); + + it('should not show results', fakeAsync(() => { + component.handleValueChanges(); + + component.control.setValue(searchBy); + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS); + + component.control.valueChanges.subscribe(); + expect(component.showResults).not.toHaveBeenCalled(); + + discardPeriodicTasks(); + })); + }); + + describe('results are not visible', () => { + beforeEach(() => { + component.areResultsVisible = false; + }); + + it('should show results', fakeAsync(() => { + component.handleValueChanges(); + + component.control.setValue(searchBy); + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS); + + component.control.valueChanges.subscribe(); + expect(component.showResults).toHaveBeenCalled(); + + discardPeriodicTasks(); + })); + + it('should not show results if debounce time not reached', fakeAsync(() => { + component.handleValueChanges(); + + component.control.setValue(searchBy); + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS - 1); + + component.control.valueChanges.subscribe(); + expect(component.showResults).not.toHaveBeenCalled(); + + discardPeriodicTasks(); + })); + + it('should not show results if not enough characters entered', fakeAsync(() => { + component.handleValueChanges(); + + component.control.setValue('q'); + tick(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS); + + component.control.valueChanges.subscribe(); + expect(component.showResults).not.toHaveBeenCalled(); + + discardPeriodicTasks(); + })); + }); + }); + + describe('ngOnDestroy', () => { + it('should subscribe to value changes', () => { + component.formControlSubscription = new Subscription(); + component.formControlSubscription.unsubscribe = jest.fn(); + + component.ngOnDestroy(); + + expect(component.formControlSubscription.unsubscribe).toHaveBeenCalled(); + }); + }); + + describe('set searchResults', () => { + describe('on different results', () => { + it('should call setSearchResults', () => { + component.setSearchResults = jest.fn(); + + component.searchResults = searchResults; + + expect(component.setSearchResults).toHaveBeenCalled(); + }); + }); + + describe('on same results', () => { + it('should not call setSearchResults', () => { + component.setSearchResults = jest.fn(); + + component.searchResults = []; + + expect(component.setSearchResults).not.toHaveBeenCalled(); + }); + }); + + describe('on null or undefined', () => { + it.each([null, undefined])( + 'should not call setSearchResults for %s', + (searchResults: InstantSearchResult<unknown>[]) => { + component.setSearchResults = jest.fn(); + + component.searchResults = searchResults; + + expect(component.setSearchResults).not.toHaveBeenCalled(); + }, + ); + }); + }); + + describe('setSearchResults', () => { + it('should set results', () => { + component.setSearchResults(searchResults); + + expect(component.results).toEqual(searchResults); + }); + + it('should call buildAriaLiveText with search results length', () => { + component.buildAriaLiveText = jest.fn(); + + component.setSearchResults(searchResults); + + expect(component.buildAriaLiveText).toHaveBeenCalledWith(searchResults.length); + }); + }); + + describe('setFocusOnResultItem', () => { + beforeEach(() => { + component.resultsRef.get = jest.fn().mockReturnValue({ setFocus: jest.fn() }); + }); + + it('should call get for resultsRef with index', () => { + component.setFocusOnResultItem(1); + + expect(component.resultsRef.get).toHaveBeenCalledWith(1); + }); + + it('should call setFocus', () => { + component.setFocusOnResultItem(1); + + expect(component.resultsRef.get(1).setFocus).toHaveBeenCalled(); + }); + }); + + describe('handleArrowNavigation', () => { + const event: KeyboardEvent = new KeyboardEvent('arrow'); + + beforeEach(() => { + component.getResultIndexForKey = jest.fn(); + component.setFocusOnResultItem = jest.fn(); + }); + + it('should call prevent default', () => { + event.preventDefault = jest.fn(); + + component.handleArrowNavigation(event); + + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should call getResultIndexForKey', () => { + component.handleArrowNavigation(event); + + expect(component.getResultIndexForKey).toHaveBeenCalledWith(event.key); + }); + + it('should call setFocusOnResultItem', () => { + component.getResultIndexForKey = jest.fn().mockReturnValue(0); + + component.handleArrowNavigation(event); + + expect(component.setFocusOnResultItem).toHaveBeenCalledWith(0); + }); + }); + + describe('handleEscape', () => { + const event: KeyboardEvent = new KeyboardEvent('esc'); + + it('should call prevent default', () => { + event.preventDefault = jest.fn(); + + component.handleEscape(event); + + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should call hideResults', () => { + component.hideResults = jest.fn(); + + component.handleEscape(event); + + expect(component.hideResults).toHaveBeenCalled(); + }); + }); + + describe('getNextResultIndex', () => { + it('should return 0 if index is undefined', () => { + const result: number = component.getNextResultIndex(undefined, 2); + + expect(result).toBe(0); + }); + + it('should return 0 if current index is last', () => { + const result: number = component.getNextResultIndex(1, 2); + + expect(result).toBe(0); + }); + + it('should return next search result index', () => { + const result: number = component.getNextResultIndex(0, 2); + + expect(result).toBe(1); + }); + }); + + describe('getPreviousResultIndex', () => { + it('should return last index if current index is undefined', () => { + const result: number = component.getPreviousResultIndex(undefined, 2); + + expect(result).toBe(1); + }); + + it('should return last index if current index is first', () => { + const result: number = component.getPreviousResultIndex(0, 2); + + expect(result).toBe(1); + }); + + it('should return previous search result index', () => { + const result: number = component.getPreviousResultIndex(1, 2); + + expect(result).toBe(0); + }); + }); + + describe('getResultIndexForKey', () => { + it('should call getNextResultIndex if ArrowDown', () => { + component.getNextResultIndex = jest.fn(); + + component.getResultIndexForKey('ArrowDown'); + + expect(component.getNextResultIndex).toHaveBeenCalled(); + }); + + it('should call getPreviousResultIndex if ArrowUp', () => { + component.getPreviousResultIndex = jest.fn(); + + component.getResultIndexForKey('ArrowUp'); + + expect(component.getPreviousResultIndex).toHaveBeenCalled(); + }); + }); + + describe('getLastItemIndex', () => { + it('should return 0', () => { + const result: number = component.getLastItemIndex(0); + + expect(result).toBe(0); + }); + + it('should return decrement of array length', () => { + const result: number = component.getLastItemIndex(5); + + expect(result).toBe(4); + }); + }); + + describe('buildAriaLiveText', () => { + beforeEach(() => { + component.control.setValue('test'); + }); + + it('should return text for one result', () => { + const result: string = component.buildAriaLiveText(1); + + expect(result).toBe( + 'Ein Suchergebnis für Eingabe test. Nutze Pfeiltaste nach unten, um das zu erreichen.', + ); + }); + + it('should return text for many results', () => { + const result: string = component.buildAriaLiveText(4); + + expect(result).toBe( + '4 Suchergebnisse für Eingabe test. Nutze Pfeiltaste nach unten, um diese zu erreichen.', + ); + }); + + it('should return text for no results', () => { + const result: string = component.buildAriaLiveText(0); + + expect(result).toBe('Keine Ergebnisse'); + }); + }); + + describe('showResults', () => { + it('should set isShowResults to true', () => { + component.showResults(); + + expect(component.areResultsVisible).toBe(true); + }); + }); + + describe('hideResults', () => { + it('should set isShowResults to false', () => { + component.hideResults(); + + expect(component.areResultsVisible).toBe(false); + }); + }); + + describe('onKeydownHandler', () => { + const keyboardEvent: KeyboardEvent = new KeyboardEvent('a'); + + beforeEach(() => { + component.isSearchResultsEmpty = jest.fn(); + component.isArrowNavigationKey = jest.fn(); + component.isEscapeKey = jest.fn(); + component.handleArrowNavigation = jest.fn(); + component.handleEscape = jest.fn(); + }); + + it('should check for empty result', () => { + component.onKeydownHandler(keyboardEvent); + + expect(component.isSearchResultsEmpty).toHaveBeenCalled(); + }); + + describe('search result is empty', () => { + beforeEach(() => { + component.isSearchResultsEmpty = jest.fn().mockReturnValue(true); + }); + + it('should ignore key navigation', () => { + component.onKeydownHandler(keyboardEvent); + + expect(component.isArrowNavigationKey).not.toHaveBeenCalled(); + }); + + it('should ignore escape key handling', () => { + component.onKeydownHandler(keyboardEvent); + + expect(component.isEscapeKey).not.toHaveBeenCalled(); + }); + }); + + describe('search result is not empty', () => { + beforeEach(() => { + component.isSearchResultsEmpty = jest.fn().mockReturnValue(false); + }); + + it('should check if arrow navigation', () => { + component.onKeydownHandler(keyboardEvent); + + expect(component.isArrowNavigationKey).toHaveBeenCalled(); + }); + + it('should handle arrow navigation', () => { + component.isArrowNavigationKey = jest.fn().mockReturnValue(true); + + component.onKeydownHandler(keyboardEvent); + + expect(component.handleArrowNavigation).toHaveBeenCalled(); + }); + + it('should not handle arrow navigation', () => { + component.isArrowNavigationKey = jest.fn().mockReturnValue(false); + + component.onKeydownHandler(keyboardEvent); + + expect(component.handleArrowNavigation).not.toHaveBeenCalled(); + }); + + describe('is not arrow navigation', () => { + beforeEach(() => { + component.isArrowNavigationKey = jest.fn().mockReturnValue(false); + }); + + it('should check for escape key', () => { + component.onKeydownHandler(keyboardEvent); + + expect(component.isEscapeKey).toHaveBeenCalled(); + }); + + it('should handle escape key', () => { + component.isEscapeKey = jest.fn().mockReturnValue(true); + + component.onKeydownHandler(keyboardEvent); + + expect(component.handleEscape).toHaveBeenCalled(); + }); + + it('should not handle escape key', () => { + component.isEscapeKey = jest.fn().mockReturnValue(false); + + component.onKeydownHandler(keyboardEvent); + + expect(component.handleEscape).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe('isSearchResultsEmpty', () => { + it('should return true', () => { + component.results = []; + + const result: boolean = component.isSearchResultsEmpty(); + + expect(result).toBe(true); + }); + + it('should return false', () => { + component.results = searchResults; + + const result: boolean = component.isSearchResultsEmpty(); + + expect(result).toBe(false); + }); + }); + + describe('isArrowNavigationKey', () => { + it.each(['ArrowUp', 'ArrowDown'])('should return true for key %s', (key: string) => { + const keyboardEvent: KeyboardEvent = { ...new KeyboardEvent('key'), key }; + + const result: boolean = component.isArrowNavigationKey(keyboardEvent); + + expect(result).toBeTruthy(); + }); + + it('should return false', () => { + const result: boolean = component.isArrowNavigationKey(new KeyboardEvent('not arrow')); + + expect(result).toBe(false); + }); + }); + + describe('isEscapeKey', () => { + it('should return true', () => { + const escapeKeyEvent = { ...new KeyboardEvent('esc'), key: 'Escape' }; + + const result: boolean = component.isEscapeKey(escapeKeyEvent); + + expect(result).toBe(true); + }); + + it('should return false', () => { + const result: boolean = component.isEscapeKey(new KeyboardEvent('not escape')); + + expect(result).toBe(false); + }); + }); + + describe('isLastItemOrOutOfArray', () => { + it.each([3, 5])('should return true for %s', (index: number) => { + const result: boolean = component.isLastItemOrOutOfArray(index, 4); + + expect(result).toBe(true); + }); + + it('should return false', () => { + const result: boolean = component.isLastItemOrOutOfArray(1, 3); + + expect(result).toBe(false); + }); + }); + + describe('isFirstItemOrOutOfArray', () => { + it.each([0, -1])('should return true for %s', (index: number) => { + const result: boolean = component.isFirstItemOrOutOfArray(index); + + expect(result).toBe(true); + }); + + it('should return false', () => { + const result: boolean = component.isFirstItemOrOutOfArray(1); + + expect(result).toBe(false); + }); + }); + + describe('onItemClicked', () => { + beforeEach(() => { + component.hideResults = jest.fn(); + }); + + it('should emit searchResultSelected', () => { + component.onItemClicked(searchResults[0], 0); + + expect(searchResultSelected.emit).toHaveBeenCalledWith(searchResults[0]); + }); + + it('should hide results', () => { + component.onItemClicked(searchResults[0], 0); + + expect(component.hideResults).toHaveBeenCalled(); + }); + }); + + describe('onClickHandler', () => { + const e: MouseEvent = { ...new MouseEvent('test') }; + + beforeEach(() => { + component.hideResults = jest.fn(); + }); + + it('should call hideResults if instant search does not contain event target', () => { + component.onClickHandler(e); + + expect(component.hideResults).toHaveBeenCalled(); + }); + + it('should not call hideResults if instant search contains event target', () => { + component.ref.nativeElement.contains = jest.fn().mockReturnValue(true); + + component.onClickHandler(e); + + expect(component.hideResults).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.ts b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..c16d9fab5189ca71bb978839e0b602d114d35882 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.component.ts @@ -0,0 +1,224 @@ +import { EMPTY_STRING, isNotNil } from '@alfa-client/tech-shared'; +import { CommonModule } from '@angular/common'; +import { + Component, + ElementRef, + EventEmitter, + HostListener, + Input, + OnDestroy, + OnInit, + Output, + QueryList, + ViewChildren, +} from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { isEqual, isUndefined } from 'lodash-es'; +import { Subscription, debounceTime, distinctUntilChanged, filter } from 'rxjs'; +import { AriaLiveRegionComponent } from '../../aria-live-region/aria-live-region.component'; +import { SearchFieldComponent } from '../search-field/search-field.component'; +import { SearchResultHeaderComponent } from '../search-result-header/search-result-header.component'; +import { SearchResultItemComponent } from '../search-result-item/search-result-item.component'; +import { SearchResultLayerComponent } from '../search-result-layer/search-result-layer.component'; +import { InstantSearchQuery, InstantSearchResult } from './instant-search.model'; + +@Component({ + selector: 'ods-instant-search', + standalone: true, + imports: [ + CommonModule, + SearchFieldComponent, + SearchResultHeaderComponent, + SearchResultItemComponent, + SearchResultLayerComponent, + AriaLiveRegionComponent, + ], + template: ` <div class="relative"> + <ods-search-field + [placeholder]="placeholder" + [label]="label" + [attr.aria-expanded]="results.length" + [control]="control" + aria-controls="results" + (inputClicked)="showResults()" + #searchField + /> + <ods-aria-live-region [text]="ariaLiveText" /> + <ods-search-result-layer + *ngIf="results.length && areResultsVisible" + class="absolute z-50 mt-3 w-full" + id="results" + > + <ods-search-result-header [text]="headerText" [count]="results.length" header /> + <ods-search-result-item + *ngFor="let result of results; let i = index" + [title]="result.title" + [description]="result.description" + (itemClicked)="onItemClicked(result, i)" + #results + ></ods-search-result-item> + </ods-search-result-layer> + </div>`, +}) +export class InstantSearchComponent implements OnInit, OnDestroy { + static readonly DEBOUNCE_TIME_IN_MILLIS: number = 300; + + @Input() label: string = EMPTY_STRING; + @Input() placeholder: string = EMPTY_STRING; + @Input() headerText: string = EMPTY_STRING; + @Input() control: FormControl<string> = new FormControl(EMPTY_STRING); + + @Input() set searchResults(searchResults: InstantSearchResult<unknown>[]) { + if (!isEqual(searchResults, this.results) && isNotNil(searchResults)) { + this.setSearchResults(searchResults); + } + } + + @Output() searchResultSelected: EventEmitter<InstantSearchResult<unknown>> = new EventEmitter< + InstantSearchResult<unknown> + >(); + @Output() searchQueryChanged: EventEmitter<InstantSearchQuery> = + new EventEmitter<InstantSearchQuery>(); + + readonly FIRST_ITEM_INDEX: number = 0; + readonly PREVIEW_SEARCH_STRING_MIN_LENGTH: number = 2; + results: InstantSearchResult<unknown>[] = []; + ariaLiveText: string = ''; + areResultsVisible: boolean = true; + private focusedResult: number | undefined = undefined; + formControlSubscription: Subscription; + + constructor(public ref: ElementRef) {} + + @ViewChildren('results') resultsRef: QueryList<SearchResultItemComponent>; + + ngOnInit(): void { + this.handleValueChanges(); + } + + handleValueChanges() { + this.formControlSubscription = this.control.valueChanges + .pipe( + debounceTime(InstantSearchComponent.DEBOUNCE_TIME_IN_MILLIS), + filter((value: string) => value.length >= this.PREVIEW_SEARCH_STRING_MIN_LENGTH), + distinctUntilChanged(), + ) + .subscribe((searchBy: string) => { + this.searchQueryChanged.emit({ searchBy }); + if (!this.areResultsVisible) { + this.showResults(); + } + }); + } + + ngOnDestroy(): void { + if (isNotNil(this.formControlSubscription)) this.formControlSubscription.unsubscribe(); + } + + @HostListener('document:keydown', ['$event']) + onKeydownHandler(e: KeyboardEvent): void { + if (this.isSearchResultsEmpty()) return; + if (this.isArrowNavigationKey(e)) this.handleArrowNavigation(e); + if (this.isEscapeKey(e)) this.handleEscape(e); + } + + @HostListener('document:click', ['$event']) + onClickHandler(e: MouseEvent): void { + if (!this.ref.nativeElement.contains(e.target)) { + this.hideResults(); + } + } + + handleArrowNavigation(e: KeyboardEvent): void { + e.preventDefault(); + const newIndex = this.getResultIndexForKey(e.key); + this.focusedResult = newIndex; + this.setFocusOnResultItem(newIndex); + } + + handleEscape(e: KeyboardEvent): void { + e.preventDefault(); + this.hideResults(); + } + + setFocusOnResultItem(index: number): void { + this.resultsRef.get(index).setFocus(); + } + + setSearchResults(searchResults: InstantSearchResult<unknown>[]): void { + this.results = searchResults; + this.ariaLiveText = this.buildAriaLiveText(searchResults.length); + } + + getNextResultIndex(index: number | undefined, resultLength: number): number { + if (isUndefined(index)) return this.FIRST_ITEM_INDEX; + if (this.isLastItemOrOutOfArray(index, resultLength)) return this.FIRST_ITEM_INDEX; + return index + 1; + } + + getPreviousResultIndex(index: number | undefined, resultLength: number): number { + if (isUndefined(index)) return this.getLastItemIndex(resultLength); + if (this.isFirstItemOrOutOfArray(index)) return this.getLastItemIndex(resultLength); + return index - 1; + } + + getLastItemIndex(arrayLength: number): number { + if (arrayLength < 1) return this.FIRST_ITEM_INDEX; + return arrayLength - 1; + } + + getResultIndexForKey(key: string): number { + switch (key) { + case 'ArrowDown': + return this.getNextResultIndex(this.focusedResult, this.results.length); + case 'ArrowUp': + return this.getPreviousResultIndex(this.focusedResult, this.results.length); + default: + console.error('Key %s not allowed', key); + } + } + + buildAriaLiveText(resultsLength: number): string { + if (resultsLength === 1) + return `Ein Suchergebnis für Eingabe ${this.control.value}. Nutze Pfeiltaste nach unten, um das zu erreichen.`; + if (resultsLength > 1) + return `${resultsLength} Suchergebnisse für Eingabe ${this.control.value}. Nutze Pfeiltaste nach unten, um diese zu erreichen.`; + return 'Keine Ergebnisse'; + } + + showResults(): void { + this.areResultsVisible = true; + this.focusedResult = undefined; + } + + hideResults(): void { + this.areResultsVisible = false; + this.focusedResult = undefined; + } + + isLastItemOrOutOfArray(index: number, arrayLength: number): boolean { + return index >= arrayLength - 1; + } + + isFirstItemOrOutOfArray(index: number): boolean { + return index <= this.FIRST_ITEM_INDEX; + } + + isSearchResultsEmpty(): boolean { + return this.results.length === 0; + } + + isArrowNavigationKey(e: KeyboardEvent): boolean { + return e.key === 'ArrowDown' || e.key === 'ArrowUp'; + } + + isEscapeKey(e: KeyboardEvent): boolean { + return e.key === 'Escape'; + } + + onItemClicked(searchResult: InstantSearchResult<unknown>, index: number) { + this.searchResultSelected.emit(searchResult); + this.focusedResult = index; + this.hideResults(); + } +} diff --git a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.model.ts b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..5debae8ceb40be63d765fbb9a401cb47d601e5c5 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.model.ts @@ -0,0 +1,9 @@ +export interface InstantSearchResult<T> { + title: string; + description: string; + data?: T; +} + +export interface InstantSearchQuery { + searchBy: string; +} diff --git a/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.stories.ts b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..bfe67d23232b417e6cddd3b33f53245a793b161e --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/instant-search/instant-search.stories.ts @@ -0,0 +1,40 @@ +import { type Meta, type StoryObj } from '@storybook/angular'; +import { InstantSearchComponent } from './instant-search.component'; + +const meta: Meta<InstantSearchComponent> = { + title: 'Instant search/Instant search', + component: InstantSearchComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<InstantSearchComponent>; + +export const Default: Story = { + args: { + label: '', + placeholder: 'zuständige Stelle suchen', + headerText: 'In der OZG-Cloud', + }, +}; + +export const SearchResults: Story = { + args: { + label: '', + placeholder: 'zuständige Stelle suchen', + headerText: 'In der OZG-Cloud', + searchResults: [ + { + text: 'Landeshauptstadt Kiel - Ordnungsamt, Gewerbe- und Schornsteinfegeraufsicht', + subText: 'Fabrikstraße 8-10, 24103 Kiel', + onClick: () => undefined, + }, + { + text: 'Amt für Digitalisierung, Breitband und Vermessung Nürnberg Außenstelle Hersbruck', + subText: 'Rathausmarkt 7, Hersbruck', + onClick: () => undefined, + }, + ], + }, +}; diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-field/search-field.component.spec.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-field/search-field.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f1f730f80e1b6cf82b90568b16df0ffd8b3a76bd --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/search-field/search-field.component.spec.ts @@ -0,0 +1,47 @@ +import { EMPTY_STRING } from '@alfa-client/tech-shared'; +import { getElementFromFixtureByType, mock } from '@alfa-client/test-utils'; +import { EventEmitter } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl } from '@angular/forms'; +import { TextInputComponent } from '../../form/text-input/text-input.component'; +import { SearchFieldComponent } from './search-field.component'; + +describe('SearchFieldComponent', () => { + let component: SearchFieldComponent; + let fixture: ComponentFixture<SearchFieldComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SearchFieldComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchFieldComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('inputClicked', () => { + it('should emit event', () => { + component.inputClicked = <any>mock(EventEmitter); + const input = getElementFromFixtureByType(fixture, TextInputComponent); + + input.inputElement.nativeElement.click(); + + expect(component.inputClicked.emit).toHaveBeenCalled(); + }); + }); + + describe('clearValue', () => { + it('should set empty value', () => { + component.control = new FormControl('test'); + + component.clearInput(); + + expect(component.control.value).toBe(EMPTY_STRING); + }); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-field/search-field.component.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-field/search-field.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f2e01bd046af2d15ce345fed69c9a9f43122cc6 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/search-field/search-field.component.ts @@ -0,0 +1,38 @@ +import { CommonModule } from '@angular/common'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { EMPTY_STRING } from '../../../../../tech-shared/src'; +import { TextInputComponent } from '../../form/text-input/text-input.component'; +import { CloseIconComponent } from '../../icons/close-icon/close-icon.component'; +import { SearchIconComponent } from '../../icons/search-icon/search-icon.component'; + +@Component({ + selector: 'ods-search-field', + standalone: true, + imports: [CommonModule, TextInputComponent, SearchIconComponent, CloseIconComponent], + template: `<ods-text-input + [label]="label" + [fieldControl]="control" + [placeholder]="placeholder" + [withPrefix]="true" + [withSuffix]="true" + (clickEmitter)="inputClicked.emit()" + role="combobox" + > + <ods-search-icon prefix aria-hidden="true" aria-label="Suchfeld" /> + <button suffix *ngIf="control.value" (click)="clearInput()" aria-label="Eingabe löschen"> + <ods-close-icon class="fill-primary hover:fill-primary-hover" /> + </button> + </ods-text-input>`, +}) +export class SearchFieldComponent { + @Input() label: string = EMPTY_STRING; + @Input() placeholder: string = EMPTY_STRING; + @Input() control = new FormControl(EMPTY_STRING); + + @Output() inputClicked: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>(); + + clearInput() { + this.control.setValue(EMPTY_STRING); + } +} diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-field/search-field.stories.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-field/search-field.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..fec43fa8f65cd51c47e4639fd002a34e33c8b119 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/search-field/search-field.stories.ts @@ -0,0 +1,19 @@ +import { type Meta, type StoryObj } from '@storybook/angular'; +import { SearchFieldComponent } from './search-field.component'; + +const meta: Meta<SearchFieldComponent> = { + title: 'Instant search/Search field', + component: SearchFieldComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj<SearchFieldComponent>; + +export const Default: Story = { + args: { + label: '', + placeholder: 'search something...', + }, +}; diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-result-header/search-result-header.component.spec.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-result-header/search-result-header.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..7c00b9a408879b4ce0c96c2a374edce5df6739c2 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/search-result-header/search-result-header.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SearchResultHeaderComponent } from './search-result-header.component'; + +describe('SearchResultHeaderComponent', () => { + let component: SearchResultHeaderComponent; + let fixture: ComponentFixture<SearchResultHeaderComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SearchResultHeaderComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchResultHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-result-header/search-result-header.component.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-result-header/search-result-header.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..4ba8d7894dbddd157532a1418f175e28fa25c3b0 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/search-result-header/search-result-header.component.ts @@ -0,0 +1,17 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'ods-search-result-header', + standalone: true, + imports: [CommonModule], + template: ` + <h3 class="mx-6 my-3 w-fit border-b-2 border-primary py-1 text-sm font-semibold text-text"> + {{ text }} ({{ count }}) + </h3> + `, +}) +export class SearchResultHeaderComponent { + @Input({ required: true }) text!: string; + @Input() count: number = 0; +} diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.spec.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..089692b213692a024ccf7d6dc07071bd9c25d812 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.spec.ts @@ -0,0 +1,46 @@ +import { getElementFromFixture, mock } from '@alfa-client/test-utils'; +import { EventEmitter } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { SearchResultItemComponent } from './search-result-item.component'; + +describe('SearchResultItemComponent', () => { + let component: SearchResultItemComponent; + let fixture: ComponentFixture<SearchResultItemComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SearchResultItemComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchResultItemComponent); + component = fixture.componentInstance; + component.title = 'Test'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('itemClicked', () => { + it('should emit event', () => { + component.itemClicked = <any>mock(EventEmitter); + const button = getElementFromFixture(fixture, getDataTestIdOf('item-button')); + + button.click(); + + expect(component.itemClicked.emit).toHaveBeenCalled(); + }); + }); + + describe('setFocus', () => { + it('should focus native element', () => { + component.buttonRef.nativeElement.focus = jest.fn(); + + component.setFocus(); + + expect(component.buttonRef.nativeElement.focus).toHaveBeenCalled(); + }); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..e02da6ddc31ea1fda0572f40be876b6a1969012b --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/search-result-item/search-result-item.component.ts @@ -0,0 +1,38 @@ +import { CommonModule } from '@angular/common'; +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; + +@Component({ + selector: 'ods-search-result-item', + standalone: true, + imports: [CommonModule], + template: `<button + *ngIf="title" + [ngClass]="[ + 'flex w-full justify-between border-2 border-transparent px-6 py-3', + 'hover:border-focus focus:border-focus focus:outline-none', + ]" + role="listitem" + tabindex="-1" + (click)="itemClicked.emit()" + data-test-id="item-button" + #button + > + <div class="flex flex-col items-start justify-between text-text"> + <p class="text-base font-medium">{{ title }}</p> + <p class="text-sm">{{ description }}</p> + </div> + <ng-content select="[action-button]" /> + </button>`, +}) +export class SearchResultItemComponent { + @Input({ required: true }) title!: string; + @Input() description: string = ''; + + @Output() public itemClicked: EventEmitter<MouseEvent> = new EventEmitter<MouseEvent>(); + + @ViewChild('button') buttonRef: ElementRef; + + public setFocus() { + this.buttonRef.nativeElement.focus(); + } +} diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-result-layer/search-result-layer.component.spec.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-result-layer/search-result-layer.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..d2c91333316ff8253ec828a228ce83b84514113c --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/search-result-layer/search-result-layer.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { SearchResultLayerComponent } from './search-result-layer.component'; + +describe('SearchResultLayerComponent', () => { + let component: SearchResultLayerComponent; + let fixture: ComponentFixture<SearchResultLayerComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [SearchResultLayerComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(SearchResultLayerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-result-layer/search-result-layer.component.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-result-layer/search-result-layer.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..971e06b585e2292b36be429b6972ba91090ccaa0 --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/search-result-layer/search-result-layer.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'ods-search-result-layer', + standalone: true, + imports: [CommonModule], + template: `<div class="rounded-lg border border-primary-600/50 bg-background-50 shadow-lg"> + <ng-content select="[header]" /> + <ul role="list"> + <ng-content /> + </ul> + </div>`, +}) +export class SearchResultLayerComponent {} diff --git a/alfa-client/libs/design-system/src/lib/instant-search/search-result-layer/search-result-layer.stories.ts b/alfa-client/libs/design-system/src/lib/instant-search/search-result-layer/search-result-layer.stories.ts new file mode 100644 index 0000000000000000000000000000000000000000..02aa94c67050ca8ec652784afb653f56d596454b --- /dev/null +++ b/alfa-client/libs/design-system/src/lib/instant-search/search-result-layer/search-result-layer.stories.ts @@ -0,0 +1,30 @@ +import { moduleMetadata, type Meta, type StoryObj } from '@storybook/angular'; +import { SearchResultHeaderComponent } from '../search-result-header/search-result-header.component'; +import { SearchResultItemComponent } from '../search-result-item/search-result-item.component'; +import { SearchResultLayerComponent } from './search-result-layer.component'; + +const meta: Meta<SearchResultLayerComponent> = { + title: 'Instant search/Search result layer', + component: SearchResultLayerComponent, + excludeStories: /.*Data$/, + tags: ['autodocs'], + decorators: [ + moduleMetadata({ + imports: [SearchResultItemComponent, SearchResultHeaderComponent], + }), + ], +}; + +export default meta; +type Story = StoryObj<SearchResultLayerComponent>; + +export const Default: Story = { + args: {}, + render: () => ({ + template: `<ods-search-result-layer> + <ods-search-result-header text="In der OZG-Cloud" [count]="2" header /> + <ods-search-result-item text="Amt Rantznau - Ordnungsamt" subText="Rathausmarkt 7, Kronshagen" /> + <ods-search-result-item text="Amt Burg-St. Michaelisdonn - Der Amtsvorsteher" subText="Holzmarkt 7, 25712 Rantznau" /> + </ods-search-result-layer>`, + }), +}; diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.html b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.html index eaa8140904dd29c50e275cf12f0a7ac6df021715..1566dc396538005f2ebe1e510fcdf04f1eaa13ac 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.html +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.html @@ -52,6 +52,20 @@ ></alfa-vorgang-detail-formular-buttons> </div> + <div + class="section one-column" + *ngIf="vorgangResource | hasLink: vorgangWithEingangLinkRel.CREATE_COLLABORATION_REQUEST" + > + <ozgcloud-expansion-panel + headline="Zusammenarbeit" + data-test-id="collaboration-expansion-panel" + > + <alfa-collaboration-in-vorgang-container + data-test-id="collaboration-in-voragng-container" + ></alfa-collaboration-in-vorgang-container> + </ozgcloud-expansion-panel> + </div> + <div class="two-column"> <div class="section" diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.spec.ts b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.spec.ts index 34870cb494be2e9160e6101076214483ad95ff0b..239dd66734f6d2a249fcad3d17b6f41c388c412a 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.spec.ts +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-area/vorgang-detail-area.component.spec.ts @@ -21,6 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { CollaborationInVorgangContainerComponent } from '@alfa-client/collaboration'; import { VorgangForwardingContainerComponent } from '@alfa-client/forwarding'; import { KommentarListInVorgangContainerComponent } from '@alfa-client/kommentar'; import { PostfachMailListContainerComponent } from '@alfa-client/postfach'; @@ -30,6 +31,7 @@ import { createEmptyStateResource, createStateResource, } from '@alfa-client/tech-shared'; +import { existsAsHtmlElement, notExistsAsHtmlElement } from '@alfa-client/test-utils'; import { ExpansionPanelComponent, OzgcloudStrokedButtonWithSpinnerComponent, @@ -57,6 +59,7 @@ describe('VorgangDetailAreaComponent', () => { let component: VorgangDetailAreaComponent; let fixture: ComponentFixture<VorgangDetailAreaComponent>; + const collaborationContainer: string = getDataTestIdOf('collaboration-in-voragng-container'); const wiedervorlagenContainer: string = getDataTestIdOf('wiedervorlagen-container-in-vorgang'); const kommentarContainer: string = getDataTestIdOf('kommentar-container-in-vorgang'); const postfachNachrichtenContainer: string = getDataTestIdOf( @@ -86,6 +89,7 @@ describe('VorgangDetailAreaComponent', () => { MockComponent(VorgangForwardingContainerComponent), MockComponent(BescheidListInVorgangContainerComponent), MockComponent(ExpansionPanelComponent), + MockComponent(CollaborationInVorgangContainerComponent), ], }).compileComponents(); }); @@ -101,6 +105,26 @@ describe('VorgangDetailAreaComponent', () => { expect(component).toBeTruthy(); }); + describe('Collaboration', () => { + it('should be visibile if link is present', () => { + component.vorgangStateResource = createStateResource( + createVorgangWithEingangResource([VorgangWithEingangLinkRel.CREATE_COLLABORATION_REQUEST]), + ); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, collaborationContainer); + }); + + it('should be hidden if link is missing', () => { + component.vorgangStateResource = createStateResource(vorgang); + + fixture.detectChanges(); + + notExistsAsHtmlElement(fixture, collaborationContainer); + }); + }); + describe('wiedervorlagen', () => { it('should be visible', () => { component.vorgangStateResource = createStateResource( diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-steps/vorgang-detail-bescheiden-steps-content/vorgang-detail-bescheiden-antrag-bescheiden/vorgang-detail-bescheiden-antrag-bescheiden.component.html b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-steps/vorgang-detail-bescheiden-steps-content/vorgang-detail-bescheiden-antrag-bescheiden/vorgang-detail-bescheiden-antrag-bescheiden.component.html index ad1704f9c19a82c2a3fe30cb866eb98730f4ff69..22c3e14619ff5c4636838a058b8e614684b64a83 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-steps/vorgang-detail-bescheiden-steps-content/vorgang-detail-bescheiden-antrag-bescheiden/vorgang-detail-bescheiden-antrag-bescheiden.component.html +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail-page/vorgang-detail-bescheiden/vorgang-detail-bescheiden-steps/vorgang-detail-bescheiden-steps-content/vorgang-detail-bescheiden-antrag-bescheiden/vorgang-detail-bescheiden-antrag-bescheiden.component.html @@ -14,7 +14,7 @@ value="false" data-test-id="button-abgelehnt" variant="bescheid_abgelehnt" - ><ods-close-icon size="medium" class="fill-abgelehnt"></ods-close-icon> + ><ods-close-icon size="large" class="fill-abgelehnt"></ods-close-icon> </ods-radio-button-card> </div> <div class="flex w-full"> diff --git a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail.module.ts b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail.module.ts index 5a6d84231e9b41aa3bc51655315edd6ff5e4e531..4ab436286109e6af58e8fa0d851edf0d074ee1f7 100644 --- a/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail.module.ts +++ b/alfa-client/libs/vorgang-detail/src/lib/vorgang-detail.module.ts @@ -23,6 +23,7 @@ */ import { BescheidModule } from '@alfa-client/bescheid'; import { BinaryFileModule } from '@alfa-client/binary-file'; +import { CollaborationModule } from '@alfa-client/collaboration'; import { ForwardingModule } from '@alfa-client/forwarding'; import { HistorieModule } from '@alfa-client/historie'; import { KommentarModule } from '@alfa-client/kommentar'; @@ -161,6 +162,7 @@ const routes: Routes = [ TextareaEditorComponent, BescheidStatusTextComponent, ErrorMessageComponent, + CollaborationModule, ], declarations: [ VorgangDetailPageComponent, diff --git a/alfa-client/libs/vorgang-shared/src/lib/vorgang.linkrel.ts b/alfa-client/libs/vorgang-shared/src/lib/vorgang.linkrel.ts index 8805fedd51dd0c1b6490f74e09df4346f6158900..21dc4295af4576f3d4e3439ada7935dcd45a5275 100644 --- a/alfa-client/libs/vorgang-shared/src/lib/vorgang.linkrel.ts +++ b/alfa-client/libs/vorgang-shared/src/lib/vorgang.linkrel.ts @@ -64,6 +64,8 @@ export enum VorgangWithEingangLinkRel { BESCHEIDE = 'bescheide', UEBERSPRINGEN_UND_ABSCHLIESSEN = 'ueberspringen_und_abschliessen', DOWNLOAD_ATTACHMENTS = 'downloadAttachments', + + CREATE_COLLABORATION_REQUEST = 'createCollaborationRequest', } export enum LoeschAnforderungLinkRel { diff --git a/alfa-client/package-lock.json b/alfa-client/package-lock.json index a7e9f6cfe77a4da3de7f151bdcbd299486c59c20..986f8539663f4c42b3f2e4cf04bc7cb7d945515a 100644 --- a/alfa-client/package-lock.json +++ b/alfa-client/package-lock.json @@ -1,12 +1,12 @@ { "name": "alfa", - "version": "0.7.0-SNAPSHOT", + "version": "0.8.0-SNAPSHOT", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "alfa", - "version": "0.7.0-SNAPSHOT", + "version": "0.8.0-SNAPSHOT", "license": "MIT", "dependencies": { "@angular/animations": "17.3.10", @@ -79,6 +79,7 @@ "@storybook/core-server": "^8.1.4", "@swc-node/register": "1.9.1", "@swc/core": "~1.5.7", + "@swc/helpers": "~0.5.2", "@testing-library/jest-dom": "6.4.5", "@types/file-saver": "2.0.7", "@types/jest": "29.4.4", @@ -202,13 +203,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1800.3", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@angular-devkit/architect/-/architect-0.1800.3.tgz", - "integrity": "sha512-ZoQuvCN/Ft4XJ+/XouYFKGoyEYTfZ8I5yI1M4t19lkRb3MwpQribWcZu4PP+SNnS6/9qnW7guxiQGS+CVlqnDg==", + "version": "0.1801.4", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@angular-devkit/architect/-/architect-0.1801.4.tgz", + "integrity": "sha512-Ch1ZwRh1N/vcCKHm4ErLcgZly3tlwdLUDGBaAIlhE3YFGq543Swv6a5IcDw0veD6iGFceJAmbrp+z5hmzI8p5A==", "dev": true, "peer": true, "dependencies": { - "@angular-devkit/core": "18.0.3", + "@angular-devkit/core": "18.1.4", "rxjs": "7.8.1" }, "engines": { @@ -218,15 +219,15 @@ } }, "node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { - "version": "18.0.3", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@angular-devkit/core/-/core-18.0.3.tgz", - "integrity": "sha512-nTs1KbNSVCVooPdDaeTh1YbggNVaqexbQjXNIvJJzRB8qPkWNPxm0pQeFjU7kWUBg2+aBXN4/CNwU1YHwxfiSQ==", + "version": "18.1.4", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@angular-devkit/core/-/core-18.1.4.tgz", + "integrity": "sha512-lKBsvbqW2QFL8terzNuSDSmKBo8//QNRO4qU5mVJ1fFf4xBJanXKoiAMuADhx+/owVIptnYT59IZ8jUAna+Srg==", "dev": true, "peer": true, "dependencies": { - "ajv": "8.13.0", + "ajv": "8.16.0", "ajv-formats": "3.0.1", - "jsonc-parser": "3.2.1", + "jsonc-parser": "3.3.1", "picomatch": "4.0.2", "rxjs": "7.8.1", "source-map": "0.7.4" @@ -246,9 +247,9 @@ } }, "node_modules/@angular-devkit/architect/node_modules/ajv": { - "version": "8.13.0", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/ajv/-/ajv-8.13.0.tgz", - "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "version": "8.16.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/ajv/-/ajv-8.16.0.tgz", + "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", "dev": true, "peer": true, "dependencies": { @@ -280,6 +281,13 @@ } } }, + "node_modules/@angular-devkit/architect/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "peer": true + }, "node_modules/@angular-devkit/architect/node_modules/picomatch": { "version": "4.0.2", "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/picomatch/-/picomatch-4.0.2.tgz", @@ -6276,30 +6284,30 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.2", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@floating-ui/core/-/core-1.6.2.tgz", - "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "version": "1.6.7", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@floating-ui/core/-/core-1.6.7.tgz", + "integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==", "dev": true, "peer": true, "dependencies": { - "@floating-ui/utils": "^0.2.0" + "@floating-ui/utils": "^0.2.7" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.5", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@floating-ui/dom/-/dom-1.6.5.tgz", - "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "version": "1.6.10", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@floating-ui/dom/-/dom-1.6.10.tgz", + "integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==", "dev": true, "peer": true, "dependencies": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.7" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.0", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@floating-ui/react-dom/-/react-dom-2.1.0.tgz", - "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==", + "version": "2.1.1", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", + "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", "dev": true, "peer": true, "dependencies": { @@ -6311,9 +6319,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.2", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@floating-ui/utils/-/utils-0.2.2.tgz", - "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==", + "version": "0.2.7", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@floating-ui/utils/-/utils-0.2.7.tgz", + "integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==", "dev": true, "peer": true }, @@ -8465,13 +8473,13 @@ } }, "node_modules/@nrwl/devkit": { - "version": "19.2.3", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@nrwl/devkit/-/devkit-19.2.3.tgz", - "integrity": "sha512-OL6sc70gR/USasvbYzyYY44Hd5ZCde2UfiA5h8VeAYAJbq+JmtscpvjcnZ7OIsXyYEOxe1rypULElqu/8qpKzQ==", + "version": "19.5.7", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@nrwl/devkit/-/devkit-19.5.7.tgz", + "integrity": "sha512-sTEwqsAT6bMturU14o/0O6v509OkwGOglxpbiL/zIYO/fDkMoNgnhlHBIT87i4YVuofMz2Z+hTfjDskzDPRSYw==", "dev": true, "peer": true, "dependencies": { - "@nx/devkit": "19.2.3" + "@nx/devkit": "19.5.7" } }, "node_modules/@nrwl/eslint-plugin-nx": { @@ -8985,13 +8993,13 @@ } }, "node_modules/@nx/devkit": { - "version": "19.2.3", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@nx/devkit/-/devkit-19.2.3.tgz", - "integrity": "sha512-if1WwRVexrQBBADObEcxVIivq4QRZWY/nYRhCQy/qfFI6Cu2jBSI6ZQ1uy7to2L2sQPLgn8v2beQZiAeZdIktg==", + "version": "19.5.7", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@nx/devkit/-/devkit-19.5.7.tgz", + "integrity": "sha512-mUtZQcdqbF0Q9HfyG14jmpPCtZ1GnVaLNIADZv5SLpFyfh4ZjaBw6wdjPj7Sp3imLoyqMrcd9nCRNO2hlem8bw==", "dev": true, "peer": true, "dependencies": { - "@nrwl/devkit": "19.2.3", + "@nrwl/devkit": "19.5.7", "ejs": "^3.1.7", "enquirer": "~2.3.6", "ignore": "^5.0.4", @@ -11055,28 +11063,61 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.0.4", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", - "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", + "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", "dev": true, "peer": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", + "dev": true, + "peer": true + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-collection": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", + "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -11087,6 +11128,167 @@ } } }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-select": { "version": "1.2.2", "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-select/-/react-select-1.2.2.tgz", @@ -11133,20 +11335,19 @@ } }, "node_modules/@radix-ui/react-separator": { - "version": "1.0.3", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-separator/-/react-separator-1.0.3.tgz", - "integrity": "sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==", + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-separator/-/react-separator-1.1.0.tgz", + "integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==", "dev": true, "peer": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.0.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -11157,6 +11358,65 @@ } } }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", @@ -11177,22 +11437,21 @@ } }, "node_modules/@radix-ui/react-toggle": { - "version": "1.0.3", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-toggle/-/react-toggle-1.0.3.tgz", - "integrity": "sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==", + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", + "integrity": "sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==", "dev": true, "peer": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -11204,26 +11463,104 @@ } }, "node_modules/@radix-ui/react-toggle-group": { - "version": "1.0.4", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-toggle-group/-/react-toggle-group-1.0.4.tgz", - "integrity": "sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==", + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.0.tgz", + "integrity": "sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==", "dev": true, "peer": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-toggle": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-toggle": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", + "dev": true, + "peer": true + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -11234,27 +11571,181 @@ } } }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", + "dev": true, + "peer": true + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toolbar": { - "version": "1.0.4", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-toolbar/-/react-toolbar-1.0.4.tgz", - "integrity": "sha512-tBgmM/O7a07xbaEkYJWYTXkIdU/1pW4/KZORR43toC/4XWyBCURK0ei9kMUdp+gTPPKBgYLxXmRSH1EVcIDp8Q==", + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-toolbar/-/react-toolbar-1.1.0.tgz", + "integrity": "sha512-ZUKknxhMTL/4hPh+4DuaTot9aO7UD6Kupj4gqXCsBTayX1pD1L+0C2/2VZKXb4tIifQklZ3pf2hG9T+ns+FclQ==", "dev": true, "peer": true, "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-separator": "1.0.3", - "@radix-ui/react-toggle-group": "1.0.4" + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-separator": "1.1.0", + "@radix-ui/react-toggle-group": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -11265,6 +11756,104 @@ } } }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", + "dev": true, + "peer": true + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toolbar/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dev": true, + "peer": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", @@ -13612,19 +14201,19 @@ } }, "node_modules/@storybook/components": { - "version": "7.6.19", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/components/-/components-7.6.19.tgz", - "integrity": "sha512-8Zw/RQ4crzKkUR7ojxvRIj8vktKiBBO8Nq93qv4JfDqDWrcR7cro0hOlZgmZmrzbFunBBt6WlsNNO6nVP7R4Xw==", + "version": "7.6.20", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/components/-/components-7.6.20.tgz", + "integrity": "sha512-0d8u4m558R+W5V+rseF/+e9JnMciADLXTpsILrG+TBhwECk0MctIWW18bkqkujdCm8kDZr5U2iM/5kS1Noy7Ug==", "dev": true, "peer": true, "dependencies": { "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-toolbar": "^1.0.4", - "@storybook/client-logger": "7.6.19", + "@storybook/client-logger": "7.6.20", "@storybook/csf": "^0.1.2", "@storybook/global": "^5.0.0", - "@storybook/theming": "7.6.19", - "@storybook/types": "7.6.19", + "@storybook/theming": "7.6.20", + "@storybook/types": "7.6.20", "memoizerific": "^1.11.3", "use-resize-observer": "^9.1.0", "util-deprecate": "^1.0.2" @@ -13639,14 +14228,14 @@ } }, "node_modules/@storybook/components/node_modules/@storybook/channels": { - "version": "7.6.19", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/channels/-/channels-7.6.19.tgz", - "integrity": "sha512-2JGh+i95GwjtjqWqhtEh15jM5ifwbRGmXeFqkY7dpdHH50EEWafYHr2mg3opK3heVDwg0rJ/VBptkmshloXuvA==", + "version": "7.6.20", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/channels/-/channels-7.6.20.tgz", + "integrity": "sha512-4hkgPSH6bJclB2OvLnkZOGZW1WptJs09mhQ6j6qLjgBZzL/ZdD6priWSd7iXrmPiN5TzUobkG4P4Dp7FjkiO7A==", "dev": true, "peer": true, "dependencies": { - "@storybook/client-logger": "7.6.19", - "@storybook/core-events": "7.6.19", + "@storybook/client-logger": "7.6.20", + "@storybook/core-events": "7.6.20", "@storybook/global": "^5.0.0", "qs": "^6.10.0", "telejson": "^7.2.0", @@ -13658,9 +14247,9 @@ } }, "node_modules/@storybook/components/node_modules/@storybook/client-logger": { - "version": "7.6.19", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/client-logger/-/client-logger-7.6.19.tgz", - "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "version": "7.6.20", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/client-logger/-/client-logger-7.6.20.tgz", + "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", "dev": true, "peer": true, "dependencies": { @@ -13672,13 +14261,13 @@ } }, "node_modules/@storybook/components/node_modules/@storybook/types": { - "version": "7.6.19", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/types/-/types-7.6.19.tgz", - "integrity": "sha512-DeGYrRPRMGTVfT7o2rEZtRzyLT2yKTI2exgpnxbwPWEFAduZCSfzBrcBXZ/nb5B0pjA9tUNWls1YzGkJGlkhpg==", + "version": "7.6.20", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/types/-/types-7.6.20.tgz", + "integrity": "sha512-GncdY3x0LpbhmUAAJwXYtJDUQEwfF175gsjH0/fxPkxPoV7Sef9TM41jQLJW/5+6TnZoCZP/+aJZTJtq3ni23Q==", "dev": true, "peer": true, "dependencies": { - "@storybook/channels": "7.6.19", + "@storybook/channels": "7.6.20", "@types/babel__core": "^7.0.0", "@types/express": "^4.7.0", "file-system-cache": "2.3.0" @@ -13853,9 +14442,9 @@ } }, "node_modules/@storybook/core-events": { - "version": "7.6.19", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/core-events/-/core-events-7.6.19.tgz", - "integrity": "sha512-K/W6Uvum0ocZSgjbi8hiotpe+wDEHDZlvN+KlPqdh9ae9xDK8aBNBq9IelCoqM+uKO1Zj+dDfSQds7CD781DJg==", + "version": "7.6.20", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/core-events/-/core-events-7.6.20.tgz", + "integrity": "sha512-tlVDuVbDiNkvPDFAu+0ou3xBBYbx9zUURQz4G9fAq0ScgBOs/bpzcRrFb4mLpemUViBAd47tfZKdH4MAX45KVQ==", "dev": true, "peer": true, "dependencies": { @@ -14383,14 +14972,14 @@ } }, "node_modules/@storybook/theming": { - "version": "7.6.19", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/theming/-/theming-7.6.19.tgz", - "integrity": "sha512-sAho13MmtA80ctOaLn8lpkQBsPyiqSdLcOPH5BWFhatQzzBQCpTAKQk+q/xGju8bNiPZ+yQBaBzbN8SfX8ceCg==", + "version": "7.6.20", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/theming/-/theming-7.6.20.tgz", + "integrity": "sha512-iT1pXHkSkd35JsCte6Qbanmprx5flkqtSHC6Gi6Umqoxlg9IjiLPmpHbaIXzoC06DSW93hPj5Zbi1lPlTvRC7Q==", "dev": true, "peer": true, "dependencies": { "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@storybook/client-logger": "7.6.19", + "@storybook/client-logger": "7.6.20", "@storybook/global": "^5.0.0", "memoizerific": "^1.11.3" }, @@ -14404,9 +14993,9 @@ } }, "node_modules/@storybook/theming/node_modules/@storybook/client-logger": { - "version": "7.6.19", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/client-logger/-/client-logger-7.6.19.tgz", - "integrity": "sha512-oGzOxbmLmciSIfd5gsxDzPmX8DttWhoYdPKxjMuCuWLTO2TWpkCWp1FTUMWO72mm/6V/FswT/aqpJJBBvdZ3RQ==", + "version": "7.6.20", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@storybook/client-logger/-/client-logger-7.6.20.tgz", + "integrity": "sha512-NwG0VIJQCmKrSaN5GBDFyQgTAHLNishUPLW1NrzqTDNAhfZUoef64rPQlinbopa0H4OXmlB+QxbQIb3ubeXmSQ==", "dev": true, "peer": true, "dependencies": { @@ -14675,6 +15264,15 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "devOptional": true }, + "node_modules/@swc/helpers": { + "version": "0.5.12", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@swc/helpers/-/helpers-0.5.12.tgz", + "integrity": "sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g==", + "devOptional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@swc/types": { "version": "0.1.8", "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/@swc/types/-/types-0.1.8.tgz", @@ -19867,9 +20465,9 @@ "dev": true }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "2.1.2" }, @@ -27768,32 +28366,32 @@ } }, "node_modules/mocha": { - "version": "10.4.0", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/mocha/-/mocha-10.4.0.tgz", - "integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==", + "version": "10.7.0", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/mocha/-/mocha-10.7.0.tgz", + "integrity": "sha512-v8/rBWr2VO5YkspYINnvu81inSz2y3ODJrhO175/Exzor1RcEZZkizgE2A+w/CAXXoESS8Kys5E62dOHGHzULA==", "dev": true, "peer": true, "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "8.1.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" }, "bin": { "_mocha": "bin/_mocha", @@ -27803,16 +28401,6 @@ "node": ">= 14.0.0" } }, - "node_modules/mocha/node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/mocha/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -27839,34 +28427,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/mocha/node_modules/chokidar": { - "version": "3.5.3", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "peer": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/mocha/node_modules/cliui": { "version": "7.0.4", "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/cliui/-/cliui-7.0.4.tgz", @@ -27879,16 +28439,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/mocha/node_modules/diff": { - "version": "5.0.0", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true, - "peer": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/mocha/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -27934,9 +28484,9 @@ } }, "node_modules/mocha/node_modules/minimatch": { - "version": "5.0.1", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "5.1.6", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "peer": true, "dependencies": { @@ -27953,16 +28503,6 @@ "dev": true, "peer": true }, - "node_modules/mocha/node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/mocha/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -28030,9 +28570,9 @@ } }, "node_modules/mocha/node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "20.2.9", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "peer": true, "engines": { @@ -37868,9 +38408,9 @@ } }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "6.5.1", + "resolved": "http://nexus.ozg-sh.de/repository/npm-proxy/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", "dev": true, "peer": true }, diff --git a/alfa-client/package.json b/alfa-client/package.json index 730267babe4d51dff0fb89982e5edf16f65f0631..a1b8f575dd488352d03aed6d2315a5b1f9e6da42 100644 --- a/alfa-client/package.json +++ b/alfa-client/package.json @@ -12,7 +12,7 @@ "build": "nx run alfa:build", "test": "nx affected --target=test --parallel 8 -- --runInBand", "test:cov": "jest --coverage", - "test:lib": "nx test ${npm_config_lib} --watchAll", + "test:lib": "nx test ${npm_config_lib}", "test:debug:lib": "nx test ${npm_config_lib} --detectOpenHandles --watchAll", "ci-build": "nx run alfa:build --outputHashing=all", "ci-build-admin": "nx container admin && cp -r dist/ apps/admin/", diff --git a/alfa-client/tsconfig.base.json b/alfa-client/tsconfig.base.json index b8645da774def8753885d8a14cfd87a1e8f887eb..dcd7a091e2e4c3e485af22ec4b92386bfee0394d 100644 --- a/alfa-client/tsconfig.base.json +++ b/alfa-client/tsconfig.base.json @@ -23,6 +23,7 @@ "@alfa-client/bescheid-shared": ["libs/bescheid-shared/src/index.ts"], "@alfa-client/binary-file": ["libs/binary-file/src/index.ts"], "@alfa-client/binary-file-shared": ["libs/binary-file-shared/src/index.ts"], + "@alfa-client/collaboration": ["libs/collaboration/src/index.ts"], "@alfa-client/command-shared": ["libs/command-shared/src/index.ts"], "@alfa-client/environment-shared": ["libs/environment-shared/src/index.ts"], "@alfa-client/forwarding": ["libs/forwarding/src/index.ts"], @@ -55,7 +56,8 @@ "@alfa-client/wiedervorlage-shared": ["libs/wiedervorlage-shared/src/index.ts"], "@ods/component": ["libs/design-component/src/index.ts"], "@ods/system": ["libs/design-system/src/index.ts"], - "authentication": ["libs/authentication/src/index.ts"] + "authentication": ["libs/authentication/src/index.ts"], + "test": ["libs/test/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/alfa-server/src/main/resources/application-dev.yml b/alfa-server/src/main/resources/application-dev.yml index 52ca56a22d8fa5ded4dcecfae01b3e22553c2da1..6f11904235f17602539afaca47cd695e69deee8b 100644 --- a/alfa-server/src/main/resources/application-dev.yml +++ b/alfa-server/src/main/resources/application-dev.yml @@ -10,6 +10,7 @@ server: ozgcloud: feature: reply-always-allowed: true + collaboration-enabled: true production: false stage: production: false diff --git a/alfa-server/src/main/resources/application-e2e.yml b/alfa-server/src/main/resources/application-e2e.yml index 585c3a5f701bd2f11ccc56211468786f077bfcd4..69d9b46a198e57b5d03f01cc78f2d83e209f9335 100644 --- a/alfa-server/src/main/resources/application-e2e.yml +++ b/alfa-server/src/main/resources/application-e2e.yml @@ -9,6 +9,7 @@ keycloak: ozgcloud: feature: reply-always-allowed: true + collaboration-enabled: true forwarding: lninfo: url: classpath:files/LandesnetzInfo.html diff --git a/alfa-server/src/main/resources/application-local.yml b/alfa-server/src/main/resources/application-local.yml index b113a7052d27746ebf67fdd3600b381e7e658dd5..74743fd293aab8aeda88d286548ff33e73ea55d7 100644 --- a/alfa-server/src/main/resources/application-local.yml +++ b/alfa-server/src/main/resources/application-local.yml @@ -18,6 +18,7 @@ grpc: ozgcloud: feature: reply-always-allowed: true + collaboration-enabled: true production: false user-assistance: documentation: diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/collaboration/CollaborationVorgangProcessor.java b/alfa-service/src/main/java/de/ozgcloud/alfa/collaboration/CollaborationVorgangProcessor.java new file mode 100644 index 0000000000000000000000000000000000000000..d8c0a04560d014f582b19b2aeecf5306d2ed39be --- /dev/null +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/collaboration/CollaborationVorgangProcessor.java @@ -0,0 +1,43 @@ +package de.ozgcloud.alfa.collaboration; + +import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; + +import java.util.Objects; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.LinkRelation; +import org.springframework.hateoas.server.RepresentationModelProcessor; +import org.springframework.stereotype.Component; + +import de.ozgcloud.alfa.common.ModelBuilder; +import de.ozgcloud.alfa.common.command.CommandController; +import de.ozgcloud.alfa.common.user.CurrentUserService; +import de.ozgcloud.alfa.common.user.UserRole; +import de.ozgcloud.alfa.vorgang.VorgangWithEingang; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Component +@ConditionalOnProperty("ozgcloud.feature.collaboration-enabled") +class CollaborationVorgangProcessor implements RepresentationModelProcessor<EntityModel<VorgangWithEingang>> { + + static final LinkRelation REL_CREATE_COLLABORATION_REQUEST = LinkRelation.of("createCollaborationRequest"); + + private final CurrentUserService currentUserService; + + @Override + public EntityModel<VorgangWithEingang> process(EntityModel<VorgangWithEingang> model) { + var vorgang = model.getContent(); + + if (Objects.isNull(vorgang)) { + return model; + } + + return ModelBuilder.fromModel(model) + .ifMatch(() -> currentUserService.hasRole(UserRole.VERWALTUNG_USER)) + .addLink(linkTo(methodOn(CommandController.CommandByRelationController.class).createCommand(vorgang.getId(), vorgang.getId(), + vorgang.getVersion(), null)).withRel(REL_CREATE_COLLABORATION_REQUEST)) + .buildModel(); + } +} diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/common/FeatureToggleProperties.java b/alfa-service/src/main/java/de/ozgcloud/alfa/common/FeatureToggleProperties.java index 80238fccb22376ea6e2ff8201f1d7b497a968dae..5b60fc67206213d2be23bda2aebddfb539873da5 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/common/FeatureToggleProperties.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/common/FeatureToggleProperties.java @@ -16,4 +16,9 @@ public class FeatureToggleProperties { * Enable mail reply option regardless of other configuration. */ private boolean replyAlwaysAllowed = false; + + /** + * Enable collaboration-feature in Vorgang + */ + private boolean collaborationEnabled = false; } diff --git a/alfa-service/src/main/java/de/ozgcloud/alfa/common/user/CurrentUserService.java b/alfa-service/src/main/java/de/ozgcloud/alfa/common/user/CurrentUserService.java index f4a517c4052a3658f8ecfc3d81e5ac5a6593c849..a62746f45d4920e8e787e2beca665ecf137d8a99 100644 --- a/alfa-service/src/main/java/de/ozgcloud/alfa/common/user/CurrentUserService.java +++ b/alfa-service/src/main/java/de/ozgcloud/alfa/common/user/CurrentUserService.java @@ -29,7 +29,6 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; @@ -37,7 +36,9 @@ import org.springframework.stereotype.Service; import de.ozgcloud.alfa.common.binaryfile.AlfaUserWithFileId; import de.ozgcloud.common.errorhandling.TechnicalException; +import lombok.RequiredArgsConstructor; +@RequiredArgsConstructor @Service public class CurrentUserService { @@ -51,10 +52,9 @@ public class CurrentUserService { static final String KEYCLOAK_USER_GIVEN_NAME = "given_name"; static final String KEYCLOAK_USER_FAMILY_NAME = "family_name"; - @Autowired - private UserService userService; - @Autowired - private RoleHierarchy roleHierarchy; + private final UserService userService; + + private final RoleHierarchy roleHierarchy; public boolean hasRole(String role) { return CurrentUserHelper.hasRole(role) || hasRoleReachable(role); diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/collaboration/CollaborationVorgangProcessorITCase.java b/alfa-service/src/test/java/de/ozgcloud/alfa/collaboration/CollaborationVorgangProcessorITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..fe189548817a2ec6f40b4eb9536d0e2f8cef3cdb --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/collaboration/CollaborationVorgangProcessorITCase.java @@ -0,0 +1,32 @@ +package de.ozgcloud.alfa.collaboration; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; + +class CollaborationVorgangProcessorITCase { + + @SpringBootTest(properties = {"ozgcloud.feature.collaboration-enabled=true"}) + @Nested + class OnFeatureEnabled { + + @Test + void shouldExistInApplicationContext(ApplicationContext context) { + assertThat(context.getBean(CollaborationVorgangProcessor.class)).isNotNull(); + } + } + + @SpringBootTest + @Nested + class OnFeatureDisabled { + + @Test + void shouldNotExistInApplicationContext(ApplicationContext context) { + assertThatThrownBy(() -> context.getBean(CollaborationVorgangProcessor.class)).isInstanceOf(NoSuchBeanDefinitionException.class); + } + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/collaboration/CollaborationVorgangProcessorTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/collaboration/CollaborationVorgangProcessorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5a51e828cc7b1eec1fbe93d2125722ee038e3954 --- /dev/null +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/collaboration/CollaborationVorgangProcessorTest.java @@ -0,0 +1,80 @@ +package de.ozgcloud.alfa.collaboration; + +import static de.ozgcloud.alfa.common.UserProfileUrlProviderTestFactory.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.springframework.hateoas.EntityModel; +import org.springframework.hateoas.Link; +import org.springframework.hateoas.UriTemplate; + +import de.ozgcloud.alfa.common.UserProfileUrlProvider; +import de.ozgcloud.alfa.common.command.CommandController; +import de.ozgcloud.alfa.common.user.CurrentUserService; +import de.ozgcloud.alfa.common.user.UserRole; +import de.ozgcloud.alfa.vorgang.VorgangHeaderTestFactory; +import de.ozgcloud.alfa.vorgang.VorgangWithEingangTestFactory; + +class CollaborationVorgangProcessorTest { + + @Spy + @InjectMocks + private CollaborationVorgangProcessor processor; + + @Mock + private CurrentUserService currentUserService; + + private final UserProfileUrlProvider urlProvider = new UserProfileUrlProvider(); + + @Nested + class TestProcess { + + @Nested + class OnNullVorgang { + + @Test + void shouldNotAddLinksIfVorgangIsNull() { + var model = processor.process(new EntityModel<>() { + }); + + assertThat(model.hasLinks()).isFalse(); + } + } + + @Nested + class OnNonNullVorgang { + + @BeforeEach + void prepareBuilder() { + initUserProfileUrlProvider(urlProvider); + } + + @Test + void shouldAddCreateCollaborationRequestRelation() { + when(currentUserService.hasRole(UserRole.VERWALTUNG_USER)).thenReturn(true); + + var model = processor.process(EntityModel.of(VorgangWithEingangTestFactory.create())); + + assertThat(model.getLink(CollaborationVorgangProcessor.REL_CREATE_COLLABORATION_REQUEST)).isPresent().get() + .extracting(Link::getHref) + .isEqualTo(UriTemplate.of(CommandController.CommandByRelationController.COMMAND_BY_RELATION_PATH) + .expand(VorgangHeaderTestFactory.ID, VorgangHeaderTestFactory.ID, VorgangHeaderTestFactory.VERSION).toString()); + } + + @Test + void shouldNotAddCreateCollaborationRequestRelation() { + when(currentUserService.hasRole(UserRole.VERWALTUNG_USER)).thenReturn(false); + + var model = processor.process(EntityModel.of(VorgangWithEingangTestFactory.create())); + + assertThat(model.getLink(CollaborationVorgangProcessor.REL_CREATE_COLLABORATION_REQUEST)).isEmpty(); + } + } + } +} diff --git a/alfa-service/src/test/java/de/ozgcloud/alfa/common/user/CurrentUserServiceTest.java b/alfa-service/src/test/java/de/ozgcloud/alfa/common/user/CurrentUserServiceTest.java index e363dc185c06592f6e7e11e35f25c131bd6a83d6..a478cf2de0cf24b12bec1c1279be4b26c3d9a2d5 100644 --- a/alfa-service/src/test/java/de/ozgcloud/alfa/common/user/CurrentUserServiceTest.java +++ b/alfa-service/src/test/java/de/ozgcloud/alfa/common/user/CurrentUserServiceTest.java @@ -35,8 +35,10 @@ import java.util.Optional; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.oauth2.jwt.Jwt; import de.ozgcloud.alfa.JwtTestFactory; @@ -44,7 +46,12 @@ import de.ozgcloud.alfa.JwtTestFactory; class CurrentUserServiceTest { @Spy + @InjectMocks private CurrentUserService service; + @Mock + private UserService userService; + @Mock + private RoleHierarchy roleHierarchy; @Nested class TestGetOrganisationseinheit {