mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	use ng-bootstrap date selector, with proper formatting/parsing according to the current locale #177
This commit is contained in:
		| @@ -3,7 +3,7 @@ import { NgModule } from '@angular/core'; | ||||
|  | ||||
| import { AppRoutingModule } from './app-routing.module'; | ||||
| import { AppComponent } from './app.component'; | ||||
| import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { NgbDateAdapter, NgbDateParserFormatter, NgbModule } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; | ||||
| import { DocumentListComponent } from './components/document-list/document-list.component'; | ||||
| import { DocumentDetailComponent } from './components/document-detail/document-detail.component'; | ||||
| @@ -39,7 +39,6 @@ import { SelectComponent } from './components/common/input/select/select.compone | ||||
| import { CheckComponent } from './components/common/input/check/check.component'; | ||||
| import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component'; | ||||
| import { InfiniteScrollModule } from 'ngx-infinite-scroll'; | ||||
| import { DateTimeComponent } from './components/common/input/date-time/date-time.component'; | ||||
| import { TagsComponent } from './components/common/input/tags/tags.component'; | ||||
| import { SortableDirective } from './directives/sortable.directive'; | ||||
| import { CookieService } from 'ngx-cookie-service'; | ||||
| @@ -60,6 +59,9 @@ import { NgSelectModule } from '@ng-select/ng-select'; | ||||
| import { NumberComponent } from './components/common/input/number/number.component'; | ||||
| import { SafePipe } from './pipes/safe.pipe'; | ||||
| import { CustomDatePipe } from './pipes/custom-date.pipe'; | ||||
| import { DateComponent } from './components/common/input/date/date.component'; | ||||
| import { ISODateAdapter } from './utils/ngb-date-adapter'; | ||||
| import { LocalizedDateParserFormatter } from './utils/ngb-date-parser-formatter'; | ||||
|  | ||||
| import localeFr from '@angular/common/locales/fr'; | ||||
| import localeNl from '@angular/common/locales/nl'; | ||||
| @@ -106,7 +108,6 @@ registerLocaleData(localeEnGb) | ||||
|     SelectComponent, | ||||
|     CheckComponent, | ||||
|     SaveViewConfigDialogComponent, | ||||
|     DateTimeComponent, | ||||
|     TagsComponent, | ||||
|     SortableDirective, | ||||
|     SavedViewWidgetComponent, | ||||
| @@ -122,7 +123,8 @@ registerLocaleData(localeEnGb) | ||||
|     SelectDialogComponent, | ||||
|     NumberComponent, | ||||
|     SafePipe, | ||||
|     CustomDatePipe | ||||
|     CustomDatePipe, | ||||
|     DateComponent | ||||
|   ], | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
| @@ -144,7 +146,9 @@ registerLocaleData(localeEnGb) | ||||
|       multi: true | ||||
|     }, | ||||
|     FilterPipe, | ||||
|     DocumentTitlePipe | ||||
|     DocumentTitlePipe, | ||||
|     {provide: NgbDateAdapter, useClass: ISODateAdapter}, | ||||
|     {provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter} | ||||
|   ], | ||||
|   bootstrap: [AppComponent] | ||||
| }) | ||||
|   | ||||
| @@ -1,13 +0,0 @@ | ||||
| <div class="form-row"> | ||||
|   <div class="form-group col"> | ||||
|       <label for="created_date">{{titleDate}}</label> | ||||
|       <input type="date" class="form-control" id="created_date" [(ngModel)]="dateValue" (change)="dateOrTimeChanged()"> | ||||
|   </div> | ||||
|   <div class="form-group col" *ngIf="titleTime"> | ||||
|       <label for="created_time">{{titleTime}}</label> | ||||
|       <input type="time" class="form-control" id="created_time" [(ngModel)]="timeValue" (change)="dateOrTimeChanged()"> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
|  | ||||
| <!-- <small *ngIf="hint" class="form-text text-muted">{{hint}}</small> --> | ||||
| @@ -1,61 +0,0 @@ | ||||
| import { formatDate } from '@angular/common'; | ||||
| import { Component, forwardRef, Input, OnInit } from '@angular/core'; | ||||
| import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||||
|  | ||||
| @Component({ | ||||
|   providers: [{ | ||||
|     provide: NG_VALUE_ACCESSOR, | ||||
|     useExisting: forwardRef(() => DateTimeComponent), | ||||
|     multi: true | ||||
|   }], | ||||
|   selector: 'app-input-date-time', | ||||
|   templateUrl: './date-time.component.html', | ||||
|   styleUrls: ['./date-time.component.scss'] | ||||
| }) | ||||
| export class DateTimeComponent implements OnInit,ControlValueAccessor  { | ||||
|  | ||||
|   constructor() { | ||||
|   } | ||||
|  | ||||
|   onChange = (newValue: any) => {}; | ||||
|    | ||||
|   onTouched = () => {}; | ||||
|  | ||||
|   writeValue(newValue: any): void { | ||||
|     this.dateValue = formatDate(newValue, 'yyyy-MM-dd', "en-US") | ||||
|     this.timeValue = formatDate(newValue, 'HH:mm:ss', 'en-US') | ||||
|   } | ||||
|   registerOnChange(fn: any): void { | ||||
|     this.onChange = fn; | ||||
|   } | ||||
|   registerOnTouched(fn: any): void { | ||||
|     this.onTouched = fn; | ||||
|   } | ||||
|   setDisabledState?(isDisabled: boolean): void { | ||||
|     this.disabled = isDisabled; | ||||
|   } | ||||
|  | ||||
|   @Input() | ||||
|   titleDate: string = "Date" | ||||
|  | ||||
|   @Input() | ||||
|   titleTime: string | ||||
|  | ||||
|   @Input() | ||||
|   disabled: boolean = false | ||||
|  | ||||
|   @Input() | ||||
|   hint: string | ||||
|  | ||||
|   timeValue | ||||
|  | ||||
|   dateValue | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|   } | ||||
|  | ||||
|   dateOrTimeChanged() { | ||||
|     this.onChange(formatDate(this.dateValue + "T" + this.timeValue,"yyyy-MM-ddTHH:mm:ssZZZZZ", "en-us", "UTC")) | ||||
|   } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,15 @@ | ||||
| <div class="form-group"> | ||||
|   <label [for]="inputId">{{title}}</label> | ||||
|   <div class="input-group"> | ||||
|     <input [class.is-invalid]="error" class="form-control" [placeholder]="placeholder" [id]="inputId" (dateSelect)="onChange(value)" (change)="onChange(value)" | ||||
|            name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel"> | ||||
|     <div class="input-group-append"> | ||||
|       <button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button"> | ||||
|         <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar" viewBox="0 0 16 16"> | ||||
|           <path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/> | ||||
|         </svg> | ||||
|       </button> | ||||
|     </div> | ||||
|     <div class="invalid-feedback" *ngIf="error" i18n>Invalid date.</div> | ||||
|   </div> | ||||
| </div> | ||||
| @@ -1,20 +1,20 @@ | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing'; | ||||
| 
 | ||||
| import { DateTimeComponent } from './date-time.component'; | ||||
| import { DateComponent } from './date.component'; | ||||
| 
 | ||||
| describe('DateTimeComponent', () => { | ||||
|   let component: DateTimeComponent; | ||||
|   let fixture: ComponentFixture<DateTimeComponent>; | ||||
| describe('DateComponent', () => { | ||||
|   let component: DateComponent; | ||||
|   let fixture: ComponentFixture<DateComponent>; | ||||
| 
 | ||||
|   beforeEach(async () => { | ||||
|     await TestBed.configureTestingModule({ | ||||
|       declarations: [ DateTimeComponent ] | ||||
|       declarations: [ DateComponent ] | ||||
|     }) | ||||
|     .compileComponents(); | ||||
|   }); | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     fixture = TestBed.createComponent(DateTimeComponent); | ||||
|     fixture = TestBed.createComponent(DateComponent); | ||||
|     component = fixture.componentInstance; | ||||
|     fixture.detectChanges(); | ||||
|   }); | ||||
| @@ -0,0 +1,32 @@ | ||||
| import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; | ||||
| import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; | ||||
| import { NgbDateAdapter, NgbDateParserFormatter, NgbDatepickerContent } from '@ng-bootstrap/ng-bootstrap'; | ||||
| import { SettingsService } from 'src/app/services/settings.service'; | ||||
| import { v4 as uuidv4 } from 'uuid'; | ||||
| import { AbstractInputComponent } from '../abstract-input'; | ||||
|  | ||||
|  | ||||
| @Component({ | ||||
|   providers: [{ | ||||
|     provide: NG_VALUE_ACCESSOR, | ||||
|     useExisting: forwardRef(() => DateComponent), | ||||
|     multi: true | ||||
|   }], | ||||
|   selector: 'app-input-date', | ||||
|   templateUrl: './date.component.html', | ||||
|   styleUrls: ['./date.component.scss'] | ||||
| }) | ||||
| export class DateComponent extends AbstractInputComponent<string> implements OnInit { | ||||
|  | ||||
|   constructor(private settings: SettingsService) { | ||||
|     super() | ||||
|   } | ||||
|  | ||||
|   ngOnInit(): void { | ||||
|     super.ngOnInit() | ||||
|     this.placeholder = this.settings.getLocalizedDateInputFormat() | ||||
|   } | ||||
|  | ||||
|   placeholder: string | ||||
|  | ||||
| } | ||||
| @@ -58,7 +58,7 @@ | ||||
|  | ||||
|                         <app-input-text #inputTitle 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-date i18n-title title="Date created" formControlName="created" [error]="error?.created"></app-input-date> | ||||
|                         <app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" | ||||
|                             (createNew)="createCorrespondent()" [suggestions]="suggestions?.correspondents"></app-input-select> | ||||
|                         <app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" | ||||
|   | ||||
| @@ -88,14 +88,15 @@ export class SettingsComponent implements OnInit { | ||||
|   } | ||||
|  | ||||
|   get displayLanguageOptions(): LanguageOption[] { | ||||
|     return [{code: "", name: $localize`Use system language`}].concat(this.settings.getLanguageOptions()) | ||||
|     return [ | ||||
|       {code: "", name: $localize`Use system language`} | ||||
|     ].concat(this.settings.getLanguageOptions()) | ||||
|   } | ||||
|  | ||||
|   get dateLocaleOptions(): LanguageOption[] { | ||||
|     return [ | ||||
|       {code: "", name: $localize`Use date format of display language`}, | ||||
|       {code: "iso-8601", name: $localize`ISO 8601`} | ||||
|     ].concat(this.settings.getLanguageOptions()) | ||||
|       {code: "", name: $localize`Use date format of display language`} | ||||
|     ].concat(this.settings.getDateLocaleOptions()) | ||||
|   } | ||||
|  | ||||
|   get today() { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { DOCUMENT } from '@angular/common'; | ||||
| import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; | ||||
| import { Inject, Injectable, LOCALE_ID, Renderer2, RendererFactory2 } from '@angular/core'; | ||||
| import { Meta } from '@angular/platform-browser'; | ||||
| import { CookieService } from 'ngx-cookie-service'; | ||||
|  | ||||
| @@ -10,9 +10,14 @@ export interface PaperlessSettings { | ||||
| } | ||||
|  | ||||
| export interface LanguageOption { | ||||
|   code: string, | ||||
|   name: string, | ||||
|   code: string | ||||
|   name: string | ||||
|   englishName?: string | ||||
|  | ||||
|   /** | ||||
|    * A date format string for use by the date selectors. MUST contain 'yyyy', 'mm' and 'dd'. | ||||
|    */ | ||||
|   dateInputFormat?: string | ||||
| } | ||||
|  | ||||
| export const SETTINGS_KEYS = { | ||||
| @@ -56,7 +61,8 @@ export class SettingsService { | ||||
|     private rendererFactory: RendererFactory2, | ||||
|     @Inject(DOCUMENT) private document, | ||||
|     private cookieService: CookieService, | ||||
|     private meta: Meta | ||||
|     private meta: Meta, | ||||
|     @Inject(LOCALE_ID) private localeId: string | ||||
|   ) { | ||||
|     this.renderer = rendererFactory.createRenderer(null, null); | ||||
|  | ||||
| @@ -79,15 +85,20 @@ export class SettingsService { | ||||
|  | ||||
|   getLanguageOptions(): LanguageOption[] { | ||||
|     return [ | ||||
|       {code: "en-us", name: $localize`English (US)`, englishName: "English (US)"}, | ||||
|       {code: "en-gb", name: $localize`English (GB)`, englishName: "English (GB)"}, | ||||
|       {code: "de", name: $localize`German`, englishName: "German"}, | ||||
|       {code: "nl", name: $localize`Dutch`, englishName: "Dutch"}, | ||||
|       {code: "fr", name: $localize`French`, englishName: "French"}, | ||||
|       {code: "pt-br", name: $localize`Portuguese (Brazil)`, englishName: "Portuguese (Brazil)"} | ||||
|       {code: "en-us", name: $localize`English (US)`, englishName: "English (US)", dateInputFormat: "mm/dd/yyyy"}, | ||||
|       {code: "en-gb", name: $localize`English (GB)`, englishName: "English (GB)", dateInputFormat: "dd/mm/yyyy"}, | ||||
|       {code: "de", name: $localize`German`, englishName: "German", dateInputFormat: "dd.mm.yyyy"}, | ||||
|       {code: "nl", name: $localize`Dutch`, englishName: "Dutch", dateInputFormat: "dd-mm-yyyy"}, | ||||
|       {code: "fr", name: $localize`French`, englishName: "French", dateInputFormat: "dd/mm/yyyy"}, | ||||
|       {code: "pt-br", name: $localize`Portuguese (Brazil)`, englishName: "Portuguese (Brazil)", dateInputFormat: "dd/mm/yyyy"} | ||||
|     ] | ||||
|   } | ||||
|  | ||||
|   getDateLocaleOptions(): LanguageOption[] { | ||||
|     let isoOption: LanguageOption = {code: "iso-8601", name: $localize`ISO 8601`, dateInputFormat: "yyyy-mm-dd"} | ||||
|     return [isoOption].concat(this.getLanguageOptions()) | ||||
|   } | ||||
|  | ||||
|   private getLanguageCookieName() { | ||||
|     let prefix = "" | ||||
|     if (this.meta.getTag('name=cookie_prefix')) { | ||||
| @@ -108,6 +119,11 @@ export class SettingsService { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   getLocalizedDateInputFormat(): string { | ||||
|     let dateLocale = this.get(SETTINGS_KEYS.DATE_LOCALE) || this.getLanguage() || this.localeId.toLowerCase() | ||||
|     return this.getDateLocaleOptions().find(o => o.code == dateLocale)?.dateInputFormat || "yyyy-mm-dd" | ||||
|   } | ||||
|  | ||||
|   get(key: string): any { | ||||
|     let setting = SETTINGS.find(s => s.key == key) | ||||
|  | ||||
|   | ||||
							
								
								
									
										23
									
								
								src-ui/src/app/utils/ngb-date-adapter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src-ui/src/app/utils/ngb-date-adapter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { Injectable } from "@angular/core"; | ||||
| import { NgbDateAdapter, NgbDateStruct } from "@ng-bootstrap/ng-bootstrap"; | ||||
|  | ||||
| @Injectable() | ||||
| export class ISODateAdapter extends NgbDateAdapter<string> { | ||||
|  | ||||
|   fromModel(value: string | null): NgbDateStruct | null { | ||||
|     if (value) { | ||||
|       let date = new Date(value) | ||||
|       return { | ||||
|         day : date.getDate(), | ||||
|         month : date.getMonth() + 1, | ||||
|         year : date.getFullYear() | ||||
|       } | ||||
|     } else { | ||||
|       return null | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   toModel(date: NgbDateStruct | null): string | null { | ||||
|     return date ? new Date(date.year, date.month - 1, date.day).toISOString() : null | ||||
|   } | ||||
| } | ||||
							
								
								
									
										59
									
								
								src-ui/src/app/utils/ngb-date-parser-formatter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src-ui/src/app/utils/ngb-date-parser-formatter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| import { Injectable } from "@angular/core" | ||||
| import { NgbDateParserFormatter, NgbDateStruct } from "@ng-bootstrap/ng-bootstrap" | ||||
| import { SettingsService } from "../services/settings.service" | ||||
|  | ||||
| @Injectable() | ||||
| export class LocalizedDateParserFormatter extends NgbDateParserFormatter { | ||||
|  | ||||
|   constructor(private settings: SettingsService) { | ||||
|     super() | ||||
|   } | ||||
|  | ||||
|   private getDateInputFormat() { | ||||
|     return this.settings.getLocalizedDateInputFormat() | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * This constructs a regular expression from a date input format which is then | ||||
|    * used to parse dates. | ||||
|    */ | ||||
|   private getDateParseRegex() { | ||||
|     return new RegExp( | ||||
|       "^" + this.getDateInputFormat() | ||||
|       .replace('dd', '(?<day>[0-9]+)') | ||||
|       .replace('mm', '(?<month>[0-9]+)') | ||||
|       .replace('yyyy', '(?<year>[0-9]+)') | ||||
|       .split('.').join('\\.\\s*') + "$" // allow whitespace(s) after dot (specific for German) | ||||
|       ) | ||||
|   } | ||||
|  | ||||
|   parse(value: string): NgbDateStruct | null { | ||||
|     let match = this.getDateParseRegex().exec(value) | ||||
|     if (match) { | ||||
|       let dateStruct = { | ||||
|         day: +match.groups.day, | ||||
|         month: +match.groups.month, | ||||
|         year: +match.groups.year | ||||
|       } | ||||
|       if (dateStruct.year <= (new Date().getFullYear() - 2000)) { | ||||
|         dateStruct.year += 2000 | ||||
|       } else if (dateStruct.year < 100) { | ||||
|         dateStruct.year += 1900 | ||||
|       } | ||||
|       return dateStruct | ||||
|     } else { | ||||
|       return null | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   format(date: NgbDateStruct | null): string { | ||||
|     if (date) { | ||||
|       return this.getDateInputFormat() | ||||
|       .replace('dd', date.day.toString().padStart(2, '0')) | ||||
|       .replace('mm', date.month.toString().padStart(2, '0')) | ||||
|       .replace('yyyy', date.year.toString().padStart(4, '0')) | ||||
|     } else { | ||||
|       return null | ||||
|     } | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 jonaswinkler
					jonaswinkler