diff --git a/alfa-client/apps/alfa-e2e/src/components/attachment/attachment.e2e.component.ts b/alfa-client/apps/alfa-e2e/src/components/attachment/attachment.e2e.component.ts index fe12579e28ed202ae4b72b4f54003a2ae149cd75..c7988c3ce98a476aa50c0a28b1341331a790d4e4 100644 --- a/alfa-client/apps/alfa-e2e/src/components/attachment/attachment.e2e.component.ts +++ b/alfa-client/apps/alfa-e2e/src/components/attachment/attachment.e2e.component.ts @@ -32,7 +32,7 @@ export class AttachmentContainerE2EComponent { } public getUploadInput() { - return cy.getTestElement(this.locatorFileUploadInput); + return cy.getTestElementContaining(this.locatorFileUploadInput); } } diff --git a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/kommentar-attachment/kommentar-attachment.cy.ts b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/kommentar-attachment/kommentar-attachment.cy.ts index c85070d6378af981222dca9bbaf595db77d6fff5..e3d96d18276ee39862dd14f25d9a0ed02169b812 100644 --- a/alfa-client/apps/alfa-e2e/src/e2e/main-tests/kommentar-attachment/kommentar-attachment.cy.ts +++ b/alfa-client/apps/alfa-e2e/src/e2e/main-tests/kommentar-attachment/kommentar-attachment.cy.ts @@ -22,10 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { sleep } from '@alfa-client/tech-shared'; -import { - AttachmentContainerE2EComponent, - AttachmentListE2EComponent, -} from '../../../components/attachment/attachment.e2e.component'; +import { AttachmentContainerE2EComponent, AttachmentListE2EComponent, } from '../../../components/attachment/attachment.e2e.component'; import { KommentareInVorgangE2EComponent } from '../../../components/kommentar/kommentar-list.e2e.component'; import { VorgangListE2EComponent } from '../../../components/vorgang/vorgang-list.e2e.component'; import { UserE2E } from '../../../model/user'; @@ -34,7 +31,7 @@ import { MainPage, waitForSpinnerToDisappear } from '../../../page-objects/main. import { VorgangPage } from '../../../page-objects/vorgang.po'; import { dropCollections, readFileFromDownloads } from '../../../support/cypress-helper'; import { exist, notExist } from '../../../support/cypress.util'; -import { TEST_FILE_WITHOUT_CONTENT, TEST_FILE_WITH_CONTENT } from '../../../support/data.util'; +import { TEST_FILE_WITH_CONTENT, TEST_FILE_WITHOUT_CONTENT } from '../../../support/data.util'; import { uploadFile } from '../../../support/file-upload'; import { getUserSabine, loginAsSabine } from '../../../support/user-util'; import { createVorgang, initVorgang } from '../../../support/vorgang-util'; @@ -46,8 +43,7 @@ describe('Kommentar attachments', () => { const vorgangPage: VorgangPage = new VorgangPage(); const kommentarContainer: KommentareInVorgangE2EComponent = vorgangPage.getKommentarContainer(); - const attachmentContainer: AttachmentContainerE2EComponent = - kommentarContainer.getAttachmentContainer(); + const attachmentContainer: AttachmentContainerE2EComponent = kommentarContainer.getAttachmentContainer(); const attachmentList: AttachmentListE2EComponent = attachmentContainer.getList(); const kommentarText: string = 'Test text to test the test text test'; diff --git a/alfa-client/apps/alfa-e2e/src/support/commands.ts b/alfa-client/apps/alfa-e2e/src/support/commands.ts index 8873b6afe8672819cf9ea75cf441955fabb0f004..30e88fd0f58f79fb5d3db717bb46a835fc2b5b54 100644 --- a/alfa-client/apps/alfa-e2e/src/support/commands.ts +++ b/alfa-client/apps/alfa-e2e/src/support/commands.ts @@ -73,6 +73,7 @@ declare namespace Cypress { interface Chainable<Subject> { getTestElementWithOid(oid: string, ...args); getTestElement(selector: string, ...args); + getTestElementContaining(selector: string, ...args); getTestElementWithClass(selector: string, ...args); findTestElementWithClass(selector: string, ...args); findElement(selector: string); @@ -90,6 +91,10 @@ Cypress.Commands.add('getTestElement', (selector, ...args) => { return cy.get(`[${DATA_TEST_ID}~="${selector}"]`, ...args); }); +Cypress.Commands.add('getTestElementContaining', (selector, ...args) => { + return cy.get(`[${DATA_TEST_ID}*="${selector}"]`, ...args); +}); + Cypress.Commands.add('getTestElementWithClass', (selector, ...args) => { console.log( 'Achtung: Potentiell nicht eindeutiges Ergebnis, weil eine data-test-class mit cy.get() von der DOM-Root aus gesucht wird.', @@ -101,13 +106,9 @@ Cypress.Commands.add('getTestElementWithOid', (oid, ...args) => { return cy.getTestElement(oid, ...args); }); -Cypress.Commands.add( - 'findTestElementWithClass', - { prevSubject: true }, - (subject: any, selector) => { - return subject.find(`[${DATA_TEST_CLASS}="${selector}"]`); - }, -); +Cypress.Commands.add('findTestElementWithClass', { prevSubject: true }, (subject: any, selector) => { + return subject.find(`[${DATA_TEST_CLASS}="${selector}"]`); +}); Cypress.Commands.add('findElement', { prevSubject: true }, (subject: any, selector: string) => { return subject.find(selector); diff --git a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts index 8e823be03c1f064fe2cd152da724bb3e0fcdc001..85dd58354b2597416aad10898d5c437edb445cfe 100644 --- a/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts +++ b/alfa-client/libs/admin/user/src/lib/user-form/user.formservice.spec.ts @@ -26,7 +26,7 @@ import { ROUTES } from '@admin-client/shared'; import { User, UserService } from '@admin-client/user-shared'; import { PatchConfig } from '@admin/keycloak-shared'; import { NavigationService } from '@alfa-client/navigation-shared'; -import { createEmptyStateResource, createStateResource, EMPTY_ARRAY, StateResource } from '@alfa-client/tech-shared'; +import { createEmptyStateResource, createStateResource, StateResource } from '@alfa-client/tech-shared'; import { Mock, mock } from '@alfa-client/test-utils'; import { SnackBarService } from '@alfa-client/ui'; import { fakeAsync, TestBed, tick } from '@angular/core/testing'; @@ -69,7 +69,7 @@ describe('UserFormService', () => { }; navigationService = mock(NavigationService); snackBarService = mock(SnackBarService); - activatedRoute = <any>{ ...mock(ActivatedRoute), url: of<UrlSegment[]>(EMPTY_ARRAY) }; + activatedRoute = <any>{ ...mock(ActivatedRoute), url: of<UrlSegment[]>([]) }; TestBed.configureTestingModule({ providers: [ diff --git a/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.spec.ts b/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.spec.ts index ab845cf6b4f7e3156ebb7a940e418780e9a410d2..ddb4bc98ad09c5415e2e3ca6c4853002c17c32d7 100644 --- a/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.spec.ts +++ b/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.spec.ts @@ -26,7 +26,6 @@ import { CommandOrder, CommandResource, CommandService, CreateCommandProps } fro import { PostfachService } from '@alfa-client/postfach-shared'; import { ApiError, - EMPTY_ARRAY, EMPTY_STRING, HttpError, StateResource, @@ -1325,7 +1324,7 @@ describe('BescheidService', () => { it('should return empty array', () => { const resultdBescheide: BescheidResource[] = service.filterBySentStatus(null); - expect(resultdBescheide).toBe(EMPTY_ARRAY); + expect(resultdBescheide).toEqual([]); }); }); diff --git a/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.ts b/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.ts index 7b8a2f737d688302a3a9f30e218b126a9fad285e..f2df99ebc8b9b47adb3244134990a129aedfe731 100644 --- a/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.ts +++ b/alfa-client/libs/bescheid-shared/src/lib/bescheid.service.ts @@ -33,7 +33,6 @@ import { } from '@alfa-client/command-shared'; import { PostfachService } from '@alfa-client/postfach-shared'; import { - EMPTY_ARRAY, HttpError, ResourceListService, StateResource, @@ -524,7 +523,7 @@ export class BescheidService { } filterBySentStatus(bescheide: BescheidResource[]): BescheidResource[] { - return isNotNil(bescheide) ? bescheide.filter(this.hasSentStatus) : EMPTY_ARRAY; + return isNotNil(bescheide) ? bescheide.filter(this.hasSentStatus) : []; } private hasSentStatus(bescheid: BescheidResource): boolean { diff --git a/alfa-client/libs/binary-file-shared/src/index.ts b/alfa-client/libs/binary-file-shared/src/index.ts index 84a2d2d428e583925dbf769d0d3e759aec1d4469..89abbed2825506534bc5be9bb248855c2c2c10dc 100644 --- a/alfa-client/libs/binary-file-shared/src/index.ts +++ b/alfa-client/libs/binary-file-shared/src/index.ts @@ -27,4 +27,3 @@ export * from './lib/binary-file-shared.module'; export * from './lib/binary-file.linkrel'; export * from './lib/binary-file.model'; export * from './lib/binary-file.service'; -export * from './lib/binary-file.util'; diff --git a/alfa-client/libs/binary-file-shared/src/lib/binary-file.model.ts b/alfa-client/libs/binary-file-shared/src/lib/binary-file.model.ts index b315da1908251ee8a163da0ac755d1f9d0fb20f7..68b3862e4a27ccfa63c8c66930f1d980af233663 100644 --- a/alfa-client/libs/binary-file-shared/src/lib/binary-file.model.ts +++ b/alfa-client/libs/binary-file-shared/src/lib/binary-file.model.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ListResource } from '@alfa-client/tech-shared'; +import { ListResource, StateResource } from '@alfa-client/tech-shared'; import { Resource } from '@ngxp/rest'; export interface BinaryFile { @@ -33,3 +33,38 @@ export interface BinaryFile { export interface BinaryFileResource extends BinaryFile, Resource {} export interface BinaryFileListResource extends ListResource {} + +export interface ToUploadFile { + type: FileUploadType; + file: File; + uploadUrl: string; +} + +export declare type FileUploadType = string; + +export interface UploadFile { + fileToUpload?: File; + uploadedFile: StateResource<BinaryFileResource>; +} + +export type UploadFileByIdentifier = { [key: string]: UploadFile }; +export type UploadFilesByType = { [type: string]: UploadFileByIdentifier }; + +export interface FileToDelete { + key: string; + binaryFileResource: BinaryFileResource; +} + +export enum BinaryFileIcon { + 'application/pdf' = 'pdf', + 'application/json' = 'json', + 'application/msword' = 'doc', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' = 'doc', + 'application/xml' = 'xml', + 'text/xml' = 'xml', + 'image/apng' = 'image', + 'image/gif' = 'image', + 'image/jpeg' = 'image', + 'image/png' = 'image', + 'image/svg+xml' = 'image', +} diff --git a/alfa-client/libs/binary-file-shared/src/lib/binary-file.repository.spec.ts b/alfa-client/libs/binary-file-shared/src/lib/binary-file.repository.spec.ts index 27377b3854e4b1e87900b83f4d957a7751641b1b..dfec001d129c806b4212ebb4dce6f0e54d40213e 100644 --- a/alfa-client/libs/binary-file-shared/src/lib/binary-file.repository.spec.ts +++ b/alfa-client/libs/binary-file-shared/src/lib/binary-file.repository.spec.ts @@ -30,15 +30,17 @@ import { ListResource, } from '@alfa-client/tech-shared'; import { mock, mockClass, useFromMock } from '@alfa-client/test-utils'; -import { HttpClient, HttpHeaders } from '@angular/common/http'; +import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; import { faker } from '@faker-js/faker'; import { Resource, ResourceFactory, ResourceUri, getUrl } from '@ngxp/rest'; import { cold, hot } from 'jest-marbles'; import { InjectorService } from 'libs/tech-shared/src/lib/injector/injector.service'; import { DummyLinkRel } from 'libs/tech-shared/test/dummy'; +import { createHttpResponse } from 'libs/tech-shared/test/http'; +import { singleCold } from 'libs/tech-shared/test/marbles'; import { createDummyListResource, createDummyResource } from 'libs/tech-shared/test/resource'; import { Observable, of } from 'rxjs'; -import { createBinaryFileResource, createBlob, createGetRequestOptions } from '../../test/binary-file'; +import { createBinaryFileResource, createBlob, createFile, createGetRequestOptions } from '../../test/binary-file'; import { BinaryFileLinkRel } from './binary-file.linkrel'; import { BinaryFileResource } from './binary-file.model'; import { BinaryFileRepository } from './binary-file.repository'; @@ -69,10 +71,52 @@ describe('BinaryFileRepository', () => { expect(repository).toBeTruthy(); }); + describe('upload file new', () => { + const dummyLinkRel: string = DummyLinkRel.DUMMY; + const dummyResource: Resource = createDummyResource([dummyLinkRel]); + const uri: ResourceUri = getUrl(dummyResource, dummyLinkRel); + + const file: File = createFile(); + const binaryFileResource: BinaryFileResource = createBinaryFileResource(); + const httpResponse: HttpResponse<Object> = createHttpResponse(); + + const blob: Blob = createBlob(); + + beforeEach(() => { + httpClient.post.mockReturnValue(of(httpResponse)); + repository.getFile = jest.fn().mockReturnValue(of(binaryFileResource)); + }); + + it('should call post on http client', () => { + const formData: FormData = new FormData(); + formData.append('file', blob, file.name); + + repository.uploadFileNew(uri, <File>blob); + + expect(httpClient.post).toHaveBeenCalledWith(getUrl(dummyResource, dummyLinkRel), formData, { + observe: 'response', + }); + }); + + it('should call get file', () => { + repository.uploadFileNew(uri, <File>blob).subscribe(); + + expect(repository.getFile).toHaveBeenCalledWith(httpResponse.headers.get(HttpHeader.LOCATION)); + }); + + it('should return binary file', () => { + repository.getFile = jest.fn().mockReturnValue(singleCold(binaryFileResource)); + + const result: Observable<BinaryFileResource> = repository.uploadFileNew(uri, <File>blob); + + expect(result).toBeObservable(singleCold(binaryFileResource)); + }); + }); + describe('uploadFile', () => { const dummyLinkRel: string = DummyLinkRel.DUMMY; const dummyResource: Resource = createDummyResource([dummyLinkRel]); - const blob: Blob = new Blob(['test text'], { type: 'text/plain' }); + const blob: Blob = createBlob(); beforeEach(() => { httpClient.post.mockReturnValue(of({})); @@ -82,7 +126,7 @@ describe('BinaryFileRepository', () => { const formData: FormData = new FormData(); formData.append('file', blob, 'filename'); - repository.uploadFile(dummyResource, dummyLinkRel, <File>blob); + repository.uploadFile(getUrl(dummyResource, dummyLinkRel), <File>blob); expect(httpClient.post).toHaveBeenCalledWith(getUrl(dummyResource, dummyLinkRel), formData, { observe: 'response', @@ -90,7 +134,7 @@ describe('BinaryFileRepository', () => { }); it('and return result', () => { - let result = repository.uploadFile(dummyResource, dummyLinkRel, <File>blob); + const result: Observable<HttpResponse<Object>> = repository.uploadFile(getUrl(dummyResource, dummyLinkRel), <File>blob); expect(result).not.toBeNull(); }); diff --git a/alfa-client/libs/binary-file-shared/src/lib/binary-file.repository.ts b/alfa-client/libs/binary-file-shared/src/lib/binary-file.repository.ts index c98079edeeaaf591b8efdb327dd5a5e5a5fe16c1..c3cdeb7e53cb71d9771066f8ad7e61138636b435 100644 --- a/alfa-client/libs/binary-file-shared/src/lib/binary-file.repository.ts +++ b/alfa-client/libs/binary-file-shared/src/lib/binary-file.repository.ts @@ -32,7 +32,7 @@ import { import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Resource, ResourceFactory, ResourceUri, getUrl } from '@ngxp/rest'; -import { Observable, map } from 'rxjs'; +import { Observable, map, mergeMap } from 'rxjs'; import { BinaryFileLinkRel } from './binary-file.linkrel'; import { BinaryFileListResource, BinaryFileResource } from './binary-file.model'; @@ -43,23 +43,33 @@ export class BinaryFileRepository { private resourceFactory: ResourceFactory, ) {} - public uploadFile( - resource: Resource, - linkRel: string, - file: File, - ): Observable<HttpResponse<Object>> { + public uploadFileNew(uri: ResourceUri, file: File): Observable<BinaryFileResource> { const formData: FormData = new FormData(); formData.append('file', file, file.name); - return this.httpClient.post(getUrl(resource, linkRel), formData, { observe: 'response' }); + return this.httpClient + .post(uri, formData, { observe: 'response' }) + .pipe(mergeMap((response: HttpResponse<Object>) => this.getFile(this.getLocation(response)))); + } + + private getLocation(response: HttpResponse<Object>): ResourceUri { + return response.headers.get(HttpHeader.LOCATION); + } + + /** + * @deprecated Use uploadFileNew instead + */ + public uploadFile(uri: ResourceUri, file: File): Observable<HttpResponse<Object>> { + const formData: FormData = new FormData(); + + formData.append('file', file, file.name); + + return this.httpClient.post(uri, formData, { observe: 'response' }); } public download(fileResource: BinaryFileResource): Observable<Blob> { - return this.doDownload( - getUrl(fileResource, BinaryFileLinkRel.DOWNLOAD), - this.buildRequestOptions(), - ); + return this.doDownload(getUrl(fileResource, BinaryFileLinkRel.DOWNLOAD), this.buildRequestOptions()); } buildRequestOptions(): GetRequestOptions { @@ -76,10 +86,7 @@ export class BinaryFileRepository { } buildPdfRequestOptions(): GetRequestOptions { - return this.buildBaseRequestOptions([ - ContentType.APPLICATION_PDF, - ContentType.APPLICATION_JSON, - ]); + return this.buildBaseRequestOptions([ContentType.APPLICATION_PDF, ContentType.APPLICATION_JSON]); } buildBaseRequestOptions(contentTypes: ContentType[]): GetRequestOptions { @@ -97,9 +104,7 @@ export class BinaryFileRepository { } public downloadArchive(uri: ResourceUri): Observable<BlobWithFileName> { - return this.httpClient - .get<HttpResponse<Blob>>(uri, this.buildRequestOptionsForArchive()) - .pipe(map(buildBlobWithFileName)); + return this.httpClient.get<HttpResponse<Blob>>(uri, this.buildRequestOptionsForArchive()).pipe(map(buildBlobWithFileName)); } buildRequestOptionsForArchive(): GetRequestOptions { diff --git a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts index 0f2d961a4b48fae71acf2ed14685e34697da1b22..9d26d35e6e9f7a882e0bad43d87f69fcb7562cc3 100644 --- a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts +++ b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.spec.ts @@ -21,22 +21,24 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { BlobWithFileName, StateResource, createEmptyStateResource, createStateResource } from '@alfa-client/tech-shared'; +import { BlobWithFileName, createEmptyStateResource, createStateResource, StateResource } from '@alfa-client/tech-shared'; import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { SnackBarService } from '@alfa-client/ui'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { fakeAsync, tick } from '@angular/core/testing'; import { faker } from '@faker-js/faker'; -import { Resource, ResourceUri } from '@ngxp/rest'; +import { expect } from '@jest/globals'; +import { getUrl, Resource, ResourceUri } from '@ngxp/rest'; import { cold, hot } from 'jest-marbles'; -import { createBinaryFileResource, createBlob } from 'libs/binary-file-shared/test/binary-file'; +import { createBinaryFileResource, createBlob, createFile, createUploadFile } from 'libs/binary-file-shared/test/binary-file'; import { VALIDATION_MESSAGES, ValidationMessageCode } from 'libs/tech-shared/src/lib/validation/tech.validation.messages'; import { DummyLinkRel } from 'libs/tech-shared/test/dummy'; import { createDummyResource } from 'libs/tech-shared/test/resource'; +import { uniqueId } from 'lodash-es'; import { Observable, of, throwError } from 'rxjs'; import { createHttpErrorResponse } from '../../../tech-shared/test/http'; -import { singleHot } from '../../../tech-shared/test/marbles'; -import { BinaryFileResource } from './binary-file.model'; +import { multipleCold, singleCold, singleHot } from '../../../tech-shared/test/marbles'; +import { BinaryFileResource, FileUploadType, ToUploadFile, UploadFile, UploadFileByIdentifier } from './binary-file.model'; import { BinaryFileRepository } from './binary-file.repository'; import { BinaryFileService } from './binary-file.service'; @@ -57,6 +59,283 @@ describe('BinaryFileService', () => { expect(service).toBeTruthy(); }); + describe('addFiles', () => { + const type: FileUploadType = faker.word.noun(); + const uniqueId: string = faker.word.noun(); + + beforeEach(() => { + service._generateUniqueId = jest.fn().mockReturnValue(uniqueId); + }); + + it('should not set uploaded file', () => { + service.addFiles(type, null); + + expect(service._uploadFiles$).toBeObservable(singleCold({})); + }); + + it('should generate unique id', () => { + const binaryFileResource: BinaryFileResource = createBinaryFileResource(); + + service.addFiles(type, [binaryFileResource]); + + expect(service._generateUniqueId).toHaveBeenCalled(); + }); + + it('should set uploaded file', () => { + const binaryFileResource: BinaryFileResource = createBinaryFileResource(); + + service.addFiles(type, [binaryFileResource]); + + const uploadedFile: UploadFile = service._uploadFiles$.value[type][uniqueId]; + expect(uploadedFile).toEqual({ uploadedFile: createStateResource(binaryFileResource) } as UploadFile); + }); + }); + + describe('upload file new', () => { + const uniqId: string = uniqueId(); + const type: string = 'dummyType'; + const file: File = createFile(); + const toUploadFile: ToUploadFile = { type, file, uploadUrl: faker.internet.url() }; + + beforeEach(() => { + service._generateUniqueId = jest.fn().mockReturnValue(uniqId); + service._addUploadFileLoading = jest.fn(); + service._doUploadFile = jest.fn(); + }); + + it('should create an empty map if type key not exists', () => { + service._uploadFiles$.next({}); + + service.uploadFileNew(toUploadFile); + + expect(service._uploadFiles$.value[type]).not.toBeUndefined(); + }); + + it('should call addUploadFileLoading', () => { + service.uploadFileNew(toUploadFile); + + expect(service._addUploadFileLoading).toHaveBeenCalledWith(uniqId, toUploadFile); + }); + + it('should call doUploadFile', () => { + service.uploadFileNew(toUploadFile); + + expect(service._doUploadFile).toHaveBeenCalledWith(uniqId, toUploadFile); + }); + }); + + describe('add upload file loading', () => { + const uniqId: string = uniqueId(); + const type: string = 'dummyType'; + const file: File = createFile(); + const toUploadFile: ToUploadFile = { type, file, uploadUrl: faker.internet.url() }; + + it('should add loading entry by type', () => { + service._addUploadFileLoading(uniqId, toUploadFile); + + expect((<UploadFileByIdentifier>service._uploadFiles$.value[type])[uniqId]).toEqual({ + fileToUpload: file, + uploadedFile: createEmptyStateResource(true), + }); + }); + + it('should add loading entry by type and keep existing entries', () => { + service._uploadFiles$.next({ + ...service._uploadFiles$.value, + [toUploadFile.type]: { + ['keepId']: { fileToUpload: toUploadFile.file, uploadedFile: createStateResource(createBinaryFileResource()) }, + }, + }); + + service._addUploadFileLoading(uniqId, toUploadFile); + + expect((<UploadFileByIdentifier>service._uploadFiles$.value[type])['keepId']).not.toBeUndefined(); + expect((<UploadFileByIdentifier>service._uploadFiles$.value[type])[uniqId]).not.toBeUndefined(); + }); + }); + + describe('do upload file', () => { + const uniqId: string = 'id'; + const type: string = 'dummyType'; + const file: File = createFile(); + const toUploadFile: ToUploadFile = { type, file, uploadUrl: faker.internet.url() }; + + const binaryFileResource: BinaryFileResource = createBinaryFileResource(); + + beforeEach(() => { + repository.uploadFileNew.mockReturnValue(of(binaryFileResource)); + service._handleError = jest.fn(); + service._updateUploadedFile = jest.fn(); + }); + + it('should call repository', () => { + service._doUploadFile(uniqId, toUploadFile); + + expect(repository.uploadFileNew).toHaveBeenCalledWith(toUploadFile.uploadUrl, file); + }); + + it('should call update uploaded file', () => { + service._doUploadFile(uniqId, toUploadFile); + + expect(service._updateUploadedFile).toHaveBeenCalledWith(uniqId, toUploadFile, createStateResource(binaryFileResource)); + }); + + it('should handle error', () => { + const errorResponse: HttpErrorResponse = createHttpErrorResponse(); + repository.uploadFileNew.mockReturnValue(throwError(() => errorResponse)); + + service._doUploadFile(uniqId, toUploadFile); + + expect(service._handleError).toHaveBeenCalledWith(errorResponse.error, false); + }); + + it('should update uploaded file on error', () => { + const errorResponse: HttpErrorResponse = createHttpErrorResponse(); + service._handleError = jest.fn().mockReturnValue(of(createStateResource(errorResponse.error))); + repository.uploadFileNew.mockReturnValue(throwError(() => errorResponse)); + + service._doUploadFile(uniqId, toUploadFile); + + expect(service._updateUploadedFile).toHaveBeenCalledWith(uniqId, toUploadFile, createStateResource(errorResponse.error)); + }); + }); + + describe('update upload file state', () => { + const type: string = 'dummyType'; + const file: File = createFile(); + const toUploadFile: ToUploadFile = { type, file, uploadUrl: faker.internet.url() }; + + const binaryFileResource: BinaryFileResource = createBinaryFileResource(); + + it('should set resource to state value', () => { + service._uploadFiles$.next({ + ...service._uploadFiles$.value, + [toUploadFile.type]: { + ['keepEntryId']: { fileToUpload: toUploadFile.file, uploadedFile: createStateResource(binaryFileResource) }, + ['updateEntryId']: { fileToUpload: toUploadFile.file, uploadedFile: createEmptyStateResource(true) }, + }, + }); + + service._updateUploadedFile('updateEntryId', toUploadFile, createStateResource(binaryFileResource)); + + expect((<UploadFileByIdentifier>service._uploadFiles$.value[type])['keepEntryId']).not.toBeUndefined(); + expect((<UploadFileByIdentifier>service._uploadFiles$.value[type])['updateEntryId']).not.toBeUndefined(); + }); + }); + + describe('get uploaded files', () => { + const type: FileUploadType = 'DummyType'; + const uploadFile: UploadFile = createUploadFile(); + + it('should return uploaded files by key', (done) => { + service._uploadFiles$.next({ + ...service._uploadFiles$.value, + [type]: { + ['keepEntryId']: uploadFile, + }, + }); + + service.getUploadedFiles(type).subscribe((uploadedFiles: UploadFileByIdentifier) => { + expect(uploadedFiles).toEqual({ ['keepEntryId']: uploadFile }); + done(); + }); + }); + + describe('on non existing key', () => { + beforeEach(() => { + service._uploadFiles$.next({}); + }); + + it('should return empty object', (done) => { + service.getUploadedFiles(type).subscribe((uploadedFiles: UploadFileByIdentifier) => { + expect(uploadedFiles).toEqual({}); + done(); + }); + }); + + it('should set state value', () => { + service.getUploadedFiles(type).subscribe(); + + expect(service._uploadFiles$.value[type]).toEqual({}); + }); + }); + }); + + describe('is upload in progress', () => { + const type: FileUploadType = 'DummyType'; + const loadingUploadedFileEntry = { + fileToUpload: createFile(), + uploadedFile: createEmptyStateResource<BinaryFileResource>(true), + }; + const loadedUploadedFileEntry = { + fileToUpload: createFile(), + uploadedFile: createStateResource<BinaryFileResource>(createBinaryFileResource()), + }; + + it('should return false on empty state', () => { + const uploadInProgress: Observable<boolean> = service.isUploadInProgress(type); + + expect(uploadInProgress).toBeObservable(singleCold(false)); + }); + + it('should return true if uploadedFiles contains loading stateResource by key', () => { + service._uploadFiles$.next({ + ...service._uploadFiles$.value, + [type]: { ['loadingEntryId']: loadingUploadedFileEntry, ['loadedEntryId']: loadedUploadedFileEntry }, + }); + + const uploadInProgress: Observable<boolean> = service.isUploadInProgress(type); + + expect(uploadInProgress).toBeObservable(singleCold(true)); + }); + + it('should return false if uploadedFiles contains loaded stateResources only', () => { + service._uploadFiles$.next({ + ...service._uploadFiles$.value, + [type]: { ['loadedEntry1Id']: loadedUploadedFileEntry, ['loadedEntry2Id']: loadedUploadedFileEntry }, + }); + + const uploadInProgress: Observable<boolean> = service.isUploadInProgress(type); + + expect(uploadInProgress).toBeObservable(singleCold(false)); + }); + }); + + describe('delete uploaded file', () => { + const type: string = 'dummyType'; + const file: File = createFile(); + const toUploadFile: ToUploadFile = { type, file, uploadUrl: faker.internet.url() }; + + it('should remove entry by type and key', () => { + service._uploadFiles$.next({ + ...service._uploadFiles$.value, + [toUploadFile.type]: { + ['keepEntryId']: { fileToUpload: toUploadFile.file, uploadedFile: createStateResource(createBinaryFileResource()) }, + ['toRemoveEntryId']: { + fileToUpload: toUploadFile.file, + uploadedFile: createStateResource(createBinaryFileResource()), + }, + }, + }); + + service.deleteUploadedFile(toUploadFile.type, 'toRemoveEntryId'); + + expect((<UploadFileByIdentifier>service._uploadFiles$.value[type])['keepEntryId']).not.toBeUndefined(); + expect((<UploadFileByIdentifier>service._uploadFiles$.value[type])['toRemoveEntryId']).toBeUndefined(); + }); + }); + + describe('clear uploaded files', () => { + it('should remove map by type from state', () => { + const type: FileUploadType = 'dummyType'; + service._uploadFiles$.next({ ...service._uploadFiles$.value, [type]: {} }); + + service.clearUploadedFiles(type); + + expect(service._uploadFiles$.value.hasOwnProperty(type)).toBeFalsy(); + }); + }); + describe('download file', () => { const binaryFileResource: BinaryFileResource = createBinaryFileResource(); const downloadNamePrefix: string = 'VorgangsNummerAsPrefix'; @@ -79,12 +358,7 @@ describe('BinaryFileService', () => { const returnValue: Observable<StateResource<Blob>> = service.downloadFile(binaryFileResource, downloadNamePrefix); - expect(returnValue).toBeObservable( - cold('ab', { - a: createEmptyStateResource(true), - b: createStateResource(blob), - }), - ); + expect(returnValue).toBeObservable(multipleCold(createEmptyStateResource(true), createStateResource(blob))); }); describe('save data', () => { @@ -203,8 +477,8 @@ describe('BinaryFileService', () => { }); describe('upload file', () => { - const dummyResource: Resource = createDummyResource(); const dummyLinkRel: string = DummyLinkRel.DUMMY; + const dummyResource: Resource = createDummyResource([dummyLinkRel]); const fileLocation: string = 'fileLocation'; const uploadFileResponse: HttpResponse<Object> = <any>{ @@ -222,7 +496,7 @@ describe('BinaryFileService', () => { it('should call repository', () => { service.uploadFile(dummyResource, dummyLinkRel, testFile); - expect(repository.uploadFile).toHaveBeenCalledWith(dummyResource, dummyLinkRel, testFile); + expect(repository.uploadFile).toHaveBeenCalledWith(getUrl(dummyResource, dummyLinkRel), testFile); }); it.skip('should call get file', fakeAsync(() => { diff --git a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts index e06c03dff2f58d8e33467a52c08fc6a5f5e2f047..223db64a230da606052caaf48a90dc45cd60f6a8 100644 --- a/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts +++ b/alfa-client/libs/binary-file-shared/src/lib/binary-file.service.ts @@ -21,52 +21,131 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { - BlobWithFileName, - EMPTY_STRING, - HttpHeader, - StateResource, - createEmptyStateResource, - createErrorStateResource, - createStateResource, - getMessageForInvalidParam, - isNotNil, - isUnprocessableEntity, - isValidationFieldFileSizeExceedError, - sanitizeFileName, -} from '@alfa-client/tech-shared'; +import { BlobWithFileName, createEmptyStateResource, createErrorStateResource, createStateResource, EMPTY_STRING, getMessageForInvalidParam, HttpHeader, isNotNil, isUnprocessableEntity, isValidationFieldFileSizeExceedError, sanitizeFileName, StateResource, } from '@alfa-client/tech-shared'; import { SnackBarService } from '@alfa-client/ui'; import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Resource, ResourceUri } from '@ngxp/rest'; +import { getUrl, Resource, ResourceUri } from '@ngxp/rest'; import { saveAs } from 'file-saver'; -import { isNil } from 'lodash-es'; -import { Observable, of, throwError } from 'rxjs'; -import { catchError, map, mergeMap, startWith } from 'rxjs/operators'; -import { BinaryFileListResource, BinaryFileResource } from './binary-file.model'; +import { isNil, uniqueId } from 'lodash-es'; +import { BehaviorSubject, Observable, of, throwError } from 'rxjs'; +import { catchError, first, map, mergeMap, startWith } from 'rxjs/operators'; +import { BinaryFileListResource, BinaryFileResource, FileUploadType, ToUploadFile, UploadFile, UploadFileByIdentifier, UploadFilesByType, } from './binary-file.model'; import { BinaryFileRepository } from './binary-file.repository'; @Injectable({ providedIn: 'root' }) export class BinaryFileService { + _uploadFiles$: BehaviorSubject<UploadFilesByType> = new BehaviorSubject({}); + constructor( private repository: BinaryFileRepository, private snackbarService: SnackBarService, ) {} + public addFiles(type: FileUploadType, binaryFileResources: BinaryFileResource[]): void { + if (isNil(binaryFileResources)) return; + binaryFileResources.forEach((resource: BinaryFileResource) => + this.setUploadedFile(this._generateUniqueId(), type, { uploadedFile: createStateResource(resource) }), + ); + } + + public uploadFileNew(toUploadFile: ToUploadFile): void { + this.createEmptyMapIfTypeNotExists(toUploadFile.type); + const uniqId: string = this._generateUniqueId(); + this._addUploadFileLoading(uniqId, toUploadFile); + this._doUploadFile(uniqId, toUploadFile); + } + + _generateUniqueId(): string { + return uniqueId(); + } + + _addUploadFileLoading(uniqId: string, toUploadFile: ToUploadFile): void { + this.setUploadedFile(uniqId, toUploadFile.type, { + fileToUpload: toUploadFile.file, + uploadedFile: createEmptyStateResource(true), + }); + } + + _doUploadFile(uniqId: string, toUploadFile: ToUploadFile): void { + this.repository + .uploadFileNew(toUploadFile.uploadUrl, toUploadFile.file) + .pipe( + first(), + map((resource: BinaryFileResource) => createStateResource(resource)), + catchError((errorResponse) => this._handleError(errorResponse.error, false)), + ) + .subscribe((stateResource: StateResource<BinaryFileResource>) => + this._updateUploadedFile(uniqId, toUploadFile, stateResource), + ); + } + + _updateUploadedFile( + uniqId: string, + toUploadFile: ToUploadFile, + binaryFileStateResource: StateResource<BinaryFileResource>, + ): void { + this.setUploadedFile(uniqId, toUploadFile.type, { + fileToUpload: toUploadFile.file, + uploadedFile: binaryFileStateResource, + }); + } + + private setUploadedFile(uniqId: string, type: FileUploadType, uploadedFile: UploadFile): void { + this._uploadFiles$.next({ + ...this._uploadFiles$.value, + [type]: { ...this._uploadFiles$.value[type], [uniqId]: uploadedFile }, + }); + } + + public getUploadedFiles(type: FileUploadType): Observable<UploadFileByIdentifier> { + this.createEmptyMapIfTypeNotExists(type); + return this._uploadFiles$.asObservable().pipe(map((files: UploadFilesByType) => files[type])); + } + + private createEmptyMapIfTypeNotExists(type: FileUploadType): void { + if (!(type in this._uploadFiles$.value)) this._uploadFiles$.value[type] = {}; + } + + public isUploadInProgress(type: FileUploadType): Observable<boolean> { + return this._uploadFiles$.asObservable().pipe( + map((files: UploadFilesByType) => Object.values(files[type] || []).map((file: UploadFile) => file.uploadedFile)), + map((files: StateResource<BinaryFileResource>[]) => + files.some((stateResource: StateResource<BinaryFileResource>) => stateResource.loading), + ), + ); + } + + public deleteUploadedFile(type: FileUploadType, key: string): void { + const currentMap: UploadFileByIdentifier = this._uploadFiles$.value[type]; + this._uploadFiles$.next({ + ...this._uploadFiles$.value, + [type]: Object.keys(currentMap).reduce((acc, uploadFileKey) => { + if (uploadFileKey !== key) acc[uploadFileKey] = currentMap[uploadFileKey]; + return acc; + }, {}), + }); + } + + public clearUploadedFiles(type: FileUploadType): void { + delete this._uploadFiles$.value[type]; + } + + //TODO Rename to uploadFileOld OR refactor all use cases to uploadFileNew public uploadFile( resource: Resource, linkRel: string, file: File, showValidationErrorSnackBar: boolean = true, ): Observable<StateResource<BinaryFileResource>> { - return this.repository.uploadFile(resource, linkRel, file).pipe( + return this.repository.uploadFile(getUrl(resource, linkRel), file).pipe( mergeMap((response: HttpResponse<Object>) => this.getFile(response.headers.get(HttpHeader.LOCATION))), - catchError((errorResponse) => this.handleError(errorResponse.error, showValidationErrorSnackBar)), + catchError((errorResponse) => this._handleError(errorResponse.error, showValidationErrorSnackBar)), startWith(createEmptyStateResource<BinaryFileResource>(true)), ); } - private handleError(errorResponse: HttpErrorResponse, showValidationErrorSnackBar: boolean): Observable<StateResource<any>> { + _handleError(errorResponse: HttpErrorResponse, showValidationErrorSnackBar: boolean): Observable<StateResource<any>> { return of(this.handleErrorByStatus(errorResponse, showValidationErrorSnackBar)); } diff --git a/alfa-client/libs/binary-file-shared/src/lib/binary-file.util.spec.ts b/alfa-client/libs/binary-file-shared/src/lib/binary-file.util.spec.ts deleted file mode 100644 index c0ef7f3e2161b4bf245df02ac4c85b8fe4e445a4..0000000000000000000000000000000000000000 --- a/alfa-client/libs/binary-file-shared/src/lib/binary-file.util.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den - * Ministerpräsidenten des Landes Schleswig-Holstein - * Staatskanzlei - * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung - * - * Lizenziert unter der EUPL, Version 1.2 oder - sobald - * diese von der Europäischen Kommission genehmigt wurden - - * Folgeversionen der EUPL ("Lizenz"); - * Sie dürfen dieses Werk ausschließlich gemäß - * dieser Lizenz nutzen. - * Eine Kopie der Lizenz finden Sie hier: - * - * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * Sofern nicht durch anwendbare Rechtsvorschriften - * gefordert oder in schriftlicher Form vereinbart, wird - * die unter der Lizenz verbreitete Software "so wie sie - * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - - * ausdrücklich oder stillschweigend - verbreitet. - * Die sprachspezifischen Genehmigungen und Beschränkungen - * unter der Lizenz sind dem Lizenztext zu entnehmen. - */ -import { StateResource, createStateResource } from '@alfa-client/tech-shared'; -import { - createBinaryFileListResource, - createBinaryFileResource, -} from 'libs/binary-file-shared/test/binary-file'; -import { BinaryFileListResource, BinaryFileResource } from './binary-file.model'; -import { getBinaryFiles } from './binary-file.util'; - -describe('BinaryFile Util', () => { - describe('getBinaryFiles', () => { - it('should extract resource', () => { - const binaryFileResources: BinaryFileResource[] = [createBinaryFileResource()]; - const binaryFileListStateResource: StateResource<BinaryFileListResource> = - createStateResource(createBinaryFileListResource(binaryFileResources)); - - const resources: BinaryFileResource[] = getBinaryFiles(binaryFileListStateResource); - - expect(resources).toEqual(binaryFileResources); - }); - }); -}); diff --git a/alfa-client/libs/binary-file-shared/src/lib/binary-file.util.ts b/alfa-client/libs/binary-file-shared/src/lib/binary-file.util.ts deleted file mode 100644 index 2f43cda4d54bbb7c13428cefc46a5580c6a63e7b..0000000000000000000000000000000000000000 --- a/alfa-client/libs/binary-file-shared/src/lib/binary-file.util.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2023 Das Land Schleswig-Holstein vertreten durch den - * Ministerpräsidenten des Landes Schleswig-Holstein - * Staatskanzlei - * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung - * - * Lizenziert unter der EUPL, Version 1.2 oder - sobald - * diese von der Europäischen Kommission genehmigt wurden - - * Folgeversionen der EUPL ("Lizenz"); - * Sie dürfen dieses Werk ausschließlich gemäß - * dieser Lizenz nutzen. - * Eine Kopie der Lizenz finden Sie hier: - * - * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * Sofern nicht durch anwendbare Rechtsvorschriften - * gefordert oder in schriftlicher Form vereinbart, wird - * die unter der Lizenz verbreitete Software "so wie sie - * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - - * ausdrücklich oder stillschweigend - verbreitet. - * Die sprachspezifischen Genehmigungen und Beschränkungen - * unter der Lizenz sind dem Lizenztext zu entnehmen. - */ -import { StateResource, getEmbeddedResources } from '@alfa-client/tech-shared'; -import { BinaryFileListLinkRel } from './binary-file.linkrel'; -import { BinaryFileListResource, BinaryFileResource } from './binary-file.model'; - -export function getBinaryFiles( - binaryFileListResource: StateResource<BinaryFileListResource>, -): BinaryFileResource[] { - return getEmbeddedResources(binaryFileListResource, BinaryFileListLinkRel.FILE_LIST); -} - -export enum BinaryFileIcon { - 'application/pdf' = 'pdf', - 'application/json' = 'json', - 'application/msword' = 'doc', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' = 'doc', - 'application/xml' = 'xml', - 'text/xml' = 'xml', - 'image/apng' = 'image', - 'image/gif' = 'image', - 'image/jpeg' = 'image', - 'image/png' = 'image', - 'image/svg+xml' = 'image', -} diff --git a/alfa-client/libs/binary-file-shared/test/binary-file.ts b/alfa-client/libs/binary-file-shared/test/binary-file.ts index 9ff52a575cc75251741005b7c38acacb30389b72..b0aca564400e4ee49266e1c529f89ce2346f211c 100644 --- a/alfa-client/libs/binary-file-shared/test/binary-file.ts +++ b/alfa-client/libs/binary-file-shared/test/binary-file.ts @@ -21,13 +21,15 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { GetRequestOptions, StateResource, createStateResource } from '@alfa-client/tech-shared'; +import { createStateResource, GetRequestOptions, StateResource } from '@alfa-client/tech-shared'; import { faker } from '@faker-js/faker'; import { BinaryFileListLinkRel } from 'libs/binary-file-shared/src/lib/binary-file.linkrel'; import { BinaryFile, BinaryFileListResource, BinaryFileResource, + ToUploadFile, + UploadFile, } from 'libs/binary-file-shared/src/lib/binary-file.model'; import { toResource } from 'libs/tech-shared/test/resource'; import { times } from 'lodash-es'; @@ -65,10 +67,35 @@ export function createLoadedBinaryFileResource(): StateResource<BinaryFileResour return createStateResource(createBinaryFileResource()); } +export function createGetRequestOptions(): GetRequestOptions { + return <GetRequestOptions>{}; +} + export function createBlob(): Blob { - return <Blob>{}; + const dummyData = new Uint8Array([65, 66, 67]); + return new Blob([dummyData], { type: 'text/plain' }); } -export function createGetRequestOptions(): GetRequestOptions { - return <GetRequestOptions>{}; +export function createFile(): File { + return { + ...createBlob(), + name: faker.string.alpha({ length: { min: 1, max: 50 } }), + lastModified: faker.date.past().getTime(), + webkitRelativePath: null, + }; +} + +export function createUploadFile(): UploadFile { + return { + fileToUpload: createFile(), + uploadedFile: createStateResource(createBinaryFileResource()), + }; +} + +export function createToUploadFile(): ToUploadFile { + return { + file: createFile(), + type: faker.word.noun(), + uploadUrl: faker.internet.url(), + }; } diff --git a/alfa-client/libs/binary-file/src/index.ts b/alfa-client/libs/binary-file/src/index.ts index 0de3c8e5665e8ef1080af949ee0833d3d7f6b762..0797fec271485748cb2070218373df404cf2f29b 100644 --- a/alfa-client/libs/binary-file/src/index.ts +++ b/alfa-client/libs/binary-file/src/index.ts @@ -27,5 +27,7 @@ export * from './lib/binary-file-list-container/binary-file-list-container.compo export * from './lib/binary-file-uri-container/binary-file-uri-container.component'; export * from './lib/binary-file.module'; export * from './lib/binary-file2-container/binary-file2-container.component'; +export * from './lib/file-upload-list-container/file-upload-list-container.component'; export * from './lib/horizontal-binary-file-list/horizontal-binary-file-list.component'; +export * from './lib/multi-file-upload-editor/multi-file-upload-editor.component'; export * from './lib/vertical-binary-file-list/vertical-binary-file-list.component'; diff --git a/alfa-client/libs/binary-file/src/lib/binary-file-attachment-container/binary-file-attachment-container.component.ts b/alfa-client/libs/binary-file/src/lib/binary-file-attachment-container/binary-file-attachment-container.component.ts index 895af8531079cc44bd5b1a76c691e9fd0c838be4..73d241e3fcf664c3a7dc19f1ac00d01a3079cfc3 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file-attachment-container/binary-file-attachment-container.component.ts +++ b/alfa-client/libs/binary-file/src/lib/binary-file-attachment-container/binary-file-attachment-container.component.ts @@ -21,15 +21,9 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Component, Input } from '@angular/core'; import { BinaryFileResource, BinaryFileService } from '@alfa-client/binary-file-shared'; -import { - EMPTY_ARRAY, - StateResource, - createEmptyStateResource, - doOnValidStateResource, - isNotNil, -} from '@alfa-client/tech-shared'; +import { StateResource, createEmptyStateResource, doOnValidStateResource, isNotNil } from '@alfa-client/tech-shared'; +import { Component, Input } from '@angular/core'; import { Resource, getUrl } from '@ngxp/rest'; import { Observable, of } from 'rxjs'; import { tap } from 'rxjs/operators'; @@ -44,11 +38,11 @@ export class BinaryFileAttachmentContainerComponent { @Input() uploadStateResource: StateResource<Resource>; @Input() linkRelUploadAttachment: string; @Input() set existFiles(value: BinaryFileResource[]) { - this.fileList = isNotNil(value) ? value : EMPTY_ARRAY; + this.fileList = isNotNil(value) ? value : []; } uploadInProgress$: Observable<StateResource<Resource>> = of(createEmptyStateResource<Resource>()); - fileList: BinaryFileResource[] = EMPTY_ARRAY; + fileList: BinaryFileResource[] = []; constructor(private binaryFileService: BinaryFileService) {} @@ -63,18 +57,11 @@ export class BinaryFileAttachmentContainerComponent { uploadAndGetFile(file: File): Observable<StateResource<BinaryFileResource>> { return this.binaryFileService .uploadFile(this.uploadStateResource.resource, this.linkRelUploadAttachment, file) - .pipe( - tap((stateResource: StateResource<BinaryFileResource>) => - this.doAfterFileUpload(stateResource), - ), - ); + .pipe(tap((stateResource: StateResource<BinaryFileResource>) => this.doAfterFileUpload(stateResource))); } doAfterFileUpload(stateResource: StateResource<BinaryFileResource>): void { - doOnValidStateResource( - stateResource, - () => (this.fileList = [...this.fileList, stateResource.resource]), - ); + doOnValidStateResource(stateResource, () => (this.fileList = [...this.fileList, stateResource.resource])); } public deleteAttachment(binaryFile: BinaryFileResource): void { diff --git a/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list-container.component.html b/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list-container.component.html index b6efd87e00828c83f8bb8b4a0ff6d3dfb509fa41..cfa93fbc6b3b9dbd01834a8d31c1bf74f4088299 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list-container.component.html +++ b/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list-container.component.html @@ -26,5 +26,6 @@ <ods-attachment-wrapper> <alfa-binary-file-list [binaryFileListStateResource]="binaryFileListStateResource$ | async" + [listOrientation]="listOrientation" ></alfa-binary-file-list> </ods-attachment-wrapper> diff --git a/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list-container.component.ts b/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list-container.component.ts index 4515dda8f1d00fb992b3b613e8dc9ab79360bf91..b4dce3b40b8590f8037ad669d5469f6c88942f93 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list-container.component.ts +++ b/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list-container.component.ts @@ -21,15 +21,12 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { - BinaryFileListLinkRel, - BinaryFileListResource, - BinaryFileService, -} from '@alfa-client/binary-file-shared'; +import { BinaryFileListResource, BinaryFileService } from '@alfa-client/binary-file-shared'; import { LinkRelationName, StateResource } from '@alfa-client/tech-shared'; import { Component, Input, OnInit } from '@angular/core'; import { Resource } from '@ngxp/rest'; import { Observable } from 'rxjs'; +import { BinaryFileListOrientation } from '../directive/binary-file-list-orientation/binary-file-list-orientation.directive'; @Component({ selector: 'alfa-binary-file-list-container', @@ -38,11 +35,10 @@ import { Observable } from 'rxjs'; export class BinaryFileListContainerComponent implements OnInit { @Input() resource: Resource; @Input() linkRel: LinkRelationName; + @Input() listOrientation: BinaryFileListOrientation = BinaryFileListOrientation.HORIZONTAL; public binaryFileListStateResource$: Observable<StateResource<BinaryFileListResource>>; - public readonly binaryFileListLinkRel = BinaryFileListLinkRel; - constructor(private service: BinaryFileService) {} ngOnInit(): void { diff --git a/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.html b/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.html index ca935c68da85787e1a6404452d9be4dd0c466cbf..70054de869940144f9b0e1d3fa0eb7c23ea72a33 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.html +++ b/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.html @@ -23,12 +23,11 @@ unter der Lizenz sind dem Lizenztext zu entnehmen. --> -<alfa-binary-file2-container - *ngFor=" - let binaryFile of binaryFileListStateResource.resource - | toEmbeddedResources: binaryFileListLinkRel.FILE_LIST - " - [file]="binaryFile" - [deletable]="false" -> -</alfa-binary-file2-container> +<div [binaryFileListOrientation]="listOrientation"> + <alfa-binary-file2-container + *ngFor="let binaryFile of binaryFileListStateResource.resource | toEmbeddedResources: binaryFileListLinkRel.FILE_LIST" + [file]="binaryFile" + [deletable]="false" + > + </alfa-binary-file2-container> +</div> diff --git a/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.spec.ts b/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.spec.ts index b90c651f44cba9b96f13aec0f6dec08348572bba..fbbc820600ee4d1b3a9235dafd3515f9c1f16404 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.spec.ts +++ b/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.spec.ts @@ -22,19 +22,13 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { BinaryFileListResource, BinaryFileResource } from '@alfa-client/binary-file-shared'; -import { - StateResource, - ToEmbeddedResourcesPipe, - createStateResource, -} from '@alfa-client/tech-shared'; +import { createStateResource, StateResource, ToEmbeddedResourcesPipe } from '@alfa-client/tech-shared'; import { getMockComponent } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { - createBinaryFileListResource, - createBinaryFileResource, -} from 'libs/binary-file-shared/test/binary-file'; -import { MockComponent } from 'ng-mocks'; +import { createBinaryFileListResource, createBinaryFileResource } from 'libs/binary-file-shared/test/binary-file'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { BinaryFile2ContainerComponent } from '../../binary-file2-container/binary-file2-container.component'; +import { BinaryFileListOrientationDirective } from '../../directive/binary-file-list-orientation/binary-file-list-orientation.directive'; import { BinaryFileListComponent } from './binary-file-list.component'; describe('BinaryFileListComponent', () => { @@ -52,6 +46,7 @@ describe('BinaryFileListComponent', () => { BinaryFileListComponent, ToEmbeddedResourcesPipe, MockComponent(BinaryFile2ContainerComponent), + MockDirective(BinaryFileListOrientationDirective), ], }).compileComponents(); @@ -67,15 +62,19 @@ describe('BinaryFileListComponent', () => { describe('binary file container', () => { it('should be called with file', () => { - const binaryFileContainerComponent: BinaryFile2ContainerComponent = - getMockComponent<BinaryFile2ContainerComponent>(fixture, BinaryFile2ContainerComponent); + const binaryFileContainerComponent: BinaryFile2ContainerComponent = getMockComponent<BinaryFile2ContainerComponent>( + fixture, + BinaryFile2ContainerComponent, + ); expect(binaryFileContainerComponent.file).toBe(binaryFile); }); it('should be called with deleteable', () => { - const binaryFileContainerComponent: BinaryFile2ContainerComponent = - getMockComponent<BinaryFile2ContainerComponent>(fixture, BinaryFile2ContainerComponent); + const binaryFileContainerComponent: BinaryFile2ContainerComponent = getMockComponent<BinaryFile2ContainerComponent>( + fixture, + BinaryFile2ContainerComponent, + ); expect(binaryFileContainerComponent.deletable).toBeFalsy(); }); diff --git a/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.ts b/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.ts index 33f6ef5e25a5b1a1dc06033c9354befc355cf204..f0ddb940344f0afeabe9b855b04a1d189b9f2840 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.ts +++ b/alfa-client/libs/binary-file/src/lib/binary-file-list-container/binary-file-list/binary-file-list.component.ts @@ -24,6 +24,7 @@ import { BinaryFileListLinkRel, BinaryFileListResource } from '@alfa-client/binary-file-shared'; import { StateResource } from '@alfa-client/tech-shared'; import { Component, Input } from '@angular/core'; +import { BinaryFileListOrientation } from '../../directive/binary-file-list-orientation/binary-file-list-orientation.directive'; @Component({ selector: 'alfa-binary-file-list', @@ -31,6 +32,7 @@ import { Component, Input } from '@angular/core'; }) export class BinaryFileListComponent { @Input() public binaryFileListStateResource: StateResource<BinaryFileListResource>; + @Input() listOrientation: BinaryFileListOrientation = BinaryFileListOrientation.HORIZONTAL; public readonly binaryFileListLinkRel = BinaryFileListLinkRel; } diff --git a/alfa-client/libs/binary-file/src/lib/binary-file.module.ts b/alfa-client/libs/binary-file/src/lib/binary-file.module.ts index 9327cbb0be7ef4527eff2aaf36b8f4ffac6e0cb3..362e1cbf5dc20d32ce56b18adbafad5e0ecd7b65 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file.module.ts +++ b/alfa-client/libs/binary-file/src/lib/binary-file.module.ts @@ -28,14 +28,7 @@ import { NgModule } from '@angular/core'; import { MatIcon } from '@angular/material/icon'; import { MatTooltip } from '@angular/material/tooltip'; import { DownloadButtonComponent } from '@ods/component'; -import { - AttachmentComponent, - AttachmentHeaderComponent, - AttachmentWrapperComponent, - CloseIconComponent, - SpinnerIconComponent, - TooltipDirective, -} from '@ods/system'; +import { AttachmentComponent, AttachmentHeaderComponent, AttachmentWrapperComponent, CloseIconComponent, SpinnerIconComponent, TooltipDirective, } from '@ods/system'; import { FileSizePlainPipe } from '../../../tech-shared/src/lib/pipe/file-size-plain.pipe'; import { FileUploadEditorComponent } from '../../../ui/src/lib/ui/editor/file-upload-editor/file-upload-editor.component'; import { BinaryFileAttachmentContainerComponent } from './binary-file-attachment-container/binary-file-attachment-container.component'; @@ -46,6 +39,7 @@ import { BinaryFileListComponent } from './binary-file-list-container/binary-fil import { BinaryFileUriContainerComponent } from './binary-file-uri-container/binary-file-uri-container.component'; import { BinaryFile2ContainerComponent } from './binary-file2-container/binary-file2-container.component'; import { BinaryFile2Component } from './binary-file2-container/binary-file2/binary-file2.component'; +import { BinaryFileListOrientationDirective } from './directive/binary-file-list-orientation/binary-file-list-orientation.directive'; import { DownloadArchiveFileButtonContainerComponent } from './download-archive-file-button-container/download-archive-file-button-container.component'; import { HorizontalBinaryFileListComponent } from './horizontal-binary-file-list/horizontal-binary-file-list.component'; import { VerticalBinaryFileListComponent } from './vertical-binary-file-list/vertical-binary-file-list.component'; @@ -70,6 +64,7 @@ import { VerticalBinaryFileListComponent } from './vertical-binary-file-list/ver ToEmbeddedResourcesPipe, FileSizePlainPipe, TooltipDirective, + BinaryFileListOrientationDirective, ], declarations: [ BinaryFileAttachmentContainerComponent, diff --git a/alfa-client/libs/binary-file/src/lib/binary-file2-container/binary-file2/binary-file2.component.html b/alfa-client/libs/binary-file/src/lib/binary-file2-container/binary-file2/binary-file2.component.html index 96927c6b043705cd333e9783282e98ceeca9bc79..28d0e68be052035a9f7b7ce8cdbb940fb3decda2 100644 --- a/alfa-client/libs/binary-file/src/lib/binary-file2-container/binary-file2/binary-file2.component.html +++ b/alfa-client/libs/binary-file/src/lib/binary-file2-container/binary-file2/binary-file2.component.html @@ -40,6 +40,7 @@ (click)="deleteFile($event)" title="Anhang löschen" aria-label="Anhang löschen Button" + data-test-class="delete-file-button" > <ods-close-icon class="fill-text"></ods-close-icon> </button> diff --git a/alfa-client/libs/binary-file/src/lib/directive/binary-file-list-orientation/binary-file-list-orientation.directive.spec.ts b/alfa-client/libs/binary-file/src/lib/directive/binary-file-list-orientation/binary-file-list-orientation.directive.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..82a862b8793d0ae55026acbcc19c83488eb0d610 --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/directive/binary-file-list-orientation/binary-file-list-orientation.directive.spec.ts @@ -0,0 +1,64 @@ +import { expect } from '@jest/globals'; +import { + _horizontalClasses, + _verticalClasses, + BinaryFileListOrientation, + BinaryFileListOrientationDirective, +} from './binary-file-list-orientation.directive'; + +describe('BinaryFileListOrientationDirective', () => { + let directive: BinaryFileListOrientationDirective; + let elementRef: any = {}; + + beforeEach(() => { + elementRef = { + nativeElement: { + classList: { + add: jest.fn(), + }, + }, + }; + directive = new BinaryFileListOrientationDirective(elementRef); + }); + + describe('set binaryFileListOrientation', () => { + beforeEach(() => { + directive._evaluateClasses = jest.fn().mockReturnValue([]); + }); + + it('should evaluate classes', () => { + directive.binaryFileListOrientation = BinaryFileListOrientation.HORIZONTAL; + + expect(directive._evaluateClasses).toHaveBeenCalledWith(BinaryFileListOrientation.HORIZONTAL); + }); + + it('should add classes', () => { + const classes: string[] = ['test']; + directive._evaluateClasses = jest.fn().mockReturnValue(classes); + + directive.binaryFileListOrientation = BinaryFileListOrientation.HORIZONTAL; + + expect(elementRef.nativeElement.classList.add).toHaveBeenCalledWith('test'); + }); + }); + + describe('_evaluateClasses', () => { + it('should return horizontal classes', () => { + const classes: string[] = directive._evaluateClasses(BinaryFileListOrientation.HORIZONTAL); + + expect(classes).toEqual(_horizontalClasses); + }); + + it('should return vertical classes', () => { + const classes: string[] = directive._evaluateClasses(BinaryFileListOrientation.VERTICAL); + + expect(classes).toEqual(_verticalClasses); + }); + + it('should return horizontal classes by default', () => { + const classes: string[] = directive._evaluateClasses(null); + + expect(classes).toEqual(_horizontalClasses); + }); + }); +}); diff --git a/alfa-client/libs/binary-file/src/lib/directive/binary-file-list-orientation/binary-file-list-orientation.directive.ts b/alfa-client/libs/binary-file/src/lib/directive/binary-file-list-orientation/binary-file-list-orientation.directive.ts new file mode 100644 index 0000000000000000000000000000000000000000..3a1a42958aca047046c5aa29edaed15dc77a1705 --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/directive/binary-file-list-orientation/binary-file-list-orientation.directive.ts @@ -0,0 +1,32 @@ +import { Directive, ElementRef, Input } from '@angular/core'; + +export const _verticalClasses: string[] = ['flex', 'flex-col']; +export const _horizontalClasses: string[] = ['flex', 'flex-row', 'flex-wrap']; + +export enum BinaryFileListOrientation { + HORIZONTAL = 'horizontal', + VERTICAL = 'vertical', +} + +@Directive({ + selector: '[binaryFileListOrientation]', + standalone: true, +}) +export class BinaryFileListOrientationDirective { + @Input() set binaryFileListOrientation(orientation: BinaryFileListOrientation) { + this.el.nativeElement.classList.add(...this._evaluateClasses(orientation)); + } + + constructor(private readonly el: ElementRef) {} + + _evaluateClasses(orientation: BinaryFileListOrientation): string[] { + switch (orientation) { + case BinaryFileListOrientation.VERTICAL: + return _verticalClasses; + case BinaryFileListOrientation.HORIZONTAL: + return _horizontalClasses; + default: + return _horizontalClasses; + } + } +} diff --git a/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-container.component.html b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-container.component.html new file mode 100644 index 0000000000000000000000000000000000000000..e95763af4b9cb08c7b3164618e4910199d2dafd5 --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-container.component.html @@ -0,0 +1,6 @@ +<ods-file-upload-list + [uploadedFiles]="uploadedFiles$ | async" + [parentFormArrayName]="parentFormArrayName" + [listOrientation]="listOrientation" + (delete)="onDelete($event)" +></ods-file-upload-list> diff --git a/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-container.component.spec.ts b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-container.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..379778effd26148200f2700038bb76a38fa23bb7 --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-container.component.spec.ts @@ -0,0 +1,134 @@ +import { BinaryFileListLinkRel, BinaryFileListResource, BinaryFileService, UploadFile } from '@alfa-client/binary-file-shared'; +import { createStateResource, getEmbeddedResources, StateResource } from '@alfa-client/tech-shared'; +import { mock, Mock } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { faker } from '@faker-js/faker'; +import { expect } from '@jest/globals'; +import { cold } from 'jest-marbles'; +import { MockComponent } from 'ng-mocks'; +import { EMPTY, of } from 'rxjs'; +import { createBinaryFileListResource, createUploadFile } from '../../../../binary-file-shared/test/binary-file'; +import { DummyLinkRel } from '../../../../tech-shared/test/dummy'; +import { singleColdCompleted } from '../../../../tech-shared/test/marbles'; +import { createDummyResource } from '../../../../tech-shared/test/resource'; +import { BinaryFileListOrientation } from '../directive/binary-file-list-orientation/binary-file-list-orientation.directive'; +import { FileUploadListContainerComponent } from './file-upload-list-container.component'; +import { FileUploadListComponent } from './file-upload-list/file-upload-list.component'; + +describe('FileUploadListContainerComponent', () => { + let component: FileUploadListContainerComponent; + let fixture: ComponentFixture<FileUploadListContainerComponent>; + + let binaryFileService: Mock<BinaryFileService>; + + beforeEach(() => { + binaryFileService = mock(BinaryFileService); + }); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileUploadListContainerComponent], + declarations: [MockComponent(FileUploadListComponent)], + providers: [{ provide: BinaryFileService, useValue: binaryFileService }], + }).compileComponents(); + + fixture = TestBed.createComponent(FileUploadListContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('component', () => { + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have default value', () => { + expect(component.listOrientation).toEqual(BinaryFileListOrientation.HORIZONTAL); + }); + + describe('ngOnInit', () => { + beforeEach(() => { + binaryFileService.getFiles.mockReturnValue(EMPTY); + }); + + describe('on existing files', () => { + beforeEach(() => { + component.filesResource = createDummyResource([DummyLinkRel.DUMMY]); + component.filesLinkRel = DummyLinkRel.DUMMY; + component.fileUploadType = faker.word.noun(); + }); + + it('should load existing files', () => { + component.filesResource = createDummyResource([DummyLinkRel.DUMMY]); + component.filesLinkRel = DummyLinkRel.DUMMY; + + component.ngOnInit(); + + expect(binaryFileService.getFiles).toHaveBeenCalledWith(component.filesResource, component.filesLinkRel); + }); + + it('should add loaded files', () => { + const stateResource: StateResource<BinaryFileListResource> = createStateResource(createBinaryFileListResource()); + binaryFileService.getFiles.mockReturnValue(of(stateResource)); + + component.ngOnInit(); + component.uploadedFiles$.subscribe(); + + expect(binaryFileService.addFiles).toHaveBeenCalledWith( + component.fileUploadType, + getEmbeddedResources(stateResource, BinaryFileListLinkRel.FILE_LIST), + ); + }); + + it('should get uploaded files', () => { + const stateResource: StateResource<BinaryFileListResource> = createStateResource(createBinaryFileListResource()); + binaryFileService.getFiles.mockReturnValue(of(stateResource)); + + component.ngOnInit(); + component.uploadedFiles$.subscribe(); + + expect(binaryFileService.getUploadedFiles).toHaveBeenCalledWith(component.fileUploadType); + }); + + it('should emit values', () => { + const stateResource: StateResource<BinaryFileListResource> = createStateResource(createBinaryFileListResource()); + binaryFileService.getFiles.mockReturnValue(cold('-a', { a: stateResource })); + const uploadedFiles: UploadFile[] = [createUploadFile()]; + binaryFileService.getUploadedFiles.mockReturnValue(cold('-a', { a: uploadedFiles })); + + component.ngOnInit(); + + expect(component.uploadedFiles$).toBeObservable(cold('a-b', { a: {}, b: uploadedFiles })); + }); + }); + + describe('on none existing files', () => { + beforeEach(() => { + component.filesResource = createDummyResource(); + component.filesLinkRel = DummyLinkRel.DUMMY; + }); + + it('should NOT load existing files', () => { + component.ngOnInit(); + + expect(binaryFileService.getFiles).not.toHaveBeenCalled(); + }); + + it('should get uploaded files', () => { + component.ngOnInit(); + + expect(binaryFileService.getUploadedFiles).toHaveBeenCalledWith(component.fileUploadType); + }); + + it('should set uploaded files', () => { + const uploadFile: UploadFile = createUploadFile(); + binaryFileService.getUploadedFiles = jest.fn().mockReturnValue(singleColdCompleted(uploadFile)); + + component.ngOnInit(); + + expect(component.uploadedFiles$).toBeObservable(singleColdCompleted(uploadFile)); + }); + }); + }); + }); +}); diff --git a/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-container.component.ts b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-container.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..b469cf13efa570da4d4e4ca4374d4a75043592fb --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-container.component.ts @@ -0,0 +1,52 @@ +import { + BinaryFileListLinkRel, + BinaryFileListResource, + BinaryFileResource, + BinaryFileService, + UploadFileByIdentifier, +} from '@alfa-client/binary-file-shared'; +import { getEmbeddedResources, isLoaded, StateResource } from '@alfa-client/tech-shared'; +import { AsyncPipe } from '@angular/common'; +import { Component, inject, Input, OnInit } from '@angular/core'; +import { hasLink, Resource } from '@ngxp/rest'; +import { filter, map, Observable, startWith, switchMap } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { BinaryFileListOrientation } from '../directive/binary-file-list-orientation/binary-file-list-orientation.directive'; +import { FileUploadListComponent } from './file-upload-list/file-upload-list.component'; + +@Component({ + selector: 'ods-file-upload-list-container', + standalone: true, + templateUrl: './file-upload-list-container.component.html', + imports: [FileUploadListComponent, AsyncPipe], +}) +export class FileUploadListContainerComponent implements OnInit { + @Input() fileUploadType: string; + @Input() parentFormArrayName: string; + @Input() listOrientation: BinaryFileListOrientation = BinaryFileListOrientation.HORIZONTAL; + @Input() filesResource: Resource; + @Input() filesLinkRel: string; + private readonly binaryFileService: BinaryFileService = inject(BinaryFileService); + + public uploadedFiles$: Observable<UploadFileByIdentifier>; + + ngOnInit(): void { + if (hasLink(this.filesResource, this.filesLinkRel)) { + this.uploadedFiles$ = this.binaryFileService.getFiles(this.filesResource, this.filesLinkRel).pipe( + filter(isLoaded), + map((files: StateResource<BinaryFileListResource>) => getEmbeddedResources(files, BinaryFileListLinkRel.FILE_LIST)), + tap((binaryFileResources: BinaryFileResource[]) => + this.binaryFileService.addFiles(this.fileUploadType, binaryFileResources), + ), + switchMap(() => this.binaryFileService.getUploadedFiles(this.fileUploadType)), + startWith({}), + ); + } else { + this.uploadedFiles$ = this.binaryFileService.getUploadedFiles(this.fileUploadType); + } + } + + public onDelete(key: string): void { + this.binaryFileService.deleteUploadedFile(this.fileUploadType, key); + } +} diff --git a/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-item/file-upload-list-item.component.html b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-item/file-upload-list-item.component.html new file mode 100644 index 0000000000000000000000000000000000000000..edb77f9c7dc133422248b79b4f99f1204900a46c --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-item/file-upload-list-item.component.html @@ -0,0 +1,20 @@ +@if (uploadFile.uploadedFile.loading || uploadFile.uploadedFile.error) { + <ods-attachment + [loadingCaption]="uploadFile.fileToUpload.name" + errorCaption="Fehler beim Hochladen" + [errorMessages]="uploadFile.uploadedFile.error | convertProblemDetailToErrorMessages" + description="Anhang wird hochgeladen" + [isLoading]="uploadFile.uploadedFile.loading" + data-test-id="file-upload-list-item-attachment-upload" + ></ods-attachment> +} @else if (uploadFile.uploadedFile.resource) { + <ods-attachment-wrapper> + <alfa-binary-file2-container + [file]="uploadFile.uploadedFile.resource" + [deletable]="true" + (startDelete)="delete.emit({ key, binaryFileResource: $event })" + data-test-id="file-upload-list-item-uploaded" + > + </alfa-binary-file2-container> + </ods-attachment-wrapper> +} diff --git a/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-item/file-upload-list-item.component.spec.ts b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-item/file-upload-list-item.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f60c72fda9d26b37343f543116eee01113fc394b --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-item/file-upload-list-item.component.spec.ts @@ -0,0 +1,153 @@ +import { BinaryFile2ContainerComponent, BinaryFileModule } from '@alfa-client/binary-file'; +import { BinaryFileResource } from '@alfa-client/binary-file-shared'; +import { + ConvertProblemDetailToErrorMessagesPipe, + createEmptyStateResource, + createErrorStateResource, + createStateResource, +} from '@alfa-client/tech-shared'; +import { + existsAsHtmlElement, + getElementComponentFromFixtureByCss, + notExistsAsHtmlElement, + triggerEvent, +} from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect } from '@jest/globals'; +import { AttachmentComponent } from '@ods/system'; +import { MockComponent, MockModule } from 'ng-mocks'; +import { + createBinaryFileResource, + createFile, + createLoadingBinaryFileStateResource, +} from '../../../../../binary-file-shared/test/binary-file'; +import { getDataTestIdOf } from '../../../../../tech-shared/test/data-test'; +import { createProblemDetail } from '../../../../../tech-shared/test/error'; +import { FileUploadListItemComponent } from './file-upload-list-item.component'; + +describe('FileUploadListItemComponent', () => { + let component: FileUploadListItemComponent; + let fixture: ComponentFixture<FileUploadListItemComponent>; + + const attachmentTestId: string = getDataTestIdOf('file-upload-list-item-attachment-upload'); + const binaryFileContainerTestId: string = getDataTestIdOf('file-upload-list-item-uploaded'); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileUploadListItemComponent, ConvertProblemDetailToErrorMessagesPipe], + declarations: [MockModule(BinaryFileModule), MockComponent(BinaryFile2ContainerComponent)], + }).compileComponents(); + + fixture = TestBed.createComponent(FileUploadListItemComponent); + component = fixture.componentInstance; + component.uploadFile = { uploadedFile: createEmptyStateResource() }; + fixture.detectChanges(); + }); + + describe('component', () => { + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('template', () => { + const file: File = createFile(); + + beforeEach(() => { + component.uploadFile.fileToUpload = file; + }); + + describe('attachment upload', () => { + it('should exists on loading', () => { + component.uploadFile.uploadedFile = createLoadingBinaryFileStateResource(); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, attachmentTestId); + }); + + it('should exists on error', () => { + component.uploadFile.uploadedFile = createErrorStateResource(createProblemDetail()); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, attachmentTestId); + }); + + it('should NOT exists on loaded', () => { + component.uploadFile.uploadedFile = createStateResource(createBinaryFileResource()); + + fixture.detectChanges(); + + notExistsAsHtmlElement(fixture, attachmentTestId); + }); + + it('should have inputs', () => { + component.uploadFile.fileToUpload = createFile(); + component.uploadFile.uploadedFile = createEmptyStateResource(true); + + fixture.detectChanges(); + + const attachmentComponent: AttachmentComponent = getElementComponentFromFixtureByCss(fixture, attachmentTestId); + + expect(attachmentComponent.loadingCaption).toEqual(component.uploadFile.fileToUpload.name); + expect(attachmentComponent.errorMessages).toEqual([]); + expect(attachmentComponent.isLoading).toEqual(true); + }); + }); + + describe('uploaded file', () => { + it('should exists', () => { + component.uploadFile.uploadedFile = createStateResource(createBinaryFileResource()); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, binaryFileContainerTestId); + }); + + it('should NOT exists', () => { + component.uploadFile.uploadedFile = createEmptyStateResource(true); + + fixture.detectChanges(); + + notExistsAsHtmlElement(fixture, binaryFileContainerTestId); + }); + + it('should have inputs', () => { + component.uploadFile.uploadedFile = createStateResource(createBinaryFileResource()); + + fixture.detectChanges(); + + const binaryFileComponent: BinaryFile2ContainerComponent = getElementComponentFromFixtureByCss( + fixture, + binaryFileContainerTestId, + ); + + expect(binaryFileComponent.file).toEqual(component.uploadFile.uploadedFile.resource); + expect(binaryFileComponent.deletable).toEqual(true); + }); + + describe('output', () => { + describe('startDelete', () => { + it('should emit', () => { + component.delete.emit = jest.fn(); + component.key = 'test'; + const binaryFileResource: BinaryFileResource = createBinaryFileResource(); + component.uploadFile.uploadedFile = createStateResource(binaryFileResource); + + fixture.detectChanges(); + + triggerEvent({ + fixture, + elementSelector: binaryFileContainerTestId, + name: 'startDelete', + data: binaryFileResource, + }); + + expect(component.delete.emit).toHaveBeenCalledWith({ key: component.key, binaryFileResource }); + }); + }); + }); + }); + }); + }); +}); diff --git a/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-item/file-upload-list-item.component.ts b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-item/file-upload-list-item.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..25527119bdaa6e87a88335557e8bf8e13054127d --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list-item/file-upload-list-item.component.ts @@ -0,0 +1,18 @@ +import { BinaryFileModule } from '@alfa-client/binary-file'; +import { FileToDelete, UploadFile } from '@alfa-client/binary-file-shared'; +import { ConvertProblemDetailToErrorMessagesPipe } from '@alfa-client/tech-shared'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { AttachmentComponent, AttachmentWrapperComponent } from '@ods/system'; + +@Component({ + selector: 'ods-file-upload-list-item', + standalone: true, + templateUrl: './file-upload-list-item.component.html', + imports: [AttachmentComponent, AttachmentWrapperComponent, BinaryFileModule, ConvertProblemDetailToErrorMessagesPipe], +}) +export class FileUploadListItemComponent { + @Input() key: string; + @Input() uploadFile: UploadFile; + + @Output() delete: EventEmitter<FileToDelete> = new EventEmitter(); +} diff --git a/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list/file-upload-list.component.html b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list/file-upload-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..b29089f98c4488b9be682850a39331f2cd46a607 --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list/file-upload-list.component.html @@ -0,0 +1,9 @@ +<div [binaryFileListOrientation]="listOrientation"> + @for (uploadItem of uploadItems | keyvalue; track uploadItem.key) { + <ods-file-upload-list-item + [key]="uploadItem.key" + [uploadFile]="uploadItem.value" + (delete)="onDelete($event)" + ></ods-file-upload-list-item> + } +</div> diff --git a/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list/file-upload-list.component.spec.ts b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list/file-upload-list.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3405027b7836f6a23dcd63d43f2bf00dbb7c07a5 --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list/file-upload-list.component.spec.ts @@ -0,0 +1,179 @@ +import { BinaryFileResource, UploadFileByIdentifier } from '@alfa-client/binary-file-shared'; +import { createEmptyStateResource, createStateResource, StateResource } from '@alfa-client/tech-shared'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroupDirective, UntypedFormArray, UntypedFormBuilder, UntypedFormControl } from '@angular/forms'; +import { faker } from '@faker-js/faker/.'; +import { getUrl } from '@ngxp/rest'; +import { createBinaryFileResource, createUploadFile } from 'libs/binary-file-shared/test/binary-file'; +import { FileUploadListComponent } from './file-upload-list.component'; + +describe('FileUploadListComponent', () => { + let component: FileUploadListComponent; + let fixture: ComponentFixture<FileUploadListComponent>; + + const fb: UntypedFormBuilder = new UntypedFormBuilder(); + const formGroupDirective: FormGroupDirective = new FormGroupDirective([], []); + formGroupDirective.form = fb.group({ + attachments: fb.control(null), + }); + + const fileKey: string = '21'; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FileUploadListComponent], + providers: [ + { + provide: FormGroupDirective, + useValue: formGroupDirective, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(FileUploadListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('component', () => { + describe('set uploaded files', () => { + it('should update upload items', () => { + component._updateUploadItems = jest.fn(); + const uploadedFiles: UploadFileByIdentifier = { [fileKey]: createUploadFile() }; + + component.uploadedFiles = uploadedFiles; + + expect(component._updateUploadItems).toHaveBeenCalledWith(uploadedFiles); + }); + }); + + describe('ngOnInit', () => { + it('should set file link controls', () => { + component.parentFormArrayName = 'attachments'; + + component.ngOnInit(); + + expect(component._fileLinkControls).toEqual(formGroupDirective.form.get('attachments')); + }); + }); + + describe('_updateUploadItems', () => { + beforeEach(() => { + component._addFileUrl = jest.fn(); + component._updateForm = jest.fn(); + }); + + it('should set upload items', () => { + const uploadedFiles: UploadFileByIdentifier = { [fileKey]: createUploadFile() }; + + component._updateUploadItems(uploadedFiles); + + expect(component.uploadItems).toEqual(uploadedFiles); + }); + + it('should set file urls', () => { + const uploadStateResource: StateResource<BinaryFileResource> = createStateResource(createBinaryFileResource()); + const uploadedFiles: UploadFileByIdentifier = { [fileKey]: { ...createUploadFile(), uploadedFile: uploadStateResource } }; + + component._updateUploadItems(uploadedFiles); + + expect(component._fileUrls).toEqual([getUrl(uploadStateResource.resource)]); + }); + + it('should NOT add file url on loading', () => { + const uploadedFiles: UploadFileByIdentifier = { [fileKey]: { uploadedFile: createEmptyStateResource(true) } }; + + component._updateUploadItems(uploadedFiles); + + expect(component._fileUrls).toEqual([]); + }); + + it('should update form', () => { + const uploadedFiles: UploadFileByIdentifier = { [fileKey]: { uploadedFile: createEmptyStateResource(true) } }; + + component._updateUploadItems(uploadedFiles); + + expect(component._updateForm).toHaveBeenCalled(); + }); + }); + + describe('_addFileUrl', () => { + beforeEach(() => { + component._updateForm = jest.fn(); + }); + + it('should add file url', () => { + const binaryFileResource: BinaryFileResource = createBinaryFileResource(); + + component._addFileUrl(binaryFileResource); + + expect(component._fileUrls).toEqual([getUrl(binaryFileResource)]); + }); + + it('should update form', () => { + const binaryFileResource: BinaryFileResource = createBinaryFileResource(); + + component._addFileUrl(binaryFileResource); + + expect(component._updateForm).toHaveBeenCalled(); + }); + }); + + describe('_updateForm', () => { + beforeEach(() => { + component._fileLinkControls = new UntypedFormArray([new UntypedFormControl(faker.internet.url())]); + }); + + it('should clear file control list', () => { + component._fileUrls = [faker.internet.url()]; + + component._updateForm(); + + expect(component._fileLinkControls.length).toEqual(1); + }); + it('should update file control list', () => { + const fileUrl: string = faker.internet.url(); + component._fileUrls = [fileUrl]; + + component._updateForm(); + + expect(component._fileLinkControls.controls[0].value).toEqual(fileUrl); + }); + }); + + describe('onDelete', () => { + const binaryFileResource: BinaryFileResource = createBinaryFileResource(); + const fileUrl1: string = faker.internet.url(); + const fileUrl2: string = getUrl(binaryFileResource); + const key: string = faker.word.noun(); + + beforeEach(() => { + component.delete.emit = jest.fn(); + component._fileUrls = [fileUrl1, fileUrl2]; + component._updateForm = jest.fn(); + }); + + it('should update file urls', () => { + component.onDelete({ binaryFileResource, key }); + + expect(component._fileUrls).toEqual([fileUrl1]); + }); + + it('should emit', () => { + component.onDelete({ binaryFileResource, key }); + + expect(component.delete.emit).toHaveBeenCalledWith(key); + }); + + it('should update form', () => { + component.onDelete({ binaryFileResource, key }); + + expect(component._updateForm).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list/file-upload-list.component.ts b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list/file-upload-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..29b1cae5ed2a6ca63e6274a2fff1ddd96bdb212b --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/file-upload-list-container/file-upload-list/file-upload-list.component.ts @@ -0,0 +1,64 @@ +import { BinaryFileResource, FileToDelete, UploadFile, UploadFileByIdentifier } from '@alfa-client/binary-file-shared'; +import { isLoaded, StateResource } from '@alfa-client/tech-shared'; +import { KeyValuePipe } from '@angular/common'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormGroupDirective, UntypedFormArray, UntypedFormControl } from '@angular/forms'; +import { getUrl } from '@ngxp/rest'; +import { + BinaryFileListOrientation, + BinaryFileListOrientationDirective, +} from '../../directive/binary-file-list-orientation/binary-file-list-orientation.directive'; +import { FileUploadListItemComponent } from '../file-upload-list-item/file-upload-list-item.component'; + +@Component({ + selector: 'ods-file-upload-list', + standalone: true, + templateUrl: './file-upload-list.component.html', + imports: [FileUploadListItemComponent, KeyValuePipe, BinaryFileListOrientationDirective], +}) +export class FileUploadListComponent implements OnInit { + @Input() parentFormArrayName: string; + @Input() listOrientation: BinaryFileListOrientation; + + @Input() set uploadedFiles(value: UploadFileByIdentifier) { + this._updateUploadItems(value); + } + + @Output() delete: EventEmitter<string> = new EventEmitter(); + + public uploadItems: UploadFileByIdentifier; + + _fileLinkControls: UntypedFormArray = new UntypedFormArray([]); + _fileUrls: string[] = []; + + constructor(public parentForm: FormGroupDirective) {} + + ngOnInit(): void { + this._fileLinkControls = this.parentForm.form.get(this.parentFormArrayName) as UntypedFormArray; + } + + _updateUploadItems(uploadFiles: UploadFileByIdentifier): void { + this.uploadItems = uploadFiles; + this._fileUrls = Object.values(uploadFiles) + .map((value: UploadFile) => value.uploadedFile) + .filter(isLoaded) + .map((stateResource: StateResource<BinaryFileResource>) => getUrl(stateResource.resource)); + this._updateForm(); + } + + _addFileUrl(binaryFileResource: BinaryFileResource): void { + this._fileUrls = [...this._fileUrls, getUrl(binaryFileResource)]; + this._updateForm(); + } + + _updateForm(): void { + this._fileLinkControls.clear(); + this._fileUrls.forEach((link: string) => this._fileLinkControls.push(new UntypedFormControl(link))); + } + + public onDelete(fileToDelete: FileToDelete): void { + this._fileUrls = this._fileUrls.filter((url: string) => url !== getUrl(fileToDelete.binaryFileResource)); + this._updateForm(); + this.delete.emit(fileToDelete.key); + } +} diff --git a/alfa-client/libs/binary-file/src/lib/multi-file-upload-editor/multi-file-upload-editor.component.html b/alfa-client/libs/binary-file/src/lib/multi-file-upload-editor/multi-file-upload-editor.component.html new file mode 100644 index 0000000000000000000000000000000000000000..1bfa1fb53740999476b2e6b342777abec551f000 --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/multi-file-upload-editor/multi-file-upload-editor.component.html @@ -0,0 +1,13 @@ +<ods-file-upload-button + [id]="uploadButtonId" + [accept]="accept" + [attr.data-test-id]="(label | convertForDataTest) + '-file-upload-button'" + [multi]="true" + [isLoading]="isUploadInProgress$ | async" + class="relative w-72" + data-test-id="binary-file-upload" +> + <ods-spinner-icon spinner size="medium" /> + <ods-attachment-icon icon size="medium" /> + <p text class="text-center">{{ label }}</p> +</ods-file-upload-button> diff --git a/alfa-client/libs/binary-file/src/lib/multi-file-upload-editor/multi-file-upload-editor.component.spec.ts b/alfa-client/libs/binary-file/src/lib/multi-file-upload-editor/multi-file-upload-editor.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0620a5f50612f4eeb3243f599d57fc9e1d1e3353 --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/multi-file-upload-editor/multi-file-upload-editor.component.spec.ts @@ -0,0 +1,119 @@ +import { BinaryFileService, ToUploadFile } from '@alfa-client/binary-file-shared'; +import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; +import { existsAsHtmlElement, getElementComponentFromFixtureByCss, mock, Mock } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect } from '@jest/globals'; +import { getUrl, Resource } from '@ngxp/rest'; +import { FileUploadButtonComponent, SpinnerIconComponent } from '@ods/system'; +import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { createFileList } from '../../../../tech-shared/test/file'; +import { singleColdCompleted } from '../../../../tech-shared/test/marbles'; +import { createDummyResource } from '../../../../tech-shared/test/resource'; +import { MultiFileUploadEditorComponent } from './multi-file-upload-editor.component'; + +describe('MultiFileUploadEditorComponent', () => { + let component: MultiFileUploadEditorComponent; + let fixture: ComponentFixture<MultiFileUploadEditorComponent>; + + const uploadLinkRel: string = 'upload'; + const uploadResource: Resource = createDummyResource([uploadLinkRel]); + + const buttonTestId: string = getDataTestIdOf('Ein_Label-file-upload-button'); + + let binaryFileService: Mock<BinaryFileService>; + + beforeEach(() => { + binaryFileService = mock(BinaryFileService); + }); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + MultiFileUploadEditorComponent, + ConvertForDataTestPipe, + MockComponent(SpinnerIconComponent), + MockComponent(FileUploadButtonComponent), + ], + providers: [ + { + provide: BinaryFileService, + useValue: binaryFileService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(MultiFileUploadEditorComponent); + component = fixture.componentInstance; + component.uploadResource = uploadResource; + component.uploadLinkRelation = uploadLinkRel; + component.label = 'Ein Label'; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('component', () => { + describe('ngOnInit', () => { + it('should set upload in progress', () => { + binaryFileService.isUploadInProgress = jest.fn().mockReturnValue(of(true)); + + component.ngOnInit(); + + expect(component.isUploadInProgress$).toBeObservable(singleColdCompleted(true)); + }); + }); + + describe('onFilesUpload', () => { + beforeEach(() => { + component._uploadFiles = jest.fn(); + }); + + it('should upload files', () => { + const fileList: FileList = createFileList(); + + component.onFilesUpload(fileList); + + expect(component._uploadFiles).toHaveBeenCalledWith(fileList); + }); + }); + + describe('_uploadFiles', () => { + it('should call binary file service', () => { + const fileList: FileList = createFileList(); + + component._uploadFiles(fileList); + + expect(binaryFileService.uploadFileNew).toHaveBeenCalledWith({ + file: fileList.item(0), + type: component.fileUploadType, + uploadUrl: getUrl(uploadResource, uploadLinkRel), + } as ToUploadFile); + }); + }); + }); + + describe('template', () => { + describe('upload button', () => { + it('should exists', () => { + existsAsHtmlElement(fixture, buttonTestId); + }); + + it('should have inputs', () => { + binaryFileService.isUploadInProgress = jest.fn().mockReturnValue(of(true)); + component.ngOnInit(); + + fixture.detectChanges(); + const fileButtonComponent: FileUploadButtonComponent = getElementComponentFromFixtureByCss(fixture, buttonTestId); + + expect(fileButtonComponent.id).toEqual(component.uploadButtonId); + expect(fileButtonComponent.accept).toEqual(component.accept); + expect(fileButtonComponent.multi).toEqual(true); + expect(fileButtonComponent.isLoading).toEqual(true); + }); + }); + }); +}); diff --git a/alfa-client/libs/binary-file/src/lib/multi-file-upload-editor/multi-file-upload-editor.component.ts b/alfa-client/libs/binary-file/src/lib/multi-file-upload-editor/multi-file-upload-editor.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..f69f78656a70336abe6b7a9cd72d8c12051177f4 --- /dev/null +++ b/alfa-client/libs/binary-file/src/lib/multi-file-upload-editor/multi-file-upload-editor.component.ts @@ -0,0 +1,59 @@ +import { BinaryFileModule } from '@alfa-client/binary-file'; +import { BinaryFileService, FileUploadType } from '@alfa-client/binary-file-shared'; +import { KOMMENTAR_UPLOADED_ATTACHMENTS } from '@alfa-client/kommentar-shared'; +import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; +import { AsyncPipe } from '@angular/common'; +import { Component, HostListener, inject, Input, OnInit } from '@angular/core'; +import { ControlContainer, FormGroupDirective, ReactiveFormsModule } from '@angular/forms'; +import { getUrl, Resource } from '@ngxp/rest'; +import { AttachmentIconComponent, FileUploadButtonComponent, SpinnerIconComponent } from '@ods/system'; +import { uniqueId } from 'lodash-es'; +import { Observable } from 'rxjs'; + +@Component({ + selector: 'ods-multi-file-upload-editor', + templateUrl: './multi-file-upload-editor.component.html', + viewProviders: [{ provide: ControlContainer, useExisting: FormGroupDirective }], + standalone: true, + imports: [ + AsyncPipe, + FileUploadButtonComponent, + AttachmentIconComponent, + SpinnerIconComponent, + ReactiveFormsModule, + BinaryFileModule, + ConvertForDataTestPipe, + ], +}) +export class MultiFileUploadEditorComponent implements OnInit { + @Input() label: string = ''; + @Input() accept: string = '*/*'; + @Input() fileUploadType: FileUploadType; + @Input() uploadResource: Resource; + @Input() uploadLinkRelation: string; + + private readonly binaryFileService: BinaryFileService = inject(BinaryFileService); + + public isUploadInProgress$: Observable<boolean>; + + public readonly uploadButtonId: string = uniqueId(); + + ngOnInit(): void { + this.isUploadInProgress$ = this.binaryFileService.isUploadInProgress(KOMMENTAR_UPLOADED_ATTACHMENTS); + } + + @HostListener('change', ['$event.target.files']) + public onFilesUpload(fileList: FileList): void { + this._uploadFiles(fileList); + } + + _uploadFiles(fileList: FileList) { + for (let i = 0; i < fileList.length; i++) { + this.binaryFileService.uploadFileNew({ + file: fileList.item(i), + type: this.fileUploadType, + uploadUrl: getUrl(this.uploadResource, this.uploadLinkRelation), + }); + } + } +} diff --git a/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.html b/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.html index ece1c85988571d733845ce9403521c3b804d0c23..dc2b6cef68ea7358023c7791fea0eb6efbaf47bf 100644 --- a/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.html +++ b/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.html @@ -31,6 +31,7 @@ [accept]="accept" (click)="resetInput()" [disabled]="isLoading" + [multiple]="multi" [attr.data-test-id]="(id | convertForDataTest) + '-file-upload-input'" /> <label diff --git a/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.spec.ts b/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.spec.ts index fee4fa492f168106fce4c7f8c97dc6b31a956ddd..54b7d3b7dde522ce018e0efae8b4210f6e2e2cff 100644 --- a/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.spec.ts +++ b/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.spec.ts @@ -25,6 +25,7 @@ import { ConvertForDataTestPipe } from '@alfa-client/tech-shared'; import { getElementFromFixture } from '@alfa-client/test-utils'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { faker } from '@faker-js/faker'; +import { expect } from '@jest/globals'; import { getDataTestIdOf } from 'libs/tech-shared/test/data-test'; import { FileUploadButtonComponent } from './file-upload-button.component'; @@ -61,4 +62,15 @@ describe('FileUploadButtonComponent', () => { expect(component.resetInput).toHaveBeenCalled(); }); }); + + describe('template', () => { + it('should have inputs', () => { + component.multi = true; + fixture.detectChanges(); + + const inputElement: HTMLInputElement = getElementFromFixture(fixture, inputTestClass); + + expect(inputElement.multiple).toEqual(component.multi); + }); + }); }); diff --git a/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.ts b/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.ts index 3e111b19ad208b4b4505a67bb8ef343ae6de0d2e..2371918f9cdba62e0ea0ee3a5e315ee9f1d9e378 100644 --- a/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.ts +++ b/alfa-client/libs/design-system/src/lib/form/file-upload-button/file-upload-button.component.ts @@ -36,6 +36,7 @@ export class FileUploadButtonComponent { @Input({ required: true }) id!: string; @Input() isLoading: boolean = false; @Input() accept: string = '*/*'; + @Input() multi: boolean = false; @ViewChild('inputElement') inputElement: ElementRef = new ElementRef({}); diff --git a/alfa-client/libs/design-system/src/lib/icons/spinner-icon/spinner-icon.component.ts b/alfa-client/libs/design-system/src/lib/icons/spinner-icon/spinner-icon.component.ts index 503f6865258a83c1ce2073c288e47f68b87e99ef..d361292b08e026c2f05e245276dae53b82fa2805 100644 --- a/alfa-client/libs/design-system/src/lib/icons/spinner-icon/spinner-icon.component.ts +++ b/alfa-client/libs/design-system/src/lib/icons/spinner-icon/spinner-icon.component.ts @@ -30,13 +30,15 @@ import { IconVariants, iconVariants } from '../iconVariants'; selector: 'ods-spinner-icon', standalone: true, imports: [NgClass], - template: `<svg + template: ` + <svg xmlns="http://www.w3.org/2000/svg" [ngClass]="iconVariants({ size })" class="animate-spin fill-primary text-gray-200 dark:text-gray-600" aria-hidden="true" viewBox="0 0 100 100" fill="none" + data-test-class="spinner" > <path d="M100 50.59c0 27.615-22.386 50.001-50 50.001s-50-22.386-50-50 22.386-50 50-50 50 22.386 50 50Zm-90.919 0c0 22.6 18.32 40.92 40.919 40.92 22.599 0 40.919-18.32 40.919-40.92 0-22.598-18.32-40.918-40.919-40.918-22.599 0-40.919 18.32-40.919 40.919Z" @@ -46,7 +48,8 @@ import { IconVariants, iconVariants } from '../iconVariants'; d="M93.968 39.04c2.425-.636 3.894-3.128 3.04-5.486A50 50 0 0 0 41.735 1.279c-2.474.414-3.922 2.919-3.285 5.344.637 2.426 3.12 3.849 5.6 3.484a40.916 40.916 0 0 1 44.131 25.769c.902 2.34 3.361 3.802 5.787 3.165Z" /> </svg> - <span class="sr-only">Loading...</span> `, + <span class="sr-only">Loading...</span> + `, }) export class SpinnerIconComponent { @Input() size: IconVariants['size'] = 'full'; diff --git a/alfa-client/libs/kommentar-shared/src/lib/kommentar.model.ts b/alfa-client/libs/kommentar-shared/src/lib/kommentar.model.ts index f6109a2ea77cff02a183eddaa6a4178244c68e93..2a6aa6711ee3df877983926e9d2d8add9c5fe663 100644 --- a/alfa-client/libs/kommentar-shared/src/lib/kommentar.model.ts +++ b/alfa-client/libs/kommentar-shared/src/lib/kommentar.model.ts @@ -32,4 +32,7 @@ export interface Kommentar { } export interface KommentarResource extends Kommentar, Resource {} + export interface KommentarListResource extends ListResource {} + +export const KOMMENTAR_UPLOADED_ATTACHMENTS: string = 'kommentar_uploaded_attachments'; diff --git a/alfa-client/libs/kommentar-shared/src/lib/kommentar.service.spec.ts b/alfa-client/libs/kommentar-shared/src/lib/kommentar.service.spec.ts index 329ac2f142356ce68c2d841605541322ac458dbb..98a918c03eb9d525d9bc2977f1885838dd873da3 100644 --- a/alfa-client/libs/kommentar-shared/src/lib/kommentar.service.spec.ts +++ b/alfa-client/libs/kommentar-shared/src/lib/kommentar.service.spec.ts @@ -22,32 +22,20 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { BinaryFileService } from '@alfa-client/binary-file-shared'; -import { - CommandOrder, - CommandResource, - CommandService, - CreateCommand, -} from '@alfa-client/command-shared'; +import { CommandOrder, CommandResource, CommandService, CreateCommand } from '@alfa-client/command-shared'; import { NavigationService } from '@alfa-client/navigation-shared'; -import { - StateResource, - createEmptyStateResource, - createStateResource, -} from '@alfa-client/tech-shared'; +import { createEmptyStateResource, createStateResource, StateResource } from '@alfa-client/tech-shared'; import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { VorgangService, VorgangWithEingangResource } from '@alfa-client/vorgang-shared'; +import { expect } from '@jest/globals'; import { cold, hot } from 'jest-marbles'; import { CommandLinkRel } from 'libs/command-shared/src/lib/command.linkrel'; import { createCommandResource } from 'libs/command-shared/test/command'; -import { - createKommentar, - createKommentarListResource, - createKommentarResource, -} from 'libs/kommentar-shared/test/kommentar'; +import { createKommentar, createKommentarListResource, createKommentarResource } from 'libs/kommentar-shared/test/kommentar'; import { createVorgangWithEingangResource } from 'libs/vorgang-shared/test/vorgang'; import { of } from 'rxjs'; import { KommentarLinkRel, KommentarListLinkRel } from './kommentar.linkrel'; -import { Kommentar, KommentarListResource, KommentarResource } from './kommentar.model'; +import { Kommentar, KOMMENTAR_UPLOADED_ATTACHMENTS, KommentarListResource, KommentarResource } from './kommentar.model'; import { KommentarRepository } from './kommentar.repository'; import { KommentarService } from './kommentar.service'; @@ -120,9 +108,7 @@ describe('KommentarService', () => { it('should be return', () => { const result = service.createKommentar(kommentar); - expect(result).toBeObservable( - cold('ab', { a: createEmptyStateResource(true), b: commandStateResource }), - ); + expect(result).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: commandStateResource })); }); }); @@ -174,9 +160,7 @@ describe('KommentarService', () => { it('should be return', () => { const result = service.editKommentar(kommentarResource, kommentar); - expect(result).toBeObservable( - cold('ab', { a: createEmptyStateResource(true), b: commandStateResource }), - ); + expect(result).toBeObservable(cold('ab', { a: createEmptyStateResource(true), b: commandStateResource })); }); }); @@ -298,10 +282,7 @@ describe('KommentarService', () => { const kommentarResource = createKommentarResource([KommentarLinkRel.ATTACHMENTS]); service.getAttachments(kommentarResource); - expect(binaryFileService.getFiles).toHaveBeenCalledWith( - kommentarResource, - KommentarLinkRel.ATTACHMENTS, - ); + expect(binaryFileService.getFiles).toHaveBeenCalledWith(kommentarResource, KommentarLinkRel.ATTACHMENTS); }); it('should not be loaded if no link available', () => { @@ -315,9 +296,7 @@ describe('KommentarService', () => { it('should create new Kommentare', () => { const canCreateNewKommentar$ = cold('a', { a: true }); - const observable = service.canCreateNewKommentar( - createKommentarListResource([KommentarListLinkRel.CREATE_KOMMENTAR]), - ); + const observable = service.canCreateNewKommentar(createKommentarListResource([KommentarListLinkRel.CREATE_KOMMENTAR])); expect(observable).toBeObservable(canCreateNewKommentar$); }); @@ -326,9 +305,7 @@ describe('KommentarService', () => { const canCreateNewKommentar$ = cold('a', { a: false }); service.formularVisibility$.next(true); - const observable = service.canCreateNewKommentar( - createKommentarListResource([KommentarListLinkRel.CREATE_KOMMENTAR]), - ); + const observable = service.canCreateNewKommentar(createKommentarListResource([KommentarListLinkRel.CREATE_KOMMENTAR])); expect(observable).toBeObservable(canCreateNewKommentar$); }); @@ -342,4 +319,12 @@ describe('KommentarService', () => { expect(observable).toBeObservable(canCreateNewKommentar$); }); }); + + describe('clearUploadedFiles', () => { + it('should call binary file service', () => { + service.clearUploadedFiles(); + + expect(binaryFileService.clearUploadedFiles).toHaveBeenCalledWith(KOMMENTAR_UPLOADED_ATTACHMENTS); + }); + }); }); diff --git a/alfa-client/libs/kommentar-shared/src/lib/kommentar.service.ts b/alfa-client/libs/kommentar-shared/src/lib/kommentar.service.ts index 1fd80c539fcdbb6c58223c3969a18e577f88d7e1..27efa8ae9da9e852906f3917b070e27e9faeca2d 100644 --- a/alfa-client/libs/kommentar-shared/src/lib/kommentar.service.ts +++ b/alfa-client/libs/kommentar-shared/src/lib/kommentar.service.ts @@ -22,35 +22,25 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { BinaryFileListResource, BinaryFileService } from '@alfa-client/binary-file-shared'; -import { - CommandOrder, - CommandResource, - CommandService, - CreateCommand, - isDone, -} from '@alfa-client/command-shared'; +import { CommandOrder, CommandResource, CommandService, CreateCommand, isDone } from '@alfa-client/command-shared'; import { NavigationService } from '@alfa-client/navigation-shared'; -import { - StateResource, - createEmptyStateResource, - createStateResource, - doIfLoadingRequired, -} from '@alfa-client/tech-shared'; +import { createEmptyStateResource, createStateResource, doIfLoadingRequired, StateResource } from '@alfa-client/tech-shared'; import { VorgangResource, VorgangService } from '@alfa-client/vorgang-shared'; import { Injectable } from '@angular/core'; import { Params } from '@angular/router'; -import { Resource, hasLink } from '@ngxp/rest'; +import { hasLink, Resource } from '@ngxp/rest'; import { isNil } from 'lodash-es'; -import { BehaviorSubject, Observable, Subscription, of } from 'rxjs'; +import { BehaviorSubject, Observable, of, Subscription } from 'rxjs'; import { map, startWith, tap } from 'rxjs/operators'; import { KommentarLinkRel, KommentarListLinkRel } from './kommentar.linkrel'; -import { Kommentar, KommentarListResource, KommentarResource } from './kommentar.model'; +import { Kommentar, KOMMENTAR_UPLOADED_ATTACHMENTS, KommentarListResource, KommentarResource } from './kommentar.model'; import { KommentarRepository } from './kommentar.repository'; @Injectable({ providedIn: 'root' }) export class KommentarService { - readonly kommentarList$: BehaviorSubject<StateResource<KommentarListResource>> = - new BehaviorSubject(createEmptyStateResource<KommentarListResource>()); + readonly kommentarList$: BehaviorSubject<StateResource<KommentarListResource>> = new BehaviorSubject( + createEmptyStateResource<KommentarListResource>(), + ); readonly formularVisibility$: BehaviorSubject<boolean> = new BehaviorSubject(false); private navigationSub: Subscription; @@ -67,9 +57,7 @@ export class KommentarService { private listenToNavigation(): void { this.unsubscribe(); - this.navigationSub = this.navigationService - .urlChanged() - .subscribe((params: Params) => this.onNavigation(params)); + this.navigationSub = this.navigationService.urlChanged().subscribe((params: Params) => this.onNavigation(params)); } private unsubscribe(): void { @@ -89,9 +77,7 @@ export class KommentarService { this.kommentarList$.next({ ...this.kommentarList$.value, reload: true }); } - public getKommentareByVorgang( - vorgang: VorgangResource, - ): Observable<StateResource<KommentarListResource>> { + public getKommentareByVorgang(vorgang: VorgangResource): Observable<StateResource<KommentarListResource>> { doIfLoadingRequired(this.kommentarList$.value, () => this.loadKommentare(vorgang)); return this.kommentarList$.asObservable(); } @@ -99,12 +85,10 @@ export class KommentarService { private loadKommentare(vorgang: VorgangResource): void { this.setListLoadingTrue(); - const sub: Subscription = this.repository - .findKommentare(vorgang) - .subscribe((kommentarList: KommentarListResource) => { - this.setKommentarList(kommentarList); - sub.unsubscribe(); - }); + const sub: Subscription = this.repository.findKommentare(vorgang).subscribe((kommentarList: KommentarListResource) => { + this.setKommentarList(kommentarList); + sub.unsubscribe(); + }); } setListLoadingTrue(): void { @@ -125,11 +109,7 @@ export class KommentarService { public canCreateNewKommentar(kommentareListResource: KommentarListResource): Observable<boolean> { return this.formularVisibility$.pipe( - map( - (formularVisibility) => - !formularVisibility && - hasLink(kommentareListResource, KommentarListLinkRel.CREATE_KOMMENTAR), - ), + map((formularVisibility) => !formularVisibility && hasLink(kommentareListResource, KommentarListLinkRel.CREATE_KOMMENTAR)), ); } @@ -153,15 +133,8 @@ export class KommentarService { return { order: CommandOrder.CREATE_KOMMENTAR, body: kommentar }; } - public editKommentar( - kommentar: KommentarResource, - toPatch: Kommentar, - ): Observable<StateResource<CommandResource>> { - return this.createKommentarCommand( - kommentar, - KommentarLinkRel.EDIT, - this.createEditKommentarCommand(toPatch), - ); + public editKommentar(kommentar: KommentarResource, toPatch: Kommentar): Observable<StateResource<CommandResource>> { + return this.createKommentarCommand(kommentar, KommentarLinkRel.EDIT, this.createEditKommentarCommand(toPatch)); } createEditKommentarCommand(kommentar: Kommentar): CreateCommand { @@ -174,9 +147,7 @@ export class KommentarService { command: CreateCommand, ): Observable<StateResource<CommandResource>> { return this.commandService.createCommand(resource, linkRel, command).pipe( - tap((createdCommand: StateResource<CommandResource>) => - this.afterCreateOrEditKommentar(createdCommand), - ), + tap((createdCommand: StateResource<CommandResource>) => this.afterCreateOrEditKommentar(createdCommand)), startWith(createEmptyStateResource<CommandResource>(true)), ); } @@ -195,4 +166,8 @@ export class KommentarService { } return of(createEmptyStateResource<BinaryFileListResource>()); } + + public clearUploadedFiles(): void { + this.binaryFileService.clearUploadedFiles(KOMMENTAR_UPLOADED_ATTACHMENTS); + } } diff --git a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.html b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.html index b6c374e27ae36f03b027cf262faa477281b79346..8a5168bf1dc59de260e7e7a5ece9c9a6167153c1 100644 --- a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.html +++ b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.html @@ -24,21 +24,22 @@ --> <form class="form" [formGroup]="formService.form"> - <ozgcloud-textarea-editor - [formControlName]="formServiceClass.TEXT" - label="Kommentar" - [required]="true" - > + <ozgcloud-textarea-editor [formControlName]="formServiceClass.TEXT" label="Kommentar" [required]="true"> </ozgcloud-textarea-editor> - <alfa-binary-file-attachment-container - data-test-id="kommentar-attachment-list" - [existFiles]="attachments$ | async" - [formArrayName]="formServiceClass.FIELD_ATTACHMENTS" - [uploadStateResource]="kommentarListStateResource" - [linkRelUploadAttachment]="kommentarListLinkRel.UPLOAD_FILE" - > - </alfa-binary-file-attachment-container> + <ods-file-upload-list-container + [parentFormArrayName]="formServiceClass.FIELD_ATTACHMENTS" + [fileUploadType]="KOMMENTAR_UPLOADED_ATTACHMENTS" + [filesResource]="kommentar" + [filesLinkRel]="KommentarLinkRel.ATTACHMENTS" + data-test-id="kommentar-multi-file-upload-list" + ></ods-file-upload-list-container> + <ods-multi-file-upload-editor + [fileUploadType]="KOMMENTAR_UPLOADED_ATTACHMENTS" + [uploadResource]="kommentarListStateResource.resource" + [uploadLinkRelation]="kommentarListLinkRel.UPLOAD_FILE" + data-test-id="kommentar-multi-file-upload-editor" + ></ods-multi-file-upload-editor> <div class="buttons"> <ozgcloud-stroked-button-with-spinner @@ -57,7 +58,7 @@ icon="clear" color="" class="cancel-button" - (clickEmitter)="cancel.emit()" + (clickEmitter)="onCancel()" > </ozgcloud-stroked-button-with-spinner> </div> diff --git a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.spec.ts b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.spec.ts index a8af3d1f1dc2208e0502f3fef2dfa759083fc1ed..15d300ef0ad665960ee7349b31d2678d60ac7c30 100644 --- a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.spec.ts +++ b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.spec.ts @@ -21,21 +21,24 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { BinaryFileAttachmentContainerComponent, FileUploadListContainerComponent, MultiFileUploadEditorComponent, } from '@alfa-client/binary-file'; +import { CommandResource } from '@alfa-client/command-shared'; +import { KOMMENTAR_UPLOADED_ATTACHMENTS, KommentarLinkRel, KommentarListLinkRel, KommentarListResource, KommentarService, } from '@alfa-client/kommentar-shared'; +import { createEmptyStateResource, createErrorStateResource, createStateResource, StateResource } from '@alfa-client/tech-shared'; +import { existsAsHtmlElement, getElementComponentFromFixtureByCss, Mock, mock, triggerEvent, useFromMock, } from '@alfa-client/test-utils'; +import { OzgcloudStrokedButtonWithSpinnerComponent, TextAreaEditorComponent } from '@alfa-client/ui'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; +import { FormBuilder, ReactiveFormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; -import { BinaryFileAttachmentContainerComponent } from '@alfa-client/binary-file'; -import { KommentarLinkRel, KommentarService } from '@alfa-client/kommentar-shared'; -import { createStateResource } from '@alfa-client/tech-shared'; -import { mock } from '@alfa-client/test-utils'; -import { - OzgcloudStrokedButtonWithSpinnerComponent, - TextAreaEditorComponent, -} from '@alfa-client/ui'; +import { expect } from '@jest/globals'; import { MockComponent } from 'ng-mocks'; -import { of } from 'rxjs'; +import { EMPTY, of } from 'rxjs'; import { createBinaryFileListResource } from '../../../../../binary-file-shared/test/binary-file'; -import { createKommentarResource } from '../../../../../kommentar-shared/test/kommentar'; +import { createSuccessfullyDoneCommandStateResource } from '../../../../../command-shared/test/command'; +import { createKommentarListResource, createKommentarResource } from '../../../../../kommentar-shared/test/kommentar'; +import { getDataTestIdOf } from '../../../../../tech-shared/test/data-test'; +import { createProblemDetail } from '../../../../../tech-shared/test/error'; +import { singleColdCompleted } from '../../../../../tech-shared/test/marbles'; import { KommentarFormComponent } from './kommentar-form.component'; import { KommentarFormService } from './kommentar.formservice'; @@ -43,8 +46,19 @@ describe('KommentarFormComponent', () => { let component: KommentarFormComponent; let fixture: ComponentFixture<KommentarFormComponent>; - const formService = mock(KommentarFormService); - const kommentarService = mock(KommentarService); + const cancelButtonTestId: string = getDataTestIdOf('cancel-button'); + const fileUploadListTestId: string = getDataTestIdOf('kommentar-multi-file-upload-list'); + const fileUploadEditorTestId: string = getDataTestIdOf('kommentar-multi-file-upload-editor'); + + const kommentarListStateResource: StateResource<KommentarListResource> = createStateResource(createKommentarListResource()); + + let kommentarService: Mock<KommentarService>; + let formService: KommentarFormService; + + beforeEach(() => { + formService = new KommentarFormService(new FormBuilder(), useFromMock(kommentarService)); + kommentarService = mock(KommentarService); + }); beforeEach(async () => { await TestBed.configureTestingModule({ @@ -53,24 +67,32 @@ describe('KommentarFormComponent', () => { MockComponent(TextAreaEditorComponent), MockComponent(OzgcloudStrokedButtonWithSpinnerComponent), MockComponent(BinaryFileAttachmentContainerComponent), + MockComponent(FileUploadListContainerComponent), + MockComponent(MultiFileUploadEditorComponent), ], imports: [MatFormFieldModule, ReactiveFormsModule], providers: [ - { - provide: KommentarFormService, - useValue: formService, - }, { provide: KommentarService, useValue: kommentarService, }, ], - }); - }); + }) + .overrideComponent(KommentarFormComponent, { + set: { + providers: [ + { + provide: KommentarFormService, + useValue: formService, + }, + ], + }, + }) + .compileComponents(); - beforeEach(() => { fixture = TestBed.createComponent(KommentarFormComponent); component = fixture.componentInstance; + component.kommentarListStateResource = kommentarListStateResource; fixture.detectChanges(); }); @@ -82,9 +104,7 @@ describe('KommentarFormComponent', () => { const patchSpy = jest.spyOn(KommentarFormService.prototype, 'patch').mockImplementation(); const kommentarResource = createKommentarResource([KommentarLinkRel.ATTACHMENTS]); component.kommentar = kommentarResource; - kommentarService.getAttachments.mockReturnValue( - of(createStateResource(createBinaryFileListResource())), - ); + kommentarService.getAttachments.mockReturnValue(of(createStateResource(createBinaryFileListResource()))); component.ngOnChanges(); @@ -93,9 +113,7 @@ describe('KommentarFormComponent', () => { it('should load attachments', (done) => { component.kommentar = createKommentarResource([KommentarLinkRel.ATTACHMENTS]); - kommentarService.getAttachments.mockReturnValue( - of(createStateResource(createBinaryFileListResource())), - ); + kommentarService.getAttachments.mockReturnValue(of(createStateResource(createBinaryFileListResource()))); component.ngOnChanges(); @@ -108,12 +126,131 @@ describe('KommentarFormComponent', () => { it('should call kommentarService', () => { const kommentarResource = createKommentarResource([KommentarLinkRel.ATTACHMENTS]); component.kommentar = kommentarResource; - kommentarService.getAttachments.mockReturnValue( - of(createStateResource(createBinaryFileListResource())), - ); + kommentarService.getAttachments.mockReturnValue(of(createStateResource(createBinaryFileListResource()))); component.ngOnChanges(); expect(kommentarService.getAttachments).toHaveBeenCalledWith(kommentarResource); }); + + describe('submit', () => { + it('should submit form', () => { + formService.submit = jest.fn().mockReturnValue(EMPTY); + + component.submit(); + + expect(formService.submit).toHaveBeenCalled(); + }); + + it('should set submit in progress', () => { + const command: StateResource<CommandResource> = createSuccessfullyDoneCommandStateResource(); + formService.submit = jest.fn().mockReturnValue(of(command)); + + component.submit(); + + expect(component.submitInProgress$).toBeObservable(singleColdCompleted(command)); + }); + + it('should clear uploaded attachments', () => { + formService.submit = jest.fn().mockReturnValue(of(createSuccessfullyDoneCommandStateResource())); + + component.submit(); + component.submitInProgress$.subscribe(); + + expect(kommentarService.clearUploadedFiles).toHaveBeenCalled(); + }); + + it('should NOT clear uploaded attachments on loading', () => { + formService.submit = jest.fn().mockReturnValue(of(createEmptyStateResource(true))); + + component.submit(); + component.submitInProgress$.subscribe(); + + expect(kommentarService.clearUploadedFiles).not.toHaveBeenCalled(); + }); + + it('should NOT clear uploaded attachments on error', () => { + formService.submit = jest.fn().mockReturnValue(of(createErrorStateResource(createProblemDetail()))); + + component.submit(); + component.submitInProgress$.subscribe(); + + expect(kommentarService.clearUploadedFiles).not.toHaveBeenCalled(); + }); + }); + + describe('onCancel', () => { + beforeEach(() => { + component.cancel.emit = jest.fn(); + }); + + it('should clear uploaded attachments', () => { + component.onCancel(); + + expect(kommentarService.clearUploadedFiles).toHaveBeenCalled(); + }); + + it('should emit', () => { + component.onCancel(); + + expect(component.cancel.emit).toHaveBeenCalled(); + }); + }); + + describe('template', () => { + describe('file upload list', () => { + it('should exists', () => { + existsAsHtmlElement(fixture, fileUploadListTestId); + }); + + it('should have inputs', () => { + fixture.detectChanges(); + + const fileUploadListComponent: FileUploadListContainerComponent = getElementComponentFromFixtureByCss( + fixture, + fileUploadListTestId, + ); + + expect(fileUploadListComponent.parentFormArrayName).toEqual(KommentarFormService.FIELD_ATTACHMENTS); + expect(fileUploadListComponent.fileUploadType).toEqual(KOMMENTAR_UPLOADED_ATTACHMENTS); + }); + }); + + describe('file upload editor', () => { + it('should exists', () => { + existsAsHtmlElement(fixture, fileUploadEditorTestId); + }); + + it('should have inputs', () => { + fixture.detectChanges(); + + const fileUploadEditorComponent: MultiFileUploadEditorComponent = getElementComponentFromFixtureByCss( + fixture, + fileUploadEditorTestId, + ); + + expect(fileUploadEditorComponent.fileUploadType).toEqual(KOMMENTAR_UPLOADED_ATTACHMENTS); + expect(fileUploadEditorComponent.uploadResource).toEqual(component.kommentarListStateResource.resource); + expect(fileUploadEditorComponent.uploadLinkRelation).toEqual(KommentarListLinkRel.UPLOAD_FILE); + }); + }); + + describe('cancel button', () => { + it('should exists', () => { + existsAsHtmlElement(fixture, cancelButtonTestId); + }); + + describe('output', () => { + describe('clickEmitter', () => { + it('should call handler', () => { + component.onCancel = jest.fn(); + + triggerEvent({ fixture, elementSelector: cancelButtonTestId, name: 'clickEmitter' }); + + expect(component.onCancel).toHaveBeenCalled(); + }); + }); + }); + }); + }); }); diff --git a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.ts b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.ts index 473d6a84137c0f56dac572f9992c6e6830c5ba44..0566d0f19efcc3e09aff3d5a726465eae21371d9 100644 --- a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.ts +++ b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component.ts @@ -21,24 +21,15 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { BinaryFileListLinkRel, BinaryFileResource } from '@alfa-client/binary-file-shared'; +import { CommandResource, tapOnCommandSuccessfullyDone } from '@alfa-client/command-shared'; +import { KOMMENTAR_UPLOADED_ATTACHMENTS, KommentarLinkRel, KommentarListLinkRel, KommentarListResource, KommentarResource, KommentarService, } from '@alfa-client/kommentar-shared'; +import { createEmptyStateResource, getEmbeddedResources, StateResource } from '@alfa-client/tech-shared'; import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; -import { CommandResource } from '@alfa-client/command-shared'; -import { - KommentarListLinkRel, - KommentarListResource, - KommentarResource, - KommentarService, -} from '@alfa-client/kommentar-shared'; -import { - createEmptyStateResource, - getEmbeddedResources, - StateResource, -} from '@alfa-client/tech-shared'; import { isNil } from 'lodash-es'; import { Observable, of } from 'rxjs'; -import { KommentarFormService } from './kommentar.formservice'; -import { BinaryFileListLinkRel, BinaryFileResource } from '@alfa-client/binary-file-shared'; import { map } from 'rxjs/operators'; +import { KommentarFormService } from './kommentar.formservice'; @Component({ selector: 'alfa-kommentar-form', @@ -52,12 +43,12 @@ export class KommentarFormComponent implements OnChanges { @Output() cancel: EventEmitter<void> = new EventEmitter(); - submitInProgress$: Observable<StateResource<CommandResource>> = of( - createEmptyStateResource<CommandResource>(), - ); + submitInProgress$: Observable<StateResource<CommandResource>> = of(createEmptyStateResource<CommandResource>()); - readonly formServiceClass = KommentarFormService; - readonly kommentarListLinkRel = KommentarListLinkRel; + public readonly formServiceClass = KommentarFormService; + public readonly kommentarListLinkRel = KommentarListLinkRel; + public readonly KommentarLinkRel = KommentarLinkRel; + public readonly KOMMENTAR_UPLOADED_ATTACHMENTS = KOMMENTAR_UPLOADED_ATTACHMENTS; attachments$: Observable<BinaryFileResource[]> = of([]); @@ -76,18 +67,21 @@ export class KommentarFormComponent implements OnChanges { private updateAttachments() { this.attachments$ = this.kommentarService .getAttachments(this.kommentar) - .pipe( - map((stateResource) => - getEmbeddedResources<BinaryFileResource>(stateResource, BinaryFileListLinkRel.FILE_LIST), - ), - ); + .pipe(map((stateResource) => getEmbeddedResources<BinaryFileResource>(stateResource, BinaryFileListLinkRel.FILE_LIST))); } patch(): void { this.formService.patch(this.kommentar); } - submit(): void { - this.submitInProgress$ = <Observable<StateResource<CommandResource>>>this.formService.submit(); + public submit(): void { + this.submitInProgress$ = <Observable<StateResource<CommandResource>>>( + this.formService.submit().pipe(tapOnCommandSuccessfullyDone(() => this.kommentarService.clearUploadedFiles())) + ); + } + + public onCancel(): void { + this.kommentarService.clearUploadedFiles(); + this.cancel.emit(); } } diff --git a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-in-vorgang.component.spec.ts b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-in-vorgang.component.spec.ts index 2e0e94f295129d7a65d7c9ca23c94ed384e83c38..4e267cefae258f6898249931a1b7495823d4e168 100644 --- a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-in-vorgang.component.spec.ts +++ b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-in-vorgang.component.spec.ts @@ -21,17 +21,13 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ +import { KommentarListLinkRel } from '@alfa-client/kommentar-shared'; +import { createEmptyStateResource, createStateResource } from '@alfa-client/tech-shared'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MockComponent } from 'ng-mocks'; +import { createKommentarListResource } from '../../../../../kommentar-shared/test/kommentar'; import { KommentarListInVorgangComponent } from './kommentar-list-in-vorgang.component'; import { KommentarListItemInVorgangComponent } from './kommentar-list-item-in-vorgang/kommentar-list-item-in-vorgang.component'; -import { - createEmptyStateResource, - createStateResource, - EMPTY_ARRAY, -} from '@alfa-client/tech-shared'; -import { createKommentarListResource } from '../../../../../kommentar-shared/test/kommentar'; -import { KommentarListLinkRel } from '@alfa-client/kommentar-shared'; describe('KommentarListInVorgangComponent', () => { let component: KommentarListInVorgangComponent; @@ -39,10 +35,7 @@ describe('KommentarListInVorgangComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ - KommentarListInVorgangComponent, - MockComponent(KommentarListItemInVorgangComponent), - ], + declarations: [KommentarListInVorgangComponent, MockComponent(KommentarListItemInVorgangComponent)], }); }); @@ -60,22 +53,20 @@ describe('KommentarListInVorgangComponent', () => { it('should return empty array if state resource is null', () => { component.kommentarListStateResource = null; - expect(component.getKommentare()).toEqual(EMPTY_ARRAY); + expect(component.getKommentare()).toEqual([]); }); it('should return empty array if resource is null', () => { component.kommentarListStateResource = createEmptyStateResource(); - expect(component.getKommentare()).toEqual(EMPTY_ARRAY); + expect(component.getKommentare()).toEqual([]); }); it('should return embedded resource', () => { const kommentareListResource = createKommentarListResource(); component.kommentarListStateResource = createStateResource(kommentareListResource); - expect(component.getKommentare()).toEqual( - kommentareListResource._embedded[KommentarListLinkRel.KOMMENTAR_LIST], - ); + expect(component.getKommentare()).toEqual(kommentareListResource._embedded[KommentarListLinkRel.KOMMENTAR_LIST]); }); }); }); diff --git a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-item-in-vorgang/kommentar-list-item-in-vorgang.component.html b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-item-in-vorgang/kommentar-list-item-in-vorgang.component.html index e1e4dbdf6cd4e216ce96f189eed122d62ed74782..78ac673c8e3c3a2b871f8f0af621b8bf5b2d09e3 100644 --- a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-item-in-vorgang/kommentar-list-item-in-vorgang.component.html +++ b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-item-in-vorgang/kommentar-list-item-in-vorgang.component.html @@ -24,10 +24,7 @@ --> <div *ngIf="!editMode" class="plain-text text-sm"> - <button - [attr.data-test-id]="'kommentar-item-' + (kommentar.text | convertForDataTest)" - (click)="edit()" - > + <button [attr.data-test-id]="'kommentar-item-' + (kommentar.text | convertForDataTest)" (click)="edit()"> <div class="kommentar-head"> <alfa-user-profile-in-kommentar-container class="username" @@ -42,12 +39,12 @@ <p class="text">{{ kommentar.text }}</p> </button> - <alfa-horizontal-binary-file-list - *ngIf="kommentar | hasLink: kommentarLinkRel.ATTACHMENTS" - [deletable]="false" - [fileListResource]="attachments$ | async" - > - </alfa-horizontal-binary-file-list> + @if (kommentar | hasLink: kommentarLinkRel.ATTACHMENTS) { + <alfa-binary-file-list-container + [resource]="kommentar" + [linkRel]="kommentarLinkRel.ATTACHMENTS" + ></alfa-binary-file-list-container> + } </div> <alfa-kommentar-form diff --git a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-item-in-vorgang/kommentar-list-item-in-vorgang.component.ts b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-item-in-vorgang/kommentar-list-item-in-vorgang.component.ts index d94fdd3c555be42544f58e660a91e7030edbac4d..cf6a951f4bd32c241281ce7c3207092387f92ce1 100644 --- a/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-item-in-vorgang/kommentar-list-item-in-vorgang.component.ts +++ b/alfa-client/libs/kommentar/src/lib/kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-item-in-vorgang/kommentar-list-item-in-vorgang.component.ts @@ -21,15 +21,10 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Component, Input, OnInit } from '@angular/core'; import { BinaryFileListResource } from '@alfa-client/binary-file-shared'; -import { - KommentarLinkRel, - KommentarListResource, - KommentarResource, - KommentarService, -} from '@alfa-client/kommentar-shared'; -import { StateResource, createEmptyStateResource } from '@alfa-client/tech-shared'; +import { KommentarLinkRel, KommentarListResource, KommentarResource, KommentarService } from '@alfa-client/kommentar-shared'; +import { createEmptyStateResource, StateResource } from '@alfa-client/tech-shared'; +import { Component, Input, OnInit } from '@angular/core'; import { hasLink } from '@ngxp/rest'; import { Observable, of } from 'rxjs'; @@ -42,9 +37,7 @@ export class KommentarListItemInVorgangComponent implements OnInit { @Input() kommentar: KommentarResource; @Input() kommentarListStateResource: StateResource<KommentarListResource>; - attachments$: Observable<StateResource<BinaryFileListResource>> = of( - createEmptyStateResource<BinaryFileListResource>(), - ); + attachments$: Observable<StateResource<BinaryFileListResource>> = of(createEmptyStateResource<BinaryFileListResource>()); editMode: boolean = false; diff --git a/alfa-client/libs/kommentar/src/lib/kommentar.module.ts b/alfa-client/libs/kommentar/src/lib/kommentar.module.ts index 4c9da054bee43f9411bd2e93ce3c4242246bf3aa..3a8e4dd40a4a53552567b81b69fad3af2c230312 100644 --- a/alfa-client/libs/kommentar/src/lib/kommentar.module.ts +++ b/alfa-client/libs/kommentar/src/lib/kommentar.module.ts @@ -30,6 +30,8 @@ import { VorgangSharedModule } from '@alfa-client/vorgang-shared'; import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { ReactiveFormsModule } from '@angular/forms'; +import { FileUploadListContainerComponent } from '../../../binary-file/src/lib/file-upload-list-container/file-upload-list-container.component'; +import { MultiFileUploadEditorComponent } from '../../../binary-file/src/lib/multi-file-upload-editor/multi-file-upload-editor.component'; import { KommentarFormComponent } from './kommentar-list-in-vorgang-container/kommentar-form/kommentar-form.component'; import { KommentarListInVorgangContainerComponent } from './kommentar-list-in-vorgang-container/kommentar-list-in-vorgang-container.component'; import { KommentarListInVorgangComponent } from './kommentar-list-in-vorgang-container/kommentar-list-in-vorgang/kommentar-list-in-vorgang.component'; @@ -42,6 +44,8 @@ import { KommentarListItemInVorgangComponent } from './kommentar-list-in-vorgang KommentarSharedModule, UserProfileModule, BinaryFileModule, + MultiFileUploadEditorComponent, + FileUploadListContainerComponent, ReactiveFormsModule, TextAreaEditorComponent, OzgcloudStrokedButtonWithSpinnerComponent, diff --git a/alfa-client/libs/postfach-shared/src/lib/postfach.service.ts b/alfa-client/libs/postfach-shared/src/lib/postfach.service.ts index 483377d3c54a2d43db97f549a428b1d00312593c..189ab9a2059f0941773bc7754a8929799889bf30 100644 --- a/alfa-client/libs/postfach-shared/src/lib/postfach.service.ts +++ b/alfa-client/libs/postfach-shared/src/lib/postfach.service.ts @@ -22,10 +22,10 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { + BinaryFileListLinkRel, BinaryFileListResource, BinaryFileResource, BinaryFileService, - getBinaryFiles, } from '@alfa-client/binary-file-shared'; import { CommandResource, @@ -41,6 +41,7 @@ import { createEmptyStateResource, createStateResource, doIfLoadingRequired, + getEmbeddedResources, isNotNull, isNotUndefined, } from '@alfa-client/tech-shared'; @@ -69,9 +70,7 @@ import { createResendPostfachMailCommand, createSendPostfachMailCommand } from ' @Injectable({ providedIn: 'root' }) export class PostfachService { - private readonly isPollSendPostachMail: BehaviorSubject<boolean> = new BehaviorSubject<boolean>( - false, - ); + private readonly isPollSendPostachMail: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); postfachMailList$: BehaviorSubject<StateResource<PostfachMailListResource>> = new BehaviorSubject< StateResource<PostfachMailListResource> >(createEmptyStateResource<PostfachMailListResource>()); @@ -108,16 +107,10 @@ export class PostfachService { postfachMailResource: PostfachMailResource, postfachMail: PostfachMail, ): Observable<StateResource<CommandResource>> { - return this.doSendNachricht( - postfachMailResource, - PostfachMailLinkRel.SEND, - createSendPostfachMailCommand(postfachMail), - ); + return this.doSendNachricht(postfachMailResource, PostfachMailLinkRel.SEND, createSendPostfachMailCommand(postfachMail)); } - public resendMail( - postfachMailResource: PostfachMailResource, - ): Observable<StateResource<CommandResource>> { + public resendMail(postfachMailResource: PostfachMailResource): Observable<StateResource<CommandResource>> { return this.doSendNachricht( postfachMailResource, PostfachMailLinkRel.RESEND_POSTFACH_MAIL, @@ -168,9 +161,7 @@ export class PostfachService { listenToNavigation(): void { this.unsubscribeToNavigation(); - this.navigationSubscription = this.navigationService - .urlChanged() - .subscribe((params) => this.onNavigation(params)); + this.navigationSubscription = this.navigationService.urlChanged().subscribe((params) => this.onNavigation(params)); } unsubscribeToNavigation(): void { @@ -205,10 +196,8 @@ export class PostfachService { } unsubscribe(): void { - if (isNotUndefined(this.sendPostfachMailSubscription)) - this.sendPostfachMailSubscription.unsubscribe(); - if (isNotUndefined(this.loadPostfachMailSubscription)) - this.loadPostfachMailSubscription.unsubscribe(); + if (isNotUndefined(this.sendPostfachMailSubscription)) this.sendPostfachMailSubscription.unsubscribe(); + if (isNotUndefined(this.loadPostfachMailSubscription)) this.loadPostfachMailSubscription.unsubscribe(); if (isNotUndefined(this.vorgangSubscription)) this.vorgangSubscription.unsubscribe(); } @@ -217,45 +206,31 @@ export class PostfachService { } resetHasNewPostfachNachrichten(): void { - this.postfachNachrichtenListSubscription = this.getPostfachMailListByVorgang().subscribe( - (postfachNachrichtenList) => { - if (isNotNull(postfachNachrichtenList.resource)) { - setTimeout(() => this.postfachNachrichtenListSubscription.unsubscribe(), 0); - this.doResetHasNewPostfachNachrichten(); - } - }, - ); + this.postfachNachrichtenListSubscription = this.getPostfachMailListByVorgang().subscribe((postfachNachrichtenList) => { + if (isNotNull(postfachNachrichtenList.resource)) { + setTimeout(() => this.postfachNachrichtenListSubscription.unsubscribe(), 0); + this.doResetHasNewPostfachNachrichten(); + } + }); } doResetHasNewPostfachNachrichten(): void { - if ( - hasLink( - this.postfachMailList$.value.resource, - PostfachMailListLinkRel.RESET_HAS_NEW_POSTFACH_NACHRICHT, - ) - ) { - this.repository - .resetHasNewPostfachNachrichten(this.postfachMailList$.value.resource) - .pipe(take(1)) - .subscribe(); + if (hasLink(this.postfachMailList$.value.resource, PostfachMailListLinkRel.RESET_HAS_NEW_POSTFACH_NACHRICHT)) { + this.repository.resetHasNewPostfachNachrichten(this.postfachMailList$.value.resource).pipe(take(1)).subscribe(); } } - pollSendPostfachMailCommand( - command: StateResource<CommandResource>, - ): StateResource<CommandResource> { + pollSendPostfachMailCommand(command: StateResource<CommandResource>): StateResource<CommandResource> { if (this.shouldPoll(command)) { this.setPollingTrue(); - this.sendPostfachMailSubscription = this.commandService - .pollCommand(command.resource) - .subscribe((updatedStateResource) => { - this.vorgangService.setPendingSendPostfachMailCommand(updatedStateResource); - - if (isDone(updatedStateResource.resource)) { - this.handleSendPostfachMailIsDone(updatedStateResource); - setTimeout(() => this.sendPostfachMailSubscription.unsubscribe(), 0); - } - }); + this.sendPostfachMailSubscription = this.commandService.pollCommand(command.resource).subscribe((updatedStateResource) => { + this.vorgangService.setPendingSendPostfachMailCommand(updatedStateResource); + + if (isDone(updatedStateResource.resource)) { + this.handleSendPostfachMailIsDone(updatedStateResource); + setTimeout(() => this.sendPostfachMailSubscription.unsubscribe(), 0); + } + }); } return command; } @@ -282,17 +257,11 @@ export class PostfachService { }); } - getEffectedResource( - updatedStateResource: StateResource<CommandResource>, - ): Observable<PostfachMailListResource> { - return this.commandService.getEffectedResource<PostfachMailListResource>( - updatedStateResource.resource, - ); + getEffectedResource(updatedStateResource: StateResource<CommandResource>): Observable<PostfachMailListResource> { + return this.commandService.getEffectedResource<PostfachMailListResource>(updatedStateResource.resource); } - public getPostfachMailListByGivenVorgang( - vorgang: VorgangResource, - ): Observable<StateResource<PostfachMailListResource>> { + public getPostfachMailListByGivenVorgang(vorgang: VorgangResource): Observable<StateResource<PostfachMailListResource>> { doIfLoadingRequired(this.postfachMailList$.value, () => { this.setPostfachMailListLoading(); this.loadPostfachMailsByVorgang(vorgang); @@ -303,14 +272,12 @@ export class PostfachService { public getPostfachMailListByVorgang(): Observable<StateResource<PostfachMailListResource>> { doIfLoadingRequired(this.postfachMailList$.value, () => { this.setPostfachMailListLoading(); - this.vorgangSubscription = this.vorgangService - .getVorgangWithEingang() - .subscribe((vorgangWithEingangStateResource) => { - if (vorgangWithEingangStateResource.resource) { - this.loadPostfachMailsByVorgang(vorgangWithEingangStateResource.resource); - setTimeout(() => this.vorgangSubscription.unsubscribe(), 0); - } - }); + this.vorgangSubscription = this.vorgangService.getVorgangWithEingang().subscribe((vorgangWithEingangStateResource) => { + if (vorgangWithEingangStateResource.resource) { + this.loadPostfachMailsByVorgang(vorgangWithEingangStateResource.resource); + setTimeout(() => this.vorgangSubscription.unsubscribe(), 0); + } + }); }); return this.postfachMailList$.asObservable(); } @@ -320,31 +287,24 @@ export class PostfachService { } public loadPostfachMailsByVorgang(vorgang: VorgangResource): void { - this.loadPostfachMailSubscription = this.repository - .loadPostfachMailList(vorgang) - .subscribe((postfachMaiList) => { - if (!isNull(postfachMaiList)) { - this.setPostfachMailList(postfachMaiList); - setTimeout(() => this.loadPostfachMailSubscription.unsubscribe(), 0); - } - }); + this.loadPostfachMailSubscription = this.repository.loadPostfachMailList(vorgang).subscribe((postfachMaiList) => { + if (!isNull(postfachMaiList)) { + this.setPostfachMailList(postfachMaiList); + setTimeout(() => this.loadPostfachMailSubscription.unsubscribe(), 0); + } + }); } setPostfachMailList(postfachMailList: PostfachMailListResource): void { this.postfachMailList$.next(createStateResource(postfachMailList)); } - public loadAttachments( - postfachNachricht: PostfachMailResource, - ): Observable<StateResource<BinaryFileListResource>> { + public loadAttachments(postfachNachricht: PostfachMailResource): Observable<StateResource<BinaryFileListResource>> { return this.binaryFileService.getFiles(postfachNachricht, PostfachMailLinkRel.ATTACHMENTS); } public isDownloadPdfInProgress(): Observable<boolean> { - return combineLatest([ - this.vorgangService.getVorgangWithEingang(), - this.postfachFacade.isDownloadPdfInProgress(), - ]).pipe( + return combineLatest([this.vorgangService.getVorgangWithEingang(), this.postfachFacade.isDownloadPdfInProgress()]).pipe( tap(([vorgang, isDownloadInProgress]) => { if (isDownloadInProgress && vorgang.resource) { this.postfachFacade.downloadPdf(vorgang.resource); @@ -361,11 +321,11 @@ export class PostfachService { public getAttachments(postfachNachricht: PostfachMailResource): Observable<BinaryFileResource[]> { return this.postfachFacade.getAttachmentList().pipe( tap((attachmentList) => - doIfLoadingRequired(attachmentList, () => - this.postfachFacade.loadAttachmentList(postfachNachricht), - ), + doIfLoadingRequired(attachmentList, () => this.postfachFacade.loadAttachmentList(postfachNachricht)), + ), + map((binaryFileListResource: StateResource<BinaryFileListResource>) => + getEmbeddedResources(binaryFileListResource, BinaryFileListLinkRel.FILE_LIST), ), - map(getBinaryFiles), ); } @@ -375,19 +335,13 @@ export class PostfachService { public getFeatures(): Observable<PostfachFeatures> { return this.getPostfachMailListByVorgang().pipe( - map( - (listStateResource: StateResource<PostfachMailListResource>) => - listStateResource.resource.features, - ), + map((listStateResource: StateResource<PostfachMailListResource>) => listStateResource.resource.features), ); } public getSettings(): Observable<PostfachSettings> { return this.getPostfachMailListByVorgang().pipe( - map( - (listStateResource: StateResource<PostfachMailListResource>) => - listStateResource.resource.settings, - ), + map((listStateResource: StateResource<PostfachMailListResource>) => listStateResource.resource.settings), ); } } diff --git a/alfa-client/libs/postfach-shared/test/postfach.ts b/alfa-client/libs/postfach-shared/test/postfach.ts index cc96efe9a93fdde304d17e460277bd4a146d429a..831f9b7eeafd5401719b392b56d1a79b2c0cb212 100644 --- a/alfa-client/libs/postfach-shared/test/postfach.ts +++ b/alfa-client/libs/postfach-shared/test/postfach.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { EMPTY_ARRAY, createStateResource } from '@alfa-client/tech-shared'; +import { createStateResource } from '@alfa-client/tech-shared'; import { faker } from '@faker-js/faker'; import { Resource } from '@ngxp/rest'; import { toResource } from 'libs/tech-shared/test/resource'; @@ -61,18 +61,14 @@ export function createPostfachMailResources(linkRelations: string[] = []): Postf return times(10, () => toResource(createPostfachMailResource(), [...linkRelations])); } -export function createPostfachMailListResource( - linkRelations: string[] = [], -): PostfachMailListResource { +export function createPostfachMailListResource(linkRelations: string[] = []): PostfachMailListResource { return toResource(createPostfachMailList(), [...linkRelations], { [PostfachMailListLinkRel.POSTFACH_MAIL_LIST]: createPostfachMailResources(), }); } function createPostfachMailList(): Resource { - return toResource({ features: createPostfachFeatures(), settings: createPostfachSettings() }, [ - 'sendPostfachMail', - ]); + return toResource({ features: createPostfachFeatures(), settings: createPostfachSettings() }, ['sendPostfachMail']); } export function createPostfachFeatures(): PostfachFeatures { @@ -100,10 +96,7 @@ export class PostfachTestFactory { attachments: faker.string.uuid(), }; - public static POSTFACH_NACHRICHT_RESOURCE: PostfachMailResource = toResource( - PostfachTestFactory.POSTFACH_NACHRICHT, - EMPTY_ARRAY, - ); + public static POSTFACH_NACHRICHT_RESOURCE: PostfachMailResource = toResource(PostfachTestFactory.POSTFACH_NACHRICHT, []); public static POSTFACH_NACHRICHT_LIST_RESOURCE = createPostfachMailListResource(); public static POSTFACH_NACHRICHT_LIST_STATE_RESOURCE = createStateResource( PostfachTestFactory.POSTFACH_NACHRICHT_LIST_RESOURCE, diff --git a/alfa-client/libs/postfach/src/lib/postfach-mail-form/postfach-nachricht-attachment-container/postfach-nachricht-attachment-container.component.ts b/alfa-client/libs/postfach/src/lib/postfach-mail-form/postfach-nachricht-attachment-container/postfach-nachricht-attachment-container.component.ts index ff5ef4cdaa751d6a7c746fee18c23aef08a0a527..0d3b7339b30a9ca15d03d791d60db92e7e42299b 100644 --- a/alfa-client/libs/postfach/src/lib/postfach-mail-form/postfach-nachricht-attachment-container/postfach-nachricht-attachment-container.component.ts +++ b/alfa-client/libs/postfach/src/lib/postfach-mail-form/postfach-nachricht-attachment-container/postfach-nachricht-attachment-container.component.ts @@ -21,7 +21,6 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Component, Input, OnDestroy } from '@angular/core'; import { BinaryFileResource } from '@alfa-client/binary-file-shared'; import { PostfachMailLinkRel, @@ -30,7 +29,8 @@ import { PostfachMailResource, PostfachService, } from '@alfa-client/postfach-shared'; -import { EMPTY_ARRAY, FormProvider, StateResource, isNotNil } from '@alfa-client/tech-shared'; +import { FormProvider, StateResource, isNotNil } from '@alfa-client/tech-shared'; +import { Component, Input, OnDestroy } from '@angular/core'; import { hasLink } from '@ngxp/rest'; import { Observable, of } from 'rxjs'; import { PostfachMailFormservice } from '../postfach-mail.formservice'; @@ -45,7 +45,7 @@ export class PostfachNachrichtAttachmentContainerComponent implements OnDestroy @Input() postfachNachricht: PostfachMailResource; postfachNachrichtListStateResource$: Observable<StateResource<PostfachMailListResource>>; - attachments$: Observable<BinaryFileResource[]> = of<BinaryFileResource[]>(EMPTY_ARRAY); + attachments$: Observable<BinaryFileResource[]> = of<BinaryFileResource[]>([]); public readonly postfachMailListLinkRel = PostfachMailListLinkRel; public readonly formServiceClass = PostfachMailFormservice; diff --git a/alfa-client/libs/tech-shared/src/lib/pipe/to-embedded-resource.pipe.spec.ts b/alfa-client/libs/tech-shared/src/lib/pipe/to-embedded-resource.pipe.spec.ts index bfd30ddd0afb673c9ab9eb285d33643eace88139..5420e9992163adac652466364d736c6c50c78860 100644 --- a/alfa-client/libs/tech-shared/src/lib/pipe/to-embedded-resource.pipe.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/pipe/to-embedded-resource.pipe.spec.ts @@ -25,7 +25,6 @@ import { getEmbeddedResource } from '@ngxp/rest'; import { DummyListLinkRel } from 'libs/tech-shared/test/dummy'; import { createDummyListResource, toResource } from 'libs/tech-shared/test/resource'; import { ListResource } from '../resource/resource.util'; -import { EMPTY_ARRAY } from '../tech.util'; import { ToEmbeddedResourcesPipe } from './to-embedded-resource.pipe'; describe('ToEmbeddedResourcesPipe', () => { @@ -47,19 +46,19 @@ describe('ToEmbeddedResourcesPipe', () => { it('should return an empty array on null as listResource', () => { const result: unknown[] = pipe.transform(null, DummyListLinkRel.LIST); - expect(result).toBe(EMPTY_ARRAY); + expect(result).toEqual([]); }); it('should return empty array on null as linkel', () => { const result: unknown[] = pipe.transform(listResource, null); - expect(result).toBe(EMPTY_ARRAY); + expect(result).toEqual([]); }); it('should return empty array non existing resources', () => { const result: unknown[] = pipe.transform(toResource({}), DummyListLinkRel.LIST); - expect(result).toBe(EMPTY_ARRAY); + expect(result).toEqual([]); }); }); }); diff --git a/alfa-client/libs/tech-shared/src/lib/pipe/to-embedded-resource.pipe.ts b/alfa-client/libs/tech-shared/src/lib/pipe/to-embedded-resource.pipe.ts index dbb7025753327feff619bd1a0559951bf8b6ebe0..7b746907bf222789faf57e003583e46bcb5e16ed 100644 --- a/alfa-client/libs/tech-shared/src/lib/pipe/to-embedded-resource.pipe.ts +++ b/alfa-client/libs/tech-shared/src/lib/pipe/to-embedded-resource.pipe.ts @@ -26,7 +26,6 @@ import { Resource, getEmbeddedResource } from '@ngxp/rest'; import { isNil, isNull } from 'lodash-es'; import { LinkRelationName } from '../resource/resource.model'; import { ListResource } from '../resource/resource.util'; -import { EMPTY_ARRAY } from '../tech.util'; @Pipe({ name: 'toEmbeddedResources', @@ -34,8 +33,8 @@ import { EMPTY_ARRAY } from '../tech.util'; }) export class ToEmbeddedResourcesPipe implements PipeTransform { transform(listResource: ListResource, linkRel: LinkRelationName): Resource[] { - if (isNil(listResource) || isNil(linkRel)) return EMPTY_ARRAY; + if (isNil(listResource) || isNil(linkRel)) return []; const embeddedReources: Resource[] = getEmbeddedResource(listResource, linkRel); - return isNull(embeddedReources) ? EMPTY_ARRAY : embeddedReources; + return isNull(embeddedReources) ? [] : embeddedReources; } } diff --git a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts index 60c4640d92c10a70d0bec7d763d80a8f9618ff33..aa2dee9b6966288e6326463b82a4568abef68ecd 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/list-resource.service.spec.ts @@ -30,7 +30,6 @@ import { DummyLinkRel, DummyListLinkRel } from 'libs/tech-shared/test/dummy'; import { createDummyListResource, createDummyResource, createFilledDummyListResource } from 'libs/tech-shared/test/resource'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { multipleCold, singleCold, singleHot } from '../../../test/marbles'; -import { EMPTY_ARRAY } from '../tech.util'; import { ResourceListService } from './list-resource.service'; import { CreateResourceData, LinkRelationName, ListItemResource, ListResourceServiceConfig } from './resource.model'; import { ResourceRepository } from './resource.repository'; @@ -547,7 +546,7 @@ describe('ListResourceService', () => { service.getList = jest.fn().mockReturnValue(of(createEmptyStateResource())); service.getItems().subscribe((items) => { - expect(items).toEqual(EMPTY_ARRAY); + expect(items).toEqual([]); done(); }); }); diff --git a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.spec.ts b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.spec.ts index 3bfc293f96169bd19c2db03eb6c3030a15d8a8f1..5e5a9f2b37a30aeda80c61e09986d021e556cd41 100644 --- a/alfa-client/libs/tech-shared/src/lib/resource/resource.util.spec.ts +++ b/alfa-client/libs/tech-shared/src/lib/resource/resource.util.spec.ts @@ -26,7 +26,6 @@ import { createCommandStateResource } from 'libs/command-shared/test/command'; import { DummyListLinkRel } from '../../../test/dummy'; import { createApiError } from '../../../test/error'; import { createDummyListResource, createDummyResource, toResource } from '../../../test/resource'; -import { EMPTY_ARRAY } from '../tech.util'; import { StateResource, containsLoading, @@ -139,13 +138,13 @@ describe('resource util', () => { it('should return empty array if state resource null', () => { const embedded = getEmbeddedResources(null, null); - expect(embedded).toEqual(EMPTY_ARRAY); + expect(embedded).toEqual([]); }); it('should return empty array if resource null', () => { const embedded = getEmbeddedResources(createEmptyStateResource(), null); - expect(embedded).toEqual(EMPTY_ARRAY); + expect(embedded).toEqual([]); }); it('should return null if embedded relation does not exist', () => { diff --git a/alfa-client/libs/tech-shared/src/lib/tech.util.ts b/alfa-client/libs/tech-shared/src/lib/tech.util.ts index 01a8ec90cdc85a6ea0145415c50095c5366c79a6..baed9187b7bf29b1d6e18b958e3bdb4888b090bd 100644 --- a/alfa-client/libs/tech-shared/src/lib/tech.util.ts +++ b/alfa-client/libs/tech-shared/src/lib/tech.util.ts @@ -30,7 +30,6 @@ import { LinkRelationName } from './resource/resource.model'; import { ApiError } from './tech.model'; export const EMPTY_STRING: string = ''; -export const EMPTY_ARRAY = []; export function getBaseUrl(): string { const { protocol, host } = window.location; @@ -44,9 +43,7 @@ export function isEmptyObject(obj: any): boolean { export function replacePlaceholders(text: string, placeholders: { [key: string]: string }): string { let replaced: string = text; - Object.keys(placeholders).forEach( - (key: string) => (replaced = replacePlaceholder(replaced, key, placeholders[key])), - ); + Object.keys(placeholders).forEach((key: string) => (replaced = replacePlaceholder(replaced, key, placeholders[key]))); return replaced; } @@ -56,9 +53,7 @@ export function replacePlaceholder(text: string, placeholder: string, value: str } export function hasExceptionId(apiError: ApiError): boolean { - return ( - isNotNil(apiError) && isNotNil(apiError.issues) && isNotNil(apiError.issues[0].exceptionId) - ); + return isNotNil(apiError) && isNotNil(apiError.issues) && isNotNil(apiError.issues[0].exceptionId); } export function sleep(delayInMs: number): void { @@ -152,9 +147,5 @@ export function notHasAnyLink(resource: Resource, ...links: LinkRelationName[]): } export function hasAnyLink(resource: Resource, ...links: LinkRelationName[]): boolean { - return !isEmpty( - links - .map((link: LinkRelationName) => hasLink(resource, link)) - .filter((hasLink: boolean) => hasLink === true), - ); + return !isEmpty(links.map((link: LinkRelationName) => hasLink(resource, link)).filter((hasLink: boolean) => hasLink === true)); } diff --git a/alfa-client/libs/tech-shared/test/file.ts b/alfa-client/libs/tech-shared/test/file.ts index 5feadc2fcd75f4e7b9144afa4c53194e995b43d3..5f4a0578aa673642049ba6ed8e54f5eb99f39222 100644 --- a/alfa-client/libs/tech-shared/test/file.ts +++ b/alfa-client/libs/tech-shared/test/file.ts @@ -26,3 +26,12 @@ import { faker } from '@faker-js/faker'; export function createFile(): File { return <any>{ name: faker.string.sample(10), type: 'image/png', size: 512 }; } + +export function createFileList(): FileList { + const file = createFile(); + return { + 0: file, + length: 2, + item: (index: number) => file, + }; +} diff --git a/alfa-client/libs/tech-shared/test/http.ts b/alfa-client/libs/tech-shared/test/http.ts index 76abc5588ba27b70f755cc07437150add45375c7..0f03449f6400f456d027b4b2fa524b101b337155 100644 --- a/alfa-client/libs/tech-shared/test/http.ts +++ b/alfa-client/libs/tech-shared/test/http.ts @@ -21,7 +21,8 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { HttpErrorResponse, HttpStatusCode } from '@angular/common/http'; +import { HttpHeader } from '@alfa-client/tech-shared'; +import { HttpErrorResponse, HttpResponse, HttpStatusCode } from '@angular/common/http'; import { createApiError } from './error'; export function createHttpErrorResponse(): HttpErrorResponse { @@ -30,3 +31,16 @@ export function createHttpErrorResponse(): HttpErrorResponse { status: HttpStatusCode.ServiceUnavailable, }; } + +export function createHttpResponse(): HttpResponse<Object> { + return { + status: HttpStatusCode.Accepted, + body: null, + clone: null, + headers: <any>{ get: (headerName: HttpHeader) => 'headerDummyUrl' }, + ok: null, + statusText: null, + type: null, + url: null, + }; +} diff --git a/alfa-client/libs/tech-shared/test/marbles.ts b/alfa-client/libs/tech-shared/test/marbles.ts index fecda8769f38a80dd34c693c9ca39bf175a69eef..c84c4c3f8b35246ac3782d714d993be1bd36d9a8 100644 --- a/alfa-client/libs/tech-shared/test/marbles.ts +++ b/alfa-client/libs/tech-shared/test/marbles.ts @@ -21,7 +21,8 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { ObservableWithSubscriptions, cold, hot } from 'jest-marbles'; +import { cold, hot, ObservableWithSubscriptions } from 'jest-marbles'; +import { HttpError } from '../src/lib/tech.model'; export function singleHot(object: any, frame: string = 'a'): ObservableWithSubscriptions { return hot(frame, { a: object }); @@ -38,3 +39,11 @@ export function singleColdCompleted(object: any, frame: string = 'a'): Observabl export function multipleCold(first: any, second: any): ObservableWithSubscriptions { return cold('ab', { a: first, b: second }); } + +export function coldError(error: HttpError): ObservableWithSubscriptions { + return cold('-#', null, { error: { error: error } }); +} + +export function coldStartWithError(startWith: any, error: any): ObservableWithSubscriptions { + return cold('a(b|)', { a: startWith, b: error }); +} diff --git a/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.ts b/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.ts index a5c00893b3fa0fe9c3745afa4a750f395f06df70..db593f08cb17ab00615fb0314f68d044491e9665 100644 --- a/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.ts +++ b/alfa-client/libs/ui/src/lib/ui/back-button/back-button.component.ts @@ -22,9 +22,6 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { Component, Input } from '@angular/core'; -import { MatIconAnchor } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; -import { MatTooltip } from '@angular/material/tooltip'; import { RouterLink } from '@angular/router'; import { ArrowBackIconComponent, ButtonComponent, TooltipDirective } from '@ods/system'; @@ -33,7 +30,7 @@ import { ArrowBackIconComponent, ButtonComponent, TooltipDirective } from '@ods/ templateUrl: './back-button.component.html', styleUrls: ['./back-button.component.scss'], standalone: true, - imports: [MatIconAnchor, RouterLink, MatTooltip, MatIcon, TooltipDirective, ButtonComponent, ArrowBackIconComponent], + imports: [RouterLink, TooltipDirective, ButtonComponent, ArrowBackIconComponent], }) export class BackButtonComponent { @Input() label: string; diff --git a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts index 49ef271f9dfe7e93af9a114ad8840972db003a6f..c3358acb510534afa4df0bca712d8b2ff8161bb5 100644 --- a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts +++ b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.spec.ts @@ -23,17 +23,11 @@ */ import { ApiRootResource } from '@alfa-client/api-root-shared'; import { BinaryFileListResource } from '@alfa-client/binary-file-shared'; -import { - CommandListResource, - CommandOrder, - CommandResource, - CreateCommand, -} from '@alfa-client/command-shared'; +import { CommandListResource, CommandOrder, CommandResource, CreateCommand } from '@alfa-client/command-shared'; import { RouteData } from '@alfa-client/navigation-shared'; import { ApiError, ApiErrorAction, - EMPTY_ARRAY, EMPTY_STRING, StateResource, createEmptyStateResource, @@ -45,11 +39,7 @@ import { Action } from '@ngrx/store'; import { Resource, ResourceUri } from '@ngxp/rest'; import { createApiRootResource } from 'libs/api-root-shared/test/api-root'; import { createBinaryFileListResource } from 'libs/binary-file-shared/test/binary-file'; -import { - createCommand, - createCommandListResource, - createCommandResource, -} from 'libs/command-shared/test/command'; +import { createCommand, createCommandListResource, createCommandResource } from 'libs/command-shared/test/command'; import { createRouteData } from 'libs/navigation-shared/test/navigation-test-factory'; import { createDummyResource } from 'libs/tech-shared/test/resource'; import { @@ -129,9 +119,7 @@ describe('Vorgang Reducer', () => { ...createVorgangListResourceWithResource(vorgaenge, [VorgangListLinkRel.NEXT]), statistic, }; - const action: VorgangListAction & Action<string> = VorgangActions.loadVorgangListSuccess( - { vorgangList }, - ); + const action: VorgangListAction & Action<string> = VorgangActions.loadVorgangListSuccess({ vorgangList }); it('should set loaded resource', () => { const state: VorgangState = reducer(initialState, action); @@ -207,10 +195,7 @@ describe('Vorgang Reducer', () => { }); it('should add vorgaenge', () => { - const state: VorgangState = reducer( - { ...initialState, vorgaenge: [createVorgangResource()] }, - action, - ); + const state: VorgangState = reducer({ ...initialState, vorgaenge: [createVorgangResource()] }, action); expect(state.vorgaenge.length).toBe(11); }); @@ -251,8 +236,7 @@ describe('Vorgang Reducer', () => { ...createVorgangListResourceWithResource(vorgaenge), statistic, }; - const action: VorgangListAction & Action<string> = - VorgangActions.searchVorgaengeBySuccess({ vorgangList }); + const action: VorgangListAction & Action<string> = VorgangActions.searchVorgaengeBySuccess({ vorgangList }); it('should set vorgangList', () => { const state: VorgangState = reducer(initialState, action); @@ -501,10 +485,7 @@ describe('Vorgang Reducer', () => { ...createCommandResource(), order: CommandOrder.REDIRECT_VORGANG, }; - const commandList: CommandListResource = createCommandListResource([ - createCommandResource(), - forwardCommand, - ]); + const commandList: CommandListResource = createCommandListResource([createCommandResource(), forwardCommand]); const action = VorgangActions.loadPendingCommandListSuccess({ commandList }); const state: VorgangState = reducer(initialState, action); @@ -525,9 +506,7 @@ describe('Vorgang Reducer', () => { const state: VorgangState = reducer(initialState, action); - expect(state.sendPostfachNachrichtPendingCommand.resource).toBe( - sendPostfachNachrichtCommand, - ); + expect(state.sendPostfachNachrichtPendingCommand.resource).toBe(sendPostfachNachrichtCommand); }); }); }); @@ -545,8 +524,7 @@ describe('Vorgang Reducer', () => { describe('on "setForwardingSingleCommand" action', () => { it('should set forward pending command', () => { - const commandStateResource: StateResource<CommandResource> = - createStateResource(createCommandResource()); + const commandStateResource: StateResource<CommandResource> = createStateResource(createCommandResource()); const action = VorgangActions.setForwardingSingleCommand({ commandStateResource }); const state: VorgangState = reducer(initialState, action); @@ -569,8 +547,7 @@ describe('Vorgang Reducer', () => { describe('on "setSendPostfachNachrichtSingleCommand" action', () => { it('should set forward pending command', () => { - const commandStateResource: StateResource<CommandResource> = - createStateResource(createCommandResource()); + const commandStateResource: StateResource<CommandResource> = createStateResource(createCommandResource()); const action = VorgangActions.setSendPostfachNachrichtSingleCommand({ commandStateResource, }); @@ -710,10 +687,7 @@ describe('Vorgang Reducer', () => { order: CommandOrder.VORGANG_ANNEHMEN, }; - const statusCommandMap: StatusCommandMap = Reducer.getStatusCommandMapByCreateCommand( - initialState, - command, - ); + const statusCommandMap: StatusCommandMap = Reducer.getStatusCommandMapByCreateCommand(initialState, command); expect(statusCommandMap[command.order].loading).toBeTruthy(); }); @@ -723,10 +697,7 @@ describe('Vorgang Reducer', () => { it('should return state value', () => { const command: CommandResource = { ...createCommandResource(), order: 'quatsch' }; - const statusCommandMap: StatusCommandMap = Reducer.getStatusCommandMapByCreateCommand( - initialState, - command, - ); + const statusCommandMap: StatusCommandMap = Reducer.getStatusCommandMapByCreateCommand(initialState, command); expect(statusCommandMap).toBe(initialState.statusCommandMap); }); @@ -741,8 +712,7 @@ describe('Vorgang Reducer', () => { order: CommandOrder.VORGANG_ANNEHMEN, }; - const statusCommandMap: StatusCommandMap = - Reducer.getStatusCommandMapByCreateCommandSuccess(initialState, command); + const statusCommandMap: StatusCommandMap = Reducer.getStatusCommandMapByCreateCommandSuccess(initialState, command); expect(statusCommandMap[command.order].resource).toBe(command); }); @@ -752,8 +722,7 @@ describe('Vorgang Reducer', () => { it('should return state value', () => { const command: CommandResource = { ...createCommandResource(), order: 'quatsch' }; - const statusCommandMap: StatusCommandMap = - Reducer.getStatusCommandMapByCreateCommandSuccess(initialState, command); + const statusCommandMap: StatusCommandMap = Reducer.getStatusCommandMapByCreateCommandSuccess(initialState, command); expect(statusCommandMap).toBe(initialState.statusCommandMap); }); @@ -768,8 +737,10 @@ describe('Vorgang Reducer', () => { order: CommandOrder.ASSIGN_USER, }; - const assignUserCommand: StateResource<CommandResource> = - Reducer.getAssignUserCommandByCreateCommand(initialState, command); + const assignUserCommand: StateResource<CommandResource> = Reducer.getAssignUserCommandByCreateCommand( + initialState, + command, + ); expect(assignUserCommand.loading).toBeTruthy(); }); @@ -779,8 +750,10 @@ describe('Vorgang Reducer', () => { it('should return state value', () => { const command: CommandResource = { ...createCommandResource(), order: 'quatsch' }; - const assignUserCommand: StateResource<CommandResource> = - Reducer.getAssignUserCommandByCreateCommand(initialState, command); + const assignUserCommand: StateResource<CommandResource> = Reducer.getAssignUserCommandByCreateCommand( + initialState, + command, + ); expect(assignUserCommand).toBe(initialState.assignUserCommand); }); @@ -795,8 +768,10 @@ describe('Vorgang Reducer', () => { order: CommandOrder.ASSIGN_USER, }; - const assignUserCommand: StateResource<CommandResource> = - Reducer.getAssignUserCommandByCreateCommandSuccess(initialState, command); + const assignUserCommand: StateResource<CommandResource> = Reducer.getAssignUserCommandByCreateCommandSuccess( + initialState, + command, + ); expect(assignUserCommand.resource).toBe(command); }); @@ -806,8 +781,10 @@ describe('Vorgang Reducer', () => { it('should return state value', () => { const command: CommandResource = { ...createCommandResource(), order: 'quatsch' }; - const assignUserCommand: StateResource<CommandResource> = - Reducer.getAssignUserCommandByCreateCommandSuccess(initialState, command); + const assignUserCommand: StateResource<CommandResource> = Reducer.getAssignUserCommandByCreateCommandSuccess( + initialState, + command, + ); expect(assignUserCommand).toBe(initialState.assignUserCommand); }); @@ -881,10 +858,7 @@ describe('Vorgang Reducer', () => { it('should clear stateResource', () => { const action = VorgangActions.exportVorgangSuccess(); - const state: VorgangState = reducer( - { ...initialState, vorgangExport: createEmptyStateResource(true) }, - action, - ); + const state: VorgangState = reducer({ ...initialState, vorgangExport: createEmptyStateResource(true) }, action); expect(state.vorgangExport).toEqual(createStateResource(true)); }); @@ -897,10 +871,7 @@ describe('Vorgang Reducer', () => { const action = NavigationActions.updateCurrentRouteData({ routeData: buildCurrentRouteData('search like me'), }); - const state: VorgangState = reducer( - { ...initialState, searchString: 'existingSearchString' }, - action, - ); + const state: VorgangState = reducer({ ...initialState, searchString: 'existingSearchString' }, action); expect(state.searchPreviewList.reload).toBeTruthy(); }); @@ -943,25 +914,18 @@ describe('Vorgang Reducer', () => { }); it('should be updated by given vorgangFilter and default view', () => { - const urlSegments: UrlSegment[] = [ - <any>{ path: ROUTE_PARAM_BY_VORGANG_FILTER[VorgangFilter.ALLE] }, - ]; + const urlSegments: UrlSegment[] = [<any>{ path: ROUTE_PARAM_BY_VORGANG_FILTER[VorgangFilter.ALLE] }]; const routeData: RouteData = { ...createRouteData(), urlSegments }; const localStorageSpy = jest.spyOn(Reducer, 'updateLocalStorage'); Reducer.updateByRouteData(initialState, routeData); - expect(localStorageSpy).toHaveBeenCalledWith( - VorgangFilter.ALLE, - VorgangView.VORGANG_LIST, - ); + expect(localStorageSpy).toHaveBeenCalledWith(VorgangFilter.ALLE, VorgangView.VORGANG_LIST); }); }); describe('filter only', () => { - const urlSegments: UrlSegment[] = [ - <any>{ path: ROUTE_PARAM_BY_VORGANG_FILTER[VorgangFilter.ALLE] }, - ]; + const urlSegments: UrlSegment[] = [<any>{ path: ROUTE_PARAM_BY_VORGANG_FILTER[VorgangFilter.ALLE] }]; const routeData: RouteData = { ...createRouteData(), urlSegments }; it('should set reload vorgangList', () => { @@ -979,9 +943,7 @@ describe('Vorgang Reducer', () => { it('should set vorgangFilter', () => { const routeData: RouteData = { ...createRouteData(), - urlSegments: [ - <any>{ path: ROUTE_PARAM_BY_VORGANG_FILTER[VorgangFilter.MEINE_VORGAENGE] }, - ], + urlSegments: [<any>{ path: ROUTE_PARAM_BY_VORGANG_FILTER[VorgangFilter.MEINE_VORGAENGE] }], }; const state: VorgangState = Reducer.updateByRouteData(initialState, routeData); @@ -992,7 +954,7 @@ describe('Vorgang Reducer', () => { it('should clear vorgaenge', () => { const state: VorgangState = Reducer.updateByRouteData(initialState, routeData); - expect(state.vorgaenge).toBe(EMPTY_ARRAY); + expect(state.vorgaenge).toEqual([]); }); }); @@ -1024,7 +986,7 @@ describe('Vorgang Reducer', () => { it('should clear vorgaenge', () => { const state: VorgangState = Reducer.updateByRouteData(initialState, routeData); - expect(state.vorgaenge).toBe(EMPTY_ARRAY); + expect(state.vorgaenge).toEqual([]); }); }); }); @@ -1057,65 +1019,56 @@ describe('Vorgang Reducer', () => { describe('vorgangStatistic', () => { describe('byStatus', () => { it('should have null as neu', () => { - const vorgangStatistic: StateResource<VorgangStatistic> = - Reducer.initialState.vorgangStatistic; + const vorgangStatistic: StateResource<VorgangStatistic> = Reducer.initialState.vorgangStatistic; expect(vorgangStatistic.resource.byStatus.neu).toBeNull(); }); it('should have null as angenommen', () => { - const vorgangStatistic: StateResource<VorgangStatistic> = - Reducer.initialState.vorgangStatistic; + const vorgangStatistic: StateResource<VorgangStatistic> = Reducer.initialState.vorgangStatistic; expect(vorgangStatistic.resource.byStatus.angenommen).toBeNull(); }); it('should have null as inBearbeitung', () => { - const vorgangStatistic: StateResource<VorgangStatistic> = - Reducer.initialState.vorgangStatistic; + const vorgangStatistic: StateResource<VorgangStatistic> = Reducer.initialState.vorgangStatistic; expect(vorgangStatistic.resource.byStatus.inBearbeitung).toBeNull(); }); it('should have null as beschieden', () => { - const vorgangStatistic: StateResource<VorgangStatistic> = - Reducer.initialState.vorgangStatistic; + const vorgangStatistic: StateResource<VorgangStatistic> = Reducer.initialState.vorgangStatistic; expect(vorgangStatistic.resource.byStatus.beschieden).toBeNull(); }); it('should have null as abgeschlossen', () => { - const vorgangStatistic: StateResource<VorgangStatistic> = - Reducer.initialState.vorgangStatistic; + const vorgangStatistic: StateResource<VorgangStatistic> = Reducer.initialState.vorgangStatistic; expect(vorgangStatistic.resource.byStatus.abgeschlossen).toBeNull(); }); it('should have null as verworfen', () => { - const vorgangStatistic: StateResource<VorgangStatistic> = - Reducer.initialState.vorgangStatistic; + const vorgangStatistic: StateResource<VorgangStatistic> = Reducer.initialState.vorgangStatistic; expect(vorgangStatistic.resource.byStatus.verworfen).toBeNull(); }); it('should have null as zuLoeschen', () => { - const vorgangStatistic: StateResource<VorgangStatistic> = - Reducer.initialState.vorgangStatistic; + const vorgangStatistic: StateResource<VorgangStatistic> = Reducer.initialState.vorgangStatistic; expect(vorgangStatistic.resource.byStatus.zuLoeschen).toBeNull(); }); }); it('should have null as wiedervorlagen', () => { - const vorgangStatistic: StateResource<VorgangStatistic> = - Reducer.initialState.vorgangStatistic; + const vorgangStatistic: StateResource<VorgangStatistic> = Reducer.initialState.vorgangStatistic; expect(vorgangStatistic.resource.wiedervorlagen).toBeNull(); }); it('should have false as existsWiedervorlageOverdue', () => { - const vorgangStatistic: StateResource<VorgangStatistic> = - Reducer.initialState.vorgangStatistic; + const vorgangStatistic: StateResource<VorgangStatistic> = Reducer.initialState.vorgangStatistic; expect(vorgangStatistic.resource.existsWiedervorlageOverdue).toBeFalsy(); }); diff --git a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts index 33bc13ea4c0b70755cc6df22869c44eb794af8eb..0f200a551da43386ab80c67938d561a277bed11d 100644 --- a/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts +++ b/alfa-client/libs/vorgang-shared/src/lib/+state/vorgang.reducer.ts @@ -21,15 +21,8 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { - removeLocalStorageView, - setFilterIntoStorage, - setViewIntoStorage, -} from '@alfa-client/app-shared'; -import { - BinaryFileListResource, - LoadBinaryFileListSuccessProps, -} from '@alfa-client/binary-file-shared'; +import { removeLocalStorageView, setFilterIntoStorage, setViewIntoStorage } from '@alfa-client/app-shared'; +import { BinaryFileListResource, LoadBinaryFileListSuccessProps } from '@alfa-client/binary-file-shared'; import { CommandOrder, CommandProps, @@ -43,7 +36,6 @@ import { import { RouteData } from '@alfa-client/navigation-shared'; import { ApiErrorAction, - EMPTY_ARRAY, EMPTY_STRING, StateResource, createEmptyStateResource, @@ -72,12 +64,7 @@ import { VorgangWithEingangResource, } from '../vorgang.model'; import { getVorgaengeFromList, isAssignUserCommand, isStatusCommand } from '../vorgang.util'; -import { - HttpErrorAction, - StringBasedProps, - VorgangListAction, - VorgangWithEingangAction, -} from './vorgang.actions'; +import { HttpErrorAction, StringBasedProps, VorgangListAction, VorgangWithEingangAction } from './vorgang.actions'; import * as CommandActions from '../../../../command-shared/src/lib/+state/command.actions'; import * as NavigationActions from '../../../../navigation-shared/src/lib/+state/navigation.actions'; @@ -113,7 +100,7 @@ export interface VorgangState { export const initialState: VorgangState = { vorgangList: createEmptyStateResource(), vorgangStatistic: createStateResource(createEmptyVorgangStatistic()), - vorgaenge: EMPTY_ARRAY, + vorgaenge: [], searchString: EMPTY_STRING, searchPreviewList: createEmptyStateResource(), vorgangView: VorgangView.VORGANG_LIST, @@ -197,7 +184,7 @@ const vorgangReducer: ActionReducer<VorgangState, Action> = createReducer( (state: VorgangState): VorgangState => ({ ...state, vorgangList: { ...state.vorgangList, loading: true }, - vorgaenge: EMPTY_ARRAY, + vorgaenge: [], searchPreviewList: createEmptyStateResource(), }), ), @@ -215,9 +202,7 @@ const vorgangReducer: ActionReducer<VorgangState, Action> = createReducer( VorgangActions.searchVorgaengeByFailure, (state: VorgangState, action: HttpErrorAction): VorgangState => ({ ...state, - vorgangList: createErrorStateResource( - getApiErrorFromHttpErrorResponse(action.httpErrorResponse), - ), + vorgangList: createErrorStateResource(getApiErrorFromHttpErrorResponse(action.httpErrorResponse)), searchPreviewList: createEmptyStateResource(), }), ), @@ -227,10 +212,7 @@ const vorgangReducer: ActionReducer<VorgangState, Action> = createReducer( (state: VorgangState, props: StringBasedProps): VorgangState => ({ ...state, searchString: props.string, - searchPreviewList: - clearPreviewList(props) ? - createEmptyStateResource() - : { ...state.searchPreviewList, reload: true }, + searchPreviewList: clearPreviewList(props) ? createEmptyStateResource() : { ...state.searchPreviewList, reload: true }, }), ), on( @@ -251,9 +233,7 @@ const vorgangReducer: ActionReducer<VorgangState, Action> = createReducer( VorgangActions.searchForPreviewFailure, (state: VorgangState, action: HttpErrorAction): VorgangState => ({ ...state, - searchPreviewList: createErrorStateResource( - getApiErrorFromHttpErrorResponse(action.httpErrorResponse), - ), + searchPreviewList: createErrorStateResource(getApiErrorFromHttpErrorResponse(action.httpErrorResponse)), }), ), @@ -347,9 +327,7 @@ const vorgangReducer: ActionReducer<VorgangState, Action> = createReducer( VorgangActions.loadPendingCommandListSuccess, (state: VorgangState, props: LoadCommandListSuccessProps): VorgangState => ({ ...state, - forwardPendingCommand: createStateResource( - getPendingCommandByOrder(props.commandList, [CommandOrder.REDIRECT_VORGANG]), - ), + forwardPendingCommand: createStateResource(getPendingCommandByOrder(props.commandList, [CommandOrder.REDIRECT_VORGANG])), sendPostfachNachrichtPendingCommand: createStateResource( getPendingCommandByOrder(props.commandList, [CommandOrder.SEND_POSTFACH_NACHRICHT]), ), @@ -420,21 +398,12 @@ const vorgangReducer: ActionReducer<VorgangState, Action> = createReducer( on(CommandActions.createCommandSuccess, (state, props: CommandProps): VorgangState => { return { ...state, - statusCommandMap: VorgangReducer.getStatusCommandMapByCreateCommandSuccess( - state, - props.command, - ), - assignUserCommand: VorgangReducer.getAssignUserCommandByCreateCommandSuccess( - state, - props.command, - ), + statusCommandMap: VorgangReducer.getStatusCommandMapByCreateCommandSuccess(state, props.command), + assignUserCommand: VorgangReducer.getAssignUserCommandByCreateCommandSuccess(state, props.command), /** * @deprecated Das Nachladen des Vorgangs im Service, nach erfolgreicher Beendigung des Commands, durchfuehren */ - vorgangWithEingang: VorgangReducer.getVorgangWithEingangStateResourceByCreateCommandSucces( - state, - props.command, - ), + vorgangWithEingang: VorgangReducer.getVorgangWithEingangStateResourceByCreateCommandSucces(state, props.command), }; }), on(CommandActions.revokeCommand, (state): VorgangState => { @@ -483,40 +452,27 @@ export function getVorgangWithEingangStateResourceByCreateCommandSucces( : state.vorgangWithEingang; } -export function getStatusCommandMapByCreateCommand( - state: VorgangState, - command: CreateCommand, -): StatusCommandMap { +export function getStatusCommandMapByCreateCommand(state: VorgangState, command: CreateCommand): StatusCommandMap { return isStatusCommand(command.order) ? { ...state.statusCommandMap, [command.order]: createEmptyStateResource(true) } : state.statusCommandMap; } -export function getStatusCommandMapByCreateCommandSuccess( - state: VorgangState, - command: CommandResource, -): StatusCommandMap { +export function getStatusCommandMapByCreateCommandSuccess(state: VorgangState, command: CommandResource): StatusCommandMap { return isStatusCommand(command.order) ? { ...state.statusCommandMap, [command.order]: createStateResource(command) } : state.statusCommandMap; } -export function getAssignUserCommandByCreateCommand( - state: VorgangState, - command: CreateCommand, -): StateResource<CommandResource> { - return isAssignUserCommand(command.order) ? - createEmptyStateResource(true) - : state.assignUserCommand; +export function getAssignUserCommandByCreateCommand(state: VorgangState, command: CreateCommand): StateResource<CommandResource> { + return isAssignUserCommand(command.order) ? createEmptyStateResource(true) : state.assignUserCommand; } export function getAssignUserCommandByCreateCommandSuccess( state: VorgangState, command: CommandResource, ): StateResource<CommandResource> { - return isAssignUserCommand(command.order) ? - createStateResource(command) - : state.assignUserCommand; + return isAssignUserCommand(command.order) ? createStateResource(command) : state.assignUserCommand; } function clearPreviewList(props: StringBasedProps): boolean { @@ -527,7 +483,7 @@ export function updateByRouteData(state: VorgangState, routeData: RouteData): Vo let newState = { ...state, vorgangList: { ...state.vorgangList, reload: true }, - vorgaenge: EMPTY_ARRAY, + vorgaenge: [], }; if (isUebersichtsSeite(routeData)) { @@ -539,10 +495,7 @@ export function updateByRouteData(state: VorgangState, routeData: RouteData): Vo return newState; } -function prepareStateOnVorgangListNavigation( - state: VorgangState, - routeData: RouteData, -): VorgangState { +function prepareStateOnVorgangListNavigation(state: VorgangState, routeData: RouteData): VorgangState { let newState: VorgangState = { ...state, vorgangFilter: getVorgangFilter(routeData), diff --git a/alfa-client/libs/vorgang-shared/src/lib/vorgang.util.ts b/alfa-client/libs/vorgang-shared/src/lib/vorgang.util.ts index 64a8f39b55caa29b47c651e43fde340d486902ab..806b5ea58f19c6a646ef6dcbd9a7c41c93b8dbd0 100644 --- a/alfa-client/libs/vorgang-shared/src/lib/vorgang.util.ts +++ b/alfa-client/libs/vorgang-shared/src/lib/vorgang.util.ts @@ -22,7 +22,7 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { CommandOrder, CreateCommand } from '@alfa-client/command-shared'; -import { EMPTY_ARRAY, isNotNil } from '@alfa-client/tech-shared'; +import { isNotNil } from '@alfa-client/tech-shared'; import { ResourceUri, getEmbeddedResource } from '@ngxp/rest'; import { isNull } from 'lodash-es'; import { VorgangListLinkRel } from './vorgang.linkrel'; @@ -72,13 +72,10 @@ export function createForwardCommand(redirectRequest: ForwardRequest): CreateFor export function getVorgaengeFromList(vorgangList: VorgangListResource): VorgangResource[] { if (isNotNil(vorgangList)) { - const embeddedResource: VorgangResource[] = getEmbeddedResource( - vorgangList, - VorgangListLinkRel.VORGANG_HEADER_LIST, - ); - return isNull(embeddedResource) ? EMPTY_ARRAY : embeddedResource; + const embeddedResource: VorgangResource[] = getEmbeddedResource(vorgangList, VorgangListLinkRel.VORGANG_HEADER_LIST); + return isNull(embeddedResource) ? [] : embeddedResource; } - return EMPTY_ARRAY; + return []; } export function createAssignUserCommand(assignedTo: ResourceUri): CreateAssignUserCommand { diff --git a/alfa-client/libs/zustaendige-stelle-shared/src/lib/externe-fachstelle/externe-fachstelle.service.spec.ts b/alfa-client/libs/zustaendige-stelle-shared/src/lib/externe-fachstelle/externe-fachstelle.service.spec.ts index 99231c0db52685014185c54da0ccff7f83393525..3c25ba4614aa332c0c79bddb04206cbb740ee7fc 100644 --- a/alfa-client/libs/zustaendige-stelle-shared/src/lib/externe-fachstelle/externe-fachstelle.service.spec.ts +++ b/alfa-client/libs/zustaendige-stelle-shared/src/lib/externe-fachstelle/externe-fachstelle.service.spec.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { EMPTY_ARRAY, StateResource, createStateResource } from '@alfa-client/tech-shared'; +import { StateResource, createStateResource } from '@alfa-client/tech-shared'; import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { faker } from '@faker-js/faker'; import { InstantSearchResult } from '@ods/system'; @@ -111,7 +111,7 @@ describe('ExterneFachstelleService', () => { createStateResource(createEmptyListResource<ExterneFachstelleListResource>()), ); - expect(result).toEqual(EMPTY_ARRAY); + expect(result).toEqual([]); }); }); diff --git a/alfa-client/libs/zustaendige-stelle-shared/src/lib/externe-fachstelle/externe-fachstelle.service.ts b/alfa-client/libs/zustaendige-stelle-shared/src/lib/externe-fachstelle/externe-fachstelle.service.ts index 6c9493c8266927d9a99517855ee5f1568ae129ed..50e3c315d376590df9cf2c20be7e9b68fa29213a 100644 --- a/alfa-client/libs/zustaendige-stelle-shared/src/lib/externe-fachstelle/externe-fachstelle.service.ts +++ b/alfa-client/libs/zustaendige-stelle-shared/src/lib/externe-fachstelle/externe-fachstelle.service.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { EMPTY_ARRAY, getEmbeddedResources, StateResource } from '@alfa-client/tech-shared'; +import { getEmbeddedResources, StateResource } from '@alfa-client/tech-shared'; import { Injectable } from '@angular/core'; import { InstantSearchResult } from '@ods/system'; import { isNull } from 'lodash-es'; @@ -60,7 +60,7 @@ export class ExterneFachstelleService implements ZustaendigeStelleService<Extern externeFachstelleStateListResource, ExterneFachstelleListLinkRel.EXTERNE_FACHSTELLE_LIST, ); - return isNull(resources) ? EMPTY_ARRAY : resources; + return isNull(resources) ? [] : resources; } mapToInstantSearchResult(externeFachstelle: ExterneFachstelleResource): InstantSearchResult<ExterneFachstelleResource> { diff --git a/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.service.spec.ts b/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.service.spec.ts index 87e9bbd0b160907d93887c58f48af63d576c0fd5..6c994b351bfe3c6dc4212a5b52f807c5ff536e47 100644 --- a/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.service.spec.ts +++ b/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.service.spec.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { EMPTY_ARRAY, ResourceSearchService, StateResource, createStateResource } from '@alfa-client/tech-shared'; +import { ResourceSearchService, StateResource, createStateResource } from '@alfa-client/tech-shared'; import { Mock, mock, useFromMock } from '@alfa-client/test-utils'; import { faker } from '@faker-js/faker'; import { Resource } from '@ngxp/rest'; @@ -109,7 +109,7 @@ describe('OrganisationsEinheitService', () => { createStateResource(createEmptyListResource<OrganisationsEinheitListResource>()), ); - expect(result).toEqual(EMPTY_ARRAY); + expect(result).toEqual([]); }); }); diff --git a/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.service.ts b/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.service.ts index 578a5ad04f1a4800ac0d2133c2403818d23acb5b..b3c4fe94f7b2e14a9c69cfb8ecf550579e33bc5c 100644 --- a/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.service.ts +++ b/alfa-client/libs/zustaendige-stelle-shared/src/lib/organisations-einheit/organisations-einheit.service.ts @@ -21,7 +21,7 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { EMPTY_ARRAY, getEmbeddedResources, ResourceSearchService, StateResource } from '@alfa-client/tech-shared'; +import { getEmbeddedResources, ResourceSearchService, StateResource } from '@alfa-client/tech-shared'; import { Resource } from '@ngxp/rest'; import { InstantSearchResult } from '@ods/system'; import { isNull } from 'lodash-es'; @@ -64,7 +64,7 @@ export class OrganisationsEinheitService implements ZustaendigeStelleService<Org organisationsEinheitStateListResource, OrganisationsEinheitListLinkRel.ORGANISATIONS_EINHEIT_HEADER_LIST, ); - return isNull(resources) ? EMPTY_ARRAY : resources; + return isNull(resources) ? [] : resources; } mapToInstantSearchResult(zustaendigeStelle: OrganisationsEinheitResource): InstantSearchResult<OrganisationsEinheitResource> { diff --git a/alfa-client/package.json b/alfa-client/package.json index 56791618d048e514e34de89a49233389e0a988c4..d6a7bffddc6984305f4bc77149fa653d2083dfd3 100644 --- a/alfa-client/package.json +++ b/alfa-client/package.json @@ -175,4 +175,4 @@ "@rollup/rollup-linux-x64-gnu": "*" }, "packageManager": "pnpm@9.15.0" -} \ No newline at end of file +} diff --git a/alfa-client/pnpm-lock.yaml b/alfa-client/pnpm-lock.yaml index f875d36ba7bae2db40b66c5570722a489edb5300..c447ee766cd2cdc982a9fcdb64d528ddd73d27e5 100644 --- a/alfa-client/pnpm-lock.yaml +++ b/alfa-client/pnpm-lock.yaml @@ -4016,6 +4016,10 @@ packages: resolution: {integrity: sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.24.0': + resolution: {integrity: sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/type-utils@7.18.0': resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4043,6 +4047,10 @@ packages: resolution: {integrity: sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.24.0': + resolution: {integrity: sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@7.18.0': resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4061,6 +4069,12 @@ packages: typescript: optional: true + '@typescript-eslint/typescript-estree@8.24.0': + resolution: {integrity: sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.8.0' + '@typescript-eslint/utils@7.18.0': resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4073,6 +4087,13 @@ packages: peerDependencies: eslint: ^8.57.0 || ^9.0.0 + '@typescript-eslint/utils@8.24.0': + resolution: {integrity: sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + '@typescript-eslint/visitor-keys@7.18.0': resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} @@ -4081,8 +4102,12 @@ packages: resolution: {integrity: sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@ungap/structured-clone@1.2.1': - resolution: {integrity: sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==} + '@typescript-eslint/visitor-keys@8.24.0': + resolution: {integrity: sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} '@vitejs/plugin-basic-ssl@1.1.0': resolution: {integrity: sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==} @@ -5855,6 +5880,10 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -9690,6 +9719,12 @@ packages: peerDependencies: typescript: '>=4.2.0' + ts-api-utils@2.0.1: + resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -14290,7 +14325,7 @@ snapshots: '@nx/js': 19.8.8(@babel/traverse@7.25.9)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.13))(@swc/types@0.1.17)(typescript@5.5.4))(@swc/core@1.5.29(@swc/helpers@0.5.13))(@types/node@20.17.6)(nx@19.8.8(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.13))(@swc/types@0.1.17)(typescript@5.5.4))(@swc/core@1.5.29(@swc/helpers@0.5.13)))(typescript@5.5.4) '@typescript-eslint/parser': 7.18.0(eslint@8.57.0)(typescript@5.5.4) '@typescript-eslint/type-utils': 8.13.0(eslint@8.57.0)(typescript@5.5.4) - '@typescript-eslint/utils': 8.13.0(eslint@8.57.0)(typescript@5.5.4) + '@typescript-eslint/utils': 8.24.0(eslint@8.57.0)(typescript@5.5.4) chalk: 4.1.2 confusing-browser-globals: 1.0.11 globals: 15.12.0 @@ -15946,6 +15981,11 @@ snapshots: '@typescript-eslint/types': 8.13.0 '@typescript-eslint/visitor-keys': 8.13.0 + '@typescript-eslint/scope-manager@8.24.0': + dependencies: + '@typescript-eslint/types': 8.24.0 + '@typescript-eslint/visitor-keys': 8.24.0 + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) @@ -15974,6 +16014,8 @@ snapshots: '@typescript-eslint/types@8.13.0': {} + '@typescript-eslint/types@8.24.0': {} + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.5.4)': dependencies: '@typescript-eslint/types': 7.18.0 @@ -16004,6 +16046,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.24.0(typescript@5.5.4)': + dependencies: + '@typescript-eslint/types': 8.24.0 + '@typescript-eslint/visitor-keys': 8.24.0 + debug: 4.3.7(supports-color@8.1.1) + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 2.0.1(typescript@5.5.4) + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@7.18.0(eslint@8.57.0)(typescript@5.5.4)': dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.0) @@ -16026,6 +16082,17 @@ snapshots: - supports-color - typescript + '@typescript-eslint/utils@8.24.0(eslint@8.57.0)(typescript@5.5.4)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.0) + '@typescript-eslint/scope-manager': 8.24.0 + '@typescript-eslint/types': 8.24.0 + '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.5.4) + eslint: 8.57.0 + typescript: 5.5.4 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@7.18.0': dependencies: '@typescript-eslint/types': 7.18.0 @@ -16036,7 +16103,12 @@ snapshots: '@typescript-eslint/types': 8.13.0 eslint-visitor-keys: 3.4.3 - '@ungap/structured-clone@1.2.1': {} + '@typescript-eslint/visitor-keys@8.24.0': + dependencies: + '@typescript-eslint/types': 8.24.0 + eslint-visitor-keys: 4.2.0 + + '@ungap/structured-clone@1.3.0': {} '@vitejs/plugin-basic-ssl@1.1.0(vite@5.4.6(@types/node@20.17.6)(less@4.2.0)(sass@1.77.6)(stylus@0.59.0)(terser@5.31.6))': dependencies: @@ -18141,6 +18213,8 @@ snapshots: eslint-visitor-keys@3.4.3: {} + eslint-visitor-keys@4.2.0: {} + eslint@8.57.0: dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@8.57.0) @@ -18150,7 +18224,7 @@ snapshots: '@humanwhocodes/config-array': 0.11.14 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.1 + '@ungap/structured-clone': 1.3.0 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 @@ -22722,6 +22796,10 @@ snapshots: dependencies: typescript: 5.5.4 + ts-api-utils@2.0.1(typescript@5.5.4): + dependencies: + typescript: 5.5.4 + ts-dedent@2.2.0: {} ts-interface-checker@0.1.13: {}