From 3a907a9cf54157f6ce0dcff976bbc84431c5b911 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 6 Mar 2023 19:12:48 +0100 Subject: [PATCH 01/22] photos can be displayed in details and child block component with caching --- .../child-block/child-block.component.html | 2 +- .../child-block/child-block.component.ts | 14 ++++++++- .../child-dev-project/children/model/child.ts | 7 +++++ src/app/core/config/config-fix.ts | 2 +- src/app/core/core-components.ts | 7 +++++ .../edit-photo/new-photo.component.html | 2 ++ .../edit-photo/new-photo.component.scss | 6 ++++ .../edit-photo/new-photo.component.spec.ts | 23 ++++++++++++++ .../edit-photo/new-photo.component.ts | 30 +++++++++++++++++++ src/app/features/file/couchdb-file.service.ts | 24 +++++++++++++++ 10 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html create mode 100644 src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss create mode 100644 src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts create mode 100644 src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts diff --git a/src/app/child-dev-project/children/child-block/child-block.component.html b/src/app/child-dev-project/children/child-block/child-block.component.html index b944536bd2..b095af8ca8 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.html +++ b/src/app/child-dev-project/children/child-block/child-block.component.html @@ -5,7 +5,7 @@ [class.inactive]="!entity.isActive" class="truncate-text container" > - + {{ entity?.toString() }} ({{ entity?.projectNumber }}) diff --git a/src/app/child-dev-project/children/child-block/child-block.component.ts b/src/app/child-dev-project/children/child-block/child-block.component.ts index cb1423bf06..42a17cdda1 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.ts +++ b/src/app/child-dev-project/children/child-block/child-block.component.ts @@ -13,6 +13,8 @@ import { DynamicComponent } from "../../../core/view/dynamic-components/dynamic- import { NgIf } from "@angular/common"; import { TemplateTooltipDirective } from "../../../core/common-components/template-tooltip/template-tooltip.directive"; import { ChildBlockTooltipComponent } from "./child-block-tooltip/child-block-tooltip.component"; +import { CouchdbFileService } from "../../../features/file/couchdb-file.service"; +import { SafeUrl } from "@angular/platform-browser"; @DynamicComponent("ChildBlock") @Component({ @@ -32,7 +34,12 @@ export class ChildBlockComponent implements OnInitDynamicComponent, OnChanges { /** prevent additional details to be displayed in a tooltip on mouse over */ @Input() tooltipDisabled: boolean; - constructor(@Optional() private childrenService: ChildrenService) {} + imgPath: SafeUrl; + + constructor( + private fileService: CouchdbFileService, + @Optional() private childrenService: ChildrenService + ) {} async ngOnChanges(changes: SimpleChanges) { if (changes.hasOwnProperty("entityId")) { @@ -50,5 +57,10 @@ export class ChildBlockComponent implements OnInitDynamicComponent, OnChanges { } this.linkDisabled = config.linkDisabled; this.tooltipDisabled = config.tooltipDisabled; + if (this.entity.photo2) { + this.fileService + .loadFile(this.entity, "photo2") + .subscribe((res) => (this.imgPath = res)); + } } } diff --git a/src/app/child-dev-project/children/model/child.ts b/src/app/child-dev-project/children/model/child.ts index 1f995fe6ad..a211888df0 100644 --- a/src/app/child-dev-project/children/model/child.ts +++ b/src/app/child-dev-project/children/model/child.ts @@ -123,6 +123,13 @@ export class Child extends Entity { ), }; + @DatabaseField({ + dataType: "file", + label: $localize`:Label for the filename of a photo of a child:Photo Filename`, + editComponent: "NewPhoto", + }) + photo2: string; + @DatabaseField({ label: $localize`:Label for the phone number of a child:Phone Number`, }) diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index 2dd6533bdc..c2beac886b 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -549,7 +549,7 @@ export const defaultJsonConfig = { "component": "Form", "config": { "cols": [ - ["photo"], + ["photo", "photo2"], [ "name", "projectNumber", diff --git a/src/app/core/core-components.ts b/src/app/core/core-components.ts index 15f45c8211..9378ca6290 100644 --- a/src/app/core/core-components.ts +++ b/src/app/core/core-components.ts @@ -99,6 +99,13 @@ export const coreComponents: ComponentTuple[] = [ "./entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component" ).then((c) => c.EditPhotoComponent), ], + [ + "NewPhoto", + () => + import( + "./entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component" + ).then((c) => c.NewPhotoComponent), + ], [ "EditNumber", () => diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html new file mode 100644 index 0000000000..b7962b6794 --- /dev/null +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html @@ -0,0 +1,2 @@ +profile photo + diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss new file mode 100644 index 0000000000..f30853f8e1 --- /dev/null +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss @@ -0,0 +1,6 @@ +.child-pic { + width: 150px; + height: 150px; + border-radius: 50%; + object-fit: cover; +} diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts new file mode 100644 index 0000000000..755a2a1e12 --- /dev/null +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NewPhotoComponent } from './new-photo.component'; + +describe('NewPhotoComponent', () => { + let component: NewPhotoComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ NewPhotoComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(NewPhotoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts new file mode 100644 index 0000000000..d5568cb284 --- /dev/null +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts @@ -0,0 +1,30 @@ +import { Component } from "@angular/core"; +import { EditComponent, EditPropertyConfig } from "../edit-component"; +import { CouchdbFileService } from "../../../../../features/file/couchdb-file.service"; +import { SafeUrl } from "@angular/platform-browser"; +import { EditFileComponent } from "../../../../../features/file/edit-file/edit-file.component"; + +@Component({ + selector: "app-new-photo", + standalone: true, + imports: [EditFileComponent], + templateUrl: "./new-photo.component.html", + styleUrls: ["./new-photo.component.scss"], +}) +export class NewPhotoComponent extends EditComponent { + imgPath: SafeUrl; + + constructor(private fileService: CouchdbFileService) { + super(); + } + + onInitFromDynamicConfig(config: EditPropertyConfig) { + super.onInitFromDynamicConfig(config); + this.fileService + .loadFile(this.entity, this.formControlName) + .subscribe((res) => { + this.imgPath = res; + console.log("url", this.imgPath); + }); + } +} diff --git a/src/app/features/file/couchdb-file.service.ts b/src/app/features/file/couchdb-file.service.ts index 56b8c1b98c..0b13232312 100644 --- a/src/app/features/file/couchdb-file.service.ts +++ b/src/app/features/file/couchdb-file.service.ts @@ -26,6 +26,7 @@ import { ProgressComponent } from "./progress/progress.component"; import { EntityRegistry } from "../../core/entity/database-entity.decorator"; import { LoggingService } from "../../core/logging/logging.service"; import { ObservableQueue } from "./observable-queue/observable-queue"; +import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; /** * Stores the files in the CouchDB. @@ -34,11 +35,13 @@ import { ObservableQueue } from "./observable-queue/observable-queue"; */ @Injectable() export class CouchdbFileService extends FileService { + cache: { [key: string]: SafeUrl } = {}; private attachmentsUrl = `${AppSettings.DB_PROXY_PREFIX}/${AppSettings.DB_NAME}-attachments`; // TODO it seems like failed requests are executed again when a new one is done private requestQueue = new ObservableQueue(); constructor( + private sanitizer: DomSanitizer, private http: HttpClient, private dialog: MatDialog, private snackbar: MatSnackBar, @@ -50,6 +53,7 @@ export class CouchdbFileService extends FileService { } uploadFile(file: File, entity: Entity, property: string): Observable { + // TODO update cache if file is cached const obs = this.requestQueue.add( this.runFileUpload(file, entity, property) ); @@ -145,6 +149,26 @@ export class CouchdbFileService extends FileService { }); } + loadFile(entity: Entity, property: string) { + const path = `${entity.getId(true)}/${property}`; + if (this.cache[path]) { + return of(this.cache[path]); + } + return this.http + .get(`${this.attachmentsUrl}/${path}`, { + responseType: "blob", + headers: { "ngsw-bypass": "" }, + }) + .pipe( + map((blob) => { + const url = URL.createObjectURL(blob); + const safe = this.sanitizer.bypassSecurityTrustUrl(url); + this.cache[path] = safe; + return safe; + }) + ); + } + private reportProgress(message: string, obs: Observable>) { const progress = obs.pipe( filter( From a91c902a980f524b0985f5ff89b4fb573842d52f Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 7 Mar 2023 10:42:46 +0100 Subject: [PATCH 02/22] refactored queue to not send multiple similar requests at the same time --- src/app/features/file/couchdb-file.service.ts | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/app/features/file/couchdb-file.service.ts b/src/app/features/file/couchdb-file.service.ts index 0b13232312..f52bec6ea5 100644 --- a/src/app/features/file/couchdb-file.service.ts +++ b/src/app/features/file/couchdb-file.service.ts @@ -35,10 +35,10 @@ import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; */ @Injectable() export class CouchdbFileService extends FileService { - cache: { [key: string]: SafeUrl } = {}; private attachmentsUrl = `${AppSettings.DB_PROXY_PREFIX}/${AppSettings.DB_NAME}-attachments`; // TODO it seems like failed requests are executed again when a new one is done private requestQueue = new ObservableQueue(); + private cache: { [key: string]: Observable } = {}; constructor( private sanitizer: DomSanitizer, @@ -151,22 +151,19 @@ export class CouchdbFileService extends FileService { loadFile(entity: Entity, property: string) { const path = `${entity.getId(true)}/${property}`; - if (this.cache[path]) { - return of(this.cache[path]); - } - return this.http - .get(`${this.attachmentsUrl}/${path}`, { - responseType: "blob", - headers: { "ngsw-bypass": "" }, - }) - .pipe( - map((blob) => { - const url = URL.createObjectURL(blob); - const safe = this.sanitizer.bypassSecurityTrustUrl(url); - this.cache[path] = safe; - return safe; + if (!this.cache[path]) { + this.cache[path] = this.http + .get(`${this.attachmentsUrl}/${path}`, { + responseType: "blob", + headers: { "ngsw-bypass": "" }, }) - ); + .pipe( + map((blob) => + this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob)) + ) + ); + } + return this.cache[path]; } private reportProgress(message: string, obs: Observable>) { From a1f591dacb372f48224c04e06428e16584fd323d Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 7 Mar 2023 14:02:52 +0100 Subject: [PATCH 03/22] added edit file dialog to photo component --- .../dynamic-form-components/edit-component.ts | 8 +-- .../edit-photo/new-photo.component.html | 7 ++- .../edit-photo/new-photo.component.ts | 8 ++- src/app/features/file/couchdb-file.service.ts | 49 ++++++++++++------- .../file/edit-file/edit-file.component.ts | 12 ++--- 5 files changed, 49 insertions(+), 35 deletions(-) diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-component.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-component.ts index 38243142f8..bebd5ffbee 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-component.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-component.ts @@ -3,6 +3,7 @@ import { AbstractControl, FormControl, FormGroup } from "@angular/forms"; import { FormFieldConfig } from "../../entity-form/entity-form/FormConfig"; import { EntitySchemaField } from "../../../entity/schema/entity-schema-field"; import { Entity } from "../../../entity/model/entity"; +import { Directive, Input } from "@angular/core"; /** * The interface for the configuration which is created by the form- or the entity-subrecord-component. @@ -33,6 +34,7 @@ export interface EditPropertyConfig { * A simple helper class which sets up all the required information for edit-components. * refers to the type of the value which is processed in the component. */ +@Directive() export abstract class EditComponent implements OnInitDynamicComponent { /** * The tooltip to be displayed. @@ -42,7 +44,7 @@ export abstract class EditComponent implements OnInitDynamicComponent { /** * The name of the form control. */ - formControlName: string; + @Input() formControlName: string; /** * A label for this component. @@ -52,7 +54,7 @@ export abstract class EditComponent implements OnInitDynamicComponent { /** * The typed form control. */ - formControl: FormControl; + @Input() formControl: FormControl; /** * The parent form of the `formControl` this is always needed to correctly setup the `mat-form-field` @@ -62,7 +64,7 @@ export abstract class EditComponent implements OnInitDynamicComponent { /** * The entity which is being edited. */ - entity: Entity; + @Input() entity: Entity; /** * Additional config details for the specific component implementation. diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html index b7962b6794..b76c43d62d 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html @@ -1,2 +1,7 @@ profile photo - + diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts index d5568cb284..b57710ced2 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts @@ -3,11 +3,12 @@ import { EditComponent, EditPropertyConfig } from "../edit-component"; import { CouchdbFileService } from "../../../../../features/file/couchdb-file.service"; import { SafeUrl } from "@angular/platform-browser"; import { EditFileComponent } from "../../../../../features/file/edit-file/edit-file.component"; +import { NgIf } from "@angular/common"; @Component({ selector: "app-new-photo", standalone: true, - imports: [EditFileComponent], + imports: [EditFileComponent, NgIf], templateUrl: "./new-photo.component.html", styleUrls: ["./new-photo.component.scss"], }) @@ -22,9 +23,6 @@ export class NewPhotoComponent extends EditComponent { super.onInitFromDynamicConfig(config); this.fileService .loadFile(this.entity, this.formControlName) - .subscribe((res) => { - this.imgPath = res; - console.log("url", this.imgPath); - }); + .subscribe((res) => (this.imgPath = res)); } } diff --git a/src/app/features/file/couchdb-file.service.ts b/src/app/features/file/couchdb-file.service.ts index f52bec6ea5..76b9024cfa 100644 --- a/src/app/features/file/couchdb-file.service.ts +++ b/src/app/features/file/couchdb-file.service.ts @@ -14,6 +14,7 @@ import { filter, map, shareReplay, + tap, } from "rxjs/operators"; import { Observable, of } from "rxjs"; import { MatDialog } from "@angular/material/dialog"; @@ -63,15 +64,24 @@ export class CouchdbFileService extends FileService { private runFileUpload(file: File, entity: Entity, property: string) { const blob = new Blob([file]); - const attachmentPath = `${this.attachmentsUrl}/${entity.getId(true)}`; - return this.getAttachmentsDocument(attachmentPath).pipe( + const path = `${entity.getId(true)}/${property}`; + return this.getAttachmentsDocument( + `${this.attachmentsUrl}/${entity.getId(true)}` + ).pipe( concatMap(({ _rev }) => - this.http.put(`${attachmentPath}/${property}?rev=${_rev}`, blob, { + this.http.put(`${this.attachmentsUrl}/${path}?rev=${_rev}`, blob, { headers: { "Content-Type": file.type, "ngsw-bypass": "" }, reportProgress: true, observe: "events", }) ), + tap(() => { + if (this.cache[`${path}`]) { + this.cache[`${path}`] = of( + this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob)) + ); + } + }), // prevent http request to be executed multiple times (whenever .subscribe is called) shareReplay() ); @@ -97,19 +107,22 @@ export class CouchdbFileService extends FileService { } private runFileRemoval(entity: Entity, property: string) { - const attachmentPath = `${this.attachmentsUrl}/${entity.getId(true)}`; - return this.http.get<{ _rev: string }>(attachmentPath).pipe( - concatMap(({ _rev }) => - this.http.delete(`${attachmentPath}/${property}?rev=${_rev}`) - ), - catchError((err) => { - if (err.status === HttpStatusCode.NotFound) { - return of({ ok: true }); - } else { - throw err; - } - }) - ); + const path = `${entity.getId(true)}/${property}`; + return this.http + .get<{ _rev: string }>(`${this.attachmentsUrl}/${entity.getId(true)}`) + .pipe( + concatMap(({ _rev }) => + this.http.delete(`${this.attachmentsUrl}/${path}?rev=${_rev}`) + ), + tap(() => delete this.cache[path]), + catchError((err) => { + if (err.status === HttpStatusCode.NotFound) { + return of({ ok: true }); + } else { + throw err; + } + }) + ); } removeAllFiles(entity: Entity): Observable { @@ -155,12 +168,12 @@ export class CouchdbFileService extends FileService { this.cache[path] = this.http .get(`${this.attachmentsUrl}/${path}`, { responseType: "blob", - headers: { "ngsw-bypass": "" }, }) .pipe( map((blob) => this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob)) - ) + ), + shareReplay() ); } return this.cache[path]; diff --git a/src/app/features/file/edit-file/edit-file.component.ts b/src/app/features/file/edit-file/edit-file.component.ts index f90221f38f..b5e5f95c8e 100644 --- a/src/app/features/file/edit-file/edit-file.component.ts +++ b/src/app/features/file/edit-file/edit-file.component.ts @@ -1,8 +1,5 @@ -import { Component, ElementRef, ViewChild } from "@angular/core"; -import { - EditComponent, - EditPropertyConfig, -} from "../../../core/entity-components/entity-utils/dynamic-form-components/edit-component"; +import { Component, ElementRef, OnInit, ViewChild } from "@angular/core"; +import { EditComponent } from "../../../core/entity-components/entity-utils/dynamic-form-components/edit-component"; import { DynamicComponent } from "../../../core/view/dynamic-components/dynamic-component.decorator"; import { AlertService } from "../../../core/alerts/alert.service"; import { LoggingService } from "../../../core/logging/logging.service"; @@ -38,7 +35,7 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; ], standalone: true, }) -export class EditFileComponent extends EditComponent { +export class EditFileComponent extends EditComponent implements OnInit { @ViewChild("fileUpload") fileInput: ElementRef; private selectedFile: File; private removeClicked = false; @@ -53,8 +50,7 @@ export class EditFileComponent extends EditComponent { super(); } - onInitFromDynamicConfig(config: EditPropertyConfig) { - super.onInitFromDynamicConfig(config); + ngOnInit() { this.initialValue = this.formControl.value; this.formControl.statusChanges .pipe( From 41e4c4c65989f428ab3064a00197d7a60d1576b8 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 20 Mar 2023 13:58:56 +0100 Subject: [PATCH 04/22] ci: update docker repository (#1792) --- .github/workflows/pull-request-update.yml | 2 +- .github/workflows/tagged-commit.yml | 2 +- build/README.md | 2 +- doc/compodoc_sources/concepts/infrastructure.md | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull-request-update.yml b/.github/workflows/pull-request-update.yml index 1888a9d1d7..6b03cccd84 100644 --- a/.github/workflows/pull-request-update.yml +++ b/.github/workflows/pull-request-update.yml @@ -47,7 +47,7 @@ jobs: file: ./build/Dockerfile builder: ${{ steps.buildx.outputs.name }} push: true - tags: aamdigital/ndb-server:pr-${{ github.event.number }} + tags: aamdigitaltravis/ndb-server:pr-${{ github.event.number }} cache-from: type=gha cache-to: type=gha,mode=max - name: Deploy updated image diff --git a/.github/workflows/tagged-commit.yml b/.github/workflows/tagged-commit.yml index 03f33ebf64..5dd573ea00 100644 --- a/.github/workflows/tagged-commit.yml +++ b/.github/workflows/tagged-commit.yml @@ -26,7 +26,7 @@ jobs: file: ./build/Dockerfile builder: ${{ steps.buildx.outputs.name }} push: true - tags: aamdigital/ndb-server:${{ env.TAG }} + tags: aamdigitaltravis/ndb-server:${{ env.TAG }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | diff --git a/build/README.md b/build/README.md index 34ca842c4c..88c169508b 100644 --- a/build/README.md +++ b/build/README.md @@ -4,7 +4,7 @@ The Angular app is build within a custom docker container to ensure it is reprod Builds are triggered through GitHub Actions CI (see /.github/workflows). -The deployable server (nginx) image is published to [Docker Hub](https://hub.docker.com/r/aamdigital/ndb-server) +The deployable server (nginx) image is published to [Docker Hub](https://hub.docker.com/r/aamdigitaltravis/ndb-server) for every official (tagged) build. ## How to build & publish a new image diff --git a/doc/compodoc_sources/concepts/infrastructure.md b/doc/compodoc_sources/concepts/infrastructure.md index 7b9ec7e820..70076189e8 100644 --- a/doc/compodoc_sources/concepts/infrastructure.md +++ b/doc/compodoc_sources/concepts/infrastructure.md @@ -25,11 +25,11 @@ Once this is done, a version of Aam Digital with the new changes included can be ## Master Updates After approving a PR and merging it into the master, semantic release automatically creates a new tag for this change. -For each new tag a tagged Docker image is uploaded to [DockerHub](https://hub.docker.com/r/aamdigital/ndb-server). +For each new tag a tagged Docker image is uploaded to [DockerHub](https://hub.docker.com/r/aamdigitaltravis/ndb-server). ## Deploying Aam Digital The Docker image from DockerHub can then be downloaded and run via Docker using the following command: -> docker pull aamdigital/ndb-server && docker run -p=80:80 aamdigital/ndb-server +> docker pull aamdigitaltravis/ndb-server && docker run -p=80:80 aamdigital/ndb-server However, this will only run Aam Digital in demo-mode. To run Aam Digital with a real database, a new `config.json` file has to be mounted into the image. From fda5556f81085ef276d1f0bc4beb0ed6122f2232 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 27 Mar 2023 08:53:39 +0200 Subject: [PATCH 05/22] added compression and mock implementation to photo upload --- .../child-block/child-block.component.ts | 4 +-- src/app/core/config/config-fix.ts | 2 +- .../edit-photo/new-photo.component.html | 3 +- .../edit-photo/new-photo.component.ts | 4 +-- src/app/features/file/couchdb-file.service.ts | 9 ++++++ .../file/edit-file/edit-file.component.ts | 22 +++++++------ src/app/features/file/file-utils.ts | 32 +++++++++++++++++++ src/app/features/file/file.service.ts | 10 ++++++ src/app/features/file/mock-file.service.ts | 19 ++++++++++- 9 files changed, 88 insertions(+), 17 deletions(-) create mode 100644 src/app/features/file/file-utils.ts diff --git a/src/app/child-dev-project/children/child-block/child-block.component.ts b/src/app/child-dev-project/children/child-block/child-block.component.ts index 42a17cdda1..ee4dab5147 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.ts +++ b/src/app/child-dev-project/children/child-block/child-block.component.ts @@ -13,8 +13,8 @@ import { DynamicComponent } from "../../../core/view/dynamic-components/dynamic- import { NgIf } from "@angular/common"; import { TemplateTooltipDirective } from "../../../core/common-components/template-tooltip/template-tooltip.directive"; import { ChildBlockTooltipComponent } from "./child-block-tooltip/child-block-tooltip.component"; -import { CouchdbFileService } from "../../../features/file/couchdb-file.service"; import { SafeUrl } from "@angular/platform-browser"; +import { FileService } from "../../../features/file/file.service"; @DynamicComponent("ChildBlock") @Component({ @@ -37,7 +37,7 @@ export class ChildBlockComponent implements OnInitDynamicComponent, OnChanges { imgPath: SafeUrl; constructor( - private fileService: CouchdbFileService, + private fileService: FileService, @Optional() private childrenService: ChildrenService ) {} diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index 938771b3f5..8208719288 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -549,7 +549,7 @@ export const defaultJsonConfig = { "component": "Form", "config": { "cols": [ - ["photo", "photo2"], + ["photo2"], [ "name", "projectNumber", diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html index b76c43d62d..2010cf4c90 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html @@ -1,7 +1,8 @@ -profile photo +profile photo diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts index b57710ced2..bfae5afda9 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts @@ -1,9 +1,9 @@ import { Component } from "@angular/core"; import { EditComponent, EditPropertyConfig } from "../edit-component"; -import { CouchdbFileService } from "../../../../../features/file/couchdb-file.service"; import { SafeUrl } from "@angular/platform-browser"; import { EditFileComponent } from "../../../../../features/file/edit-file/edit-file.component"; import { NgIf } from "@angular/common"; +import { FileService } from "../../../../../features/file/file.service"; @Component({ selector: "app-new-photo", @@ -15,7 +15,7 @@ import { NgIf } from "@angular/common"; export class NewPhotoComponent extends EditComponent { imgPath: SafeUrl; - constructor(private fileService: CouchdbFileService) { + constructor(private fileService: FileService) { super(); } diff --git a/src/app/features/file/couchdb-file.service.ts b/src/app/features/file/couchdb-file.service.ts index 76b9024cfa..c47bfd1eb4 100644 --- a/src/app/features/file/couchdb-file.service.ts +++ b/src/app/features/file/couchdb-file.service.ts @@ -62,6 +62,15 @@ export class CouchdbFileService extends FileService { return obs; } + uploadImage( + file: File, + entity: Entity, + property: string, + maxSize: 360 + ): Observable { + return of({}); + } + private runFileUpload(file: File, entity: Entity, property: string) { const blob = new Blob([file]); const path = `${entity.getId(true)}/${property}`; diff --git a/src/app/features/file/edit-file/edit-file.component.ts b/src/app/features/file/edit-file/edit-file.component.ts index b5e5f95c8e..d9a9486afb 100644 --- a/src/app/features/file/edit-file/edit-file.component.ts +++ b/src/app/features/file/edit-file/edit-file.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, OnInit, ViewChild } from "@angular/core"; +import { Component, ElementRef, Input, OnInit, ViewChild } from "@angular/core"; import { EditComponent } from "../../../core/entity-components/entity-utils/dynamic-form-components/edit-component"; import { DynamicComponent } from "../../../core/view/dynamic-components/dynamic-component.decorator"; import { AlertService } from "../../../core/alerts/alert.service"; @@ -37,6 +37,7 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; }) export class EditFileComponent extends EditComponent implements OnInit { @ViewChild("fileUpload") fileInput: ElementRef; + @Input() compressImage = false; private selectedFile: File; private removeClicked = false; private initialValue: string; @@ -79,15 +80,16 @@ export class EditFileComponent extends EditComponent implements OnInit { private uploadFile(file: File) { // The maximum file size which can be processed by CouchDB before a timeout is around 200mb - this.fileService - .uploadFile(file, this.entity, this.formControlName) - .subscribe({ - error: (err) => this.handleError(err), - complete: () => { - this.initialValue = this.formControl.value; - this.selectedFile = undefined; - }, - }); + const obs = this.compressImage + ? this.fileService.uploadImage(file, this.entity, this.formControlName) + : this.fileService.uploadFile(file, this.entity, this.formControlName); + obs.subscribe({ + error: (err) => this.handleError(err), + complete: () => { + this.initialValue = this.formControl.value; + this.selectedFile = undefined; + }, + }); } private handleError(err) { diff --git a/src/app/features/file/file-utils.ts b/src/app/features/file/file-utils.ts new file mode 100644 index 0000000000..539b790aa9 --- /dev/null +++ b/src/app/features/file/file-utils.ts @@ -0,0 +1,32 @@ +export function resizeImage(file: File, maxSize = 360): Promise { + const image = new Image(); + image.src = URL.createObjectURL(file); + + return new Promise((resolve) => { + image.onload = () => { + let imageWidth = image.width, + imageHeight = image.height; + + if (imageWidth > imageHeight) { + if (imageWidth > maxSize) { + imageHeight *= maxSize / imageWidth; + imageWidth = maxSize; + } + } else { + if (imageHeight > maxSize) { + imageWidth *= maxSize / imageHeight; + imageHeight = maxSize; + } + } + + const canvas = document.createElement("canvas"); + canvas.width = imageWidth; + canvas.height = imageHeight; + + const ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0, imageWidth, imageHeight); + + resolve(canvas.toDataURL(file.type)); + }; + }); +} diff --git a/src/app/features/file/file.service.ts b/src/app/features/file/file.service.ts index f75f28c1eb..62c93e2501 100644 --- a/src/app/features/file/file.service.ts +++ b/src/app/features/file/file.service.ts @@ -5,6 +5,7 @@ import { EntityRegistry } from "../../core/entity/database-entity.decorator"; import { fileDataType } from "./file-data-type"; import { filter } from "rxjs/operators"; import { LoggingService } from "../../core/logging/logging.service"; +import { SafeUrl } from "@angular/platform-browser"; /** * This service allow handles the logic for files/attachments. @@ -78,6 +79,8 @@ export abstract class FileService { */ abstract showFile(entity: Entity, property: string): void; + abstract loadFile(entity: Entity, property: string): Observable; + /** * Uploads the file * @param file to be uploaded @@ -89,4 +92,11 @@ export abstract class FileService { entity: Entity, property: string ): Observable; + + abstract uploadImage( + file: File, + entity: Entity, + property: string, + maxSize?: number + ): Observable; } diff --git a/src/app/features/file/mock-file.service.ts b/src/app/features/file/mock-file.service.ts index b6f8716258..be66fce6c1 100644 --- a/src/app/features/file/mock-file.service.ts +++ b/src/app/features/file/mock-file.service.ts @@ -5,6 +5,10 @@ import { FileService } from "./file.service"; import { EntityMapperService } from "../../core/entity/entity-mapper.service"; import { EntityRegistry } from "../../core/entity/database-entity.decorator"; import { LoggingService } from "../../core/logging/logging.service"; +import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; +import { fromPromise } from "rxjs/internal/observable/innerFrom"; +import { resizeImage } from "./file-utils"; +import { tap } from "rxjs/operators"; /** * A mock implementation of the file service which only stores the file temporarily in the browser. @@ -18,7 +22,8 @@ export class MockFileService extends FileService { constructor( entityMapper: EntityMapperService, entities: EntityRegistry, - logger: LoggingService + logger: LoggingService, + private sanitizer: DomSanitizer ) { super(entityMapper, entities, logger); } @@ -36,9 +41,21 @@ export class MockFileService extends FileService { window.open(this.fileMap.get(entity + property), "_blank"); } + loadFile(entity: Entity, property: string): Observable { + return of( + this.sanitizer.bypassSecurityTrustUrl(this.fileMap.get(entity + property)) + ); + } + uploadFile(file: File, entity: Entity, property: string): Observable { const fileURL = URL.createObjectURL(file); this.fileMap.set(entity + property, fileURL); return of({ ok: true }); } + + uploadImage(file: File, entity: Entity, property: string): Observable { + return fromPromise(resizeImage(file)).pipe( + tap((fileUrl) => this.fileMap.set(entity + property, fileUrl)) + ); + } } From e0b06bc7d998ca2dea96e061d860531a6cd5e211 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 27 Mar 2023 17:48:59 +0200 Subject: [PATCH 06/22] implemented compressed upload for couchdb file service --- .../edit-photo/new-photo.component.html | 3 +- .../edit-photo/new-photo.component.ts | 21 ++++-- src/app/features/file/couchdb-file.service.ts | 68 ++++++++++++------- .../file/edit-file/edit-file.component.ts | 35 ++++++---- src/app/features/file/file-utils.ts | 9 ++- src/app/features/file/file.service.ts | 9 +-- src/app/features/file/mock-file.service.ts | 28 +++++--- 7 files changed, 111 insertions(+), 62 deletions(-) diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html index 2010cf4c90..403bb600e0 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html @@ -4,5 +4,6 @@ [entity]="entity" [formControl]="formControl" [formControlName]="formControlName" - [compressImage]="true" + [compressImage]="360" + (fileUpload)="updateFile($event)" > diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts index bfae5afda9..7739228663 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts @@ -1,6 +1,6 @@ import { Component } from "@angular/core"; import { EditComponent, EditPropertyConfig } from "../edit-component"; -import { SafeUrl } from "@angular/platform-browser"; +import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; import { EditFileComponent } from "../../../../../features/file/edit-file/edit-file.component"; import { NgIf } from "@angular/common"; import { FileService } from "../../../../../features/file/file.service"; @@ -15,14 +15,25 @@ import { FileService } from "../../../../../features/file/file.service"; export class NewPhotoComponent extends EditComponent { imgPath: SafeUrl; - constructor(private fileService: FileService) { + constructor( + private fileService: FileService, + private sanitizer: DomSanitizer + ) { super(); } onInitFromDynamicConfig(config: EditPropertyConfig) { super.onInitFromDynamicConfig(config); - this.fileService - .loadFile(this.entity, this.formControlName) - .subscribe((res) => (this.imgPath = res)); + if (this.entity[this.formControlName]) { + this.fileService + .loadFile(this.entity, this.formControlName) + .subscribe((res) => (this.imgPath = res)); + } + } + + updateFile(file: File) { + this.imgPath = this.sanitizer.bypassSecurityTrustUrl( + URL.createObjectURL(file) + ); } } diff --git a/src/app/features/file/couchdb-file.service.ts b/src/app/features/file/couchdb-file.service.ts index c47bfd1eb4..9735617173 100644 --- a/src/app/features/file/couchdb-file.service.ts +++ b/src/app/features/file/couchdb-file.service.ts @@ -10,6 +10,7 @@ import { import { AppSettings } from "../../core/app-config/app-settings"; import { catchError, + combineLatestWith, concatMap, filter, map, @@ -28,6 +29,8 @@ import { EntityRegistry } from "../../core/entity/database-entity.decorator"; import { LoggingService } from "../../core/logging/logging.service"; import { ObservableQueue } from "./observable-queue/observable-queue"; import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; +import { fromPromise } from "rxjs/internal/observable/innerFrom"; +import { resizeImage } from "./file-utils"; /** * Stores the files in the CouchDB. @@ -53,44 +56,61 @@ export class CouchdbFileService extends FileService { super(entityMapper, entities, logger); } - uploadFile(file: File, entity: Entity, property: string): Observable { - // TODO update cache if file is cached + uploadFile( + file: File, + entity: Entity, + property: string, + compression + ): Observable { const obs = this.requestQueue.add( - this.runFileUpload(file, entity, property) + this.runFileUpload(file, entity, property, compression) ); this.reportProgress($localize`Uploading "${file.name}"`, obs); return obs; } - uploadImage( + private runFileUpload( file: File, entity: Entity, property: string, - maxSize: 360 - ): Observable { - return of({}); - } - - private runFileUpload(file: File, entity: Entity, property: string) { - const blob = new Blob([file]); + compression: number + ) { + let blobObs: Observable; + if (compression) { + blobObs = fromPromise( + new Promise((res) => + resizeImage(file, compression).then((cvs) => cvs.toBlob(res)) + ) + ); + } else { + blobObs = of(new Blob([file])); + } const path = `${entity.getId(true)}/${property}`; return this.getAttachmentsDocument( `${this.attachmentsUrl}/${entity.getId(true)}` ).pipe( - concatMap(({ _rev }) => - this.http.put(`${this.attachmentsUrl}/${path}?rev=${_rev}`, blob, { - headers: { "Content-Type": file.type, "ngsw-bypass": "" }, - reportProgress: true, - observe: "events", - }) + combineLatestWith(blobObs), + concatMap(([{ _rev }, blob]) => + this.http + .put(`${this.attachmentsUrl}/${path}?rev=${_rev}`, blob, { + headers: { "Content-Type": file.type, "ngsw-bypass": "" }, + reportProgress: true, + observe: "events", + }) + .pipe( + tap({ + complete: () => { + if (this.cache[`${path}`]) { + this.cache[`${path}`] = of( + this.sanitizer.bypassSecurityTrustUrl( + URL.createObjectURL(blob) + ) + ); + } + }, + }) + ) ), - tap(() => { - if (this.cache[`${path}`]) { - this.cache[`${path}`] = of( - this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob)) - ); - } - }), // prevent http request to be executed multiple times (whenever .subscribe is called) shareReplay() ); diff --git a/src/app/features/file/edit-file/edit-file.component.ts b/src/app/features/file/edit-file/edit-file.component.ts index d9a9486afb..df1698488c 100644 --- a/src/app/features/file/edit-file/edit-file.component.ts +++ b/src/app/features/file/edit-file/edit-file.component.ts @@ -1,4 +1,12 @@ -import { Component, ElementRef, Input, OnInit, ViewChild } from "@angular/core"; +import { + Component, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, +} from "@angular/core"; import { EditComponent } from "../../../core/entity-components/entity-utils/dynamic-form-components/edit-component"; import { DynamicComponent } from "../../../core/view/dynamic-components/dynamic-component.decorator"; import { AlertService } from "../../../core/alerts/alert.service"; @@ -37,7 +45,8 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; }) export class EditFileComponent extends EditComponent implements OnInit { @ViewChild("fileUpload") fileInput: ElementRef; - @Input() compressImage = false; + @Input() compressImage?: number; + @Output() fileUpload = new EventEmitter(); private selectedFile: File; private removeClicked = false; private initialValue: string; @@ -79,17 +88,19 @@ export class EditFileComponent extends EditComponent implements OnInit { } private uploadFile(file: File) { + this.fileUpload.emit(file); // The maximum file size which can be processed by CouchDB before a timeout is around 200mb - const obs = this.compressImage - ? this.fileService.uploadImage(file, this.entity, this.formControlName) - : this.fileService.uploadFile(file, this.entity, this.formControlName); - obs.subscribe({ - error: (err) => this.handleError(err), - complete: () => { - this.initialValue = this.formControl.value; - this.selectedFile = undefined; - }, - }); + this.fileService + .uploadFile(file, this.entity, this.formControlName, this.compressImage) + .subscribe({ + error: (err) => this.handleError(err), + complete: () => { + // TODO somehow this doesn't trigger @Output binding in template + this.fileUpload.emit(file); + this.initialValue = this.formControl.value; + this.selectedFile = undefined; + }, + }); } private handleError(err) { diff --git a/src/app/features/file/file-utils.ts b/src/app/features/file/file-utils.ts index 539b790aa9..39350acb08 100644 --- a/src/app/features/file/file-utils.ts +++ b/src/app/features/file/file-utils.ts @@ -1,8 +1,11 @@ -export function resizeImage(file: File, maxSize = 360): Promise { +export function resizeImage( + file: File, + maxSize = 360 +): Promise { const image = new Image(); image.src = URL.createObjectURL(file); - return new Promise((resolve) => { + return new Promise((resolve) => { image.onload = () => { let imageWidth = image.width, imageHeight = image.height; @@ -26,7 +29,7 @@ export function resizeImage(file: File, maxSize = 360): Promise { const ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, imageWidth, imageHeight); - resolve(canvas.toDataURL(file.type)); + resolve(canvas); }; }); } diff --git a/src/app/features/file/file.service.ts b/src/app/features/file/file.service.ts index 62c93e2501..9f30101d2f 100644 --- a/src/app/features/file/file.service.ts +++ b/src/app/features/file/file.service.ts @@ -86,17 +86,12 @@ export abstract class FileService { * @param file to be uploaded * @param entity * @param property where the information about the file should be stored + * @param compression */ abstract uploadFile( - file: File, - entity: Entity, - property: string - ): Observable; - - abstract uploadImage( file: File, entity: Entity, property: string, - maxSize?: number + compression?: number ): Observable; } diff --git a/src/app/features/file/mock-file.service.ts b/src/app/features/file/mock-file.service.ts index be66fce6c1..7edbac8f5c 100644 --- a/src/app/features/file/mock-file.service.ts +++ b/src/app/features/file/mock-file.service.ts @@ -8,7 +8,7 @@ import { LoggingService } from "../../core/logging/logging.service"; import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; import { fromPromise } from "rxjs/internal/observable/innerFrom"; import { resizeImage } from "./file-utils"; -import { tap } from "rxjs/operators"; +import { map, tap } from "rxjs/operators"; /** * A mock implementation of the file service which only stores the file temporarily in the browser. @@ -47,15 +47,23 @@ export class MockFileService extends FileService { ); } - uploadFile(file: File, entity: Entity, property: string): Observable { - const fileURL = URL.createObjectURL(file); - this.fileMap.set(entity + property, fileURL); - return of({ ok: true }); - } - - uploadImage(file: File, entity: Entity, property: string): Observable { - return fromPromise(resizeImage(file)).pipe( - tap((fileUrl) => this.fileMap.set(entity + property, fileUrl)) + uploadFile( + file: File, + entity: Entity, + property: string, + compression?: number + ): Observable { + let dataUrl: Observable; + if (compression) { + dataUrl = fromPromise(resizeImage(file, compression)).pipe( + map((cvs) => cvs.toDataURL()) + ); + } else { + dataUrl = of(URL.createObjectURL(file)); + } + return dataUrl.pipe( + tap((url) => this.fileMap.set(entity + property, url)), + map(() => ({ ok: true })) ); } } From b60c3377f3190f56f137375ab0ad3db0d70ebb1d Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 27 Mar 2023 17:49:20 +0200 Subject: [PATCH 07/22] demo code for uploading children pictures --- src/app/app.module.ts | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3e871d9c96..55ef893e5b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -78,6 +78,10 @@ import { TodosModule } from "./features/todos/todos.module"; import { SessionService } from "./core/session/session-service/session.service"; import { waitForChangeTo } from "./core/session/session-states/session-utils"; import { LoginState } from "./core/session/session-states/login-state.enum"; +import { CouchdbFileService } from "./features/file/couchdb-file.service"; +import { EntityMapperService } from "./core/entity/entity-mapper.service"; +import { Child } from "./child-dev-project/children/model/child"; +import { lastValueFrom } from "rxjs"; /** * Main entry point of the application. @@ -158,7 +162,30 @@ import { LoginState } from "./core/session/session-states/login-state.enum"; bootstrap: [AppComponent], }) export class AppModule { - constructor(icons: FaIconLibrary) { + constructor( + icons: FaIconLibrary, + private fileService: CouchdbFileService, + private entityMapper: EntityMapperService + ) { icons.addIconPacks(fas, far); + // setTimeout(() => this.uploadFotos(), 10000); + } + + async uploadFotos() { + const files = await fetch("assets/files.txt") + .then((res) => res.text()) + .then((res) => res.split("\n")); + for (let i = 1; i < 121; i++) { + const child = await this.entityMapper.load(Child, `${i}`); + child.photo2 = files[i]; + const blob = await fetch(`assets/data/${files[i]}`).then((res) => + res.blob() + ); + const file = new File([blob], files[i]); + await lastValueFrom( + this.fileService.uploadFile(file, child, "photo2", 480) + ); + await this.entityMapper.save(child); + } } } From d8d46ef1f5f8a90e974a09df9b25bfc5644eba27 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 27 Mar 2023 17:59:56 +0200 Subject: [PATCH 08/22] Revert "ci: update docker repository (#1792)" This reverts commit 41e4c4c65989f428ab3064a00197d7a60d1576b8. --- .github/workflows/pull-request-update.yml | 2 +- .github/workflows/tagged-commit.yml | 2 +- build/README.md | 2 +- doc/compodoc_sources/concepts/infrastructure.md | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pull-request-update.yml b/.github/workflows/pull-request-update.yml index 6b03cccd84..1888a9d1d7 100644 --- a/.github/workflows/pull-request-update.yml +++ b/.github/workflows/pull-request-update.yml @@ -47,7 +47,7 @@ jobs: file: ./build/Dockerfile builder: ${{ steps.buildx.outputs.name }} push: true - tags: aamdigitaltravis/ndb-server:pr-${{ github.event.number }} + tags: aamdigital/ndb-server:pr-${{ github.event.number }} cache-from: type=gha cache-to: type=gha,mode=max - name: Deploy updated image diff --git a/.github/workflows/tagged-commit.yml b/.github/workflows/tagged-commit.yml index 5dd573ea00..03f33ebf64 100644 --- a/.github/workflows/tagged-commit.yml +++ b/.github/workflows/tagged-commit.yml @@ -26,7 +26,7 @@ jobs: file: ./build/Dockerfile builder: ${{ steps.buildx.outputs.name }} push: true - tags: aamdigitaltravis/ndb-server:${{ env.TAG }} + tags: aamdigital/ndb-server:${{ env.TAG }} cache-from: type=gha cache-to: type=gha,mode=max build-args: | diff --git a/build/README.md b/build/README.md index 88c169508b..34ca842c4c 100644 --- a/build/README.md +++ b/build/README.md @@ -4,7 +4,7 @@ The Angular app is build within a custom docker container to ensure it is reprod Builds are triggered through GitHub Actions CI (see /.github/workflows). -The deployable server (nginx) image is published to [Docker Hub](https://hub.docker.com/r/aamdigitaltravis/ndb-server) +The deployable server (nginx) image is published to [Docker Hub](https://hub.docker.com/r/aamdigital/ndb-server) for every official (tagged) build. ## How to build & publish a new image diff --git a/doc/compodoc_sources/concepts/infrastructure.md b/doc/compodoc_sources/concepts/infrastructure.md index 70076189e8..7b9ec7e820 100644 --- a/doc/compodoc_sources/concepts/infrastructure.md +++ b/doc/compodoc_sources/concepts/infrastructure.md @@ -25,11 +25,11 @@ Once this is done, a version of Aam Digital with the new changes included can be ## Master Updates After approving a PR and merging it into the master, semantic release automatically creates a new tag for this change. -For each new tag a tagged Docker image is uploaded to [DockerHub](https://hub.docker.com/r/aamdigitaltravis/ndb-server). +For each new tag a tagged Docker image is uploaded to [DockerHub](https://hub.docker.com/r/aamdigital/ndb-server). ## Deploying Aam Digital The Docker image from DockerHub can then be downloaded and run via Docker using the following command: -> docker pull aamdigitaltravis/ndb-server && docker run -p=80:80 aamdigital/ndb-server +> docker pull aamdigital/ndb-server && docker run -p=80:80 aamdigital/ndb-server However, this will only run Aam Digital in demo-mode. To run Aam Digital with a real database, a new `config.json` file has to be mounted into the image. From fa561a540fca52b95bc3b27453a8c6972cd474a0 Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 27 Mar 2023 19:18:08 +0200 Subject: [PATCH 09/22] improved general support of child photos --- .../child-block-tooltip.component.html | 3 ++- .../child-block-tooltip.component.ts | 27 ++++++++++++++++--- .../child-block/child-block.component.html | 3 ++- .../child-block/child-block.component.ts | 9 ++++++- .../edit-photo/new-photo.component.html | 2 +- .../edit-photo/new-photo.component.ts | 2 +- 6 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.html b/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.html index 57efdbe931..55cd7bc1d3 100644 --- a/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.html +++ b/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.html @@ -1,4 +1,5 @@ - + +

{{ entity?.name }}

diff --git a/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.ts b/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.ts index 13d86fa0fa..d799d7fc60 100644 --- a/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.ts +++ b/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.ts @@ -1,8 +1,11 @@ -import { Component, Input } from "@angular/core"; +import { Component, Input, OnInit } from "@angular/core"; import { Child } from "../../model/child"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; import { NgForOf, NgIf } from "@angular/common"; import { SchoolBlockComponent } from "../../../schools/school-block/school-block.component"; +import { FaDynamicIconComponent } from "../../../../core/view/fa-dynamic-icon/fa-dynamic-icon.component"; +import { SafeUrl } from "@angular/platform-browser"; +import { FileService } from "../../../../features/file/file.service"; /** * Tooltip that is shown when hovering over a child block and the tooltip is enabled. @@ -11,10 +14,28 @@ import { SchoolBlockComponent } from "../../../schools/school-block/school-block selector: "app-child-block-tooltip", templateUrl: "./child-block-tooltip.component.html", styleUrls: ["./child-block-tooltip.component.scss"], - imports: [FontAwesomeModule, NgIf, SchoolBlockComponent, NgForOf], + imports: [ + FontAwesomeModule, + NgIf, + SchoolBlockComponent, + NgForOf, + FaDynamicIconComponent, + ], standalone: true, }) -export class ChildBlockTooltipComponent { +export class ChildBlockTooltipComponent implements OnInit { /** The entity to show the tooltip for */ @Input() entity: Child; + icon = Child.icon; + imgPath: SafeUrl; + + constructor(private fileService: FileService) {} + + ngOnInit() { + if (this.entity.photo2) { + this.fileService + .loadFile(this.entity, "photo2") + .subscribe((res) => (this.imgPath = res)); + } + } } diff --git a/src/app/child-dev-project/children/child-block/child-block.component.html b/src/app/child-dev-project/children/child-block/child-block.component.html index b095af8ca8..2a7b1b3c28 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.html +++ b/src/app/child-dev-project/children/child-block/child-block.component.html @@ -5,7 +5,8 @@ [class.inactive]="!entity.isActive" class="truncate-text container" > - + + {{ entity?.toString() }} ({{ entity?.projectNumber }}) diff --git a/src/app/child-dev-project/children/child-block/child-block.component.ts b/src/app/child-dev-project/children/child-block/child-block.component.ts index ee4dab5147..a39e82a525 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.ts +++ b/src/app/child-dev-project/children/child-block/child-block.component.ts @@ -15,13 +15,19 @@ import { TemplateTooltipDirective } from "../../../core/common-components/templa import { ChildBlockTooltipComponent } from "./child-block-tooltip/child-block-tooltip.component"; import { SafeUrl } from "@angular/platform-browser"; import { FileService } from "../../../features/file/file.service"; +import { FaDynamicIconComponent } from "../../../core/view/fa-dynamic-icon/fa-dynamic-icon.component"; @DynamicComponent("ChildBlock") @Component({ selector: "app-child-block", templateUrl: "./child-block.component.html", styleUrls: ["./child-block.component.scss"], - imports: [NgIf, TemplateTooltipDirective, ChildBlockTooltipComponent], + imports: [ + NgIf, + TemplateTooltipDirective, + ChildBlockTooltipComponent, + FaDynamicIconComponent, + ], standalone: true, }) export class ChildBlockComponent implements OnInitDynamicComponent, OnChanges { @@ -35,6 +41,7 @@ export class ChildBlockComponent implements OnInitDynamicComponent, OnChanges { @Input() tooltipDisabled: boolean; imgPath: SafeUrl; + icon = Child.icon; constructor( private fileService: FileService, diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html index 403bb600e0..1f0393a77c 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html @@ -1,4 +1,4 @@ -profile photo +profile photo { - imgPath: SafeUrl; + imgPath: SafeUrl = "assets/child.png"; constructor( private fileService: FileService, From 44c59587d98e41ff3c2c2e9c821e387613a7a76a Mon Sep 17 00:00:00 2001 From: Simon Date: Mon, 27 Mar 2023 19:33:17 +0200 Subject: [PATCH 10/22] only loading pictures if component is shown --- .../child-block/child-block.component.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/app/child-dev-project/children/child-block/child-block.component.ts b/src/app/child-dev-project/children/child-block/child-block.component.ts index a39e82a525..e6f7cbb91a 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.ts +++ b/src/app/child-dev-project/children/child-block/child-block.component.ts @@ -2,6 +2,7 @@ import { Component, Input, OnChanges, + OnInit, Optional, SimpleChange, SimpleChanges, @@ -30,7 +31,9 @@ import { FaDynamicIconComponent } from "../../../core/view/fa-dynamic-icon/fa-dy ], standalone: true, }) -export class ChildBlockComponent implements OnInitDynamicComponent, OnChanges { +export class ChildBlockComponent + implements OnInitDynamicComponent, OnChanges, OnInit +{ @Input() entity: Child; @Input() entityId: string; @@ -54,6 +57,14 @@ export class ChildBlockComponent implements OnInitDynamicComponent, OnChanges { } } + ngOnInit() { + if (this.entity.photo2) { + this.fileService + .loadFile(this.entity, "photo2") + .subscribe((res) => (this.imgPath = res)); + } + } + onInitFromDynamicConfig(config: any) { this.entity = config.entity; if (config.hasOwnProperty("entityId")) { @@ -64,10 +75,5 @@ export class ChildBlockComponent implements OnInitDynamicComponent, OnChanges { } this.linkDisabled = config.linkDisabled; this.tooltipDisabled = config.tooltipDisabled; - if (this.entity.photo2) { - this.fileService - .loadFile(this.entity, "photo2") - .subscribe((res) => (this.imgPath = res)); - } } } From b87ccef185c20b45a75a055d555c089a85b98647 Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 28 Mar 2023 13:25:15 +0200 Subject: [PATCH 11/22] improved usability of new photo component --- .../edit-photo/new-photo.component.html | 20 +++--- .../edit-photo/new-photo.component.scss | 6 ++ .../edit-photo/new-photo.component.ts | 65 +++++++++++++++++-- src/app/features/file/couchdb-file.service.ts | 38 +++++------ .../file/edit-file/edit-file.component.ts | 20 +++--- src/app/features/file/file-utils.ts | 1 + 6 files changed, 106 insertions(+), 44 deletions(-) diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html index 1f0393a77c..951252c2d4 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html @@ -1,9 +1,13 @@ + profile photo - +
+ + +
diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss index f30853f8e1..5ebfdc9408 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss @@ -4,3 +4,9 @@ border-radius: 50%; object-fit: cover; } + +:host { + display: flex; + flex-direction: column; + align-items: center; +} diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts index 93146919ff..385e1b7519 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts @@ -1,25 +1,50 @@ import { Component } from "@angular/core"; -import { EditComponent, EditPropertyConfig } from "../edit-component"; +import { EditPropertyConfig } from "../edit-component"; import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; import { EditFileComponent } from "../../../../../features/file/edit-file/edit-file.component"; import { NgIf } from "@angular/common"; import { FileService } from "../../../../../features/file/file.service"; +import { MatButtonModule } from "@angular/material/button"; +import { MatTooltipModule } from "@angular/material/tooltip"; +import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { AlertService } from "../../../../alerts/alert.service"; +import { LoggingService } from "../../../../logging/logging.service"; +import { EntityMapperService } from "../../../../entity/entity-mapper.service"; @Component({ selector: "app-new-photo", standalone: true, - imports: [EditFileComponent, NgIf], + imports: [ + EditFileComponent, + NgIf, + MatButtonModule, + MatTooltipModule, + FontAwesomeModule, + ], templateUrl: "./new-photo.component.html", styleUrls: ["./new-photo.component.scss"], }) -export class NewPhotoComponent extends EditComponent { - imgPath: SafeUrl = "assets/child.png"; +export class NewPhotoComponent extends EditFileComponent { + private readonly defaultImage = "assets/child.png"; + private initialImg: SafeUrl = this.defaultImage; + imgPath: SafeUrl = this.initialImg; + compressImage = 480; constructor( - private fileService: FileService, + fileService: FileService, + alertService: AlertService, + logger: LoggingService, + entityMapper: EntityMapperService, private sanitizer: DomSanitizer ) { - super(); + super(fileService, alertService, logger, entityMapper); + } + + async onFileSelected(event): Promise { + this.imgPath = this.sanitizer.bypassSecurityTrustUrl( + URL.createObjectURL(event.target.files[0]) + ); + return super.onFileSelected(event); } onInitFromDynamicConfig(config: EditPropertyConfig) { @@ -27,7 +52,10 @@ export class NewPhotoComponent extends EditComponent { if (this.entity[this.formControlName]) { this.fileService .loadFile(this.entity, this.formControlName) - .subscribe((res) => (this.imgPath = res)); + .subscribe((res) => { + this.imgPath = res; + this.initialImg = res; + }); } } @@ -36,4 +64,27 @@ export class NewPhotoComponent extends EditComponent { URL.createObjectURL(file) ); } + + protected resetFile() { + this.resetPreview(this.initialImg); + super.resetFile(); + } + + delete() { + this.resetPreview(this.defaultImage); + super.delete(); + } + + private resetPreview(resetImage: SafeUrl) { + if (this.imgPath !== this.initialImg) { + URL.revokeObjectURL(this.imgPath as string); + } + this.imgPath = resetImage; + } + + protected deleteExistingFile() { + URL.revokeObjectURL(this.initialImg as string); + this.initialImg = this.defaultImage; + super.deleteExistingFile(); + } } diff --git a/src/app/features/file/couchdb-file.service.ts b/src/app/features/file/couchdb-file.service.ts index 9735617173..1a60f32d93 100644 --- a/src/app/features/file/couchdb-file.service.ts +++ b/src/app/features/file/couchdb-file.service.ts @@ -13,6 +13,7 @@ import { combineLatestWith, concatMap, filter, + last, map, shareReplay, tap, @@ -42,7 +43,7 @@ export class CouchdbFileService extends FileService { private attachmentsUrl = `${AppSettings.DB_PROXY_PREFIX}/${AppSettings.DB_NAME}-attachments`; // TODO it seems like failed requests are executed again when a new one is done private requestQueue = new ObservableQueue(); - private cache: { [key: string]: Observable } = {}; + private cache: { [key: string]: Observable } = {}; constructor( private sanitizer: DomSanitizer, @@ -66,6 +67,11 @@ export class CouchdbFileService extends FileService { this.runFileUpload(file, entity, property, compression) ); this.reportProgress($localize`Uploading "${file.name}"`, obs); + this.cache[`${entity.getId(true)}/${property}`] = obs.pipe( + last(), + map((blob: Blob) => URL.createObjectURL(blob)), + shareReplay() + ); return obs; } @@ -97,19 +103,8 @@ export class CouchdbFileService extends FileService { reportProgress: true, observe: "events", }) - .pipe( - tap({ - complete: () => { - if (this.cache[`${path}`]) { - this.cache[`${path}`] = of( - this.sanitizer.bypassSecurityTrustUrl( - URL.createObjectURL(blob) - ) - ); - } - }, - }) - ) + // using blob as last emitted value + .pipe(map((e) => (e.type === HttpEventType.Response ? blob : e))) ), // prevent http request to be executed multiple times (whenever .subscribe is called) shareReplay() @@ -191,7 +186,7 @@ export class CouchdbFileService extends FileService { }); } - loadFile(entity: Entity, property: string) { + loadFile(entity: Entity, property: string): Observable { const path = `${entity.getId(true)}/${property}`; if (!this.cache[path]) { this.cache[path] = this.http @@ -199,16 +194,19 @@ export class CouchdbFileService extends FileService { responseType: "blob", }) .pipe( - map((blob) => - this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob)) - ), + map((blob) => URL.createObjectURL(blob)), shareReplay() ); } - return this.cache[path]; + return this.cache[path].pipe( + map((url) => this.sanitizer.bypassSecurityTrustUrl(url)) + ); } - private reportProgress(message: string, obs: Observable>) { + private reportProgress( + message: string, + obs: Observable | any> + ) { const progress = obs.pipe( filter( (e) => diff --git a/src/app/features/file/edit-file/edit-file.component.ts b/src/app/features/file/edit-file/edit-file.component.ts index df1698488c..72299a3fe0 100644 --- a/src/app/features/file/edit-file/edit-file.component.ts +++ b/src/app/features/file/edit-file/edit-file.component.ts @@ -46,13 +46,12 @@ import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; export class EditFileComponent extends EditComponent implements OnInit { @ViewChild("fileUpload") fileInput: ElementRef; @Input() compressImage?: number; - @Output() fileUpload = new EventEmitter(); private selectedFile: File; private removeClicked = false; private initialValue: string; constructor( - private fileService: FileService, + protected fileService: FileService, private alertService: AlertService, private logger: LoggingService, private entityMapper: EntityMapperService @@ -72,9 +71,11 @@ export class EditFileComponent extends EditComponent implements OnInit { this.selectedFile && this.selectedFile.name === this.formControl.value ) { - this.uploadFile(this.selectedFile); + this.saveNewFile(this.selectedFile); } else if (this.removeClicked && !this.formControl.value) { - this.removeFile(); + this.deleteExistingFile(); + } else { + this.resetFile(); } }); } @@ -87,16 +88,13 @@ export class EditFileComponent extends EditComponent implements OnInit { this.formControl.setValue(file.name); } - private uploadFile(file: File) { - this.fileUpload.emit(file); + protected saveNewFile(file: File) { // The maximum file size which can be processed by CouchDB before a timeout is around 200mb this.fileService .uploadFile(file, this.entity, this.formControlName, this.compressImage) .subscribe({ error: (err) => this.handleError(err), complete: () => { - // TODO somehow this doesn't trigger @Output binding in template - this.fileUpload.emit(file); this.initialValue = this.formControl.value; this.selectedFile = undefined; }, @@ -131,7 +129,7 @@ export class EditFileComponent extends EditComponent implements OnInit { this.removeClicked = !!this.initialValue; } - private removeFile() { + protected deleteExistingFile() { this.fileService .removeFile(this.entity, this.formControlName) .subscribe(() => { @@ -142,4 +140,8 @@ export class EditFileComponent extends EditComponent implements OnInit { this.removeClicked = false; }); } + + protected resetFile() { + this.selectedFile = undefined; + } } diff --git a/src/app/features/file/file-utils.ts b/src/app/features/file/file-utils.ts index 39350acb08..a2c19ed33d 100644 --- a/src/app/features/file/file-utils.ts +++ b/src/app/features/file/file-utils.ts @@ -28,6 +28,7 @@ export function resizeImage( const ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, imageWidth, imageHeight); + URL.revokeObjectURL(image.src); resolve(canvas); }; From dbea3b8dc9a0fab366336891bbca148fc0fe3764 Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 29 Mar 2023 14:29:11 +0200 Subject: [PATCH 12/22] fixed tests --- .../child-block-tooltip.component.spec.ts | 4 ++++ .../child-block/child-block.component.spec.ts | 6 +++++- src/app/features/file/couchdb-file.service.ts | 2 +- .../file/edit-file/edit-file.component.spec.ts | 15 +++++++++++---- 4 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.spec.ts b/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.spec.ts index 9cede69de1..643cbc08ec 100644 --- a/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.spec.ts +++ b/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ChildBlockTooltipComponent } from "./child-block-tooltip.component"; import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; +import { FileService } from "../../../../features/file/file.service"; +import { Child } from "../../model/child"; describe("ChildBlockTooltipComponent", () => { let component: ChildBlockTooltipComponent; @@ -10,12 +12,14 @@ describe("ChildBlockTooltipComponent", () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ChildBlockTooltipComponent, FontAwesomeTestingModule], + providers: [{ provide: FileService, useValue: {} }], }).compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(ChildBlockTooltipComponent); component = fixture.componentInstance; + component.entity = new Child(); fixture.detectChanges(); }); diff --git a/src/app/child-dev-project/children/child-block/child-block.component.spec.ts b/src/app/child-dev-project/children/child-block/child-block.component.spec.ts index 906e8e5d0d..69f5bd721f 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.spec.ts +++ b/src/app/child-dev-project/children/child-block/child-block.component.spec.ts @@ -4,6 +4,7 @@ import { ChildBlockComponent } from "./child-block.component"; import { RouterTestingModule } from "@angular/router/testing"; import { ChildrenService } from "../children.service"; import { Child } from "../model/child"; +import { FileService } from "app/features/file/file.service"; describe("ChildBlockComponent", () => { let component: ChildBlockComponent; @@ -18,7 +19,10 @@ describe("ChildBlockComponent", () => { TestBed.configureTestingModule({ imports: [ChildBlockComponent, RouterTestingModule], - providers: [{ provide: ChildrenService, useValue: mockChildrenService }], + providers: [ + { provide: ChildrenService, useValue: mockChildrenService }, + { provide: FileService, useValue: {} }, + ], }).compileComponents(); })); diff --git a/src/app/features/file/couchdb-file.service.ts b/src/app/features/file/couchdb-file.service.ts index 1a60f32d93..1805e87550 100644 --- a/src/app/features/file/couchdb-file.service.ts +++ b/src/app/features/file/couchdb-file.service.ts @@ -61,7 +61,7 @@ export class CouchdbFileService extends FileService { file: File, entity: Entity, property: string, - compression + compression?: number ): Observable { const obs = this.requestQueue.add( this.runFileUpload(file, entity, property, compression) diff --git a/src/app/features/file/edit-file/edit-file.component.spec.ts b/src/app/features/file/edit-file/edit-file.component.spec.ts index a4bee3b3ec..b3d1bf4ac1 100644 --- a/src/app/features/file/edit-file/edit-file.component.spec.ts +++ b/src/app/features/file/edit-file/edit-file.component.spec.ts @@ -30,7 +30,11 @@ describe("EditFileComponent", () => { mockAlertService = jasmine.createSpyObj(["addDanger", "addInfo"]); mockEntityMapper = jasmine.createSpyObj(["save"]); await TestBed.configureTestingModule({ - imports: [EditFileComponent, FontAwesomeTestingModule, NoopAnimationsModule], + imports: [ + EditFileComponent, + FontAwesomeTestingModule, + NoopAnimationsModule, + ], providers: [ EntitySchemaService, { provide: AlertService, useValue: mockAlertService }, @@ -107,7 +111,8 @@ describe("EditFileComponent", () => { expect(mockFileService.uploadFile).toHaveBeenCalledWith( file, component.entity, - component.formControlName + component.formControlName, + undefined ); }); @@ -147,7 +152,8 @@ describe("EditFileComponent", () => { expect(mockFileService.uploadFile).toHaveBeenCalledWith( otherFile, component.entity, - component.formControlName + component.formControlName, + undefined ); }); @@ -213,7 +219,8 @@ describe("EditFileComponent", () => { expect(mockFileService.uploadFile).toHaveBeenCalledWith( otherFile, component.entity, - component.formControlName + component.formControlName, + undefined ); }); From f0521951ec56ed43880f4ab6d53b3e9a3d2242ce Mon Sep 17 00:00:00 2001 From: Simon Date: Wed, 29 Mar 2023 15:22:35 +0200 Subject: [PATCH 13/22] added tests for new photo component --- .../edit-photo/new-photo.component.spec.ts | 109 ++++++++++++++++-- .../edit-photo/new-photo.component.ts | 12 +- .../file/edit-file/edit-file.component.ts | 10 +- 3 files changed, 106 insertions(+), 25 deletions(-) diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts index 755a2a1e12..66040712f2 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts @@ -1,23 +1,118 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { NewPhotoComponent } from './new-photo.component'; +import { NewPhotoComponent } from "./new-photo.component"; +import { FileService } from "../../../../../features/file/file.service"; +import { AlertService } from "../../../../alerts/alert.service"; +import { EntityMapperService } from "../../../../entity/entity-mapper.service"; +import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; +import { NoopAnimationsModule } from "@angular/platform-browser/animations"; +import { EntitySchemaService } from "../../../../entity/schema/entity-schema.service"; +import { FormControl } from "@angular/forms"; +import { Entity } from "../../../../entity/model/entity"; +import { DomSanitizer } from "@angular/platform-browser"; +import { of } from "rxjs"; +import { EditPropertyConfig } from "../edit-component"; -describe('NewPhotoComponent', () => { +describe("NewPhotoComponent", () => { let component: NewPhotoComponent; let fixture: ComponentFixture; + let mockFileService: jasmine.SpyObj; + let mockAlertService: jasmine.SpyObj; + let mockEntityMapper: jasmine.SpyObj; + let config: EditPropertyConfig; + + const file = { name: "test.file" } as File; + const fileEvent = { target: { files: [file] } }; beforeEach(async () => { + mockFileService = jasmine.createSpyObj([ + "uploadFile", + "loadFile", + "removeFile", + ]); + mockFileService.removeFile.and.returnValue(of(undefined)); + mockAlertService = jasmine.createSpyObj(["addDanger", "addInfo"]); + mockEntityMapper = jasmine.createSpyObj(["save"]); await TestBed.configureTestingModule({ - imports: [ NewPhotoComponent ] - }) - .compileComponents(); + imports: [ + NewPhotoComponent, + FontAwesomeTestingModule, + NoopAnimationsModule, + ], + providers: [ + EntitySchemaService, + { provide: AlertService, useValue: mockAlertService }, + { provide: FileService, useValue: mockFileService }, + { provide: EntityMapperService, useValue: mockEntityMapper }, + ], + }).compileComponents(); fixture = TestBed.createComponent(NewPhotoComponent); component = fixture.componentInstance; + config = { + formControl: new FormControl(), + entity: new Entity(), + formFieldConfig: { id: "testProp" }, + propertySchema: undefined, + }; + component.onInitFromDynamicConfig(config); fixture.detectChanges(); }); - it('should create', () => { + it("should create", () => { expect(component).toBeTruthy(); }); + + it("should show the image once it is selected", () => { + spyOn( + TestBed.inject(DomSanitizer), + "bypassSecurityTrustUrl" + ).and.returnValue("image.path"); + spyOn(URL, "createObjectURL"); + + component.onFileSelected(fileEvent); + + expect(component.imgPath).toBe("image.path"); + }); + + it("should load the picture on initialisation", () => { + mockFileService.loadFile.and.returnValue(of("some.path")); + config.entity[config.formFieldConfig.id] = "file.name"; + + component.onInitFromDynamicConfig(config); + + expect(component.imgPath).toBe("some.path"); + }); + + it("should display the default image when clicking delete", () => { + component.imgPath = "some.path"; + + component.delete(); + + expect(component.imgPath).toBe("assets/child.png"); + }); + + it("should reset the shown image when pressing cancel", () => { + mockFileService.loadFile.and.returnValue(of("initial.path")); + config.entity[config.formFieldConfig.id] = "initial.image"; + component.onInitFromDynamicConfig(config); + + component.imgPath = "new.path"; + component.formControl.disable(); + + expect(component.imgPath).toBe("initial.path"); + }); + + it("should revoke initial image if file is deleted", () => { + mockFileService.loadFile.and.returnValue(of("initial.path")); + config.entity[config.formFieldConfig.id] = "initial.image"; + config.formControl.setValue("initial.image"); + component.onInitFromDynamicConfig(config); + component.ngOnInit(); + + component.delete(); + component.formControl.disable(); + + expect(component.imgPath).toBe("assets/child.png"); + }); }); diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts index 385e1b7519..129781b3f5 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts @@ -59,10 +59,9 @@ export class NewPhotoComponent extends EditFileComponent { } } - updateFile(file: File) { - this.imgPath = this.sanitizer.bypassSecurityTrustUrl( - URL.createObjectURL(file) - ); + delete() { + this.resetPreview(this.defaultImage); + super.delete(); } protected resetFile() { @@ -70,11 +69,6 @@ export class NewPhotoComponent extends EditFileComponent { super.resetFile(); } - delete() { - this.resetPreview(this.defaultImage); - super.delete(); - } - private resetPreview(resetImage: SafeUrl) { if (this.imgPath !== this.initialImg) { URL.revokeObjectURL(this.imgPath as string); diff --git a/src/app/features/file/edit-file/edit-file.component.ts b/src/app/features/file/edit-file/edit-file.component.ts index 72299a3fe0..22b241fa2d 100644 --- a/src/app/features/file/edit-file/edit-file.component.ts +++ b/src/app/features/file/edit-file/edit-file.component.ts @@ -1,12 +1,4 @@ -import { - Component, - ElementRef, - EventEmitter, - Input, - OnInit, - Output, - ViewChild, -} from "@angular/core"; +import { Component, ElementRef, Input, OnInit, ViewChild } from "@angular/core"; import { EditComponent } from "../../../core/entity-components/entity-utils/dynamic-form-components/edit-component"; import { DynamicComponent } from "../../../core/view/dynamic-components/dynamic-component.decorator"; import { AlertService } from "../../../core/alerts/alert.service"; From 3b61b2f6c378be7ddc7c548cae986e1f7d348aee Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 30 Mar 2023 09:52:47 +0200 Subject: [PATCH 14/22] added tests for couchdb file service --- src/app/app.module.ts | 29 +---------------- .../file/couchdb-file.service.spec.ts | 31 +++++++++++++++++++ src/app/features/file/mock-file.service.ts | 25 +++------------ 3 files changed, 36 insertions(+), 49 deletions(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 55ef893e5b..3e871d9c96 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -78,10 +78,6 @@ import { TodosModule } from "./features/todos/todos.module"; import { SessionService } from "./core/session/session-service/session.service"; import { waitForChangeTo } from "./core/session/session-states/session-utils"; import { LoginState } from "./core/session/session-states/login-state.enum"; -import { CouchdbFileService } from "./features/file/couchdb-file.service"; -import { EntityMapperService } from "./core/entity/entity-mapper.service"; -import { Child } from "./child-dev-project/children/model/child"; -import { lastValueFrom } from "rxjs"; /** * Main entry point of the application. @@ -162,30 +158,7 @@ import { lastValueFrom } from "rxjs"; bootstrap: [AppComponent], }) export class AppModule { - constructor( - icons: FaIconLibrary, - private fileService: CouchdbFileService, - private entityMapper: EntityMapperService - ) { + constructor(icons: FaIconLibrary) { icons.addIconPacks(fas, far); - // setTimeout(() => this.uploadFotos(), 10000); - } - - async uploadFotos() { - const files = await fetch("assets/files.txt") - .then((res) => res.text()) - .then((res) => res.split("\n")); - for (let i = 1; i < 121; i++) { - const child = await this.entityMapper.load(Child, `${i}`); - child.photo2 = files[i]; - const blob = await fetch(`assets/data/${files[i]}`).then((res) => - res.blob() - ); - const file = new File([blob], files[i]); - await lastValueFrom( - this.fileService.uploadFile(file, child, "photo2", 480) - ); - await this.entityMapper.save(child); - } } } diff --git a/src/app/features/file/couchdb-file.service.spec.ts b/src/app/features/file/couchdb-file.service.spec.ts index fadc1702cb..f1a4b566fd 100644 --- a/src/app/features/file/couchdb-file.service.spec.ts +++ b/src/app/features/file/couchdb-file.service.spec.ts @@ -279,4 +279,35 @@ describe("CouchdbFileService", () => { jasmine.anything() ); }); + + it("should only request a file once per session", async () => { + mockHttp.get.and.returnValue(of(new Blob([]))); + const entity = new Entity(); + entity["file"] = "file.name"; + + const first = await firstValueFrom(service.loadFile(entity, "file")); + + expect(mockHttp.get).toHaveBeenCalled(); + + mockHttp.get.calls.reset(); + const second = await firstValueFrom(service.loadFile(entity, "file")); + + expect(first).toEqual(second); + expect(mockHttp.get).not.toHaveBeenCalled(); + + URL.revokeObjectURL(second as string); + }); + + it("should cache uploaded files", () => { + const file = { type: "image/png", name: "file.name" } as File; + const entity = new Entity("testId"); + mockHttp.get.and.returnValue(of({ _rev: "1-rev" })); + mockHttp.put.and.returnValue(of({ type: HttpEventType.Response })); + service.uploadFile(file, entity, "testProp").subscribe(); + mockHttp.get.calls.reset(); + + service.loadFile(entity, "testProp").subscribe(); + + expect(mockHttp.get).not.toHaveBeenCalled(); + }); }); diff --git a/src/app/features/file/mock-file.service.ts b/src/app/features/file/mock-file.service.ts index 7edbac8f5c..70f20d19c7 100644 --- a/src/app/features/file/mock-file.service.ts +++ b/src/app/features/file/mock-file.service.ts @@ -6,9 +6,6 @@ import { EntityMapperService } from "../../core/entity/entity-mapper.service"; import { EntityRegistry } from "../../core/entity/database-entity.decorator"; import { LoggingService } from "../../core/logging/logging.service"; import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; -import { fromPromise } from "rxjs/internal/observable/innerFrom"; -import { resizeImage } from "./file-utils"; -import { map, tap } from "rxjs/operators"; /** * A mock implementation of the file service which only stores the file temporarily in the browser. @@ -47,23 +44,9 @@ export class MockFileService extends FileService { ); } - uploadFile( - file: File, - entity: Entity, - property: string, - compression?: number - ): Observable { - let dataUrl: Observable; - if (compression) { - dataUrl = fromPromise(resizeImage(file, compression)).pipe( - map((cvs) => cvs.toDataURL()) - ); - } else { - dataUrl = of(URL.createObjectURL(file)); - } - return dataUrl.pipe( - tap((url) => this.fileMap.set(entity + property, url)), - map(() => ({ ok: true })) - ); + uploadFile(file: File, entity: Entity, property: string): Observable { + const fileURL = URL.createObjectURL(file); + this.fileMap.set(entity + property, fileURL); + return of({ ok: true }); } } From 8c5dd1a6735ae8fcfca49a2cc4efa44e8fa25f56 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 30 Mar 2023 10:27:02 +0200 Subject: [PATCH 15/22] added test for file resize utility --- src/app/features/file/file-utils.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/app/features/file/file-utils.spec.ts diff --git a/src/app/features/file/file-utils.spec.ts b/src/app/features/file/file-utils.spec.ts new file mode 100644 index 0000000000..02aa075db1 --- /dev/null +++ b/src/app/features/file/file-utils.spec.ts @@ -0,0 +1,16 @@ +import { resizeImage } from "./file-utils"; + +describe("FileUtils", () => { + it("should resize a file", async () => { + const blob = await fetch("assets/child.png").then((res) => res.blob()); + const file = new File([blob], "image"); + + const cvs = await resizeImage(file, 200); + + expect(cvs.height).toBe(200); + expect(cvs.width).toBeLessThan(200); + const cvsBlob = await new Promise((res) => cvs.toBlob(res)); + const cvsFile = new File([cvsBlob], "image2"); + expect(cvsFile.size).toBeLessThan(file.size); + }); +}); From 27d0ce964746ef5e70683d2347c77f734ea11a82 Mon Sep 17 00:00:00 2001 From: Simon Date: Thu, 30 Mar 2023 11:00:38 +0200 Subject: [PATCH 16/22] removed old photo functionality --- .storybook/utils/addDefaultChildPhoto.ts | 10 -- .../activity-card/activity-card.stories.ts | 2 - .../roll-call-setup.stories.ts | 2 - .../child-block-tooltip.component.ts | 4 +- .../child-block-tooltip.stories.ts | 8 +- .../child-block/child-block.component.ts | 4 +- .../child-block/child-block.stories.ts | 2 - .../child-photo.service.spec.ts | 27 ---- .../child-photo.service.ts | 35 ------ .../datatype-photo.spec.ts | 105 ---------------- .../child-photo-service/datatype-photo.ts | 63 ---------- .../children/child-photo-service/photo.ts | 19 --- .../children/children.module.ts | 8 +- .../demo-child-generator.service.ts | 5 - .../child-dev-project/children/model/child.ts | 20 +-- src/app/core/admin/admin/admin.component.html | 28 ++--- src/app/core/admin/admin/admin.component.ts | 9 -- .../child-photo-update.service.spec.ts | 24 ---- .../services/child-photo-update.service.ts | 66 ---------- src/app/core/config/config-fix.ts | 2 +- src/app/core/core-components.ts | 7 -- .../entity-form/entity-form.stories.ts | 4 - .../display-entity-array.stories.ts | 21 ---- .../display-entity/display-entity.stories.ts | 5 - .../entity-select/entity-select.stories.ts | 13 -- .../edit-photo/edit-photo.component.html | 37 ++---- .../edit-photo/edit-photo.component.scss | 15 +-- .../edit-photo/edit-photo.component.spec.ts | 107 +++++++++++++--- .../edit-photo/edit-photo.component.ts | 86 +++++++++---- .../edit-photo/new-photo.component.html | 13 -- .../edit-photo/new-photo.component.scss | 12 -- .../edit-photo/new-photo.component.spec.ts | 118 ------------------ .../edit-photo/new-photo.component.ts | 84 ------------- 33 files changed, 186 insertions(+), 779 deletions(-) delete mode 100644 .storybook/utils/addDefaultChildPhoto.ts delete mode 100644 src/app/child-dev-project/children/child-photo-service/child-photo.service.spec.ts delete mode 100644 src/app/child-dev-project/children/child-photo-service/child-photo.service.ts delete mode 100644 src/app/child-dev-project/children/child-photo-service/datatype-photo.spec.ts delete mode 100644 src/app/child-dev-project/children/child-photo-service/datatype-photo.ts delete mode 100644 src/app/child-dev-project/children/child-photo-service/photo.ts delete mode 100644 src/app/core/admin/services/child-photo-update.service.spec.ts delete mode 100644 src/app/core/admin/services/child-photo-update.service.ts delete mode 100644 src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html delete mode 100644 src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss delete mode 100644 src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts delete mode 100644 src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts diff --git a/.storybook/utils/addDefaultChildPhoto.ts b/.storybook/utils/addDefaultChildPhoto.ts deleted file mode 100644 index e60678b18a..0000000000 --- a/.storybook/utils/addDefaultChildPhoto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Child } from "../../src/app/child-dev-project/children/model/child"; -import { BehaviorSubject } from "rxjs"; -import { SafeUrl } from "@angular/platform-browser"; - -export function addDefaultChildPhoto(child: Child) { - child.photo = { - path: "", - photo: new BehaviorSubject("assets/child.png"), - }; -} diff --git a/src/app/child-dev-project/attendance/activity-card/activity-card.stories.ts b/src/app/child-dev-project/attendance/activity-card/activity-card.stories.ts index e60dbc5075..9ff4c0cf1e 100644 --- a/src/app/child-dev-project/attendance/activity-card/activity-card.stories.ts +++ b/src/app/child-dev-project/attendance/activity-card/activity-card.stories.ts @@ -3,7 +3,6 @@ import { moduleMetadata } from "@storybook/angular"; import { ActivityCardComponent } from "./activity-card.component"; import { Note } from "../../notes/model/note"; import { DemoChildGenerator } from "../../children/demo-data-generators/demo-child-generator.service"; -import { addDefaultChildPhoto } from "../../../../../.storybook/utils/addDefaultChildPhoto"; import { MatCardModule } from "@angular/material/card"; import { RecurringActivity } from "../model/recurring-activity"; import { MatTooltipModule } from "@angular/material/tooltip"; @@ -37,7 +36,6 @@ const demoChildren = [ DemoChildGenerator.generateEntity("2"), DemoChildGenerator.generateEntity("3"), ]; -demoChildren.forEach((c) => addDefaultChildPhoto(c)); const simpleEvent = Note.create(new Date(), "some meeting"); demoChildren.forEach((c) => simpleEvent.addChild(c.getId())); diff --git a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.stories.ts b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.stories.ts index d59c2d78c0..723fbf773b 100644 --- a/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.stories.ts +++ b/src/app/child-dev-project/attendance/add-day-attendance/roll-call-setup/roll-call-setup.stories.ts @@ -1,6 +1,5 @@ import { Meta, Story } from "@storybook/angular/types-6-0"; import { DemoChildGenerator } from "../../../children/demo-data-generators/demo-child-generator.service"; -import { addDefaultChildPhoto } from "../../../../../../.storybook/utils/addDefaultChildPhoto"; import { moduleMetadata } from "@storybook/angular"; import { RollCallSetupComponent } from "./roll-call-setup.component"; import moment from "moment"; @@ -32,7 +31,6 @@ const demoChildren = [ DemoChildGenerator.generateEntity("2"), DemoChildGenerator.generateEntity("3"), ]; -demoChildren.forEach((c) => addDefaultChildPhoto(c)); demoChildren.forEach((c) => demoEvent.addChild(c.getId())); const demoActivities = [ diff --git a/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.ts b/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.ts index d799d7fc60..711ce27ce7 100644 --- a/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.ts +++ b/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.component.ts @@ -32,9 +32,9 @@ export class ChildBlockTooltipComponent implements OnInit { constructor(private fileService: FileService) {} ngOnInit() { - if (this.entity.photo2) { + if (this.entity.photo) { this.fileService - .loadFile(this.entity, "photo2") + .loadFile(this.entity, "photo") .subscribe((res) => (this.imgPath = res)); } } diff --git a/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.stories.ts b/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.stories.ts index 7f90c7bc24..af1ee0d17c 100644 --- a/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.stories.ts +++ b/src/app/child-dev-project/children/child-block/child-block-tooltip/child-block-tooltip.stories.ts @@ -2,7 +2,6 @@ import { Story, Meta } from "@storybook/angular/types-6-0"; import { Child } from "../../model/child"; import { moduleMetadata } from "@storybook/angular"; import { CommonModule } from "@angular/common"; -import { addDefaultChildPhoto } from "../../../../../../.storybook/utils/addDefaultChildPhoto"; import { StorybookBaseModule } from "../../../../utils/storybook-base.module"; import { ChildBlockTooltipComponent } from "./child-block-tooltip.component"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; @@ -19,13 +18,14 @@ export default { const demoChild = new Child("1"); demoChild.name = "John Doe"; -addDefaultChildPhoto(demoChild); demoChild.projectNumber = "99"; demoChild.phone = "+49 199 1234567"; demoChild.schoolClass = "5"; -demoChild.schoolId = "0"; +demoChild.schoolId = ["0"]; -const Template: Story = (args: ChildBlockTooltipComponent) => ({ +const Template: Story = ( + args: ChildBlockTooltipComponent +) => ({ component: ChildBlockTooltipComponent, props: args, }); diff --git a/src/app/child-dev-project/children/child-block/child-block.component.ts b/src/app/child-dev-project/children/child-block/child-block.component.ts index e6f7cbb91a..3d9631ab07 100644 --- a/src/app/child-dev-project/children/child-block/child-block.component.ts +++ b/src/app/child-dev-project/children/child-block/child-block.component.ts @@ -58,9 +58,9 @@ export class ChildBlockComponent } ngOnInit() { - if (this.entity.photo2) { + if (this.entity.photo) { this.fileService - .loadFile(this.entity, "photo2") + .loadFile(this.entity, "photo") .subscribe((res) => (this.imgPath = res)); } } diff --git a/src/app/child-dev-project/children/child-block/child-block.stories.ts b/src/app/child-dev-project/children/child-block/child-block.stories.ts index e45c0aa261..be459c452d 100644 --- a/src/app/child-dev-project/children/child-block/child-block.stories.ts +++ b/src/app/child-dev-project/children/child-block/child-block.stories.ts @@ -3,7 +3,6 @@ import { ChildBlockComponent } from "./child-block.component"; import { Child } from "../model/child"; import { moduleMetadata } from "@storybook/angular"; import { CommonModule } from "@angular/common"; -import { addDefaultChildPhoto } from "../../../../../.storybook/utils/addDefaultChildPhoto"; import { StorybookBaseModule } from "../../../utils/storybook-base.module"; export default { @@ -18,7 +17,6 @@ export default { const demoChild = new Child("1"); demoChild.name = "John Doe"; -addDefaultChildPhoto(demoChild); demoChild.projectNumber = "99"; demoChild.phone = "+49 199 1234567"; demoChild.schoolClass = "5"; diff --git a/src/app/child-dev-project/children/child-photo-service/child-photo.service.spec.ts b/src/app/child-dev-project/children/child-photo-service/child-photo.service.spec.ts deleted file mode 100644 index c4774e4487..0000000000 --- a/src/app/child-dev-project/children/child-photo-service/child-photo.service.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { TestBed } from "@angular/core/testing"; - -import { ChildPhotoService } from "./child-photo.service"; -import { Child } from "../model/child"; - -describe("ChildPhotoService", () => { - let service: ChildPhotoService; - - const DEFAULT_IMG = "assets/child.png"; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [], - }); - service = TestBed.inject(ChildPhotoService); - }); - - it("should be created", () => { - expect(service).toBeTruthy(); - }); - - it("should getFile default", async () => { - const testChild = new Child("1"); - const actualImage = await service.getImage(testChild); - expect(actualImage).toBe(DEFAULT_IMG); - }); -}); diff --git a/src/app/child-dev-project/children/child-photo-service/child-photo.service.ts b/src/app/child-dev-project/children/child-photo-service/child-photo.service.ts deleted file mode 100644 index e6d9ea220c..0000000000 --- a/src/app/child-dev-project/children/child-photo-service/child-photo.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Injectable } from "@angular/core"; -import { SafeUrl } from "@angular/platform-browser"; -import { Child } from "../model/child"; - -@Injectable({ - providedIn: "root", -}) -export class ChildPhotoService { - public static getImageFromAssets(photoFile: string): SafeUrl { - if (typeof photoFile !== "string" || photoFile.trim() === "") { - return ChildPhotoService.getDefaultImage(); - } - return ChildPhotoService.generatePhotoPath(photoFile); - } - - /** - * Returns the full relative filePath to a child photo given a filename, adding the relevant folders to it. - * @param filename The given filename with file extension. - */ - public static generatePhotoPath(filename: string): string { - return "assets/child-photos/" + filename; - } - - public static getDefaultImage(): SafeUrl { - return "assets/child.png"; - } - - /** - * Creates an ArrayBuffer of the photo for that Child or the default image url. - * @param child - */ - public async getImage(child: Child): Promise { - return ChildPhotoService.getImageFromAssets(child.photo?.path); - } -} diff --git a/src/app/child-dev-project/children/child-photo-service/datatype-photo.spec.ts b/src/app/child-dev-project/children/child-photo-service/datatype-photo.spec.ts deleted file mode 100644 index e808226b3c..0000000000 --- a/src/app/child-dev-project/children/child-photo-service/datatype-photo.spec.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * This file is part of ndb-core. - * - * ndb-core is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ndb-core is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ndb-core. If not, see . - */ - -import { TestBed } from "@angular/core/testing"; -import { SafeUrl } from "@angular/platform-browser"; -import { EntitySchemaService } from "../../../core/entity/schema/entity-schema.service"; -import { DatabaseField } from "../../../core/entity/database-field.decorator"; -import { Entity } from "../../../core/entity/model/entity"; -import { ChildPhotoService } from "./child-photo.service"; -import { BehaviorSubject } from "rxjs"; -import { PhotoDatatype } from "./datatype-photo"; -import { Photo } from "./photo"; -import { Child } from "../model/child"; - -describe("dataType photo", () => { - let entitySchemaService: EntitySchemaService; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [EntitySchemaService], - }); - - entitySchemaService = TestBed.inject( - EntitySchemaService - ); - entitySchemaService.registerSchemaDatatype(new PhotoDatatype()); - }); - - it("should only save the path of an image", () => { - class TestEntity extends Entity { - @DatabaseField({ dataType: "photo" }) photo: Photo; - } - const id = "test1"; - const entity = new TestEntity(id); - entity.photo = { path: "12345", photo: new BehaviorSubject(null) }; - - const rawData = entitySchemaService.transformEntityToDatabaseFormat(entity); - expect(rawData.photo).toEqual(entity.photo.path); - }); - - it("should set the default photo when loading", () => { - class TestEntity extends Entity { - @DatabaseField({ dataType: "photo", defaultValue: "" }) - photo: Photo; - } - const defaultImg = "default-img"; - spyOn(ChildPhotoService, "getImageFromAssets").and.returnValue(defaultImg); - - const data = { _id: "someId" }; - const entity = new TestEntity(); - entitySchemaService.loadDataIntoEntity(entity, data); - - expect(entity.photo.photo.value).toEqual(defaultImg); - }); - - it("should migrate a child with the old photo format", () => { - const oldFormatInDb = entitySchemaService.transformEntityToDatabaseFormat( - new Child() - ); - oldFormatInDb["photoFile"] = "oldPhotoFile.jpg"; - - const newFormatChild = new Child(); - entitySchemaService.loadDataIntoEntity(newFormatChild, oldFormatInDb); - - expect(newFormatChild.photo.path).toEqual(oldFormatInDb.photoFile); - }); - - it("should not safe the default value", () => { - class TestEntity extends Entity { - @DatabaseField({ dataType: "photo", defaultValue: "someFile.jpg" }) - photo: Photo; - } - - const entity = new TestEntity(); - entitySchemaService.loadDataIntoEntity(entity, {}); - const result = entitySchemaService.transformEntityToDatabaseFormat(entity); - - expect(result.photo).toBeUndefined(); - }); - - it("should not throw an error if deprecated value is null", () => { - const oldChild = { - _id: "oldChild", - photoFile: null, - }; - - expect(() => - entitySchemaService.loadDataIntoEntity(new Child(), oldChild) - ).not.toThrowError(); - }); -}); diff --git a/src/app/child-dev-project/children/child-photo-service/datatype-photo.ts b/src/app/child-dev-project/children/child-photo-service/datatype-photo.ts deleted file mode 100644 index 51cfb2534f..0000000000 --- a/src/app/child-dev-project/children/child-photo-service/datatype-photo.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * This file is part of ndb-core. - * - * ndb-core is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ndb-core is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with ndb-core. If not, see . - */ - -import { EntitySchemaDatatype } from "../../../core/entity/schema/entity-schema-datatype"; -import { ChildPhotoService } from "./child-photo.service"; -import { EntitySchemaField } from "../../../core/entity/schema/entity-schema-field"; -import { EntitySchemaService } from "../../../core/entity/schema/entity-schema.service"; -import { Entity } from "../../../core/entity/model/entity"; -import { Photo } from "./photo"; -import { SafeUrl } from "@angular/platform-browser"; -import { BehaviorSubject } from "rxjs"; - -/** - * Dynamically load the child's photo through the ChildPhotoService during Entity loading process. - */ -export class PhotoDatatype implements EntitySchemaDatatype { - public readonly name = "photo"; - public readonly editComponent = "EditPhoto"; - - public transformToDatabaseFormat(value: Photo, schema: EntitySchemaField) { - if (value.path === schema.defaultValue) { - return undefined; - } else { - return value.path; - } - } - - public transformToObjectFormat( - value: string, - schemaField: EntitySchemaField, - schemaService: EntitySchemaService, - parent: Entity - ): Photo { - // Using of old photoFile values - if ( - typeof parent["photoFile"] === "string" && - parent["photoFile"].trim() !== "" - ) { - value = parent["photoFile"]; - } - return { - path: value, - photo: new BehaviorSubject( - // reactivate the integration of cloud file loading here after testing and performance improvements - ChildPhotoService.getImageFromAssets(value) - ), - }; - } -} diff --git a/src/app/child-dev-project/children/child-photo-service/photo.ts b/src/app/child-dev-project/children/child-photo-service/photo.ts deleted file mode 100644 index e578166752..0000000000 --- a/src/app/child-dev-project/children/child-photo-service/photo.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { BehaviorSubject } from "rxjs"; -import { SafeUrl } from "@angular/platform-browser"; - -/** - * A simple interface for creating photo attributes. - */ -export interface Photo { - /** - * The path to the photo. This will be saved to the database. - */ - path: string; - - /** - * The actual photo which can be used in a template: - * `` - * This is not saved to the database but build from the `path` attribute when loaded from the database. - */ - photo: BehaviorSubject; -} diff --git a/src/app/child-dev-project/children/children.module.ts b/src/app/child-dev-project/children/children.module.ts index 7eafec81dc..1852887fa8 100644 --- a/src/app/child-dev-project/children/children.module.ts +++ b/src/app/child-dev-project/children/children.module.ts @@ -16,8 +16,6 @@ */ import { NgModule } from "@angular/core"; -import { EntitySchemaService } from "../../core/entity/schema/entity-schema.service"; -import { PhotoDatatype } from "./child-photo-service/datatype-photo"; import { ComponentRegistry } from "../../dynamic-components"; import { childrenComponents } from "./children-components"; import { Aser } from "./aser/model/aser"; @@ -36,11 +34,7 @@ export class ChildrenModule { ChildSchoolRelation, ]; - constructor( - entitySchemaService: EntitySchemaService, - components: ComponentRegistry - ) { - entitySchemaService.registerSchemaDatatype(new PhotoDatatype()); + constructor(components: ComponentRegistry) { components.addAll(childrenComponents); } } diff --git a/src/app/child-dev-project/children/demo-data-generators/demo-child-generator.service.ts b/src/app/child-dev-project/children/demo-data-generators/demo-child-generator.service.ts index 45120d5700..1a3c7cdc63 100644 --- a/src/app/child-dev-project/children/demo-data-generators/demo-child-generator.service.ts +++ b/src/app/child-dev-project/children/demo-data-generators/demo-child-generator.service.ts @@ -6,7 +6,6 @@ import { Injectable } from "@angular/core"; import { DemoDataGenerator } from "../../../core/demo-data/demo-data-generator"; import { faker } from "../../../core/demo-data/faker"; import { centersWithProbability } from "./fixtures/centers"; -import { addDefaultChildPhoto } from "../../../../../.storybook/utils/addDefaultChildPhoto"; import { genders } from "../model/genders"; import { calculateAge } from "../../../utils/utils"; import { DateWithAge } from "../model/dateWithAge"; @@ -53,10 +52,6 @@ export class DemoChildGenerator extends DemoDataGenerator { if (faker.datatype.number(100) > 90) { DemoChildGenerator.makeChildDropout(child); } - - // add default photo for easier use in storybook stories - addDefaultChildPhoto(child); - return child; } diff --git a/src/app/child-dev-project/children/model/child.ts b/src/app/child-dev-project/children/model/child.ts index a0f04258f3..186f663232 100644 --- a/src/app/child-dev-project/children/model/child.ts +++ b/src/app/child-dev-project/children/model/child.ts @@ -19,10 +19,6 @@ import { Entity } from "../../../core/entity/model/entity"; import { DatabaseEntity } from "../../../core/entity/database-entity.decorator"; import { DatabaseField } from "../../../core/entity/database-field.decorator"; import { ConfigurableEnumValue } from "../../../core/configurable-enum/configurable-enum.interface"; -import { Photo } from "../child-photo-service/photo"; -import { BehaviorSubject } from "rxjs"; -import { SafeUrl } from "@angular/platform-browser"; -import { ChildPhotoService } from "../child-photo-service/child-photo.service"; import { DateWithAge } from "./dateWithAge"; import { IconName } from "@fortawesome/fontawesome-svg-core"; @@ -112,27 +108,17 @@ export class Child extends Entity { schoolClass: string; @DatabaseField({ - dataType: "photo", - defaultValue: "", + dataType: "file", label: $localize`:Label for the filename of a photo of a child:Photo Filename`, + editComponent: "EditPhoto", }) - photo: Photo = { - path: "", - photo: new BehaviorSubject( - ChildPhotoService.getImageFromAssets(undefined) - ), - }; + photo: string; @DatabaseField({ dataType: "file", label: $localize`:Label for the filename of a photo of a child:Photo Filename`, editComponent: "NewPhoto", }) - photo2: string; - - @DatabaseField({ - label: $localize`:Label for the phone number of a child:Phone Number`, - }) phone: string; get isActive(): boolean { diff --git a/src/app/core/admin/admin/admin.component.html b/src/app/core/admin/admin/admin.component.html index 78e3c29c02..6eac7d3b0a 100644 --- a/src/app/core/admin/admin/admin.component.html +++ b/src/app/core/admin/admin/admin.component.html @@ -5,18 +5,6 @@

Administration & Configuration

you know what you are doing. -
-
-

Utility Functions

-

- -

-

Backup

@@ -107,15 +95,15 @@

Debug the PouchDB

Alert Log

- - - + + + - - - - - + + + + +
TimestampTypeMessageTimestampTypeMessage
{{alert.timestamp | date: "medium"}}{{ alert.type }}{{ alert.message }}
{{alert.timestamp | date: "medium"}}{{ alert.type }}{{ alert.message }}
diff --git a/src/app/core/admin/admin/admin.component.ts b/src/app/core/admin/admin/admin.component.ts index f78003ae42..11dbf8ffd4 100644 --- a/src/app/core/admin/admin/admin.component.ts +++ b/src/app/core/admin/admin/admin.component.ts @@ -3,7 +3,6 @@ import { AlertService } from "../../alerts/alert.service"; import { BackupService } from "../services/backup.service"; import { ConfirmationDialogService } from "../../confirmation-dialog/confirmation-dialog.service"; import { MatSnackBar } from "@angular/material/snack-bar"; -import { ChildPhotoUpdateService } from "../services/child-photo-update.service"; import { ConfigService } from "../../config/config.service"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; import { readFile } from "../../../utils/utils"; @@ -38,7 +37,6 @@ export class AdminComponent implements OnInit { private db: Database, private confirmationDialog: ConfirmationDialogService, private snackBar: MatSnackBar, - private childPhotoUpdateService: ChildPhotoUpdateService, private configService: ConfigService ) {} @@ -46,13 +44,6 @@ export class AdminComponent implements OnInit { this.alerts = this.alertService.alerts; } - /** - * Trigger an automatic detection & update of Child entities' photo filenames. - */ - updatePhotoFilenames() { - this.childPhotoUpdateService.updateChildrenPhotoFilenames(); - } - /** * Send a reference of the PouchDB to the browser's developer console for real-time debugging. */ diff --git a/src/app/core/admin/services/child-photo-update.service.spec.ts b/src/app/core/admin/services/child-photo-update.service.spec.ts deleted file mode 100644 index cea08705d4..0000000000 --- a/src/app/core/admin/services/child-photo-update.service.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { TestBed } from "@angular/core/testing"; - -import { ChildPhotoUpdateService } from "./child-photo-update.service"; -import { EntityMapperService } from "../../entity/entity-mapper.service"; -import { HttpClientTestingModule } from "@angular/common/http/testing"; - -describe("ChildPhotoUpdateService", () => { - beforeEach(() => { - const mockEntityMapper = jasmine.createSpyObj(["loadType", "save"]); - mockEntityMapper.loadType.and.returnValue(Promise.resolve([])); - - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [{ provide: EntityMapperService, useValue: mockEntityMapper }], - }); - }); - - it("should be created", () => { - const service: ChildPhotoUpdateService = TestBed.inject( - ChildPhotoUpdateService - ); - expect(service).toBeTruthy(); - }); -}); diff --git a/src/app/core/admin/services/child-photo-update.service.ts b/src/app/core/admin/services/child-photo-update.service.ts deleted file mode 100644 index aef584acef..0000000000 --- a/src/app/core/admin/services/child-photo-update.service.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Injectable } from "@angular/core"; -import { EntityMapperService } from "../../entity/entity-mapper.service"; -import { HttpClient } from "@angular/common/http"; -import { Child } from "../../../child-dev-project/children/model/child"; -import { ChildPhotoService } from "../../../child-dev-project/children/child-photo-service/child-photo.service"; -import { firstValueFrom } from "rxjs"; - -/** - * Utility service to automatically detect and update filenames for Child entities' photos. - */ -@Injectable({ - providedIn: "root", -}) -export class ChildPhotoUpdateService { - constructor( - private entityService: EntityMapperService, - private httpClient: HttpClient - ) {} - - /** - * Tries to detect and update the filename of an existing photo for all Child entities, - * saving the entities that were updated. - */ - public async updateChildrenPhotoFilenames() { - const children = await this.entityService.loadType(Child); - for (const child of children) { - await this.updatePhotoIfFileExists(child, `${child.projectNumber}.png`); - await this.updatePhotoIfFileExists(child, `${child.projectNumber}.jpg`); - } - } - - /** - * Check (and if it exists update) the Child's photo file. - * @param child The child to be updated - * @param filename A guess for a likely filename that needs to be checked - */ - private async updatePhotoIfFileExists(child: Child, filename: string) { - if (child.photo?.path && child.photo.path !== "") { - // do not overwrite existing path - return; - } - - const fileExists = await this.checkIfFileExists( - ChildPhotoService.generatePhotoPath(filename) - ); - if (fileExists) { - const currentPhoto = child.photo; - child.photo = { path: filename, photo: currentPhoto?.photo }; - this.entityService.save(child); - console.log( - `set photoFile for Child:${child.getId()} (${ - child.projectNumber - }) to ${filename}` - ); - } - } - - private async checkIfFileExists(filename): Promise { - try { - await firstValueFrom(this.httpClient.get(filename)); - return true; - } catch (e) { - return e.status === 200; - } - } -} diff --git a/src/app/core/config/config-fix.ts b/src/app/core/config/config-fix.ts index 8208719288..cf8eee6086 100644 --- a/src/app/core/config/config-fix.ts +++ b/src/app/core/config/config-fix.ts @@ -549,7 +549,7 @@ export const defaultJsonConfig = { "component": "Form", "config": { "cols": [ - ["photo2"], + ["photo"], [ "name", "projectNumber", diff --git a/src/app/core/core-components.ts b/src/app/core/core-components.ts index 9378ca6290..15f45c8211 100644 --- a/src/app/core/core-components.ts +++ b/src/app/core/core-components.ts @@ -99,13 +99,6 @@ export const coreComponents: ComponentTuple[] = [ "./entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component" ).then((c) => c.EditPhotoComponent), ], - [ - "NewPhoto", - () => - import( - "./entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component" - ).then((c) => c.NewPhotoComponent), - ], [ "EditNumber", () => diff --git a/src/app/core/entity-components/entity-form/entity-form/entity-form.stories.ts b/src/app/core/entity-components/entity-form/entity-form/entity-form.stories.ts index 072a95815a..3723398885 100644 --- a/src/app/core/entity-components/entity-form/entity-form/entity-form.stories.ts +++ b/src/app/core/entity-components/entity-form/entity-form/entity-form.stories.ts @@ -1,6 +1,5 @@ import { moduleMetadata } from "@storybook/angular"; import { Meta, Story } from "@storybook/angular/types-6-0"; -import { ChildPhotoService } from "../../../../child-dev-project/children/child-photo-service/child-photo.service"; import { Child } from "../../../../child-dev-project/children/model/child"; import { EntityFormComponent } from "./entity-form.component"; import { School } from "../../../../child-dev-project/schools/model/school"; @@ -25,9 +24,6 @@ export default { StorybookBaseModule, MockedTestingModule.withState(LoginState.LOGGED_IN, [s1, s2, s3]), ], - providers: [ - { provide: ChildPhotoService, useValue: { canSetImage: () => true } }, - ], }), ], } as Meta; diff --git a/src/app/core/entity-components/entity-select/display-entity-array/display-entity-array.stories.ts b/src/app/core/entity-components/entity-select/display-entity-array/display-entity-array.stories.ts index 88e990776e..1a5fa8f36d 100644 --- a/src/app/core/entity-components/entity-select/display-entity-array/display-entity-array.stories.ts +++ b/src/app/core/entity-components/entity-select/display-entity-array/display-entity-array.stories.ts @@ -2,7 +2,6 @@ import { Story, Meta } from "@storybook/angular/types-6-0"; import { moduleMetadata } from "@storybook/angular"; import { Child } from "../../../../child-dev-project/children/model/child"; import { DisplayEntityArrayComponent } from "./display-entity-array.component"; -import { BehaviorSubject } from "rxjs"; import { StorybookBaseModule } from "../../../../utils/storybook-base.module"; import { EntityMapperService } from "../../../entity/entity-mapper.service"; import { mockEntityMapper } from "../../../entity/mock-entity-mapper-service"; @@ -11,38 +10,18 @@ import { ChildrenService } from "../../../../child-dev-project/children/children const child1 = new Child(); child1.name = "Test Name"; child1.projectNumber = "10"; -child1.photo = { - path: "", - photo: new BehaviorSubject("assets/child.png"), -}; const child2 = new Child(); child2.name = "First Name"; child2.projectNumber = "14"; -child2.photo = { - path: "", - photo: new BehaviorSubject("assets/child.png"), -}; const child3 = new Child(); child3.name = "Second Name"; child3.projectNumber = "11"; -child3.photo = { - path: "", - photo: new BehaviorSubject("assets/child.png"), -}; const child4 = new Child(); child4.name = "Third Name"; child4.projectNumber = "12"; -child4.photo = { - path: "", - photo: new BehaviorSubject("assets/child.png"), -}; const child5 = new Child(); child5.name = "Fifth Name"; child5.projectNumber = "13"; -child5.photo = { - path: "", - photo: new BehaviorSubject("assets/child.png"), -}; export default { title: "Core/EntityComponents/DisplayEntityArray", diff --git a/src/app/core/entity-components/entity-select/display-entity/display-entity.stories.ts b/src/app/core/entity-components/entity-select/display-entity/display-entity.stories.ts index 9a10dbcb4b..a163f16ba8 100644 --- a/src/app/core/entity-components/entity-select/display-entity/display-entity.stories.ts +++ b/src/app/core/entity-components/entity-select/display-entity/display-entity.stories.ts @@ -2,7 +2,6 @@ import { Story, Meta } from "@storybook/angular/types-6-0"; import { moduleMetadata } from "@storybook/angular"; import { DisplayEntityComponent } from "./display-entity.component"; import { Child } from "../../../../child-dev-project/children/model/child"; -import { BehaviorSubject } from "rxjs"; import { School } from "../../../../child-dev-project/schools/model/school"; import { User } from "../../../user/user"; import { StorybookBaseModule } from "../../../../utils/storybook-base.module"; @@ -34,10 +33,6 @@ const Template: Story = ( const testChild = new Child(); testChild.name = "Test Name"; testChild.projectNumber = "10"; -testChild.photo = { - path: "", - photo: new BehaviorSubject("assets/child.png"), -}; export const ChildComponent = Template.bind({}); ChildComponent.args = { entityToDisplay: testChild, diff --git a/src/app/core/entity-components/entity-select/entity-select/entity-select.stories.ts b/src/app/core/entity-components/entity-select/entity-select/entity-select.stories.ts index 811268a77b..4a94246222 100644 --- a/src/app/core/entity-components/entity-select/entity-select/entity-select.stories.ts +++ b/src/app/core/entity-components/entity-select/entity-select/entity-select.stories.ts @@ -3,7 +3,6 @@ import { moduleMetadata } from "@storybook/angular"; import { Child } from "../../../../child-dev-project/children/model/child"; import { BackupService } from "../../../admin/services/backup.service"; import { EntityMapperService } from "../../../entity/entity-mapper.service"; -import { BehaviorSubject } from "rxjs"; import { EntitySelectComponent } from "./entity-select.component"; import { StorybookBaseModule } from "../../../../utils/storybook-base.module"; import { School } from "../../../../child-dev-project/schools/model/school"; @@ -19,24 +18,12 @@ import { ChildrenService } from "../../../../child-dev-project/children/children const child1 = new Child(); child1.name = "First Child"; child1.projectNumber = "1"; -child1.photo = { - path: "", - photo: new BehaviorSubject("assets/child.png"), -}; const child2 = new Child(); child2.name = "Second Child"; child2.projectNumber = "2"; -child2.photo = { - path: "", - photo: new BehaviorSubject("assets/child.png"), -}; const child3 = new Child(); child3.name = "Third Child"; child3.projectNumber = "3"; -child3.photo = { - path: "", - photo: new BehaviorSubject("assets/child.png"), -}; export default { title: "Core/EntityComponents/EntitySelect", diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html index 6e3fa27d51..951252c2d4 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html @@ -1,24 +1,13 @@ - -

No photo set

- - {{label}} - - - + +profile photo +
+ + +
diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.scss b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.scss index e79836ea24..5ebfdc9408 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.scss +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.scss @@ -1,8 +1,3 @@ -.child-pic-container { - position: relative; - margin-right: 10px; -} - .child-pic { width: 150px; height: 150px; @@ -10,10 +5,8 @@ object-fit: cover; } -.child-pic-photofile { - position: absolute; - left: 1px; - top: 115px; - width: 120px; - background: white; +:host { + display: flex; + flex-direction: column; + align-items: center; } diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.spec.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.spec.ts index a42e9cf962..79e0f9ce14 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.spec.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.spec.ts @@ -2,26 +2,60 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { EditPhotoComponent } from "./edit-photo.component"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; -import { SessionService } from "../../../../session/session-service/session.service"; -import { setupEditComponent } from "../edit-component.spec"; +import { FileService } from "../../../../../features/file/file.service"; +import { AlertService } from "../../../../alerts/alert.service"; +import { EntityMapperService } from "../../../../entity/entity-mapper.service"; +import { EditPropertyConfig } from "../edit-component"; +import { of } from "rxjs"; +import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; +import { EntitySchemaService } from "../../../../entity/schema/entity-schema.service"; +import { FormControl } from "@angular/forms"; +import { Entity } from "../../../../entity/model/entity"; +import { DomSanitizer } from "@angular/platform-browser"; describe("EditPhotoComponent", () => { let component: EditPhotoComponent; let fixture: ComponentFixture; - let mockSessionService: jasmine.SpyObj; + let mockFileService: jasmine.SpyObj; + let mockAlertService: jasmine.SpyObj; + let mockEntityMapper: jasmine.SpyObj; + let config: EditPropertyConfig; + + const file = { name: "test.file" } as File; + const fileEvent = { target: { files: [file] } }; beforeEach(async () => { - mockSessionService = jasmine.createSpyObj(["getCurrentUser"]); + mockFileService = jasmine.createSpyObj([ + "uploadFile", + "loadFile", + "removeFile", + ]); + mockFileService.removeFile.and.returnValue(of(undefined)); + mockAlertService = jasmine.createSpyObj(["addDanger", "addInfo"]); + mockEntityMapper = jasmine.createSpyObj(["save"]); await TestBed.configureTestingModule({ - imports: [EditPhotoComponent, NoopAnimationsModule], - providers: [{ provide: SessionService, useValue: mockSessionService }], + imports: [ + EditPhotoComponent, + FontAwesomeTestingModule, + NoopAnimationsModule, + ], + providers: [ + EntitySchemaService, + { provide: AlertService, useValue: mockAlertService }, + { provide: FileService, useValue: mockFileService }, + { provide: EntityMapperService, useValue: mockEntityMapper }, + ], }).compileComponents(); - }); - beforeEach(() => { fixture = TestBed.createComponent(EditPhotoComponent); component = fixture.componentInstance; - setupEditComponent(component); + config = { + formControl: new FormControl(), + entity: new Entity(), + formFieldConfig: { id: "testProp" }, + propertySchema: undefined, + }; + component.onInitFromDynamicConfig(config); fixture.detectChanges(); }); @@ -29,21 +63,56 @@ describe("EditPhotoComponent", () => { expect(component).toBeTruthy(); }); - it("should correctly update the photo path", () => { - component.changeFilename("new_file.name"); + it("should show the image once it is selected", () => { + spyOn( + TestBed.inject(DomSanitizer), + "bypassSecurityTrustUrl" + ).and.returnValue("image.path"); + spyOn(URL, "createObjectURL"); + + component.onFileSelected(fileEvent); + + expect(component.imgPath).toBe("image.path"); + }); + + it("should load the picture on initialisation", () => { + mockFileService.loadFile.and.returnValue(of("some.path")); + config.entity[config.formFieldConfig.id] = "file.name"; - expect(component.formControl.value.path).toBe("new_file.name"); + component.onInitFromDynamicConfig(config); + + expect(component.imgPath).toBe("some.path"); }); - it("should allow editing photos when the user is admin", () => { - mockSessionService.getCurrentUser.and.returnValue({ - name: "User", - roles: ["admin_app"], - }); - expect(component.editPhotoAllowed).toBeFalse(); + it("should display the default image when clicking delete", () => { + component.imgPath = "some.path"; + + component.delete(); + expect(component.imgPath).toBe("assets/child.png"); + }); + + it("should reset the shown image when pressing cancel", () => { + mockFileService.loadFile.and.returnValue(of("initial.path")); + config.entity[config.formFieldConfig.id] = "initial.image"; + component.onInitFromDynamicConfig(config); + + component.imgPath = "new.path"; + component.formControl.disable(); + + expect(component.imgPath).toBe("initial.path"); + }); + + it("should revoke initial image if file is deleted", () => { + mockFileService.loadFile.and.returnValue(of("initial.path")); + config.entity[config.formFieldConfig.id] = "initial.image"; + config.formControl.setValue("initial.image"); + component.onInitFromDynamicConfig(config); component.ngOnInit(); - expect(component.editPhotoAllowed).toBeTrue(); + component.delete(); + component.formControl.disable(); + + expect(component.imgPath).toBe("assets/child.png"); }); }); diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.ts index 3e01007a3e..932b7580d3 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.ts +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.ts @@ -1,48 +1,82 @@ -import { Component, OnInit } from "@angular/core"; -import { EditComponent } from "../edit-component"; -import { Photo } from "../../../../../child-dev-project/children/child-photo-service/photo"; -import { BehaviorSubject } from "rxjs"; -import { ChildPhotoService } from "../../../../../child-dev-project/children/child-photo-service/child-photo.service"; -import { SessionService } from "../../../../session/session-service/session.service"; +import { Component } from "@angular/core"; +import { EditPropertyConfig } from "../edit-component"; import { DynamicComponent } from "../../../../view/dynamic-components/dynamic-component.decorator"; import { NgIf } from "@angular/common"; import { MatFormFieldModule } from "@angular/material/form-field"; import { MatTooltipModule } from "@angular/material/tooltip"; import { MatInputModule } from "@angular/material/input"; import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; +import { EditFileComponent } from "../../../../../features/file/edit-file/edit-file.component"; +import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; +import { FileService } from "../../../../../features/file/file.service"; +import { AlertService } from "../../../../alerts/alert.service"; +import { LoggingService } from "../../../../logging/logging.service"; +import { EntityMapperService } from "../../../../entity/entity-mapper.service"; +import { MatButtonModule } from "@angular/material/button"; @DynamicComponent("EditPhoto") @Component({ selector: "app-edit-photo", templateUrl: "./edit-photo.component.html", styleUrls: ["./edit-photo.component.scss"], - imports: [ - NgIf, - MatFormFieldModule, - MatTooltipModule, - MatInputModule, - FontAwesomeModule, - ], + imports: [MatButtonModule, MatTooltipModule, FontAwesomeModule, NgIf], standalone: true, }) -export class EditPhotoComponent extends EditComponent implements OnInit { - editPhotoAllowed = false; +export class EditPhotoComponent extends EditFileComponent { + private readonly defaultImage = "assets/child.png"; + private initialImg: SafeUrl = this.defaultImage; + imgPath: SafeUrl = this.initialImg; + compressImage = 480; - constructor(private sessionService: SessionService) { - super(); + constructor( + fileService: FileService, + alertService: AlertService, + logger: LoggingService, + entityMapper: EntityMapperService, + private sanitizer: DomSanitizer + ) { + super(fileService, alertService, logger, entityMapper); } - ngOnInit() { - if (this.sessionService.getCurrentUser()?.roles?.includes("admin_app")) { - this.editPhotoAllowed = true; + async onFileSelected(event): Promise { + this.imgPath = this.sanitizer.bypassSecurityTrustUrl( + URL.createObjectURL(event.target.files[0]) + ); + return super.onFileSelected(event); + } + + onInitFromDynamicConfig(config: EditPropertyConfig) { + super.onInitFromDynamicConfig(config); + if (this.entity[this.formControlName]) { + this.fileService + .loadFile(this.entity, this.formControlName) + .subscribe((res) => { + this.imgPath = res; + this.initialImg = res; + }); + } + } + + delete() { + this.resetPreview(this.defaultImage); + super.delete(); + } + + protected resetFile() { + this.resetPreview(this.initialImg); + super.resetFile(); + } + + private resetPreview(resetImage: SafeUrl) { + if (this.imgPath !== this.initialImg) { + URL.revokeObjectURL(this.imgPath as string); } + this.imgPath = resetImage; } - changeFilename(path: string) { - const newValue: Photo = { - path: path, - photo: new BehaviorSubject(ChildPhotoService.getImageFromAssets(path)), - }; - this.formControl.setValue(newValue); + protected deleteExistingFile() { + URL.revokeObjectURL(this.initialImg as string); + this.initialImg = this.defaultImage; + super.deleteExistingFile(); } } diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html deleted file mode 100644 index 951252c2d4..0000000000 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.html +++ /dev/null @@ -1,13 +0,0 @@ - -profile photo -
- - -
diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss deleted file mode 100644 index 5ebfdc9408..0000000000 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.scss +++ /dev/null @@ -1,12 +0,0 @@ -.child-pic { - width: 150px; - height: 150px; - border-radius: 50%; - object-fit: cover; -} - -:host { - display: flex; - flex-direction: column; - align-items: center; -} diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts deleted file mode 100644 index 66040712f2..0000000000 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.spec.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; - -import { NewPhotoComponent } from "./new-photo.component"; -import { FileService } from "../../../../../features/file/file.service"; -import { AlertService } from "../../../../alerts/alert.service"; -import { EntityMapperService } from "../../../../entity/entity-mapper.service"; -import { FontAwesomeTestingModule } from "@fortawesome/angular-fontawesome/testing"; -import { NoopAnimationsModule } from "@angular/platform-browser/animations"; -import { EntitySchemaService } from "../../../../entity/schema/entity-schema.service"; -import { FormControl } from "@angular/forms"; -import { Entity } from "../../../../entity/model/entity"; -import { DomSanitizer } from "@angular/platform-browser"; -import { of } from "rxjs"; -import { EditPropertyConfig } from "../edit-component"; - -describe("NewPhotoComponent", () => { - let component: NewPhotoComponent; - let fixture: ComponentFixture; - let mockFileService: jasmine.SpyObj; - let mockAlertService: jasmine.SpyObj; - let mockEntityMapper: jasmine.SpyObj; - let config: EditPropertyConfig; - - const file = { name: "test.file" } as File; - const fileEvent = { target: { files: [file] } }; - - beforeEach(async () => { - mockFileService = jasmine.createSpyObj([ - "uploadFile", - "loadFile", - "removeFile", - ]); - mockFileService.removeFile.and.returnValue(of(undefined)); - mockAlertService = jasmine.createSpyObj(["addDanger", "addInfo"]); - mockEntityMapper = jasmine.createSpyObj(["save"]); - await TestBed.configureTestingModule({ - imports: [ - NewPhotoComponent, - FontAwesomeTestingModule, - NoopAnimationsModule, - ], - providers: [ - EntitySchemaService, - { provide: AlertService, useValue: mockAlertService }, - { provide: FileService, useValue: mockFileService }, - { provide: EntityMapperService, useValue: mockEntityMapper }, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(NewPhotoComponent); - component = fixture.componentInstance; - config = { - formControl: new FormControl(), - entity: new Entity(), - formFieldConfig: { id: "testProp" }, - propertySchema: undefined, - }; - component.onInitFromDynamicConfig(config); - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); - - it("should show the image once it is selected", () => { - spyOn( - TestBed.inject(DomSanitizer), - "bypassSecurityTrustUrl" - ).and.returnValue("image.path"); - spyOn(URL, "createObjectURL"); - - component.onFileSelected(fileEvent); - - expect(component.imgPath).toBe("image.path"); - }); - - it("should load the picture on initialisation", () => { - mockFileService.loadFile.and.returnValue(of("some.path")); - config.entity[config.formFieldConfig.id] = "file.name"; - - component.onInitFromDynamicConfig(config); - - expect(component.imgPath).toBe("some.path"); - }); - - it("should display the default image when clicking delete", () => { - component.imgPath = "some.path"; - - component.delete(); - - expect(component.imgPath).toBe("assets/child.png"); - }); - - it("should reset the shown image when pressing cancel", () => { - mockFileService.loadFile.and.returnValue(of("initial.path")); - config.entity[config.formFieldConfig.id] = "initial.image"; - component.onInitFromDynamicConfig(config); - - component.imgPath = "new.path"; - component.formControl.disable(); - - expect(component.imgPath).toBe("initial.path"); - }); - - it("should revoke initial image if file is deleted", () => { - mockFileService.loadFile.and.returnValue(of("initial.path")); - config.entity[config.formFieldConfig.id] = "initial.image"; - config.formControl.setValue("initial.image"); - component.onInitFromDynamicConfig(config); - component.ngOnInit(); - - component.delete(); - component.formControl.disable(); - - expect(component.imgPath).toBe("assets/child.png"); - }); -}); diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts deleted file mode 100644 index 129781b3f5..0000000000 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/new-photo.component.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Component } from "@angular/core"; -import { EditPropertyConfig } from "../edit-component"; -import { DomSanitizer, SafeUrl } from "@angular/platform-browser"; -import { EditFileComponent } from "../../../../../features/file/edit-file/edit-file.component"; -import { NgIf } from "@angular/common"; -import { FileService } from "../../../../../features/file/file.service"; -import { MatButtonModule } from "@angular/material/button"; -import { MatTooltipModule } from "@angular/material/tooltip"; -import { FontAwesomeModule } from "@fortawesome/angular-fontawesome"; -import { AlertService } from "../../../../alerts/alert.service"; -import { LoggingService } from "../../../../logging/logging.service"; -import { EntityMapperService } from "../../../../entity/entity-mapper.service"; - -@Component({ - selector: "app-new-photo", - standalone: true, - imports: [ - EditFileComponent, - NgIf, - MatButtonModule, - MatTooltipModule, - FontAwesomeModule, - ], - templateUrl: "./new-photo.component.html", - styleUrls: ["./new-photo.component.scss"], -}) -export class NewPhotoComponent extends EditFileComponent { - private readonly defaultImage = "assets/child.png"; - private initialImg: SafeUrl = this.defaultImage; - imgPath: SafeUrl = this.initialImg; - compressImage = 480; - - constructor( - fileService: FileService, - alertService: AlertService, - logger: LoggingService, - entityMapper: EntityMapperService, - private sanitizer: DomSanitizer - ) { - super(fileService, alertService, logger, entityMapper); - } - - async onFileSelected(event): Promise { - this.imgPath = this.sanitizer.bypassSecurityTrustUrl( - URL.createObjectURL(event.target.files[0]) - ); - return super.onFileSelected(event); - } - - onInitFromDynamicConfig(config: EditPropertyConfig) { - super.onInitFromDynamicConfig(config); - if (this.entity[this.formControlName]) { - this.fileService - .loadFile(this.entity, this.formControlName) - .subscribe((res) => { - this.imgPath = res; - this.initialImg = res; - }); - } - } - - delete() { - this.resetPreview(this.defaultImage); - super.delete(); - } - - protected resetFile() { - this.resetPreview(this.initialImg); - super.resetFile(); - } - - private resetPreview(resetImage: SafeUrl) { - if (this.imgPath !== this.initialImg) { - URL.revokeObjectURL(this.imgPath as string); - } - this.imgPath = resetImage; - } - - protected deleteExistingFile() { - URL.revokeObjectURL(this.initialImg as string); - this.initialImg = this.defaultImage; - super.deleteExistingFile(); - } -} From 034fb15c4eb24b686db3b4fe73c50da901e522ed Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Apr 2023 19:27:22 +0200 Subject: [PATCH 17/22] undone change --- src/app/child-dev-project/children/model/child.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/child-dev-project/children/model/child.ts b/src/app/child-dev-project/children/model/child.ts index 186f663232..f74a89ac72 100644 --- a/src/app/child-dev-project/children/model/child.ts +++ b/src/app/child-dev-project/children/model/child.ts @@ -115,9 +115,7 @@ export class Child extends Entity { photo: string; @DatabaseField({ - dataType: "file", - label: $localize`:Label for the filename of a photo of a child:Photo Filename`, - editComponent: "NewPhoto", + label: $localize`:Label for the phone number of a child:Phone Number`, }) phone: string; From f0fdb500ab982c816a25ce4d5349192ba435a8ad Mon Sep 17 00:00:00 2001 From: Simon Date: Tue, 4 Apr 2023 20:40:15 +0200 Subject: [PATCH 18/22] moved compression to component level --- .../edit-photo/edit-photo.component.html | 8 ++- .../edit-photo/edit-photo.component.ts | 22 ++++---- src/app/features/file/couchdb-file.service.ts | 54 +++++-------------- .../file/edit-file/edit-file.component.html | 2 +- .../file/edit-file/edit-file.component.ts | 8 ++- src/app/features/file/file.service.ts | 4 +- src/app/features/file/mock-file.service.ts | 11 ++-- 7 files changed, 42 insertions(+), 67 deletions(-) diff --git a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html index 951252c2d4..04f4c482a9 100644 --- a/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html +++ b/src/app/core/entity-components/entity-utils/dynamic-form-components/edit-photo/edit-photo.component.html @@ -1,4 +1,10 @@ - + profile photo