mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	form field validation (much better error messages)
This commit is contained in:
		| @@ -57,6 +57,7 @@ import { DocumentTitlePipe } from './pipes/document-title.pipe'; | |||||||
| import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'; | import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component'; | ||||||
| import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'; | import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component'; | ||||||
| import { NgSelectModule } from '@ng-select/ng-select'; | import { NgSelectModule } from '@ng-select/ng-select'; | ||||||
|  | import { NumberComponent } from './components/common/input/number/number.component'; | ||||||
|  |  | ||||||
| @NgModule({ | @NgModule({ | ||||||
|   declarations: [ |   declarations: [ | ||||||
| @@ -104,7 +105,8 @@ import { NgSelectModule } from '@ng-select/ng-select'; | |||||||
|     FilterPipe, |     FilterPipe, | ||||||
|     DocumentTitlePipe, |     DocumentTitlePipe, | ||||||
|     MetadataCollapseComponent, |     MetadataCollapseComponent, | ||||||
|     SelectDialogComponent |     SelectDialogComponent, | ||||||
|  |     NumberComponent | ||||||
|   ], |   ], | ||||||
|   imports: [ |   imports: [ | ||||||
|     BrowserModule, |     BrowserModule, | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core'; | |||||||
| import { FormGroup } from '@angular/forms'; | import { FormGroup } from '@angular/forms'; | ||||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; | import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; | ||||||
| import { Observable } from 'rxjs'; | import { Observable } from 'rxjs'; | ||||||
|  | import { map } from 'rxjs/operators'; | ||||||
| import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'; | import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model'; | ||||||
| import { ObjectWithId } from 'src/app/data/object-with-id'; | import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||||
| import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; | import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; | ||||||
| @@ -24,6 +25,10 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI | |||||||
|   @Output() |   @Output() | ||||||
|   success = new EventEmitter() |   success = new EventEmitter() | ||||||
|  |  | ||||||
|  |   networkActive = false | ||||||
|  |  | ||||||
|  |   error = null | ||||||
|  |  | ||||||
|   abstract getForm(): FormGroup |   abstract getForm(): FormGroup | ||||||
|  |  | ||||||
|   objectForm: FormGroup = this.getForm() |   objectForm: FormGroup = this.getForm() | ||||||
| @@ -77,11 +82,14 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI | |||||||
|       default: |       default: | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
|  |     this.networkActive = true | ||||||
|     serverResponse.subscribe(result => { |     serverResponse.subscribe(result => { | ||||||
|       this.activeModal.close() |       this.activeModal.close() | ||||||
|       this.success.emit(result) |       this.success.emit(result) | ||||||
|  |       this.networkActive = false | ||||||
|     }, error => { |     }, error => { | ||||||
|       this.toastService.showError(this.getSaveErrorMessage(error.error.name)) |       this.error = error.error | ||||||
|  |       this.networkActive = false | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,6 +30,9 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor { | |||||||
|   @Input() |   @Input() | ||||||
|   disabled = false; |   disabled = false; | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   error: string | ||||||
|  |  | ||||||
|   value: T |   value: T | ||||||
|  |  | ||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|   | |||||||
| @@ -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> | ||||||
| @@ -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(); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -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() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -1,5 +1,8 @@ | |||||||
| <div class="form-group"> | <div class="form-group"> | ||||||
|   <label [for]="inputId">{{title}}</label> |   <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> |   <small *ngIf="hint" class="form-text text-muted">{{hint}}</small> | ||||||
|  |   <div class="invalid-feedback"> | ||||||
|  |     {{error}} | ||||||
|  |   </div> | ||||||
| </div> | </div> | ||||||
| @@ -56,12 +56,8 @@ | |||||||
|                     <a ngbNavLink i18n>Details</a> |                     <a ngbNavLink i18n>Details</a> | ||||||
|                     <ng-template ngbNavContent> |                     <ng-template ngbNavContent> | ||||||
|  |  | ||||||
|                         <app-input-text i18n-title title="Title" formControlName="title"></app-input-text> |                         <app-input-text i18n-title title="Title" formControlName="title" [error]="error?.title"></app-input-text> | ||||||
|                         <div class="form-group"> |                         <app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number> | ||||||
|                             <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-date-time i18n-titleDate titleDate="Date created" formControlName="created"></app-input-date-time> |                         <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" |                         <app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" | ||||||
|                             (createNew)="createCorrespondent()"></app-input-select> |                             (createNew)="createCorrespondent()"></app-input-select> | ||||||
|   | |||||||
| @@ -24,8 +24,12 @@ import { PDFDocumentProxy } from 'ng2-pdf-viewer'; | |||||||
| }) | }) | ||||||
| export class DocumentDetailComponent implements OnInit { | export class DocumentDetailComponent implements OnInit { | ||||||
|  |  | ||||||
|   public expandOriginalMetadata = false; |   expandOriginalMetadata = false | ||||||
|   public expandArchivedMetadata = false; |   expandArchivedMetadata = false | ||||||
|  |  | ||||||
|  |   error: any | ||||||
|  |  | ||||||
|  |   networkActive = false | ||||||
|  |  | ||||||
|   documentId: number |   documentId: number | ||||||
|   document: PaperlessDocument |   document: PaperlessDocument | ||||||
| @@ -131,19 +135,33 @@ export class DocumentDetailComponent implements OnInit { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   save() { |   save() { | ||||||
|  |     this.networkActive = true | ||||||
|     this.documentsService.update(this.document).subscribe(result => { |     this.documentsService.update(this.document).subscribe(result => { | ||||||
|       this.close() |       this.close() | ||||||
|  |       this.networkActive = false | ||||||
|  |       this.error = null | ||||||
|  |     }, error => { | ||||||
|  |       this.networkActive = false | ||||||
|  |       this.error = error.error | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   saveEditNext() { |   saveEditNext() { | ||||||
|  |     this.networkActive = true | ||||||
|     this.documentsService.update(this.document).subscribe(result => { |     this.documentsService.update(this.document).subscribe(result => { | ||||||
|  |       this.error = null | ||||||
|       this.documentListViewService.getNext(this.document.id).subscribe(nextDocId => { |       this.documentListViewService.getNext(this.document.id).subscribe(nextDocId => { | ||||||
|  |         this.networkActive = false | ||||||
|         if (nextDocId) { |         if (nextDocId) { | ||||||
|           this.openDocumentService.closeDocument(this.document) |           this.openDocumentService.closeDocument(this.document) | ||||||
|           this.router.navigate(['documents', nextDocId]) |           this.router.navigate(['documents', nextDocId]) | ||||||
|         } |         } | ||||||
|  |       }, error => { | ||||||
|  |         this.networkActive = false | ||||||
|       }) |       }) | ||||||
|  |     }, error => { | ||||||
|  |       this.networkActive = false | ||||||
|  |       this.error = error.error | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| <form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()"> | <form [formGroup]="objectForm" (ngSubmit)="save()"> | ||||||
|   <div class="modal-header"> |   <div class="modal-header"> | ||||||
|     <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> |     <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||||
|     <button type="button" class="close" aria-label="Close" (click)="cancel()"> |     <button type="button" class="close" aria-label="Close" (click)="cancel()"> | ||||||
| @@ -7,10 +7,10 @@ | |||||||
|   </div> |   </div> | ||||||
|   <div class="modal-body"> |   <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-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-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> | ||||||
|   <div class="modal-footer"> |   <div class="modal-footer"> | ||||||
|     <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button> |     <button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n>Cancel</button> | ||||||
|   | |||||||
| @@ -25,10 +25,6 @@ export class CorrespondentEditDialogComponent extends EditDialogComponent<Paperl | |||||||
|     return $localize`Edit correspondent` |     return $localize`Edit correspondent` | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getSaveErrorMessage(error: string) { |  | ||||||
|     return $localize`Could not save correspondent: ${error}` |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   getForm(): FormGroup { |   getForm(): FormGroup { | ||||||
|     return new FormGroup({ |     return new FormGroup({ | ||||||
|       name: new FormControl(''), |       name: new FormControl(''), | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| <form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()"> | <form [formGroup]="objectForm" (ngSubmit)="save()"> | ||||||
|     <div class="modal-header"> |     <div class="modal-header"> | ||||||
|       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> |       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||||
|       <button type="button" class="close" aria-label="Close" (click)="cancel()"> |       <button type="button" class="close" aria-label="Close" (click)="cancel()"> | ||||||
| @@ -7,7 +7,7 @@ | |||||||
|     </div> |     </div> | ||||||
|     <div class="modal-body"> |     <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-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-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"></app-input-check> | ||||||
|   | |||||||
| @@ -25,10 +25,6 @@ export class DocumentTypeEditDialogComponent extends EditDialogComponent<Paperle | |||||||
|     return $localize`Edit document type` |     return $localize`Edit document type` | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getSaveErrorMessage(error: string) { |  | ||||||
|     return $localize`Could not save document type: ${error}` |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   getForm(): FormGroup { |   getForm(): FormGroup { | ||||||
|     return new FormGroup({ |     return new FormGroup({ | ||||||
|       name: new FormControl(''), |       name: new FormControl(''), | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
|   <form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()"> |   <form [formGroup]="objectForm" (ngSubmit)="save()"> | ||||||
|     <div class="modal-header"> |     <div class="modal-header"> | ||||||
|       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> |       <h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4> | ||||||
|       <button type="button" class="close" aria-label="Close" (click)="cancel()"> |       <button type="button" class="close" aria-label="Close" (click)="cancel()"> | ||||||
| @@ -6,7 +6,7 @@ | |||||||
|       </button> |       </button> | ||||||
|     </div> |     </div> | ||||||
|     <div class="modal-body"> |     <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"> |       <div class="form-group paperless-input-select"> | ||||||
|   | |||||||
| @@ -25,10 +25,6 @@ export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | |||||||
|     return $localize`Edit tag` |     return $localize`Edit tag` | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getSaveErrorMessage(error: string) { |  | ||||||
|     return $localize`Could not save tag: ${error}` |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   getForm(): FormGroup { |   getForm(): FormGroup { | ||||||
|     return new FormGroup({ |     return new FormGroup({ | ||||||
|       name: new FormControl(''), |       name: new FormControl(''), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 jonaswinkler
					jonaswinkler