diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-clone-dossier-template-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-clone-dossier-template-dialog.component.ts index 6a16fb99b..d7b154c2c 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-clone-dossier-template-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-dossier-template-dialog/add-edit-clone-dossier-template-dialog.component.ts @@ -25,15 +25,29 @@ const downloadTypes = ['ORIGINAL', 'PREVIEW', 'DELTA_PREVIEW', 'REDACTED'].map(t styleUrls: ['./add-edit-clone-dossier-template-dialog.component.scss'], }) export class AddEditCloneDossierTemplateDialogComponent extends BaseDialogComponent { - private _previousValidFrom: Dayjs; - private _previousValidTo: Dayjs; - private _lastValidFrom: Dayjs; - private _lastValidTo: Dayjs; readonly isDocumine = getConfig().IS_DOCUMINE; hasValidFrom: boolean; hasValidTo: boolean; readonly downloadTypes = downloadTypes; readonly dossierTemplate: DossierTemplate; + private _previousValidFrom: Dayjs; + private _previousValidTo: Dayjs; + private _lastValidFrom: Dayjs; + private _lastValidTo: Dayjs; + + get disabled(): boolean { + if (!this.data?.clone) { + return super.disabled; + } + return !this.valid; + } + + get translateParams() { + return { + type: this.dossierTemplate ? (this.data.clone ? 'clone' : 'edit') : 'create', + name: this.dossierTemplate?.name, + }; + } constructor( private readonly _dossierTemplatesService: DossierTemplatesService, @@ -51,20 +65,6 @@ export class AddEditCloneDossierTemplateDialogComponent extends BaseDialogCompon this._previousValidTo = this._lastValidTo = this.form.get('validTo').value; } - get disabled(): boolean { - if (!this.data?.clone) { - return super.disabled; - } - return !this.valid; - } - - get translateParams() { - return { - type: this.dossierTemplate ? (this.data.clone ? 'clone' : 'edit') : 'create', - name: this.dossierTemplate?.name, - }; - } - toggleHasValid(extremity: string) { if (extremity === 'from') { this.hasValidFrom = !this.hasValidFrom; @@ -117,11 +117,11 @@ export class AddEditCloneDossierTemplateDialogComponent extends BaseDialogCompon name: [this.#getCloneName(), Validators.required], description: [this.dossierTemplate?.description], validFrom: [ - this.dossierTemplate?.validFrom ? dayjs(this.dossierTemplate?.validFrom).toDate() : null, + this.dossierTemplate?.validFrom ? dayjs(this.dossierTemplate?.validFrom) : null, this.#requiredIfValidator(() => this.hasValidFrom), ], validTo: [ - this.dossierTemplate?.validTo ? dayjs(this.dossierTemplate?.validTo).toDate() : null, + this.dossierTemplate?.validTo ? dayjs(this.dossierTemplate?.validTo) : null, this.#requiredIfValidator(() => this.hasValidTo), ], applyDictionaryUpdatesToAllDossiersByDefault: [this.dossierTemplate?.applyDictionaryUpdatesToAllDossiersByDefault], diff --git a/apps/red-ui/src/app/modules/shared/CustomDateAdapter.ts b/apps/red-ui/src/app/modules/shared/CustomDateAdapter.ts index 962b8c7ac..44edb6b08 100644 --- a/apps/red-ui/src/app/modules/shared/CustomDateAdapter.ts +++ b/apps/red-ui/src/app/modules/shared/CustomDateAdapter.ts @@ -1,17 +1,238 @@ -import { NativeDateAdapter } from '@angular/material/core'; -import dayjs from 'dayjs'; +/** + * https://github.com/tabuckner/material-dayjs-adapter/blob/master/projects/material-dayjs-adapter/src/lib/adapter/dayjs-date-adapter.ts + * The above library is no longer maintained for our version of Angular. + */ + +import { Inject, Injectable, InjectionToken, Optional } from '@angular/core'; +import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core'; +import dayjs, { Dayjs } from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import localeData from 'dayjs/plugin/localeData'; +import LocalizedFormat from 'dayjs/plugin/localizedFormat'; import customParseFormat from 'dayjs/plugin/customParseFormat'; -import { Injectable } from '@angular/core'; -dayjs.extend(customParseFormat); +export interface DayJsDateAdapterOptions { + /** + * Turns the use of utc dates on or off. + * Changing this will change how Angular Material components like DatePicker output dates. + * {@default false} + */ + useUtc?: boolean; +} +/** InjectionToken for Dayjs date adapter to configure options. */ +export const MAT_DAYJS_DATE_ADAPTER_OPTIONS = new InjectionToken('MAT_DAYJS_DATE_ADAPTER_OPTIONS', { + providedIn: 'root', + factory: MAT_DAYJS_DATE_ADAPTER_OPTIONS_FACTORY, +}); + +export function MAT_DAYJS_DATE_ADAPTER_OPTIONS_FACTORY(): DayJsDateAdapterOptions { + return { + useUtc: false, + }; +} + +/** Creates an array and fills it with values. */ +function range(length: number, valueFunction: (index: number) => T): T[] { + const valuesArray = Array(length); + for (let i = 0; i < length; i++) { + valuesArray[i] = valueFunction(i); + } + return valuesArray; +} + +/** Adapts Dayjs Dates for use with Angular Material. */ @Injectable() -export class CustomDateAdapter extends NativeDateAdapter { - parse(value: any, parseFormat: string): Date | null { - return dayjs(value, parseFormat).toDate(); +export class CustomDateAdapter extends DateAdapter { + private _localeData: { + firstDayOfWeek: number; + longMonths: string[]; + shortMonths: string[]; + dates: string[]; + longDaysOfWeek: string[]; + shortDaysOfWeek: string[]; + narrowDaysOfWeek: string[]; + }; + + private get _shouldUseUtc(): boolean { + const { useUtc }: DayJsDateAdapterOptions = this._options || {}; + return !!useUtc; } - format(date: Date, displayFormat: string): string { - return dayjs(date).format(displayFormat); + constructor( + @Optional() @Inject(MAT_DATE_LOCALE) public dateLocale: string, + @Optional() @Inject(MAT_DAYJS_DATE_ADAPTER_OPTIONS) private _options?: DayJsDateAdapterOptions, + ) { + super(); + + this._initializeParser(dateLocale); + } + + // TODO: Implement + setLocale(locale: string) { + super.setLocale(locale); + + const dayJsLocaleData = this._dayJs().localeData(); + this._localeData = { + firstDayOfWeek: dayJsLocaleData.firstDayOfWeek(), + longMonths: dayJsLocaleData.months(), + shortMonths: dayJsLocaleData.monthsShort(), + dates: range(31, i => this.createDate(2017, 0, i + 1).format('D')), + longDaysOfWeek: range(7, i => this._dayJs().set('day', i).format('dddd')), + shortDaysOfWeek: dayJsLocaleData.weekdaysShort(), + narrowDaysOfWeek: dayJsLocaleData.weekdaysMin(), + }; + } + + getYear(date: Dayjs): number { + return this._dayJs(date).year(); + } + + getMonth(date: Dayjs): number { + return this._dayJs(date).month(); + } + + getDate(date: Dayjs): number { + return this._dayJs(date).date(); + } + + getDayOfWeek(date: Dayjs): number { + return this._dayJs(date).day(); + } + + getMonthNames(style: 'long' | 'short' | 'narrow'): string[] { + return style === 'long' ? this._localeData.longMonths : this._localeData.shortMonths; + } + + getDateNames(): string[] { + return this._localeData.dates; + } + + getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] { + if (style === 'long') { + return this._localeData.longDaysOfWeek; + } + if (style === 'short') { + return this._localeData.shortDaysOfWeek; + } + return this._localeData.narrowDaysOfWeek; + } + + getYearName(date: Dayjs): string { + return this._dayJs(date).format('YYYY'); + } + + getFirstDayOfWeek(): number { + return this._localeData.firstDayOfWeek; + } + + getNumDaysInMonth(date: Dayjs): number { + return this._dayJs(date).daysInMonth(); + } + + clone(date: Dayjs): Dayjs { + return date.clone(); + } + + createDate(year: number, month: number, date: number): Dayjs { + const returnDayjs = this._dayJs().set('year', year).set('month', month).set('date', date); + return returnDayjs; + } + + today(): Dayjs { + return this._dayJs(); + } + + parse(value: any, parseFormat: string): Dayjs | null { + if (value && typeof value === 'string') { + return this._dayJs(value, dayjs().localeData().longDateFormat(parseFormat), this.locale); + } + return value ? this._dayJs(value).locale(this.locale) : null; + } + + format(date: Dayjs, displayFormat: string): string { + if (!this.isValid(date)) { + throw Error('DayjsDateAdapter: Cannot format invalid date.'); + } + return date.locale(this.locale).format(displayFormat); + } + + addCalendarYears(date: Dayjs, years: number): Dayjs { + return date.add(years, 'year'); + } + + addCalendarMonths(date: Dayjs, months: number): Dayjs { + return date.add(months, 'month'); + } + + addCalendarDays(date: Dayjs, days: number): Dayjs { + return date.add(days, 'day'); + } + + toIso8601(date: Dayjs): string { + return date.toISOString(); + } + + /** + * Attempts to deserialize a value to a valid date object. This is different from parsing in that + * deserialize should only accept non-ambiguous, locale-independent formats (e.g. a ISO 8601 + * string). The default implementation does not allow any deserialization, it simply checks that + * the given value is already a valid date object or null. The `` will call this + * method on all of it's `@Input()` properties that accept dates. It is therefore possible to + * support passing values from your backend directly to these properties by overriding this method + * to also deserialize the format used by your backend. + * @param value The value to be deserialized into a date object. + * @returns The deserialized date object, either a valid date, null if the value can be + * deserialized into a null date (e.g. the empty string), or an invalid date. + */ + deserialize(value: any): Dayjs | null { + let date; + if (value instanceof Date) { + date = this._dayJs(value); + } else if (this.isDateInstance(value)) { + // NOTE: assumes that cloning also sets the correct locale. + return this.clone(value); + } + if (typeof value === 'string') { + if (!value) { + return null; + } + date = this._dayJs(value).toISOString(); + } + if (date && this.isValid(date)) { + return this._dayJs(date); // NOTE: Is this necessary since Dayjs is immutable and Moment was not? + } + return super.deserialize(value); + } + + isDateInstance(obj: any): boolean { + return dayjs.isDayjs(obj); + } + + isValid(date: Dayjs): boolean { + return this._dayJs(date).isValid(); + } + + invalid(): Dayjs { + return this._dayJs(null); + } + + private _dayJs(input?: any, format?: string, locale?: string): Dayjs { + if (!this._shouldUseUtc) { + return dayjs(input, { format, locale }, locale); + } + return dayjs(input, { format, locale, utc: this._shouldUseUtc }, locale).utc(); + } + + private _initializeParser(dateLocale: string) { + if (this._shouldUseUtc) { + dayjs.extend(utc); + } + + dayjs.extend(LocalizedFormat); + dayjs.extend(customParseFormat); + dayjs.extend(localeData); + + this.setLocale(dateLocale); } }