From f6c09109ba898529fd67f30717e48bba1a19e28b Mon Sep 17 00:00:00 2001 From: Alex Inkin Date: Sat, 6 May 2023 01:06:10 +0400 Subject: [PATCH] feat: add datetime spec support (#2264) --- frontend/package-lock.json | 70 +++++++++---------- frontend/package.json | 10 +-- frontend/projects/ui/src/app/app.providers.ts | 23 ++++++ .../form-control/form-control.component.html | 1 + .../form-datetime.component.html | 34 +++++++++ .../form-datetime/form-datetime.component.ts | 33 +++++++++ .../form/form-group/form-group.providers.ts | 12 +++- .../ui/src/app/components/form/form.module.ts | 13 +++- .../ui/src/app/services/api/api.fixures.ts | 44 ++++++++++++ .../app/services/date-transformer.service.ts | 19 +++++ .../services/datetime-transformer.service.ts | 35 ++++++++++ .../ui/src/app/services/form.service.ts | 52 ++++++++++++++ 12 files changed, 303 insertions(+), 43 deletions(-) create mode 100644 frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html create mode 100644 frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts create mode 100644 frontend/projects/ui/src/app/services/date-transformer.service.ts create mode 100644 frontend/projects/ui/src/app/services/datetime-transformer.service.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e55fdb63d..4817c8c88 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,11 +27,11 @@ "@ng-web-apis/resize-observer": "^2.0.0", "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", - "@taiga-ui/addon-charts": "3.25.0", - "@taiga-ui/cdk": "3.25.0", - "@taiga-ui/core": "3.25.0", - "@taiga-ui/icons": "3.25.0", - "@taiga-ui/kit": "3.25.0", + "@taiga-ui/addon-charts": "3.26.0", + "@taiga-ui/cdk": "3.26.0", + "@taiga-ui/core": "3.26.0", + "@taiga-ui/icons": "3.26.0", + "@taiga-ui/kit": "3.26.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", @@ -3840,9 +3840,9 @@ } }, "node_modules/@taiga-ui/addon-charts": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.25.0.tgz", - "integrity": "sha512-XE5s6XYjZYgxjO8sygVRVE5Cy2cAGMsRkX+nbpVCFaf0fMLhgpaWYzT7VnuddgVqaYtpLHb+mUMYkV8H83wX5w==", + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.26.0.tgz", + "integrity": "sha512-nkAzI+B4CcPogUrpEwANu3D8n3cJzuIakF//8MyOzxvg0S4olpL81t9/Mx4+zyXxqjVTaU8q2a/rJNaV+7SyRg==", "dependencies": { "tslib": ">=2.0.0" }, @@ -3850,15 +3850,15 @@ "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0", "@ng-web-apis/common": ">=2.0.0", - "@taiga-ui/cdk": ">=3.25.0", - "@taiga-ui/core": ">=3.25.0", + "@taiga-ui/cdk": ">=3.26.0", + "@taiga-ui/core": ">=3.26.0", "@tinkoff/ng-polymorpheus": ">=4.0.0" } }, "node_modules/@taiga-ui/cdk": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.25.0.tgz", - "integrity": "sha512-dHV5FdYiq5qBOJRyWqu+iwc2dxHtBHGK6xQXd+yk3FRXuAsfj22cZl1i3TL8RQptQZjL+6AyHKABTga6uUMliA==", + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.26.0.tgz", + "integrity": "sha512-vd2CMQ/Z6bhzCQSBSHjSoCIJEE2g4RKmjl3RBK/OdA/L46s9/nQS8oTRBG8I0zk8lNx7YHqqC6u9IY6BZgOeAg==", "dependencies": { "@ng-web-apis/common": "2.1.0", "@ng-web-apis/mutation-observer": "2.0.0", @@ -3868,7 +3868,7 @@ "tslib": "2.5.0" }, "optionalDependencies": { - "ng-morph": "2.1.3", + "ng-morph": "2.2.0", "parse5": "6.0.1" }, "peerDependencies": { @@ -3880,11 +3880,11 @@ } }, "node_modules/@taiga-ui/core": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.25.0.tgz", - "integrity": "sha512-I2ezwnWJcVEMqnMXKdBWQn7IPJYbTttFqPVrzW0UhmUGyp8eI++v2A8I+Ns/I8oDCSb/kwhbFIXmXCFZQBoWZg==", + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.26.0.tgz", + "integrity": "sha512-+IYn0ssZ3dO8Cm1HYAtbL5t+dvhp0RVzljdS72HBcr7IsnEhr2UDWWvsLv4DqsG4tXigWq6sL9wjXqg6/ylH4g==", "dependencies": { - "@taiga-ui/i18n": "^3.25.0", + "@taiga-ui/i18n": "^3.26.0", "tslib": ">=2.0.0" }, "peerDependencies": { @@ -3896,17 +3896,17 @@ "@angular/router": ">=12.0.0", "@ng-web-apis/common": ">=2.0.0", "@ng-web-apis/mutation-observer": ">=2.0.0", - "@taiga-ui/cdk": ">=3.25.0", - "@taiga-ui/i18n": ">=3.25.0", + "@taiga-ui/cdk": ">=3.26.0", + "@taiga-ui/i18n": ">=3.26.0", "@tinkoff/ng-event-plugins": ">=3.1.0", "@tinkoff/ng-polymorpheus": ">=4.0.0", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/i18n": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.25.0.tgz", - "integrity": "sha512-E/lLv84soT2qgu6q3ilF1bUOriMfW052QYpORYrJQvF9O1hxTLW5yv7AYEPYHnJFXEvt7rLFyvjoI56bFeLryw==", + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.26.0.tgz", + "integrity": "sha512-pI8IIQPYe3I7f/HQ4prCNpttEzwR1VA6ooJoaygVcSQDS8KVr03yyl9RBUzKpl57vnemuduVdfqM9LxX4bPeWQ==", "dependencies": { "tslib": ">=2.0.0" }, @@ -3916,17 +3916,17 @@ } }, "node_modules/@taiga-ui/icons": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.25.0.tgz", - "integrity": "sha512-kXsKCifldnmrk/HcVZ8HqX68gHyZXsskaH0mrgq4CE3/8iiVmUBtqkgay1Rc6dubmzlPjD7/fr9nl/Wkf9zALA==", + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.26.0.tgz", + "integrity": "sha512-q42C7LYqmOEf1P6GZPl6we5YZe9dboke4kNmbSYxWMT1EWCsgPWK8QmK02BsDeltUwSp7cnCP7jGZG1lkbuzKg==", "dependencies": { "tslib": "^2.2.0" } }, "node_modules/@taiga-ui/kit": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.25.0.tgz", - "integrity": "sha512-mLTpcwpkMrwo8WPtezhJTBcvNPvQwZOUuMD3LkEWQ9g04o3flg+W9W6TJTQeg7Eqwp8ciq/INuSMcsmzgBd2uA==", + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.26.0.tgz", + "integrity": "sha512-Sdp9FKSi/+C2PgirSLr03YQNyboewhFOaFRtT6cBXzscHJLfTWLSv6nNq1kMDLueVTtuPJjksAXsHj+fpnWIiQ==", "dependencies": { "@ng-web-apis/intersection-observer": "3.0.0", "text-mask-core": "5.1.2", @@ -3940,9 +3940,9 @@ "@ng-web-apis/common": ">=2.0.0", "@ng-web-apis/mutation-observer": ">=2.0.0", "@ng-web-apis/resize-observer": ">=2.0.0", - "@taiga-ui/cdk": ">=3.25.0", - "@taiga-ui/core": ">=3.25.0", - "@taiga-ui/i18n": ">=3.25.0", + "@taiga-ui/cdk": ">=3.26.0", + "@taiga-ui/core": ">=3.26.0", + "@taiga-ui/i18n": ">=3.26.0", "@tinkoff/ng-polymorpheus": ">=4.0.0", "rxjs": ">=6.0.0" } @@ -10270,9 +10270,9 @@ } }, "node_modules/ng-morph": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ng-morph/-/ng-morph-2.1.3.tgz", - "integrity": "sha512-bFeSMSn2ORgtYw4ZmwISJ/RGdZxi03IwODrnXB6FbTEvmyfuTCB7x0FyQsm8euNX43fTp3FZclCZpRmO8t5w8w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ng-morph/-/ng-morph-2.2.0.tgz", + "integrity": "sha512-0CEswQ+QrxPBWv1dBBu/N6idk0wIXkdFmqk+GW55/Ta7DJTKMCPZLVGXpp+Lia9XF55vVyxnOBw9J3QNN2Dv5A==", "optional": true, "dependencies": { "jsonc-parser": "3.0.0", diff --git a/frontend/package.json b/frontend/package.json index f7f139f9b..14bad0717 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,11 +52,11 @@ "@ng-web-apis/resize-observer": "^2.0.0", "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", - "@taiga-ui/addon-charts": "3.25.0", - "@taiga-ui/cdk": "3.25.0", - "@taiga-ui/core": "3.25.0", - "@taiga-ui/icons": "3.25.0", - "@taiga-ui/kit": "3.25.0", + "@taiga-ui/addon-charts": "3.26.0", + "@taiga-ui/cdk": "3.26.0", + "@taiga-ui/core": "3.26.0", + "@taiga-ui/icons": "3.26.0", + "@taiga-ui/kit": "3.26.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", diff --git a/frontend/projects/ui/src/app/app.providers.ts b/frontend/projects/ui/src/app/app.providers.ts index dea3e6292..daa87f24b 100644 --- a/frontend/projects/ui/src/app/app.providers.ts +++ b/frontend/projects/ui/src/app/app.providers.ts @@ -2,10 +2,15 @@ import { APP_INITIALIZER, Provider } from '@angular/core' import { UntypedFormBuilder } from '@angular/forms' import { Router, RouteReuseStrategy } from '@angular/router' import { IonicRouteStrategy, IonNav } from '@ionic/angular' +import { TUI_DATE_FORMAT, TUI_DATE_SEPARATOR } from '@taiga-ui/cdk' import { tuiButtonOptionsProvider, tuiNumberFormatProvider, } from '@taiga-ui/core' +import { + TUI_DATE_TIME_VALUE_TRANSFORMER, + TUI_DATE_VALUE_TRANSFORMER, +} from '@taiga-ui/kit' import { RELATIVE_URL, THEME, WorkspaceConfig } from '@start9labs/shared' import { ApiService } from './services/api/embassy-api.service' import { MockApiService } from './services/api/embassy-mock-api.service' @@ -14,6 +19,8 @@ import { AuthService } from './services/auth.service' import { ClientStorageService } from './services/client-storage.service' import { FilterPackagesPipe } from '../../../marketplace/src/pipes/filter-packages.pipe' import { ThemeSwitcherService } from './services/theme-switcher.service' +import { DateTransformerService } from './services/date-transformer.service' +import { DatetimeTransformerService } from './services/datetime-transformer.service' const { useMocks, @@ -26,6 +33,22 @@ export const APP_PROVIDERS: Provider[] = [ IonNav, tuiNumberFormatProvider({ decimalSeparator: '.', thousandSeparator: '' }), tuiButtonOptionsProvider({ size: 'm' }), + { + provide: TUI_DATE_FORMAT, + useValue: 'MDY', + }, + { + provide: TUI_DATE_SEPARATOR, + useValue: '/', + }, + { + provide: TUI_DATE_VALUE_TRANSFORMER, + useClass: DateTransformerService, + }, + { + provide: TUI_DATE_TIME_VALUE_TRANSFORMER, + useClass: DatetimeTransformerService, + }, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy, diff --git a/frontend/projects/ui/src/app/components/form/form-control/form-control.component.html b/frontend/projects/ui/src/app/components/form/form-control/form-control.component.html index 1a03a97b7..11dbe0068 100644 --- a/frontend/projects/ui/src/app/components/form/form-control/form-control.component.html +++ b/frontend/projects/ui/src/app/components/form/form-control/form-control.component.html @@ -1,5 +1,6 @@ + diff --git a/frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html b/frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html new file mode 100644 index 000000000..e312cf989 --- /dev/null +++ b/frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.html @@ -0,0 +1,34 @@ + + + {{ spec.name }} + * + + + {{ spec.name }} + * + + + {{ spec.name }} + * + + diff --git a/frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts b/frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts new file mode 100644 index 000000000..ff3e5bdd9 --- /dev/null +++ b/frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core' +import { + TUI_FIRST_DAY, + TUI_LAST_DAY, + TuiDay, + tuiPure, + TuiTime, +} from '@taiga-ui/cdk' +import { ValueSpecDatetime } from 'start-sdk/lib/config/configTypes' +import { Control } from '../control' + +@Component({ + selector: 'form-datetime', + templateUrl: './form-datetime.component.html', +}) +export class FormDatetimeComponent extends Control { + readonly min = TUI_FIRST_DAY + readonly max = TUI_LAST_DAY + + @tuiPure + getTime(value: string | null) { + return value ? TuiTime.fromString(value) : null + } + + getLimit(limit: string): [TuiDay, TuiTime] { + return [ + TuiDay.jsonParse(limit.slice(0, 10)), + limit.length === 10 + ? new TuiTime(0, 0) + : TuiTime.fromString(limit.slice(-5)), + ] + } +} diff --git a/frontend/projects/ui/src/app/components/form/form-group/form-group.providers.ts b/frontend/projects/ui/src/app/components/form/form-group/form-group.providers.ts index d0f9a7a2f..5c9039f40 100644 --- a/frontend/projects/ui/src/app/components/form/form-group/form-group.providers.ts +++ b/frontend/projects/ui/src/app/components/form/form-group/form-group.providers.ts @@ -1,5 +1,9 @@ import { Provider, SkipSelf } from '@angular/core' -import { TUI_ARROW_MODE } from '@taiga-ui/kit' +import { + TUI_ARROW_MODE, + tuiInputDateOptionsProvider, + tuiInputTimeOptionsProvider, +} from '@taiga-ui/kit' import { TUI_DEFAULT_ERROR_MESSAGE } from '@taiga-ui/core' import { ControlContainer } from '@angular/forms' import { identity, of } from 'rxjs' @@ -21,4 +25,10 @@ export const FORM_GROUP_PROVIDERS: Provider[] = [ disabled: null, }, }, + tuiInputDateOptionsProvider({ + nativePicker: true, + }), + tuiInputTimeOptionsProvider({ + nativePicker: true, + }), ] diff --git a/frontend/projects/ui/src/app/components/form/form.module.ts b/frontend/projects/ui/src/app/components/form/form.module.ts index f2bb5a042..4f2028f43 100644 --- a/frontend/projects/ui/src/app/components/form/form.module.ts +++ b/frontend/projects/ui/src/app/components/form/form.module.ts @@ -1,7 +1,8 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { TuiValueChangesModule } from '@taiga-ui/cdk' +import { MaskitoModule } from '@maskito/angular' +import { TuiMapperPipeModule, TuiValueChangesModule } from '@taiga-ui/cdk' import { TuiButtonModule, TuiErrorModule, @@ -17,9 +18,12 @@ import { import { TuiElasticContainerModule, TuiFieldErrorPipeModule, + TuiInputDateModule, + TuiInputDateTimeModule, TuiInputFilesModule, TuiInputModule, TuiInputNumberModule, + TuiInputTimeModule, TuiMultiSelectModule, TuiPromptModule, TuiSelectModule, @@ -43,7 +47,7 @@ import { FormControlComponent } from './form-control/form-control.component' import { MustachePipe } from './mustache.pipe' import { ControlDirective } from './control.directive' import { FormColorComponent } from './form-color/form-color.component' -import { MaskitoModule } from '@maskito/angular' +import { FormDatetimeComponent } from './form-datetime/form-datetime.component' @NgModule({ imports: [ @@ -73,11 +77,16 @@ import { MaskitoModule } from '@maskito/angular' MaskitoModule, TuiSvgModule, TuiWrapperModule, + TuiInputDateModule, + TuiInputTimeModule, + TuiInputDateTimeModule, + TuiMapperPipeModule, ], declarations: [ FormGroupComponent, FormControlComponent, FormColorComponent, + FormDatetimeComponent, FormTextComponent, FormToggleComponent, FormTextareaComponent, diff --git a/frontend/projects/ui/src/app/services/api/api.fixures.ts b/frontend/projects/ui/src/app/services/api/api.fixures.ts index b8a53aaa5..f57c5dd50 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -774,6 +774,50 @@ export module Mock { required: false, default: '#000000', }, + chronos: { + name: 'Chronos', + type: 'object', + description: 'Various time related settings', + warning: null, + spec: { + time: { + name: 'Time', + type: 'datetime', + inputmode: 'time', + description: 'Time of day', + warning: null, + required: true, + min: '12:00', + max: '16:00', + step: null, + default: '12:00', + }, + date: { + name: 'Date', + type: 'datetime', + inputmode: 'date', + description: 'Just a date', + warning: null, + required: true, + min: '2023-01-01', + max: '2023-12-31', + step: null, + default: '2023-05-01', + }, + datetime: { + name: 'Date and time', + type: 'datetime', + inputmode: 'datetime-local', + description: 'Both date and time', + warning: null, + required: true, + min: '2023-01-01T12:00', + max: '2023-12-31T16:00', + step: null, + default: '2023-05-01T18:30', + }, + }, + }, advanced: { name: 'Advanced', type: 'object', diff --git a/frontend/projects/ui/src/app/services/date-transformer.service.ts b/frontend/projects/ui/src/app/services/date-transformer.service.ts new file mode 100644 index 000000000..8d1639f72 --- /dev/null +++ b/frontend/projects/ui/src/app/services/date-transformer.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core' +import { AbstractTuiValueTransformer, TuiDay } from '@taiga-ui/cdk' + +type From = TuiDay | null +type To = string | null + +@Injectable() +export class DateTransformerService extends AbstractTuiValueTransformer< + From, + To +> { + fromControlValue(controlValue: To): From { + return controlValue ? TuiDay.jsonParse(controlValue) : null + } + + toControlValue(componentValue: From): To { + return componentValue?.toJSON() || null + } +} diff --git a/frontend/projects/ui/src/app/services/datetime-transformer.service.ts b/frontend/projects/ui/src/app/services/datetime-transformer.service.ts new file mode 100644 index 000000000..4ffb851c4 --- /dev/null +++ b/frontend/projects/ui/src/app/services/datetime-transformer.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core' +import { AbstractTuiValueTransformer, TuiDay, TuiTime } from '@taiga-ui/cdk' + +type From = [TuiDay | null, TuiTime | null] | null +type To = string | null + +@Injectable() +export class DatetimeTransformerService extends AbstractTuiValueTransformer< + From, + To +> { + fromControlValue(controlValue: To): From { + if (!controlValue) { + return null + } + + const day = TuiDay.jsonParse(controlValue.slice(0, 10)) + const time = + controlValue.length === 16 + ? TuiTime.fromString(controlValue.slice(-5)) + : null + + return [day, time] + } + + toControlValue(componentValue: From): To { + if (!componentValue) { + return null + } + + const [day, time] = componentValue + + return day?.toJSON() + (time ? `T${time.toString()}` : '') + } +} diff --git a/frontend/projects/ui/src/app/services/form.service.ts b/frontend/projects/ui/src/app/services/form.service.ts index 77c49bfc0..e74177dba 100644 --- a/frontend/projects/ui/src/app/services/form.service.ts +++ b/frontend/projects/ui/src/app/services/form.service.ts @@ -29,6 +29,7 @@ import { ValueSpecTextarea, unionValueKey, ValueSpecColor, + ValueSpecDatetime, } from 'start-sdk/lib/config/configTypes' const Mustache = require('mustache') @@ -131,6 +132,13 @@ export class FormService { value = spec.default || null } return this.formBuilder.control(value, colorValidators(spec)) + case 'datetime': + if (currentValue !== undefined) { + value = currentValue + } else { + value = spec.default || null + } + return this.formBuilder.control(value, datetimeValidators(spec)) case 'object': return this.getFormGroup(spec.spec, [], currentValue) case 'list': @@ -216,6 +224,28 @@ function colorValidators({ required }: ValueSpecColor): ValidatorFn[] { return validators } +function datetimeValidators({ + required, + min, + max, +}: ValueSpecDatetime): ValidatorFn[] { + const validators: ValidatorFn[] = [] + + if (required) { + validators.push(Validators.required) + } + + if (min) { + validators.push(datetimeMin(min)) + } + + if (max) { + validators.push(datetimeMax(max)) + } + + return validators +} + function numberValidators( spec: ValueSpecNumber | ListValueSpecNumber, ): ValidatorFn[] { @@ -316,6 +346,28 @@ export function listInRange( } } +export function datetimeMin(min: string): ValidatorFn { + return ({ value }) => { + if (!value) return null + + const date = new Date(value.length === 5 ? `2000-01-01T${value}` : value) + const minDate = new Date(min.length === 5 ? `2000-01-01T${min}` : min) + + return date < minDate ? { datetimeMin: `Minimum is ${min}` } : null + } +} + +export function datetimeMax(max: string): ValidatorFn { + return ({ value }) => { + if (!value) return null + + const date = new Date(value.length === 5 ? `2000-01-01T${value}` : value) + const maxDate = new Date(max.length === 5 ? `2000-01-01T${max}` : max) + + return date > maxDate ? { datetimeMin: `Maximum is ${max}` } : null + } +} + export function textLengthInRange( minLength: number | null, maxLength: number | null,