feat: add datetime spec support (#2264)

This commit is contained in:
Alex Inkin
2023-05-06 01:06:10 +04:00
committed by Aiden McClelland
parent 273b5768c4
commit f6c09109ba
12 changed files with 303 additions and 43 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -1,5 +1,6 @@
<ng-container [ngSwitch]="spec.type">
<form-color *ngSwitchCase="'color'"></form-color>
<form-datetime *ngSwitchCase="'datetime'"></form-datetime>
<form-file *ngSwitchCase="'file'"></form-file>
<form-multiselect *ngSwitchCase="'multiselect'"></form-multiselect>
<form-number *ngSwitchCase="'number'"></form-number>

View File

@@ -0,0 +1,34 @@
<ng-container [ngSwitch]="spec.inputmode" [tuiHintContent]="spec.description">
<tui-input-time
*ngSwitchCase="'time'"
[pseudoInvalid]="invalid"
[ngModel]="getTime(value)"
(ngModelChange)="value = $event?.toString() || null"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
<span *ngIf="spec.required">*</span>
</tui-input-time>
<tui-input-date
*ngSwitchCase="'date'"
[pseudoInvalid]="invalid"
[min]="spec.min ? (spec.min | tuiMapper : getLimit)[0] : min"
[max]="spec.max ? (spec.max | tuiMapper : getLimit)[0] : max"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
<span *ngIf="spec.required">*</span>
</tui-input-date>
<tui-input-date-time
*ngSwitchCase="'datetime-local'"
[pseudoInvalid]="invalid"
[min]="spec.min ? (spec.min | tuiMapper : getLimit) : min"
[max]="spec.max ? (spec.max | tuiMapper : getLimit) : max"
[(ngModel)]="value"
(focusedChange)="onFocus($event)"
>
{{ spec.name }}
<span *ngIf="spec.required">*</span>
</tui-input-date-time>
</ng-container>

View File

@@ -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<ValueSpecDatetime, string> {
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)),
]
}
}

View File

@@ -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,
}),
]

View File

@@ -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,

View File

@@ -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',

View File

@@ -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
}
}

View File

@@ -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()}` : '')
}
}

View File

@@ -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,