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 793380861ceff5e4e5d639eefd089b8da9947521..064f9d02f798c0aa88ac73b0614fa62be584390a 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,38 +21,16 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { - BlobWithFileName, - createEmptyStateResource, - createErrorStateResource, - createStateResource, - EMPTY_STRING, - getMessageForInvalidParam, - HttpError, - HttpHeader, - isNotNil, - isUnprocessableEntity, - isValidationFieldFileSizeExceedError, - sanitizeFileName, - StateResource, -} from '@alfa-client/tech-shared'; +import { BlobWithFileName, createEmptyStateResource, createErrorStateResource, createStateResource, EMPTY_STRING, getMessageForInvalidParam, HttpError, 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 { getUrl, Resource, ResourceUri } from '@ngxp/rest'; -import { randomUUID } from 'crypto'; import { saveAs } from 'file-saver'; import { isNil } from 'lodash-es'; import { BehaviorSubject, forkJoin, Observable, of, throwError } from 'rxjs'; import { catchError, map, mergeMap, startWith, switchMap } from 'rxjs/operators'; -import { - BinaryFileListResource, - BinaryFileResource, - FileUploadType, - ToUploadFile, - UploadFile, - UploadFilesByType, -} from './binary-file.model'; +import { BinaryFileListResource, BinaryFileResource, FileUploadType, ToUploadFile, UploadFile, UploadFilesByType, } from './binary-file.model'; import { BinaryFileRepository } from './binary-file.repository'; @Injectable({ providedIn: 'root' }) @@ -89,7 +67,7 @@ export class BinaryFileService { _buildUploadFile(toUploadFile: ToUploadFile): UploadFile { return { - key: randomUUID(), + key: crypto.randomUUID(), fileToUpload: toUploadFile.file, uploadedFile: this._handleUpload(toUploadFile), }; diff --git a/alfa-client/libs/design-component/src/lib/form/multi-file-upload-editor/multi-file-upload-editor.component.ts b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-editor/multi-file-upload-editor.component.ts index cd8fa52d652eeb371b9c3c89a5ba89c18e46393c..1845ef59e0f670837f887258ef97027894285fbb 100644 --- a/alfa-client/libs/design-component/src/lib/form/multi-file-upload-editor/multi-file-upload-editor.component.ts +++ b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-editor/multi-file-upload-editor.component.ts @@ -22,21 +22,15 @@ * unter der Lizenz sind dem Lizenztext zu entnehmen. */ import { BinaryFileModule } from '@alfa-client/binary-file'; -import { BinaryFileResource, BinaryFileService, FileUploadType } from '@alfa-client/binary-file-shared'; -import { StateResource, TechSharedModule } from '@alfa-client/tech-shared'; +import { BinaryFileService, FileUploadType } from '@alfa-client/binary-file-shared'; +import { TechSharedModule } from '@alfa-client/tech-shared'; import { Component, HostListener, inject, Input } 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'; import { FormControlEditorAbstractComponent } from '../formcontrol-editor.abstract.component'; -export interface MultiUploadItem { - file?: File; - uploadStateResource: Observable<StateResource<BinaryFileResource>>; -} - @Component({ selector: 'ods-multi-file-upload-editor', templateUrl: './multi-file-upload-editor.component.html', diff --git a/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-container.component.html b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-container.component.html new file mode 100644 index 0000000000000000000000000000000000000000..59c512375bac8d881e3fea4a146d9b60fa47fc0b --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-container.component.html @@ -0,0 +1,6 @@ +<ods-multi-file-upload-list + [uploadedFiles]="uploadedFiles$ | async" + [parentFormArrayName]="parentFormArrayName" + [listOrientation]="listOrientation" + (delete)="onDelete($event)" +></ods-multi-file-upload-list> diff --git a/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-container.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-container.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7676cc0126f1013b265eb17e97cf31dbe1608d5 --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-container.component.spec.ts @@ -0,0 +1,62 @@ +import { BinaryFileService, UploadFile } from '@alfa-client/binary-file-shared'; +import { mock, Mock } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect } from '@jest/globals'; +import { MockComponent } from 'ng-mocks'; +import { createUploadFile } from '../../../../../binary-file-shared/test/binary-file'; +import { singleColdCompleted } from '../../../../../tech-shared/test/marbles'; +import { + MultiFileUploadListContainerComponent, + MultiFileUploadListOrientation, +} from './multi-file-upload-list-container.component'; +import { MultiFileUploadListComponent } from './multi-file-upload-list/multi-file-upload-list.component'; + +describe('MultiFileUploadListContainerComponent', () => { + let component: MultiFileUploadListContainerComponent; + let fixture: ComponentFixture<MultiFileUploadListContainerComponent>; + + let binaryFileService: Mock<BinaryFileService>; + + beforeEach(() => { + binaryFileService = mock(BinaryFileService); + }); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MultiFileUploadListContainerComponent], + declarations: [MockComponent(MultiFileUploadListComponent)], + providers: [{ provide: BinaryFileService, useValue: binaryFileService }], + }).compileComponents(); + + fixture = TestBed.createComponent(MultiFileUploadListContainerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('component', () => { + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have default value', () => { + expect(component.listOrientation).toEqual(MultiFileUploadListOrientation.HORIZONTAL); + }); + + describe('ngOnInit', () => { + 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/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-container.component.ts b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-container.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..91ee20e2ab1dad2fcd2ca637d9e00d17f81080e8 --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-container.component.ts @@ -0,0 +1,34 @@ +import { BinaryFileService, UploadFile } from '@alfa-client/binary-file-shared'; +import { AsyncPipe } from '@angular/common'; +import { Component, inject, Input, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { MultiFileUploadListComponent } from './multi-file-upload-list/multi-file-upload-list.component'; + +export enum MultiFileUploadListOrientation { + HORIZONTAL = 'horizontal', + VERTICAL = 'vertical', +} + +@Component({ + selector: 'ods-multi-file-upload-list-container', + standalone: true, + templateUrl: './multi-file-upload-list-container.component.html', + imports: [MultiFileUploadListComponent, AsyncPipe], +}) +export class MultiFileUploadListContainerComponent implements OnInit { + @Input() fileUploadType: string; + @Input() parentFormArrayName: string; + @Input() listOrientation: MultiFileUploadListOrientation = MultiFileUploadListOrientation.HORIZONTAL; + + private readonly binaryFileService: BinaryFileService = inject(BinaryFileService); + + public uploadedFiles$: Observable<UploadFile[]>; + + ngOnInit(): void { + this.uploadedFiles$ = this.binaryFileService.getUploadedFiles(this.fileUploadType); + } + + public onDelete(key: string): void { + this.binaryFileService.deleteUploadedFile(this.fileUploadType, key); + } +} diff --git a/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-item/multi-file-upload-list-item.component.html b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-item/multi-file-upload-list-item.component.html new file mode 100644 index 0000000000000000000000000000000000000000..fd105dcbb217c89836879c291d6c03536e021c20 --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-item/multi-file-upload-list-item.component.html @@ -0,0 +1,20 @@ +@if (uploadStateResource.loading || uploadStateResource.error) { + <ods-attachment + [loadingCaption]="file.name" + errorCaption="Fehler beim Hochladen" + [errorMessages]="uploadStateResource.error | convertProblemDetailToErrorMessages" + description="Anhang wird hochgeladen" + [isLoading]="uploadStateResource.loading" + data-test-id="multi-file-upload-list-item-attachment-upload" + ></ods-attachment> +} @else if (uploadStateResource.resource) { + <ods-attachment-wrapper> + <alfa-binary-file2-container + [file]="uploadStateResource.resource" + [deletable]="true" + (startDelete)="onDelete($event)" + data-test-id="multi-file-upload-list-item-uploaded" + > + </alfa-binary-file2-container> + </ods-attachment-wrapper> +} diff --git a/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-item/multi-file-upload-list-item.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-item/multi-file-upload-list-item.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..68154b8030af28de4d2b35d0a9f2bd99a694154e --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-item/multi-file-upload-list-item.component.spec.ts @@ -0,0 +1,89 @@ +import { BinaryFileResource } from '@alfa-client/binary-file-shared'; +import { createEmptyStateResource, createErrorStateResource, createStateResource } from '@alfa-client/tech-shared'; +import { existsAsHtmlElement, notExistsAsHtmlElement } from '@alfa-client/test-utils'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { expect } from '@jest/globals'; +import { + createBinaryFileResource, + createLoadingBinaryFileStateResource, +} from '../../../../../../binary-file-shared/test/binary-file'; +import { getDataTestIdOf } from '../../../../../../tech-shared/test/data-test'; +import { createProblemDetail } from '../../../../../../tech-shared/test/error'; +import { createFile } from '../../../../../../tech-shared/test/file'; +import { MultiFileUploadListItemComponent } from './multi-file-upload-list-item.component'; + +describe('MultiFileUploadListItemComponent', () => { + let component: MultiFileUploadListItemComponent; + let fixture: ComponentFixture<MultiFileUploadListItemComponent>; + + const attachmentTestId: string = getDataTestIdOf('multi-file-upload-list-item-attachment-upload'); + const binaryFileContainerTestId: string = getDataTestIdOf('multi-file-upload-list-item-uploaded'); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MultiFileUploadListItemComponent], + // declarations: [MockComponent(BinaryFile2ContainerComponent)], + }).compileComponents(); + + fixture = TestBed.createComponent(MultiFileUploadListItemComponent); + component = fixture.componentInstance; + component.uploadStateResource = createEmptyStateResource(); + fixture.detectChanges(); + }); + + describe('component', () => { + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('onDelete', () => { + beforeEach(() => { + component.delete.emit = jest.fn(); + }); + + it('should emit', () => { + component.key = 'test'; + const binaryFileResource: BinaryFileResource = createBinaryFileResource(); + + component.onDelete(binaryFileResource); + + expect(component.delete.emit).toHaveBeenCalledWith({ key: component.key, binaryFileResource }); + }); + }); + }); + + // TODO: testing standalone components with mocked components + xdescribe('template', () => { + const file: File = createFile(); + + beforeEach(() => { + component.file = file; + }); + + describe('attachment upload', () => { + it('should exists on loading', () => { + component.uploadStateResource = createLoadingBinaryFileStateResource(); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, attachmentTestId); + }); + + it('should exists on error', () => { + component.uploadStateResource = createErrorStateResource(createProblemDetail()); + + fixture.detectChanges(); + + existsAsHtmlElement(fixture, attachmentTestId); + }); + + it('should not exists on loaded', () => { + component.uploadStateResource = createStateResource(createBinaryFileResource()); + + fixture.detectChanges(); + + notExistsAsHtmlElement(fixture, attachmentTestId); + }); + }); + }); +}); diff --git a/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-item/multi-file-upload-list-item.component.ts b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-item/multi-file-upload-list-item.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..8c0fe26c612f23e409e5835f1e55223aabdb6797 --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-item/multi-file-upload-list-item.component.ts @@ -0,0 +1,28 @@ +import { BinaryFileModule } from '@alfa-client/binary-file'; +import { BinaryFileResource } from '@alfa-client/binary-file-shared'; +import { StateResource, TechSharedModule } from '@alfa-client/tech-shared'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { AttachmentComponent, AttachmentWrapperComponent } from '@ods/system'; + +export interface FileToDelete { + key: string; + binaryFileResource: BinaryFileResource; +} + +@Component({ + selector: 'ods-multi-file-upload-list-item', + standalone: true, + templateUrl: './multi-file-upload-list-item.component.html', + imports: [AttachmentComponent, AttachmentWrapperComponent, TechSharedModule, BinaryFileModule], +}) +export class MultiFileUploadListItemComponent { + @Input() uploadStateResource: StateResource<BinaryFileResource>; + @Input() file: File; + @Input() key: string; + + @Output() delete: EventEmitter<FileToDelete> = new EventEmitter(); + + public onDelete(binaryFileResource: BinaryFileResource): void { + this.delete.emit({ key: this.key, binaryFileResource }); + } +} diff --git a/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list/multi-file-upload-list.component.html b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list/multi-file-upload-list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..9eb45abe65843e9dc19cf2c14acf7f4b9475b10e --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list/multi-file-upload-list.component.html @@ -0,0 +1,10 @@ +<div [class]="listOrientationClasses"> + @for (uploadItem of uploadItems; track uploadItem.key) { + <ods-multi-file-upload-list-item + [key]="uploadItem.key" + [uploadStateResource]="uploadItem.uploadedFile | async" + [file]="uploadItem.fileToUpload" + (delete)="onDelete($event)" + ></ods-multi-file-upload-list-item> + } +</div> diff --git a/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list/multi-file-upload-list.component.spec.ts b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list/multi-file-upload-list.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ef70db01f0a4e2137ca97eb02833b95bc7605f9 --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list/multi-file-upload-list.component.spec.ts @@ -0,0 +1,138 @@ +import { BinaryFileResource, UploadFile } from '@alfa-client/binary-file-shared'; +import { createEmptyStateResource, createErrorStateResource, createStateResource, StateResource } from '@alfa-client/tech-shared'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormGroupDirective, ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; +import { expect } from '@jest/globals'; +import { getUrl } from '@ngxp/rest'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { createBinaryFileResource, createUploadFile } from '../../../../../../binary-file-shared/test/binary-file'; +import { createProblemDetail } from '../../../../../../tech-shared/test/error'; +import { MultiFileUploadListOrientation } from '../multi-file-upload-list-container.component'; +import { MultiFileUploadListItemComponent } from '../multi-file-upload-list-item/multi-file-upload-list-item.component'; +import { MultiFileUploadListComponent } from './multi-file-upload-list.component'; + +describe('MultiFileUploadListComponent', () => { + let component: MultiFileUploadListComponent; + let fixture: ComponentFixture<MultiFileUploadListComponent>; + + const fb = new UntypedFormBuilder(); + const formGroupDirective = new FormGroupDirective([], []); + formGroupDirective.form = fb.group({ + attachments: fb.control(null), + }); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MultiFileUploadListComponent, ReactiveFormsModule], + declarations: [MockComponent(MultiFileUploadListItemComponent)], + providers: [ + { + provide: FormGroupDirective, + useValue: formGroupDirective, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(MultiFileUploadListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('component', () => { + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('set list orientation', () => { + it('should update class', () => { + component._updateListOrientationClass = jest.fn(); + + component.listOrientation = MultiFileUploadListOrientation.VERTICAL; + + expect(component._updateListOrientationClass).toHaveBeenCalledWith(MultiFileUploadListOrientation.VERTICAL); + }); + }); + + describe('set uploaded files', () => { + it('should update upload items', () => { + component._updateUploadItems = jest.fn(); + const uploadedFiles: UploadFile[] = [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(); + }); + + it('should set upload items', () => { + const uploadFiles: UploadFile[] = [createUploadFile()]; + + component._updateUploadItems(uploadFiles); + + expect(component.uploadItems).toEqual(uploadFiles); + }); + + it('should add file url on successful upload', () => { + const uploadStateResource: StateResource<BinaryFileResource> = createStateResource(createBinaryFileResource()); + const uploadFiles: UploadFile[] = [{ ...createUploadFile(), uploadedFile: of(uploadStateResource) }]; + + component._updateUploadItems(uploadFiles); + component.uploadItems[0].uploadedFile.subscribe(); + + expect(component._addFileUrl).toHaveBeenCalledWith(uploadStateResource.resource); + }); + + it('should NOT add file url on loading', () => { + const uploadStateResource: StateResource<BinaryFileResource> = createEmptyStateResource(true); + const uploadFiles: UploadFile[] = [{ ...createUploadFile(), uploadedFile: of(uploadStateResource) }]; + + component._updateUploadItems(uploadFiles); + component.uploadItems[0].uploadedFile.subscribe(); + + expect(component._addFileUrl).not.toHaveBeenCalled(); + }); + + it('should NOT add file url on error', () => { + const uploadStateResource: StateResource<BinaryFileResource> = createErrorStateResource(createProblemDetail()); + const uploadFiles: UploadFile[] = [{ ...createUploadFile(), uploadedFile: of(uploadStateResource) }]; + + component._updateUploadItems(uploadFiles); + component.uploadItems[0].uploadedFile.subscribe(); + + expect(component._addFileUrl).not.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', () => {}); + }); + }); +}); diff --git a/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list/multi-file-upload-list.component.ts b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list/multi-file-upload-list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..689d116c59a2481498cbb7bf088cc12a17662c7f --- /dev/null +++ b/alfa-client/libs/design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list/multi-file-upload-list.component.ts @@ -0,0 +1,88 @@ +import { BinaryFileModule } from '@alfa-client/binary-file'; +import { BinaryFileResource, UploadFile } from '@alfa-client/binary-file-shared'; +import { doOnValidStateResource, StateResource, TechSharedModule } from '@alfa-client/tech-shared'; +import { AsyncPipe } 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 { tap } from 'rxjs'; +import { MultiFileUploadListOrientation } from '../multi-file-upload-list-container.component'; +import { + FileToDelete, + MultiFileUploadListItemComponent, +} from '../multi-file-upload-list-item/multi-file-upload-list-item.component'; + +const verticalClasses: string = 'flex flex-col'; +const horizontalClasses: string = 'flex flex-row flex-wrap'; + +@Component({ + selector: 'ods-multi-file-upload-list', + standalone: true, + templateUrl: './multi-file-upload-list.component.html', + imports: [AsyncPipe, TechSharedModule, BinaryFileModule, MultiFileUploadListItemComponent], +}) +export class MultiFileUploadListComponent implements OnInit { + @Input() parentFormArrayName: string; + + @Input() set listOrientation(value: MultiFileUploadListOrientation) { + this._updateListOrientationClass(value); + } + + @Input() set uploadedFiles(value: UploadFile[]) { + this._updateUploadItems(value); + } + + @Output() delete: EventEmitter<string> = new EventEmitter(); + + public uploadItems: UploadFile[]; + public listOrientationClasses: string = 'flex flex-row'; + + _fileLinkControls: UntypedFormArray = new UntypedFormArray([]); + _fileUrls: string[] = []; + + constructor(public parentForm: FormGroupDirective) {} + + ngOnInit(): void { + this._fileLinkControls = this.parentForm.form.get(this.parentFormArrayName) as UntypedFormArray; + } + + _updateUploadItems(uploadFiles: UploadFile[]): void { + this.uploadItems = uploadFiles; + this.uploadItems.forEach((item) => { + item.uploadedFile = item.uploadedFile.pipe( + tap((stateResource: StateResource<BinaryFileResource>) => + doOnValidStateResource(stateResource, () => this._addFileUrl(stateResource.resource)), + ), + ); + }); + } + + _addFileUrl(binaryFileResource: BinaryFileResource): void { + this._fileUrls = [...this._fileUrls, getUrl(binaryFileResource)]; + this._updateForm(this._fileUrls); + } + + _updateForm(fileUrls: string[]): void { + this._fileLinkControls.clear(); + fileUrls.forEach((link: string) => this._fileLinkControls.push(new UntypedFormControl(link))); + } + + _updateListOrientationClass(listOrientation: MultiFileUploadListOrientation): void { + switch (listOrientation) { + case MultiFileUploadListOrientation.VERTICAL: + this.listOrientationClasses = verticalClasses; + break; + case MultiFileUploadListOrientation.HORIZONTAL: + this.listOrientationClasses = horizontalClasses; + break; + default: + this.listOrientationClasses = horizontalClasses; + break; + } + } + + public onDelete(fileToDelete: FileToDelete): void { + this._fileUrls = this._fileUrls.filter((url: string) => url !== getUrl(fileToDelete.binaryFileResource)); + this.delete.emit(fileToDelete.key); + } +} 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..ef16d6d28624d06222c7d33336efa4c013a901bf 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 = '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..18cb63b7f3129d4f586aebfa5ddefeb347c6f1e9 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,18 @@ --> <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-multi-file-upload-list-container + [parentFormArrayName]="formServiceClass.FIELD_ATTACHMENTS" + [fileUploadType]="KOMMENTAR_UPLOADED_ATTACHMENTS" + ></ods-multi-file-upload-list-container> + <ods-multi-file-upload-editor + [fileUploadType]="KOMMENTAR_UPLOADED_ATTACHMENTS" + [uploadResource]="kommentarListStateResource.resource" + [uploadLinkRelation]="kommentarListLinkRel.UPLOAD_FILE" + ></ods-multi-file-upload-editor> <div class="buttons"> <ozgcloud-stroked-button-with-spinner 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..750e7a42caa8701ee60436a903826f2f3f6eb162 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,16 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; +import { BinaryFileListLinkRel, BinaryFileResource } from '@alfa-client/binary-file-shared'; 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 { KOMMENTAR_UPLOADED_ATTACHMENTS, 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 { 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 { MultiFileUploadListOrientation } from '../../../../../design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-container.component'; +import { KommentarFormService } from './kommentar.formservice'; @Component({ selector: 'alfa-kommentar-form', @@ -52,12 +44,11 @@ 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 KOMMENTAR_UPLOADED_ATTACHMENTS = KOMMENTAR_UPLOADED_ATTACHMENTS; attachments$: Observable<BinaryFileResource[]> = of([]); @@ -76,11 +67,7 @@ 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 { @@ -90,4 +77,6 @@ export class KommentarFormComponent implements OnChanges { submit(): void { this.submitInProgress$ = <Observable<StateResource<CommandResource>>>this.formService.submit(); } + + protected readonly ListOrientation = MultiFileUploadListOrientation; } diff --git a/alfa-client/libs/kommentar/src/lib/kommentar.module.ts b/alfa-client/libs/kommentar/src/lib/kommentar.module.ts index 978a2bf16d4cdd9e1c53149ffc1aefefab5e8cdb..10537ca0612c52d538269641346968f5058f460d 100644 --- a/alfa-client/libs/kommentar/src/lib/kommentar.module.ts +++ b/alfa-client/libs/kommentar/src/lib/kommentar.module.ts @@ -21,14 +21,16 @@ * Die sprachspezifischen Genehmigungen und Beschränkungen * unter der Lizenz sind dem Lizenztext zu entnehmen. */ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; import { BinaryFileModule } from '@alfa-client/binary-file'; import { KommentarSharedModule } from '@alfa-client/kommentar-shared'; import { TechSharedModule } from '@alfa-client/tech-shared'; import { UiModule } from '@alfa-client/ui'; import { UserProfileModule } from '@alfa-client/user-profile'; import { VorgangSharedModule } from '@alfa-client/vorgang-shared'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MultiFileUploadEditorComponent } from '../../../design-component/src/lib/form/multi-file-upload-editor/multi-file-upload-editor.component'; +import { MultiFileUploadListContainerComponent } from '../../../design-component/src/lib/form/multi-file-upload-list-container/multi-file-upload-list-container.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'; @@ -43,6 +45,8 @@ import { KommentarListItemInVorgangComponent } from './kommentar-list-in-vorgang TechSharedModule, UserProfileModule, BinaryFileModule, + MultiFileUploadEditorComponent, + MultiFileUploadListContainerComponent, ], declarations: [ KommentarListInVorgangContainerComponent, 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, + }; +}