From a96ab9a9a414fb4a367efb9322b0c73692f1ac22 Mon Sep 17 00:00:00 2001
From: jonaswinkler <jonas.winkler@jpwinkler.de>
Date: Sun, 3 Jan 2021 13:09:16 +0100
Subject: [PATCH] form field validation (much better error messages)

---
 src-ui/src/app/app.module.ts                  |  4 ++-
 .../edit-dialog/edit-dialog.component.ts      | 10 +++++++-
 .../components/common/input/abstract-input.ts |  3 +++
 .../common/input/number/number.component.html |  8 ++++++
 .../common/input/number/number.component.scss |  0
 .../input/number/number.component.spec.ts     | 25 +++++++++++++++++++
 .../common/input/number/number.component.ts   | 21 ++++++++++++++++
 .../common/input/text/text.component.html     |  5 +++-
 .../document-detail.component.html            |  8 ++----
 .../document-detail.component.ts              | 22 ++++++++++++++--
 .../correspondent-edit-dialog.component.html  |  6 ++---
 .../correspondent-edit-dialog.component.ts    |  4 ---
 .../document-type-edit-dialog.component.html  |  4 +--
 .../document-type-edit-dialog.component.ts    |  4 ---
 .../tag-edit-dialog.component.html            |  4 +--
 .../tag-edit-dialog.component.ts              |  4 ---
 16 files changed, 102 insertions(+), 30 deletions(-)
 create mode 100644 src-ui/src/app/components/common/input/number/number.component.html
 create mode 100644 src-ui/src/app/components/common/input/number/number.component.scss
 create mode 100644 src-ui/src/app/components/common/input/number/number.component.spec.ts
 create mode 100644 src-ui/src/app/components/common/input/number/number.component.ts

diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts
index c78dc3cfe..8cd28b6fc 100644
--- a/src-ui/src/app/app.module.ts
+++ b/src-ui/src/app/app.module.ts
@@ -57,6 +57,7 @@ import { DocumentTitlePipe } from './pipes/document-title.pipe';
 import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component';
 import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component';
 import { NgSelectModule } from '@ng-select/ng-select';
+import { NumberComponent } from './components/common/input/number/number.component';
 
 @NgModule({
   declarations: [
@@ -104,7 +105,8 @@ import { NgSelectModule } from '@ng-select/ng-select';
     FilterPipe,
     DocumentTitlePipe,
     MetadataCollapseComponent,
-    SelectDialogComponent
+    SelectDialogComponent,
+    NumberComponent
   ],
   imports: [
     BrowserModule,
diff --git a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts
index 8f7af1418..ff4660ca3 100644
--- a/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts
+++ b/src-ui/src/app/components/common/edit-dialog/edit-dialog.component.ts
@@ -2,6 +2,7 @@ import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core';
 import { FormGroup } from '@angular/forms';
 import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
 import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model';
 import { ObjectWithId } from 'src/app/data/object-with-id';
 import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
@@ -24,6 +25,10 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
   @Output()
   success = new EventEmitter()
 
+  networkActive = false
+
+  error = null
+
   abstract getForm(): FormGroup
 
   objectForm: FormGroup = this.getForm()
@@ -77,11 +82,14 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
       default:
         break;
     }
+    this.networkActive = true
     serverResponse.subscribe(result => {
       this.activeModal.close()
       this.success.emit(result)
+      this.networkActive = false
     }, error => {
-      this.toastService.showError(this.getSaveErrorMessage(error.error.name))
+      this.error = error.error
+      this.networkActive = false
     })
   }
 
diff --git a/src-ui/src/app/components/common/input/abstract-input.ts b/src-ui/src/app/components/common/input/abstract-input.ts
index 78a4a1b69..f6039d2ec 100644
--- a/src-ui/src/app/components/common/input/abstract-input.ts
+++ b/src-ui/src/app/components/common/input/abstract-input.ts
@@ -30,6 +30,9 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
   @Input()
   disabled = false;
 
+  @Input()
+  error: string
+
   value: T
 
   ngOnInit(): void {
diff --git a/src-ui/src/app/components/common/input/number/number.component.html b/src-ui/src/app/components/common/input/number/number.component.html
new file mode 100644
index 000000000..aa3a893d3
--- /dev/null
+++ b/src-ui/src/app/components/common/input/number/number.component.html
@@ -0,0 +1,8 @@
+<div class="form-group">
+  <label [for]="inputId">{{title}}</label>
+  <input type="number" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
+  <small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
+  <div class="invalid-feedback">
+    {{error}}
+  </div>
+</div>
\ No newline at end of file
diff --git a/src-ui/src/app/components/common/input/number/number.component.scss b/src-ui/src/app/components/common/input/number/number.component.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/src-ui/src/app/components/common/input/number/number.component.spec.ts b/src-ui/src/app/components/common/input/number/number.component.spec.ts
new file mode 100644
index 000000000..3476cbc22
--- /dev/null
+++ b/src-ui/src/app/components/common/input/number/number.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NumberComponent } from './number.component';
+
+describe('NumberComponent', () => {
+  let component: NumberComponent;
+  let fixture: ComponentFixture<NumberComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ NumberComponent ]
+    })
+    .compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(NumberComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src-ui/src/app/components/common/input/number/number.component.ts b/src-ui/src/app/components/common/input/number/number.component.ts
new file mode 100644
index 000000000..987a4090b
--- /dev/null
+++ b/src-ui/src/app/components/common/input/number/number.component.ts
@@ -0,0 +1,21 @@
+import { Component, forwardRef } from '@angular/core';
+import { NG_VALUE_ACCESSOR } from '@angular/forms';
+import { AbstractInputComponent } from '../abstract-input';
+
+@Component({
+  providers: [{
+    provide: NG_VALUE_ACCESSOR,
+    useExisting: forwardRef(() => NumberComponent),
+    multi: true
+  }],
+  selector: 'app-input-number',
+  templateUrl: './number.component.html',
+  styleUrls: ['./number.component.scss']
+})
+export class NumberComponent extends AbstractInputComponent<number> {
+
+  constructor() {
+    super()
+  }
+
+}
diff --git a/src-ui/src/app/components/common/input/text/text.component.html b/src-ui/src/app/components/common/input/text/text.component.html
index 3a43b052f..78aa76577 100644
--- a/src-ui/src/app/components/common/input/text/text.component.html
+++ b/src-ui/src/app/components/common/input/text/text.component.html
@@ -1,5 +1,8 @@
 <div class="form-group">
   <label [for]="inputId">{{title}}</label>
-  <input type="text" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
+  <input type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
   <small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
+  <div class="invalid-feedback">
+    {{error}}
+  </div>
 </div>
\ No newline at end of file
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html
index eae3367c1..4092b5a60 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.html
+++ b/src-ui/src/app/components/document-detail/document-detail.component.html
@@ -56,12 +56,8 @@
                     <a ngbNavLink i18n>Details</a>
                     <ng-template ngbNavContent>
 
-                        <app-input-text i18n-title title="Title" formControlName="title"></app-input-text>
-                        <div class="form-group">
-                            <label for="archive_serial_number" i18n>Archive serial number</label>
-                            <input type="number" class="form-control" id="archive_serial_number"
-                                formControlName='archive_serial_number'>
-                        </div>
+                        <app-input-text i18n-title title="Title" formControlName="title" [error]="error?.title"></app-input-text>
+                        <app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number>
                         <app-input-date-time i18n-titleDate titleDate="Date created" formControlName="created"></app-input-date-time>
                         <app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true"
                             (createNew)="createCorrespondent()"></app-input-select>
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts
index 053258f34..84a03446a 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.ts
+++ b/src-ui/src/app/components/document-detail/document-detail.component.ts
@@ -24,8 +24,12 @@ import { PDFDocumentProxy } from 'ng2-pdf-viewer';
 })
 export class DocumentDetailComponent implements OnInit {
 
-  public expandOriginalMetadata = false;
-  public expandArchivedMetadata = false;
+  expandOriginalMetadata = false
+  expandArchivedMetadata = false
+
+  error: any
+
+  networkActive = false
 
   documentId: number
   document: PaperlessDocument
@@ -131,19 +135,33 @@ export class DocumentDetailComponent implements OnInit {
   }
 
   save() {
+    this.networkActive = true
     this.documentsService.update(this.document).subscribe(result => {
       this.close()
+      this.networkActive = false
+      this.error = null
+    }, error => {
+      this.networkActive = false
+      this.error = error.error
     })
   }
 
   saveEditNext() {
+    this.networkActive = true
     this.documentsService.update(this.document).subscribe(result => {
+      this.error = null
       this.documentListViewService.getNext(this.document.id).subscribe(nextDocId => {
+        this.networkActive = false
         if (nextDocId) {
           this.openDocumentService.closeDocument(this.document)
           this.router.navigate(['documents', nextDocId])
         }
+      }, error => {
+        this.networkActive = false
       })
+    }, error => {
+      this.networkActive = false
+      this.error = error.error
     })
   }
 
diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html b/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html
index 48477b229..e2b7f13e0 100644
--- a/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html
+++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html
@@ -1,4 +1,4 @@
-<form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()">
+<form [formGroup]="objectForm" (ngSubmit)="save()">
   <div class="modal-header">
     <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
     <button type="button" class="close" aria-label="Close" (click)="cancel()">
@@ -7,10 +7,10 @@
   </div>
   <div class="modal-body">
 
-    <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
+    <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
     <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
     <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match"></app-input-text>
-    <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
+    <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></app-input-check>
   </div>
   <div class="modal-footer">
     <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button>
diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.ts b/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.ts
index b6c3e08d4..a94cbf909 100644
--- a/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.ts
+++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.ts
@@ -25,10 +25,6 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl
     return $localize`Edit correspondent`
   }
 
-  getSaveErrorMessage(error: string) {
-    return $localize`Could not save correspondent: ${error}`
-  }
-
   getForm(): FormGroup {
     return new FormGroup({
       name: new FormControl(''),
diff --git a/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.html b/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.html
index ba017faf7..d672c5bd8 100644
--- a/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.html
+++ b/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.html
@@ -1,4 +1,4 @@
-<form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()">
+<form [formGroup]="objectForm" (ngSubmit)="save()">
     <div class="modal-header">
       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
       <button type="button" class="close" aria-label="Close" (click)="cancel()">
@@ -7,7 +7,7 @@
     </div>
     <div class="modal-body">
 
-      <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
+      <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
       <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
       <app-input-text *ngIf="patternRequired" i18n-title title="Matching pattern" formControlName="match"></app-input-text>
       <app-input-check *ngIf="patternRequired" i18n-title title="Case insensitive" formControlName="is_insensitive"></app-input-check>
diff --git a/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.ts b/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.ts
index df81f88c9..a0cbd25f1 100644
--- a/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.ts
+++ b/src-ui/src/app/components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component.ts
@@ -25,10 +25,6 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle
     return $localize`Edit document type`
   }
 
-  getSaveErrorMessage(error: string) {
-    return $localize`Could not save document type: ${error}`
-  }
-
   getForm(): FormGroup {
     return new FormGroup({
       name: new FormControl(''),
diff --git a/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html b/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html
index 9929b54d5..502502c5c 100644
--- a/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html
+++ b/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.html
@@ -1,4 +1,4 @@
-  <form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()">
+  <form [formGroup]="objectForm" (ngSubmit)="save()">
     <div class="modal-header">
       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
       <button type="button" class="close" aria-label="Close" (click)="cancel()">
@@ -6,7 +6,7 @@
       </button>
     </div>
     <div class="modal-body">
-      <app-input-text title="Name" formControlName="name"></app-input-text>
+      <app-input-text title="Name" formControlName="name" [error]="error?.name"></app-input-text>
 
 
       <div class="form-group paperless-input-select">
diff --git a/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.ts b/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.ts
index ceca19142..d67369a8b 100644
--- a/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.ts
+++ b/src-ui/src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component.ts
@@ -25,10 +25,6 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
     return $localize`Edit tag`
   }
 
-  getSaveErrorMessage(error: string) {
-    return $localize`Could not save tag: ${error}`
-  }
-
   getForm(): FormGroup {
     return new FormGroup({
       name: new FormControl(''),