diff --git a/build.gradle b/build.gradle index da72c6e40..683f30ea7 100644 --- a/build.gradle +++ b/build.gradle @@ -67,7 +67,7 @@ publishing { tasks.register( 'pnpmCheck', PnpmTask ) { dependsOn tasks.named( 'pnpmInstall' ) args = ['run', 'check'] - outputs.cacheIf { false } + outputs.upToDateWhen { false } } tasks.named( 'check' ).configure { diff --git a/package.json b/package.json index 033694385..af72644aa 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "jquery-simulate": "^1.0.2", "jquery-ui": "^1.14.1", "mousetrap": "^1.6.5", - "q": "^1.5.1" + "q": "^1.5.1", + "dayjs": "^1.11.19" }, "devDependencies": { "@enonic/eslint-config": "^2.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index efdd07581..2da59891b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + dayjs: + specifier: ^1.11.19 + version: 1.11.19 dompurify: specifier: ~3.3.1 version: 3.3.1 @@ -722,6 +725,9 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2030,6 +2036,8 @@ snapshots: dependencies: css-tree: 2.2.1 + dayjs@1.11.19: {} + debug@4.4.3: dependencies: ms: 2.1.3 diff --git a/src/main/resources/assets/admin/common/js/data/Property.ts b/src/main/resources/assets/admin/common/js/data/Property.ts index d29467d07..0e2feec17 100644 --- a/src/main/resources/assets/admin/common/js/data/Property.ts +++ b/src/main/resources/assets/admin/common/js/data/Property.ts @@ -16,6 +16,7 @@ import {PropertyValueChangedEvent} from './PropertyValueChangedEvent'; import {assertNotNull} from '../util/Assert'; import {ValueTypes} from './ValueTypes'; import {ValueType} from './ValueType'; +import {Instant} from '../util/Instant'; /** * A Property has a [[name]] and a [[value]], @@ -239,6 +240,10 @@ export class Property return this.value.getBinaryReference(); } + getInstant(): Instant { + return this.value.getInstant(); + } + equals(o: Equitable): boolean { if (!ObjectHelper.iFrameSafeInstanceOf(o, Property)) { diff --git a/src/main/resources/assets/admin/common/js/data/Value.ts b/src/main/resources/assets/admin/common/js/data/Value.ts index 4d5d31c10..1a4dd10fb 100644 --- a/src/main/resources/assets/admin/common/js/data/Value.ts +++ b/src/main/resources/assets/admin/common/js/data/Value.ts @@ -13,8 +13,9 @@ import {Link} from '../util/Link'; import {PropertySet} from './PropertySet'; import {ValueType} from './ValueType'; import {Typable} from './Typable'; +import {Instant} from '../util/Instant'; -export type ValueData = string | number | boolean | PropertySet | Reference | BinaryReference | GeoPoint | Date | DateTime | LocalDate | LocalDateTime | LocalTime | Link; +export type ValueData = string | number | boolean | PropertySet | Reference | BinaryReference | GeoPoint | Date | Instant | DateTime | LocalDate | LocalDateTime | LocalTime | Link; export class Value implements Equitable, Cloneable, Typable { @@ -145,6 +146,13 @@ export class Value return this.value as Link; } + getInstant(): Instant { + if (this.isNull()) { + return null; + } + return this.value as Instant; + } + equals(o: Equitable): boolean { if (!ObjectHelper.iFrameSafeInstanceOf(o, Value)) { diff --git a/src/main/resources/assets/admin/common/js/data/ValueTypeConverter.ts b/src/main/resources/assets/admin/common/js/data/ValueTypeConverter.ts index dfeca61b5..b8865259c 100644 --- a/src/main/resources/assets/admin/common/js/data/ValueTypeConverter.ts +++ b/src/main/resources/assets/admin/common/js/data/ValueTypeConverter.ts @@ -44,6 +44,8 @@ export class ValueTypeConverter { return ValueTypeConverter.convertToReference(value); } else if (toType === ValueTypes.BINARY_REFERENCE) { return ValueTypeConverter.convertToBinaryReference(value); + } else if (toType === ValueTypes.INSTANT) { + return ValueTypeConverter.convertToInstant(value); } throw Error(`Unknown ValueType: ${toType.toString()}`); @@ -153,7 +155,7 @@ export class ValueTypeConverter { if (value.getType() === ValueTypes.STRING && ValueTypes.DATE_TIME.isConvertible(value.getString())) { // from string return ValueTypes.DATE_TIME.newValue(value.getString()); } else if (value.getType() === ValueTypes.LOCAL_DATE && value.isNotNull()) { // from LocalDate - return ValueTypes.DATE_TIME.newValue(value.getString() + 'T00:00:00+00:00'); + return ValueTypes.DATE_TIME.newValue(`${value.getString()}T00:00:00`); } else if (value.getType() === ValueTypes.LOCAL_DATE_TIME && value.isNotNull()) { // from LocalDateTime let dateTime = value.getString(); return ValueTypes.DATE_TIME.newValue(dateTime); @@ -161,6 +163,22 @@ export class ValueTypeConverter { return ValueTypes.DATE_TIME.newNullValue(); } + private static convertToInstant(value: Value): Value { + if (value.getType() === ValueTypes.STRING && ValueTypes.INSTANT.isConvertible(value.getString())) { // from string + return ValueTypes.INSTANT.newValue(value.getString()); + } else if (value.getType() === ValueTypes.LOCAL_DATE && value.isNotNull()) { // from LocalDate + return ValueTypes.INSTANT.newValue(`${value.getString()}T00:00:00Z`); + } else if (value.getType() === ValueTypes.LOCAL_DATE_TIME && value.isNotNull()) { // from LocalDateTime + const localDateTime = new Date(value.getString()); + return ValueTypes.INSTANT.newValue(localDateTime.toISOString()); + } else if (value.getType() === ValueTypes.DATE_TIME && value.isNotNull()) { + const dateTime = new Date(value.getString()); + return ValueTypes.INSTANT.newValue(dateTime.toISOString()); + } else { + return ValueTypes.INSTANT.newNullValue(); + } + } + private static convertToLocalTime(value: Value): Value { if (value.getType() === ValueTypes.STRING && ValueTypes.LOCAL_TIME.isConvertible(value.getString())) { // from string return ValueTypes.LOCAL_TIME.newValue(value.getString()); diff --git a/src/main/resources/assets/admin/common/js/data/ValueTypeDateTime.ts b/src/main/resources/assets/admin/common/js/data/ValueTypeDateTime.ts index 4b3af7ab4..5c0acb6d7 100644 --- a/src/main/resources/assets/admin/common/js/data/ValueTypeDateTime.ts +++ b/src/main/resources/assets/admin/common/js/data/ValueTypeDateTime.ts @@ -1,8 +1,8 @@ -import {DateTime} from '../util/DateTime'; import {ObjectHelper} from '../ObjectHelper'; import {StringHelper} from '../util/StringHelper'; import {ValueType} from './ValueType'; import {Value} from './Value'; +import {DateTime} from '../util/DateTime'; export class ValueTypeDateTime extends ValueType { @@ -22,10 +22,7 @@ export class ValueTypeDateTime if (StringHelper.isBlank(value)) { return false; } - // 2010-01-01T10:55:00+01:00 - if (value.length < 19) { - return false; - } + return this.isValid(value); } @@ -40,7 +37,7 @@ export class ValueTypeDateTime return new Value(date, this); } - // 2010-01-01T10:55:00+01:00 + // 2010-01-01T10:55:00 toJsonValue(value: Value): string { return value.isNull() ? null : value.getDateTime().toString(); } @@ -52,5 +49,4 @@ export class ValueTypeDateTime valueEquals(a: DateTime, b: DateTime): boolean { return ObjectHelper.equals(a, b); } - } diff --git a/src/main/resources/assets/admin/common/js/data/ValueTypeInstant.ts b/src/main/resources/assets/admin/common/js/data/ValueTypeInstant.ts new file mode 100644 index 000000000..e53316fb5 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/data/ValueTypeInstant.ts @@ -0,0 +1,49 @@ +import {Instant} from '../util/Instant'; +import {ObjectHelper} from '../ObjectHelper'; +import {StringHelper} from '../util/StringHelper'; +import {ValueType} from './ValueType'; +import {Value} from './Value'; + +export class ValueTypeInstant + extends ValueType { + + constructor() { + super('Instant'); + } + + isValid(value: any): boolean { + if (ObjectHelper.iFrameSafeInstanceOf(value, Instant)) { + return true; + } + + return Instant.isValidInstant(value); + } + + isConvertible(value: string): boolean { + if (StringHelper.isBlank(value)) { + return false; + } + + return this.isValid(value); + } + + newValue(value: string): Value { + if (!value || !this.isConvertible(value)) { + return this.newNullValue(); + } + const date: Instant = Instant.fromString(value); + return new Value(date, this); + } + + toJsonValue(value: Value): string { + return value.isNull() ? null : this.valueToString(value); + } + + valueToString(value: Value): string { + return value.getInstant().toString(); + } + + valueEquals(a: Instant, b: Instant): boolean { + return ObjectHelper.equals(a, b); + } +} diff --git a/src/main/resources/assets/admin/common/js/data/ValueTypeLocalDateTime.ts b/src/main/resources/assets/admin/common/js/data/ValueTypeLocalDateTime.ts index 396fe423f..b77758e37 100644 --- a/src/main/resources/assets/admin/common/js/data/ValueTypeLocalDateTime.ts +++ b/src/main/resources/assets/admin/common/js/data/ValueTypeLocalDateTime.ts @@ -23,10 +23,6 @@ export class ValueTypeLocalDateTime if (StringHelper.isBlank(value)) { return false; } - // 2010-01-01T10:55:00 - if (value.length !== 19) { - return false; - } return this.isValid(value); } diff --git a/src/main/resources/assets/admin/common/js/data/ValueTypes.ts b/src/main/resources/assets/admin/common/js/data/ValueTypes.ts index b0a863d90..3192f8c45 100644 --- a/src/main/resources/assets/admin/common/js/data/ValueTypes.ts +++ b/src/main/resources/assets/admin/common/js/data/ValueTypes.ts @@ -15,6 +15,7 @@ import {ValueTypeGeoPoint} from './ValueTypeGeoPoint'; import {ValueTypeReference} from './ValueTypeReference'; import {ValueTypeBinaryReference} from './ValueTypeBinaryReference'; import {ValueType} from './ValueType'; +import {ValueTypeInstant} from './ValueTypeInstant'; export class ValueTypes { @@ -44,6 +45,8 @@ export class ValueTypes { static BINARY_REFERENCE: ValueTypeBinaryReference = new ValueTypeBinaryReference(); + static INSTANT: ValueTypeInstant = new ValueTypeInstant(); + static ALL: ValueType[] = [ ValueTypes.DATA, ValueTypes.STRING, @@ -58,6 +61,7 @@ export class ValueTypes { ValueTypes.GEO_POINT, ValueTypes.REFERENCE, ValueTypes.BINARY_REFERENCE, + ValueTypes.INSTANT, ]; public static fromName(name: string): ValueType { diff --git a/src/main/resources/assets/admin/common/js/form/Input.ts b/src/main/resources/assets/admin/common/js/form/Input.ts index 6066c93d4..606f3dd59 100644 --- a/src/main/resources/assets/admin/common/js/form/Input.ts +++ b/src/main/resources/assets/admin/common/js/form/Input.ts @@ -1,5 +1,3 @@ -import {Value} from '../data/Value'; -import {ValueTypes} from '../data/ValueTypes'; import {Equitable} from '../Equitable'; import {InputJson} from './json/InputJson'; import {ObjectHelper} from '../ObjectHelper'; @@ -16,22 +14,12 @@ export class InputBuilder { label: string; - immutable: boolean = false; - occurrences: Occurrences; - indexed: boolean = true; - - validationRegex: string; - helpText: string; inputTypeConfig: object; - maximizeUIInputWidth: boolean; - - defaultValue: Value; - setName(value: string): InputBuilder { this.name = value; return this; @@ -47,26 +35,11 @@ export class InputBuilder { return this; } - setImmutable(value: boolean): InputBuilder { - this.immutable = value; - return this; - } - setOccurrences(value: Occurrences): InputBuilder { this.occurrences = value; return this; } - setIndexed(value: boolean): InputBuilder { - this.indexed = value; - return this; - } - - setValidationRegex(value: string): InputBuilder { - this.validationRegex = value; - return this; - } - setHelpText(value: string): InputBuilder { this.helpText = value; return this; @@ -77,26 +50,13 @@ export class InputBuilder { return this; } - setMaximizeUIInputWidth(value: boolean): InputBuilder { - this.maximizeUIInputWidth = value; - return this; - } - fromJson(json: InputJson): InputBuilder { this.name = json.name; this.inputType = InputTypeName.parseInputTypeName(json.inputType); this.label = json.label; - this.immutable = json.immutable; this.occurrences = Occurrences.fromJson(json.occurrences); - this.indexed = json.indexed; - this.validationRegex = json.validationRegexp; this.helpText = json.helpText; this.inputTypeConfig = json.config; - this.maximizeUIInputWidth = json.maximizeUIInputWidth; - if (json.defaultValue) { - let type = ValueTypes.fromName(json.defaultValue.type); - this.defaultValue = type.fromJsonValue(json.defaultValue.value); - } return this; } @@ -121,34 +81,19 @@ export class Input private label: string; - private immutable: boolean; - private occurrences: Occurrences; - private indexed: boolean; - - private validationRegex: string; - private helpText: string; private inputTypeConfig: object; - private maximizeUIInputWidth: boolean; - - private defaultValue: Value; - constructor(builder: InputBuilder) { super(builder.name); this.inputType = builder.inputType; this.inputTypeConfig = builder.inputTypeConfig; this.label = builder.label; - this.immutable = builder.immutable; this.occurrences = builder.occurrences; - this.indexed = builder.indexed; - this.validationRegex = builder.validationRegex; this.helpText = builder.helpText; - this.maximizeUIInputWidth = builder.maximizeUIInputWidth; - this.defaultValue = builder.defaultValue; } static fromJson(json: InputJson): Input { @@ -165,26 +110,10 @@ export class Input return this.label; } - isImmutable(): boolean { - return this.immutable; - } - getOccurrences(): Occurrences { return this.occurrences; } - isIndexed(): boolean { - return this.indexed; - } - - isMaximizeUIInputWidth(): boolean { - return this.maximizeUIInputWidth; - } - - getValidationRegex(): string { - return this.validationRegex; - } - getHelpText(): string { return this.helpText; } @@ -193,10 +122,6 @@ export class Input return this.inputTypeConfig; } - getDefaultValue(): Value { - return this.defaultValue; - } - equals(o: Equitable): boolean { if (!ObjectHelper.iFrameSafeInstanceOf(o, Input)) { @@ -217,22 +142,10 @@ export class Input return false; } - if (!ObjectHelper.booleanEquals(this.immutable, other.immutable)) { - return false; - } - if (!ObjectHelper.equals(this.occurrences, other.occurrences)) { return false; } - if (!ObjectHelper.booleanEquals(this.indexed, other.indexed)) { - return false; - } - - if (!ObjectHelper.stringEquals(this.validationRegex, other.validationRegex)) { - return false; - } - if (!ObjectHelper.stringEquals(this.helpText, other.helpText)) { return false; } @@ -246,14 +159,10 @@ export class Input Input: { name: this.getName(), helpText: this.getHelpText(), - immutable: this.isImmutable(), - indexed: this.isIndexed(), label: this.getLabel(), occurrences: this.getOccurrences().toJson(), - validationRegexp: this.getValidationRegex(), inputType: this.getInputType().toJson(), config: this.getInputTypeConfig(), - maximizeUIInputWidth: this.isMaximizeUIInputWidth() } }; } diff --git a/src/main/resources/assets/admin/common/js/form/InputView.ts b/src/main/resources/assets/admin/common/js/form/InputView.ts index 07bca844a..f268b4188 100644 --- a/src/main/resources/assets/admin/common/js/form/InputView.ts +++ b/src/main/resources/assets/admin/common/js/form/InputView.ts @@ -95,10 +95,6 @@ export class InputView } } - if (this.input.isMaximizeUIInputWidth() === false) { - this.addClass('label-inline'); - } - this.propertyArray = this.getPropertyArray(this.parentPropertySet); return this.inputTypeView.layout(this.input, this.propertyArray).then(() => { @@ -213,7 +209,8 @@ export class InputView hasNonDefaultValues(): boolean { return this.propertyArray.some((property: Property) => { - return !StringHelper.isEmpty(property.getValue().getString()) && !property.getValue().equals(this.input.getDefaultValue()); + return !StringHelper.isEmpty(property.getValue().getString()) && + !property.getValue().equals(this.inputTypeView.getDefaultValue()); }); } @@ -304,11 +301,12 @@ export class InputView propertySet.addPropertyArray(array); - let initialValue = this.input.getDefaultValue(); - if (!initialValue) { - initialValue = this.inputTypeView.newInitialValue(); - } - if (initialValue) { + const rawDefaultValue = this.inputTypeView.getRawDefaultValue(); + const initialValue = rawDefaultValue + ? this.inputTypeView.createDefaultValue(rawDefaultValue) + : this.inputTypeView.newInitialValue(); + + if (initialValue?.isNotNull()) { array.add(initialValue); } } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/InputTypeView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/InputTypeView.ts index 24dc6a413..eb49b33f5 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/InputTypeView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/InputTypeView.ts @@ -30,6 +30,12 @@ export interface InputTypeView { newInitialValue(): Value; + getDefaultValue(): Value; + + getRawDefaultValue(): unknown; + + createDefaultValue(raw: unknown): Value; + /* * Whether the InputTypeView it self is managing adding new occurrences or not. * If false, then this is expected to implement interface InputTypeViewNotManagingOccurrences. diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/InputTypeViewContext.ts b/src/main/resources/assets/admin/common/js/form/inputtype/InputTypeViewContext.ts index 5396b79be..6588e4afa 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/InputTypeViewContext.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/InputTypeViewContext.ts @@ -8,7 +8,7 @@ export interface InputTypeViewContext { input: Input; - inputConfig: Record[]>; + inputConfig: Record[]>; parentDataPath: PropertyPath; } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/checkbox/Checkbox.ts b/src/main/resources/assets/admin/common/js/form/inputtype/checkbox/Checkbox.ts index 51a790290..222aec9c4 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/checkbox/Checkbox.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/checkbox/Checkbox.ts @@ -16,7 +16,6 @@ import {BaseInputTypeSingleOccurrence} from '../support/BaseInputTypeSingleOccur export class Checkbox extends BaseInputTypeSingleOccurrence { - public static debug: boolean = false; private checkbox: CheckboxEl; private inputAlignment: InputAlignment = InputAlignment.LEFT; @@ -30,6 +29,10 @@ export class Checkbox return ValueTypes.BOOLEAN; } + createDefaultValue(rawValue: unknown): Value { + return this.getValueType().fromJsonValue(rawValue === 'checked'); + } + newInitialValue(): Value { return ValueTypes.BOOLEAN.newBoolean(false); } @@ -122,7 +125,7 @@ export class Checkbox this.checkbox.unBlur(listener); } - private readConfig(inputConfig: Record[]>): void { + private readConfig(inputConfig: Record[]>): void { if (inputConfig) { this.setInputAlignment(inputConfig['alignment']); } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/combobox/ComboBox.ts b/src/main/resources/assets/admin/common/js/form/inputtype/combobox/ComboBox.ts index 3ca256142..cf7e97b6b 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/combobox/ComboBox.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/combobox/ComboBox.ts @@ -37,6 +37,14 @@ export class ComboBox return null; } + createDefaultValue(rawValue: unknown): Value { + const valueType = this.getValueType(); + if (typeof rawValue !== 'string') { + return valueType.newNullValue(); + } + return valueType.newValue(rawValue); + } + layout(input: Input, propertyArray: PropertyArray): Q.Promise { if (!ValueTypes.STRING.equals(propertyArray.getType())) { ValueTypeConverter.convertArrayValues(propertyArray, ValueTypes.STRING); @@ -152,13 +160,13 @@ export class ComboBox protected readInputConfig(): void { const options: ComboBoxOption[] = []; - const optionValues: Record[] = this.context.inputConfig['option'] || []; + const optionValues: Record[] = this.context.inputConfig['options'] || []; const l: number = optionValues.length; - let optionValue: Record; + let optionValue: Record; for (let i = 0; i < l; i++) { optionValue = optionValues[i]; - options.push({label: optionValue['value'], value: optionValue['@value']}); + options.push({label: optionValue['value'] as string, value: optionValue['@value'] as string}); } this.comboBoxOptions = options; diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/geo/GeoPoint.ts b/src/main/resources/assets/admin/common/js/form/inputtype/geo/GeoPoint.ts index cf56006e1..1fb7f4aa3 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/geo/GeoPoint.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/geo/GeoPoint.ts @@ -5,7 +5,6 @@ import {Property} from '../../../data/Property'; import {BaseInputTypeNotManagingAdd} from '../support/BaseInputTypeNotManagingAdd'; import {Element} from '../../../dom/Element'; import {ValueChangedEvent} from '../../../ValueChangedEvent'; -import {GeoPoint as GeoPointUtil} from '../../../util/GeoPoint'; import {GeoPoint as GeoPointEl} from '../../../ui/geo/GeoPoint'; import {FormInputEl} from '../../../dom/FormInputEl'; import {InputTypeManager} from '../InputTypeManager'; @@ -40,6 +39,13 @@ export class GeoPoint return isValid ? this.getValueType().newValue(event.getNewValue()) : this.getValueType().newNullValue(); } + createDefaultValue(rawValue: unknown): Value { + if (typeof rawValue !== 'string') { + return this.getValueType().newNullValue(); + } + return this.getValueType().newValue(rawValue); + } + resetInputOccurrenceElement(occurrence: Element): void { super.resetInputOccurrenceElement(occurrence); diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/number/Long.ts b/src/main/resources/assets/admin/common/js/form/inputtype/number/Long.ts index b43ded605..a77302043 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/number/Long.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/number/Long.ts @@ -1,11 +1,8 @@ import {ValueType} from '../../../data/ValueType'; import {ValueTypes} from '../../../data/ValueTypes'; -import {Value} from '../../../data/Value'; import {InputTypeViewContext} from '../InputTypeViewContext'; -import {Element} from '../../../dom/Element'; import {TextInput} from '../../../ui/text/TextInput'; import {NumberHelper} from '../../../util/NumberHelper'; -import {FormInputEl} from '../../../dom/FormInputEl'; import {InputTypeManager} from '../InputTypeManager'; import {Class} from '../../../Class'; import {NumberInputType} from './NumberInputType'; diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/number/NumberInputType.ts b/src/main/resources/assets/admin/common/js/form/inputtype/number/NumberInputType.ts index 2b385cf35..c6939b89f 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/number/NumberInputType.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/number/NumberInputType.ts @@ -4,7 +4,6 @@ import {i18n} from '../../../util/Messages'; import {InputTypeViewContext} from '../InputTypeViewContext'; import {FormInputEl} from '../../../dom/FormInputEl'; import {Property} from '../../../data/Property'; -import {StringHelper} from '../../../util/StringHelper'; import {AdditionalValidationRecord} from '../../AdditionalValidationRecord'; import {Element} from '../../../dom/Element'; import {ValueTypeConverter} from '../../../data/ValueTypeConverter'; @@ -32,6 +31,14 @@ export abstract class NumberInputType occurrence.setValue(this.getPropertyValue(property)); } + createDefaultValue(rawValue: unknown): Value { + const valueType = this.getValueType(); + if (typeof rawValue !== 'number') { + return valueType.newNullValue(); + } + return valueType.fromJsonValue(rawValue); + } + resetInputOccurrenceElement(occurrence: Element): void { super.resetInputOccurrenceElement(occurrence); @@ -114,9 +121,8 @@ export abstract class NumberInputType } } - private getConfigProperty(config: InputTypeViewContext, propertyName: string) { - const configProperty = config.inputConfig[propertyName] ? config.inputConfig[propertyName][0] : {}; - return NumberHelper.toNumber(configProperty['value']); + private getConfigProperty(config: InputTypeViewContext, propertyName: string): number { + return config.inputConfig[propertyName]?.[0]?.value as number ?? null; } private isValidMin(value: number) { diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/principal/PrincipalSelector.ts b/src/main/resources/assets/admin/common/js/form/inputtype/principal/PrincipalSelector.ts index 8dcf67a50..aa667f12c 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/principal/PrincipalSelector.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/principal/PrincipalSelector.ts @@ -46,6 +46,10 @@ export class PrincipalSelector return null; } + createDefaultValue(rawValue: unknown): Value { + return this.getValueType().newNullValue(); + } + layout(input: Input, propertyArray: PropertyArray): Q.Promise { return super.layout(input, propertyArray).then(() => { this.initiallySelectedItems = this.getSelectedItemsIds(); @@ -117,7 +121,7 @@ export class PrincipalSelector } protected readInputConfig(): void { - const principalTypeConfig: Record[] = this.context.inputConfig['principalType'] || []; + const principalTypeConfig: Record[] = this.context.inputConfig['principalType'] || []; this.principalTypes = [].concat(principalTypeConfig) .map((cfg: any) => { @@ -131,7 +135,7 @@ export class PrincipalSelector }) .filter((val) => val !== null); - const skipPrincipalsConfig: Record[] = this.context.inputConfig['skipPrincipals'] || []; + const skipPrincipalsConfig: Record[] = this.context.inputConfig['skipPrincipals'] || []; this.skipPrincipals = [].concat(skipPrincipalsConfig) .map((cfg: any) => { diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/radiobutton/RadioButton.ts b/src/main/resources/assets/admin/common/js/form/inputtype/radiobutton/RadioButton.ts index 23663b88d..a777dd793 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/radiobutton/RadioButton.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/radiobutton/RadioButton.ts @@ -108,18 +108,25 @@ export class RadioButton protected readInputConfig(): void { const options: RadioButtonOption[] = []; - const optionValues: Record[] = this.context.inputConfig['option'] || []; + const optionValues: Record[] = this.context.inputConfig['options'] || []; const l: number = optionValues.length; - let optionValue: Record; + let optionValue: Record; for (let i = 0; i < l; i++) { optionValue = optionValues[i]; - options.push({label: optionValue['value'], value: optionValue['@value']}); + options.push({label: optionValue['value'] as string, value: optionValue['@value'] as string}); } this.radioButtonOptions = options; } + createDefaultValue(rawValue: unknown): Value { + if (typeof rawValue !== 'string') { + return this.getValueType().newNullValue(); + } + return this.getValueType().newValue(rawValue); + } + private createRadioElement(name: string, property: Property): RadioGroup { const value: string = property?.hasNonNullValue ? property.getString() : undefined; const radioGroup = new RadioGroup(name, value); diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputType.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputType.ts index afab9616c..c1f537235 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputType.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputType.ts @@ -62,6 +62,22 @@ export abstract class BaseInputType extends DivEl abstract getValueType(): ValueType; + getRawDefaultValue(): unknown { + return this.context.inputConfig['default']?.[0]?.value ?? null; + } + + getDefaultValue(): Value { + const raw = this.getRawDefaultValue(); + + if (raw == null) { + return this.getValueType().newNullValue(); + } + + return this.createDefaultValue(raw); + } + + abstract createDefaultValue(raw: unknown): Value; + hasValidUserInput(): boolean { return true; } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts index 9d2dec87a..cafae7343 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/BaseInputTypeNotManagingAdd.ts @@ -329,7 +329,7 @@ export abstract class BaseInputTypeNotManagingAdd abstract setEnabledInputOccurrenceElement(_occurrence: Element, enable: boolean); newInitialValue(): Value { - return this.input?.getDefaultValue() || this.newValueTypeInitialValue(); + return this.getDefaultValue() || this.newValueTypeInitialValue(); } protected newValueTypeInitialValue(): Value { diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/support/NoInputTypeFoundView.ts b/src/main/resources/assets/admin/common/js/form/inputtype/support/NoInputTypeFoundView.ts index 24c9e607f..c1ba1df93 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/support/NoInputTypeFoundView.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/support/NoInputTypeFoundView.ts @@ -22,6 +22,13 @@ export class NoInputTypeFoundView return ValueTypes.STRING; } + createDefaultValue(rawValue: unknown): Value { + if (typeof rawValue !== 'string') { + return this.getValueType().newNullValue(); + } + return this.getValueType().newValue(rawValue); + } + layout(input: Input, property?: PropertyArray): Q.Promise { const divEl: DivEl = new DivEl(); divEl.getEl().setInnerHtml('Warning: no input type found: ' + input.getInputType().toString()); diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts b/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts index e27b5392b..beb313e69 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/text/TextInputType.ts @@ -9,7 +9,6 @@ import {FormInputEl} from '../../../dom/FormInputEl'; import {Locale} from '../../../locale/Locale'; import {TextInput} from '../../../ui/text/TextInput'; import {i18n} from '../../../util/Messages'; -import {NumberHelper} from '../../../util/NumberHelper'; import {StringHelper} from '../../../util/StringHelper'; import {ValueChangedEvent} from '../../../ValueChangedEvent'; import {AdditionalValidationRecord} from '../../AdditionalValidationRecord'; @@ -34,14 +33,20 @@ export abstract class TextInputType return ValueTypes.STRING; } - protected readConfig(inputConfig: Record[]>): void { + createDefaultValue(rawValue: unknown): Value { + if (typeof rawValue !== 'string') { + return this.getValueType().newNullValue(); + } + return this.getValueType().newValue(rawValue); + } + + protected readConfig(inputConfig: Record[]>): void { const maxLengthConfig: object = inputConfig['maxLength'] ? inputConfig['maxLength'][0] : {}; - const maxLength: number = NumberHelper.toNumber(maxLengthConfig['value']); + const maxLength: number = maxLengthConfig['value']; this.maxLength = maxLength > 0 ? maxLength : -1; const showCounterConfig: object = inputConfig['showCounter'] ? inputConfig['showCounter'][0] : {}; - const value: string = showCounterConfig['value'] || ''; - this.showTotalCounter = value.toLowerCase() === 'true'; + this.showTotalCounter = showCounterConfig['value'] || false; } protected updateFormInputElValue(occurrence: FormInputEl, property: Property) { diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/text/TextLine.ts b/src/main/resources/assets/admin/common/js/form/inputtype/text/TextLine.ts index f5d64942f..eb4dd7d51 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/text/TextLine.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/text/TextLine.ts @@ -52,13 +52,12 @@ export class TextLine input.setEnabled(enable); } - protected readConfig(inputConfig: Record[]>): void { + protected readConfig(inputConfig: Record[]>): void { super.readConfig(inputConfig); const regexpConfig = inputConfig['regexp'] ? inputConfig['regexp'][0] : {}; - const regexp = regexpConfig ? regexpConfig['value'] : ''; + const regexp = regexpConfig ? regexpConfig['value'] as string : ''; this.regexp = !StringHelper.isBlank(regexp) ? new RegExp(regexp) : null; - } protected updateValidationStatusOnUserInput(inputEl: TextInput, isValid: boolean) { diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/time/Date.ts b/src/main/resources/assets/admin/common/js/form/inputtype/time/Date.ts index 3ae0818ab..8d305bd40 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/time/Date.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/time/Date.ts @@ -12,6 +12,7 @@ import {Class} from '../../../Class'; import {ValueTypeConverter} from '../../../data/ValueTypeConverter'; import {AdditionalValidationRecord} from '../../AdditionalValidationRecord'; import {i18n} from '../../../util/Messages'; +import {RelativeTimeParser} from './RelativeTimeParser'; /** * Uses [[ValueType]] [[ValueTypeLocalDate]]. @@ -19,8 +20,23 @@ import {i18n} from '../../../util/Messages'; export class DateType extends BaseInputTypeNotManagingAdd { - getDefaultValue(): Date { - return this.getContext().input.getDefaultValue()?.getDateTime()?.toDate(); + private static readonly PATTERN = /^\d{4}-\d{2}-\d{2}$/; + + resolveDefaultValue(): Date { + return this.getDefaultValue()?.getDateTime()?.toDate(); + } + + createDefaultValue(rawValue: unknown): Value { + if (typeof rawValue !== 'string') { + return this.getValueType().newNullValue(); + } + + if (DateType.PATTERN.test(rawValue)) { + return this.getValueType().newValue(rawValue); + } else { + const value = LocalDate.fromDate(RelativeTimeParser.parseToDate(rawValue)); + return new Value(value, ValueTypes.LOCAL_DATE); + } } getValueType(): ValueType { @@ -39,7 +55,7 @@ export class DateType datePickerBuilder.setDateTime(date.toDate()); } - const defaultDate: Date = this.getDefaultValue(); + const defaultDate: Date = this.resolveDefaultValue(); if (defaultDate) { datePickerBuilder.setDefaultValue(defaultDate); } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/time/DateTime.ts b/src/main/resources/assets/admin/common/js/form/inputtype/time/DateTime.ts index c153dae7b..4f45edffb 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/time/DateTime.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/time/DateTime.ts @@ -10,12 +10,10 @@ import {SelectedDateChangedEvent} from '../../../ui/time/SelectedDateChangedEven import {LocalDateTime} from '../../../util/LocalDateTime'; import {InputTypeManager} from '../InputTypeManager'; import {Class} from '../../../Class'; -import {DateTime as DateTimeUtil} from '../../../util/DateTime'; import {ValueTypeConverter} from '../../../data/ValueTypeConverter'; import {AdditionalValidationRecord} from '../../AdditionalValidationRecord'; import {i18n} from '../../../util/Messages'; -import {ValueTypeDateTime} from '../../../data/ValueTypeDateTime'; -import {ObjectHelper} from '../../../ObjectHelper'; +import {RelativeTimeParser} from './RelativeTimeParser'; /** * Uses [[ValueType]] [[ValueTypeLocalDateTime]]. @@ -23,6 +21,8 @@ import {ObjectHelper} from '../../../ObjectHelper'; export class DateTime extends BaseInputTypeNotManagingAdd { + private static readonly PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?$/; + private valueType: ValueType = ValueTypes.LOCAL_DATE_TIME; constructor(config: InputTypeViewContext) { @@ -30,8 +30,27 @@ export class DateTime this.readConfig(config.inputConfig); } - getDefaultValue(): Date { - return this.getContext().input.getDefaultValue()?.getDateTime()?.toDate(); + createDefaultValue(rawValue: unknown): Value { + if (typeof rawValue !== 'string') { + return this.getValueType().newNullValue(); + } + + if (DateTime.PATTERN.test(rawValue)) { + return this.getValueType().newValue(rawValue); + } + + const value = LocalDateTime.fromDate(RelativeTimeParser.parseToDateTime(rawValue)); + return new Value(value, ValueTypes.LOCAL_DATE_TIME); + } + + resolveDefaultValue(): Date { + const defaultValue = this.getDefaultValue(); + + if (defaultValue?.isNull()) { + return null; + } + + return defaultValue.getDateTime().toDate(); } getValueType(): ValueType { @@ -40,12 +59,11 @@ export class DateTime createInputOccurrenceElement(_index: number, property: Property): Element { const valueType: ValueType = this.getValueType(); - const useLocalTimeZone: boolean = ObjectHelper.iFrameSafeInstanceOf(valueType, ValueTypeDateTime); const dateTimeBuilder: DateTimePickerBuilder = new DateTimePickerBuilder(); - dateTimeBuilder.setUseLocalTimezone(useLocalTimeZone); + dateTimeBuilder.setUseLocalTimezone(false); - const defaultDate: Date = this.getDefaultValue(); + const defaultDate: Date = this.resolveDefaultValue(); if (defaultDate) { dateTimeBuilder.setDefaultValue(defaultDate); } @@ -55,7 +73,7 @@ export class DateTime } if (property.hasNonNullValue()) { - const date: DateTimeUtil | LocalDateTime = useLocalTimeZone ? property.getDateTime() : property.getLocalDateTime(); + const date: LocalDateTime = property.getLocalDateTime(); dateTimeBuilder.setDateTime(date.toDate()); } @@ -74,10 +92,8 @@ export class DateTime if (!unchangedOnly || !dateTimePicker.isDirty()) { const date = property.hasNonNullValue() - ? this.getValueType() === ValueTypes.DATE_TIME - ? property.getDateTime().toDate() - : property.getLocalDateTime().toDate() - : null; + ? property.getLocalDateTime().toDate() + : null; dateTimePicker.setDateTime(date); } else if (dateTimePicker.isDirty()) { dateTimePicker.forceSelectedDateTimeChangedEvent(); @@ -113,19 +129,12 @@ export class DateTime } } - private readConfig(inputConfig: Record[]>): void { - const timeZoneConfig = inputConfig['timezone'] && inputConfig['timezone'][0]; - const timeZone = timeZoneConfig && timeZoneConfig['value']; - - if (timeZone === 'true') { - this.valueType = ValueTypes.DATE_TIME; - } + private readConfig(inputConfig: Record[]>): void { + // do nothing } protected getValue(inputEl: Element, event: SelectedDateChangedEvent): Value { - return new Value(event.getDate() != null ? this.getValueType() === ValueTypes.LOCAL_DATE_TIME - ? LocalDateTime.fromDate(event.getDate()) - : DateTimeUtil.fromDate(event.getDate()) : null, this.getValueType()); + return new Value(event.getDate() != null ? LocalDateTime.fromDate(event.getDate()) : null, this.getValueType()); } } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/time/DateTimeRange.ts b/src/main/resources/assets/admin/common/js/form/inputtype/time/DateTimeRange.ts index f644a808c..12f302e8b 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/time/DateTimeRange.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/time/DateTimeRange.ts @@ -17,13 +17,11 @@ import {Class} from '../../../Class'; import {InputTypeName} from '../../InputTypeName'; import {TimeHM} from '../../../util/TimeHM'; import {ObjectHelper} from '../../../ObjectHelper'; -import {ArrayHelper} from '../../../util/ArrayHelper'; declare const Date: DateConstructor; export class DateTimeRange extends BaseInputTypeNotManagingAdd { - private useTimezone: boolean; private from: DateTime | LocalDateTime; private to: DateTime | LocalDateTime; @@ -54,6 +52,10 @@ export class DateTimeRange return new InputTypeName('DateTimeRange', false); } + createDefaultValue(rawValue: unknown): Value { + return this.getValueType().newNullValue(); + } + getValueType(): ValueType { return ValueTypes.DATA; } diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/time/Instant.ts b/src/main/resources/assets/admin/common/js/form/inputtype/time/Instant.ts new file mode 100644 index 000000000..da3abb9d0 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/form/inputtype/time/Instant.ts @@ -0,0 +1,145 @@ +import {Property} from '../../../data/Property'; +import {Value} from '../../../data/Value'; +import {ValueType} from '../../../data/ValueType'; +import {ValueTypes} from '../../../data/ValueTypes'; +import {DateTimePicker, DateTimePickerBuilder} from '../../../ui/time/DateTimePicker'; +import {BaseInputTypeNotManagingAdd} from '../support/BaseInputTypeNotManagingAdd'; +import {InputTypeViewContext} from '../InputTypeViewContext'; +import {Element} from '../../../dom/Element'; +import {SelectedDateChangedEvent} from '../../../ui/time/SelectedDateChangedEvent'; +import {InputTypeManager} from '../InputTypeManager'; +import {Class} from '../../../Class'; +import {Instant as InstantUtil} from '../../../util/Instant'; +import {ValueTypeConverter} from '../../../data/ValueTypeConverter'; +import {AdditionalValidationRecord} from '../../AdditionalValidationRecord'; +import {i18n} from '../../../util/Messages'; +import {RelativeTimeParser} from './RelativeTimeParser'; + +/** + * Uses [[ValueType]] [[ValueTypeInstant]]. + */ +export class Instant + extends BaseInputTypeNotManagingAdd { + + private static readonly PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?Z$/; + + private valueType: ValueType = ValueTypes.INSTANT; + + constructor(config: InputTypeViewContext) { + super(config); + this.readConfig(config.inputConfig); + } + + createDefaultValue(rawValue: unknown): Value { + if (typeof rawValue !== 'string') { + return this.getValueType().newNullValue(); + } + + if (Instant.PATTERN.test(rawValue)) { + return this.getValueType().newValue(rawValue); + } else { + const value = InstantUtil.fromDate(RelativeTimeParser.parseToInstant(rawValue)); + return new Value(value, ValueTypes.INSTANT); + } + } + + resolveDefaultValue(): Date { + const defaultValue = this.getDefaultValue(); + + if (defaultValue?.isNull()) { + return null; + } + + return defaultValue.getInstant().toDate(); + } + + getValueType(): ValueType { + return this.valueType; + } + + createInputOccurrenceElement(_index: number, property: Property): Element { + const valueType: ValueType = this.getValueType(); + + const dateTimeBuilder: DateTimePickerBuilder = new DateTimePickerBuilder(); + dateTimeBuilder.setUseLocalTimezone(true); + + const defaultDate: Date = this.resolveDefaultValue(); + if (defaultDate) { + dateTimeBuilder.setDefaultValue(defaultDate); + } + + if (!valueType.equals(property.getType())) { + ValueTypeConverter.convertPropertyValueType(property, valueType); + } + + if (property.hasNonNullValue()) { + dateTimeBuilder.setDateTime(property.getInstant().toDate()); + } + + const dateTimePicker: DateTimePicker = dateTimeBuilder.build(); + + dateTimePicker.onSelectedDateTimeChanged((event: SelectedDateChangedEvent) => + this.handleOccurrenceInputValueChanged(dateTimePicker, event) + ); + + return dateTimePicker; + } + + updateInputOccurrenceElement(occurrence: Element, property: Property, unchangedOnly: boolean) { + const dateTimePicker: DateTimePicker = occurrence as DateTimePicker; + + if (!unchangedOnly || !dateTimePicker.isDirty()) { + + const date = property.hasNonNullValue() + ? property.getInstant().toDate() + : null; + dateTimePicker.setDateTime(date); + } else if (dateTimePicker.isDirty()) { + dateTimePicker.forceSelectedDateTimeChangedEvent(); + } + } + + resetInputOccurrenceElement(occurrence: Element): void { + super.resetInputOccurrenceElement(occurrence); + + const input: DateTimePicker = occurrence as DateTimePicker; + input.resetBase(); + } + + clearInputOccurrenceElement(occurrence: Element): void { + super.clearInputOccurrenceElement(occurrence); + (occurrence as DateTimePicker).clear(); + } + + setEnabledInputOccurrenceElement(occurrence: Element, enable: boolean) { + const input: DateTimePicker = occurrence as DateTimePicker; + + input.setEnabled(enable); + } + + doValidateUserInput(inputEl: DateTimePicker) { + super.doValidateUserInput(inputEl); + + if (!inputEl.isValid()) { + const record: AdditionalValidationRecord = + AdditionalValidationRecord.create().setMessage(i18n('field.value.invalid')).build(); + + this.occurrenceValidationState.get(inputEl.getId()).addAdditionalValidation(record); + } + } + + private readConfig(inputConfig: Record[]>): void { + // do nothing + } + + protected getValue(inputEl: Element, event: SelectedDateChangedEvent): Value { + const date = event.getDate(); + if (!date) { + return new Value(null, this.getValueType()); + } + + return new Value(InstantUtil.fromDate(date), this.getValueType()); + } +} + +InputTypeManager.register(new Class('Instant', Instant), true); diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/time/RelativeTimeParser.ts b/src/main/resources/assets/admin/common/js/form/inputtype/time/RelativeTimeParser.ts new file mode 100644 index 000000000..bd1592f9b --- /dev/null +++ b/src/main/resources/assets/admin/common/js/form/inputtype/time/RelativeTimeParser.ts @@ -0,0 +1,60 @@ +import dayjs, {Dayjs} from 'dayjs'; +import {Instant} from '../../../util/Instant'; +import {DateTime} from '../../../util/DateTime'; +import {DateHelper} from '../../../util/DateHelper'; +import {LocalDate} from '../../../util/LocalDate'; + +export class RelativeTimeParser { + + private static parseRelative( + expr: string, + factory: (iso: string) => { toDate(): Date }, + omitTimezone = true, + mode: 'datetime' | 'date' | 'time' = 'datetime' + ): Date { + const base = dayjs(); + + if (!expr || expr.trim() === 'now') { + return DateHelper.isoValueToDate(base, factory, omitTimezone, mode); + } + + const result: Dayjs = expr + .trim() + .split(/\s+/) + .reduce((date: Dayjs, token: string) => { + const match = token.match(/^([+-])(\d+)([a-zA-Z]+)$/); + if (!match) { + return date; + } + + const [, sign, value, unit] = match; + + return sign === '+' + ? date.add(Number(value), unit as dayjs.ManipulateType) + : date.subtract(Number(value), unit as dayjs.ManipulateType); + }, base); + + return DateHelper.isoValueToDate(result, factory, omitTimezone, mode); + } + + static parseToDateTime(expr?: string): Date { + return this.parseRelative(expr, DateTime.fromString, true); + } + + static parseToInstant(expr?: string): Date { + return this.parseRelative(expr, Instant.fromString, false); + } + + static parseToDate(expr?: string): Date { + return this.parseRelative(expr, LocalDate.fromISOString, true, 'date'); + } + + static parseToTime(expr?: string): Date { + return this.parseRelative(expr, (iso: string) => ({ + toDate: () => { + const time = DateHelper.parseTime(iso); + return DateHelper.dateFromTime(time.hours, time.minutes); + } + }), true, 'time'); + } +} diff --git a/src/main/resources/assets/admin/common/js/form/inputtype/time/Time.ts b/src/main/resources/assets/admin/common/js/form/inputtype/time/Time.ts index e27028b3e..bfd01f3c8 100644 --- a/src/main/resources/assets/admin/common/js/form/inputtype/time/Time.ts +++ b/src/main/resources/assets/admin/common/js/form/inputtype/time/Time.ts @@ -14,6 +14,7 @@ import {AdditionalValidationRecord} from '../../AdditionalValidationRecord'; import {i18n} from '../../../util/Messages'; import {TimeHMS} from '../../../util/TimeHMS'; import {TimeHM} from '../../../util/TimeHM'; +import {RelativeTimeParser} from './RelativeTimeParser'; /** * Uses [[ValueType]] [[ValueTypeLocalTime]]. @@ -21,11 +22,14 @@ import {TimeHM} from '../../../util/TimeHM'; export class Time extends BaseInputTypeNotManagingAdd { - getDefaultValue(): Date { - const defaultTime: LocalTime = this.getContext().input.getDefaultValue()?.getLocalTime(); + private static readonly PATTERN = /^\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?$/; + + resolveDefaultValue(): Date { + const defaultTime: LocalTime = this.getDefaultValue()?.getLocalTime(); if (!defaultTime) { return null; } + const result: Date = new Date(); result.setHours(defaultTime.getHours()); result.setMinutes(defaultTime.getMinutes()); @@ -33,6 +37,19 @@ export class Time return result; } + createDefaultValue(rawValue: unknown): Value { + if (typeof rawValue !== 'string') { + return this.getValueType().newNullValue(); + } + + if (Time.PATTERN.test(rawValue)) { + return this.getValueType().newValue(rawValue); + } else { + const value = LocalTime.fromDate(RelativeTimeParser.parseToTime(rawValue)); + return new Value(value, ValueTypes.LOCAL_TIME); + } + } + getValueType(): ValueType { return ValueTypes.LOCAL_TIME; } @@ -48,7 +65,7 @@ export class Time timePickerBuilder.setHours(value.hours).setMinutes(value.minutes); } - const defaultDate: Date = this.getDefaultValue(); + const defaultDate: Date = this.resolveDefaultValue(); if (defaultDate) { timePickerBuilder.setDefaultValue(defaultDate); } diff --git a/src/main/resources/assets/admin/common/js/form/json/InputJson.ts b/src/main/resources/assets/admin/common/js/form/json/InputJson.ts index e3b8822ce..1113c7542 100644 --- a/src/main/resources/assets/admin/common/js/form/json/InputJson.ts +++ b/src/main/resources/assets/admin/common/js/form/json/InputJson.ts @@ -6,25 +6,11 @@ export interface InputJson helpText?: string; - immutable?: boolean; - - indexed?: boolean; - label: string; occurrences: OccurrencesJson; - validationRegexp?: string; - inputType: string; config?: any; - - maximizeUIInputWidth?: boolean; - - defaultValue?: { - type: string; - value: any; - }; - } diff --git a/src/main/resources/assets/admin/common/js/lib.ts b/src/main/resources/assets/admin/common/js/lib.ts index 6322a61c3..b601c2f55 100644 --- a/src/main/resources/assets/admin/common/js/lib.ts +++ b/src/main/resources/assets/admin/common/js/lib.ts @@ -17,6 +17,7 @@ import './form/inputtype/time/Date'; import './form/inputtype/time/DateTime'; import './form/inputtype/time/DateTimeRange'; import './form/inputtype/time/Time'; +import './form/inputtype/time/Instant'; const hasJQuery = Store.instance().has('$'); if (!hasJQuery) { diff --git a/src/main/resources/assets/admin/common/js/util/DateHelper.ts b/src/main/resources/assets/admin/common/js/util/DateHelper.ts index 0549be0f3..5573fa95b 100644 --- a/src/main/resources/assets/admin/common/js/util/DateHelper.ts +++ b/src/main/resources/assets/admin/common/js/util/DateHelper.ts @@ -1,6 +1,7 @@ import {i18n} from './Messages'; import {TimeHM} from './TimeHM'; import {LongTimeHMS} from './LongTimeHMS'; +import {Dayjs} from 'dayjs'; export class DateHelper { public static DATE_SEPARATOR: string = '-'; @@ -354,4 +355,43 @@ export class DateHelper { return new LongTimeHMS(hours, minutes, seconds, fractions); } + + public static isoValueToDate( + value: Dayjs | { + year: () => number; + month: () => number; + date: () => number; + hour: () => number; + minute: () => number; + second: () => number; + millisecond: () => number + }, + factory: (iso: string) => { toDate(): Date }, + omitTimezone = false, + mode: 'datetime' | 'date' | 'time' = 'datetime' + ): Date { + let isoString: string; + + if (omitTimezone) { + const y = value.year().toString().padStart(4, '0'); + const m = (value.month() + 1).toString().padStart(2, '0'); // month() 0-based + const d = value.date().toString().padStart(2, '0'); + const h = value.hour().toString().padStart(2, '0'); + const min = value.minute().toString().padStart(2, '0'); + const s = value.second().toString().padStart(2, '0'); + const ms = value.millisecond() ? `.${value.millisecond().toString().padStart(3, '0')}` : ''; + + if (mode === 'date') { + isoString = `${y}-${m}-${d}`; + } else if (mode === 'time') { + isoString = `${h}:${min}`; + } else { + isoString = `${y}-${m}-${d}T${h}:${min}:${s}${ms}`; + } + } else { + isoString = (value as Dayjs).toISOString(); + } + + return factory(isoString).toDate(); + } } diff --git a/src/main/resources/assets/admin/common/js/util/DateTime.ts b/src/main/resources/assets/admin/common/js/util/DateTime.ts index 8cb0251f3..43e5eff41 100644 --- a/src/main/resources/assets/admin/common/js/util/DateTime.ts +++ b/src/main/resources/assets/admin/common/js/util/DateTime.ts @@ -7,6 +7,8 @@ import {DateHelper} from './DateHelper'; export class DateTime implements Equitable { + private static readonly PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/; + private static DATE_TIME_SEPARATOR: string = 'T'; private static DATE_SEPARATOR: string = '-'; @@ -29,8 +31,6 @@ export class DateTime private readonly fractions: number; - private readonly timezone: Timezone; - constructor(builder: DateTimeBuilder) { this.year = builder.year; this.month = builder.month; @@ -39,24 +39,14 @@ export class DateTime this.minutes = builder.minutes; this.seconds = builder.seconds; this.fractions = builder.fractions; - this.timezone = builder.timezone || Timezone.getZeroOffsetTimezone(); } static isValidDateTime(s: string): boolean { if (StringHelper.isBlank(s)) { return false; } - /* - matches: - 2015-02-29T12:05 - 2015-02-29T12:05:59 - 2015-02-29T12:05:59Z - 2015-02-29T12:05:59+01:00 - 2015-02-29T12:05:59.001+01:00 - */ - - const regex = /^(\d{2}|\d{4})(?:\-)?([0]{1}\d{1}|[1]{1}[0-2]{1})(?:\-)?([0-2]{1}\d{1}|[3]{1}[0-1]{1})(T)([0-1]{1}\d{1}|[2]{1}[0-3]{1})(?::)?([0-5]{1}\d{1})((:[0-5]{1}\d{1})(\.\d{3})?)?((\+|\-)([0-1]{1}\d{1}|[2]{1}[0-3]{1})(:)([0-5]{1}\d{1})|(z|Z)|$)$/; - return regex.test(s); + + return this.PATTERN.test(s); } /** @@ -70,11 +60,9 @@ export class DateTime } let date; - let timezone; if (DateHelper.isUTCdate(s)) { date = DateHelper.makeDateFromUTCString(s); - timezone = Timezone.getDateTimezone(date); if (DateHelper.isDST(date)) { // when converting from UTC date, Date object may have an extra hour added due to DST date.setHours(date.getHours() - 1); } @@ -86,30 +74,13 @@ export class DateTime DateTime.TIME_SEPARATOR, DateTime.FRACTION_SEPARATOR ); - let offset = DateTime.parseOffset(s); - if (offset != null) { - timezone = Timezone.fromOffset(offset); - } else { - // assume that if passed date string is not in UTC format and does not contain explicit offset, - // like '2015-02-29T12:05:59' - use zero offset timezone - timezone = Timezone.getZeroOffsetTimezone(); - } } if (!date) { throw new Error('Cannot parse DateTime from string: ' + s); } - return DateTime.create() - .setYear(date.getFullYear()) - .setMonth(date.getMonth()) - .setDay(date.getDate()) - .setHours(date.getHours()) - .setMinutes(date.getMinutes()) - .setSeconds(date.getSeconds()) - .setFractions(date.getMilliseconds()) - .setTimezone(timezone) - .build(); + return DateTime.fromDate(date); } static fromDate(s: Date): DateTime { @@ -121,7 +92,6 @@ export class DateTime .setMinutes(s.getMinutes()) .setSeconds(s.getSeconds()) .setFractions(s.getMilliseconds()) - .setTimezone(Timezone.getDateTimezone(s))// replace with timezone picker value if implemented tz selection .build(); } @@ -129,42 +99,6 @@ export class DateTime return new DateTimeBuilder(); } - private static parseOffset(value: string): number { - if (DateHelper.isUTCdate(value)) { - return 0; - } else { - const dateStr = (value || '').trim(); - - if (dateStr.indexOf('+') > 0) { // case with positive offset - const parts = dateStr.split('+'); - if (parts.length === 2) { - const offsetPart = parts[1]; - - const offset = parseFloat(offsetPart); - if (isNaN(offset)) { - return 0; - } - - return offset; - } else { - return 0; - } - } else if (dateStr.split('-').length === 4) { // case with negative offset ('2015-02-29T12:05:59-01:00') - const parts = dateStr.split('-'); - const offsetPart = parts[3]; - - const offset = parseFloat(offsetPart); - if (isNaN(offset)) { - return 0; - } - - return -offset; - } else { - return 0; - } - } - } - private static trimTZ(dateString: string): string { let tzStartIndex = dateString.indexOf('+'); if (tzStartIndex > 0) { @@ -210,10 +144,6 @@ export class DateTime return this.fractions || 0; } - getTimezone(): Timezone { - return this.timezone; - } - dateToString(): string { return this.year + DateTime.DATE_SEPARATOR + this.padNumber(this.month + 1) + @@ -230,7 +160,7 @@ export class DateTime /** Returns date in ISO format. Month value is incremented because ISO month range is 1-12, whereas JS Date month range is 0-11 */ toString(): string { - return this.dateToString() + DateTime.DATE_TIME_SEPARATOR + this.timeToString() + this.timezone.toString(); + return this.dateToString() + DateTime.DATE_TIME_SEPARATOR + this.timeToString(); } equals(o: Equitable): boolean { @@ -323,11 +253,6 @@ export class DateTimeBuilder { return this; } - public setTimezone(value: Timezone): DateTimeBuilder { - this.timezone = value; - return this; - } - public build(): DateTime { return new DateTime(this); } diff --git a/src/main/resources/assets/admin/common/js/util/Instant.ts b/src/main/resources/assets/admin/common/js/util/Instant.ts new file mode 100644 index 000000000..60218f9a4 --- /dev/null +++ b/src/main/resources/assets/admin/common/js/util/Instant.ts @@ -0,0 +1,218 @@ +import {Equitable} from '../Equitable'; +import {StringHelper} from './StringHelper'; +import {ObjectHelper} from '../ObjectHelper'; + +export class Instant + implements Equitable { + + private static readonly PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?Z$/; + + private static DATE_TIME_SEPARATOR: string = 'T'; + + private static DATE_SEPARATOR: string = '-'; + + private static TIME_SEPARATOR: string = ':'; + + private static FRACTION_SEPARATOR: string = '.'; + + private readonly year: number; + + private readonly month: number; // 0-11 + + private readonly day: number; + + private readonly hours: number; + + private readonly minutes: number; + + private readonly seconds: number; + + private readonly fractions: number; + + constructor(builder: InstantBuilder) { + this.year = builder.year; + this.month = builder.month; + this.day = builder.day; + this.hours = builder.hours; + this.minutes = builder.minutes; + this.seconds = builder.seconds; + this.fractions = builder.fractions; + } + + static isValidInstant(s: string): boolean { + if (StringHelper.isBlank(s)) { + return false; + } + + return this.PATTERN.test(s); + } + + /** + * Parses passed string into Instant object + * @param s - date to parse in ISO 8601 instant format (UTC, with 'Z') + * @returns {Instant} + */ + static fromString(s: string): Instant { + if (!Instant.isValidInstant(s)) { + throw new Error('Cannot parse Instant from string: ' + s); + } + + const date = new Date(s); + + if (isNaN(date.getTime())) { + throw new Error('Invalid date string for Instant: ' + s); + } + + return Instant.fromDate(date); + } + + static fromDate(s: Date): Instant { + return Instant.create() + .setYear(s.getUTCFullYear()) + .setMonth(s.getUTCMonth()) + .setDay(s.getUTCDate()) + .setHours(s.getUTCHours()) + .setMinutes(s.getUTCMinutes()) + .setSeconds(s.getUTCSeconds()) + .setFractions(s.getUTCMilliseconds()) + .build(); + } + + public static create(): InstantBuilder { + return new InstantBuilder(); + } + + getYear(): number { + return this.year; + } + + getMonth(): number { + return this.month; + } + + getDay(): number { + return this.day; + } + + getHours(): number { + return this.hours; + } + + getMinutes(): number { + return this.minutes; + } + + getSeconds(): number { + return this.seconds || 0; + } + + getFractions(): number { + return this.fractions || 0; + } + + dateToString(): string { + return this.year + + Instant.DATE_SEPARATOR + this.padNumber(this.month + 1) + + Instant.DATE_SEPARATOR + this.padNumber(this.day); + } + + timeToString(): string { + let fractions = this.fractions + ? Instant.FRACTION_SEPARATOR + this.fractions.toString().padStart(3, '0') + : StringHelper.EMPTY_STRING; + + return this.padNumber(this.hours) + Instant.TIME_SEPARATOR + + this.padNumber(this.minutes) + Instant.TIME_SEPARATOR + + this.padNumber(this.seconds ? this.seconds : 0) + fractions; + } + + toString(): string { + return this.dateToString() + Instant.DATE_TIME_SEPARATOR + this.timeToString() + 'Z'; + } + + equals(o: Equitable): boolean { + if (!ObjectHelper.iFrameSafeInstanceOf(o, Instant)) { + return false; + } + + let other = o as Instant; + + if (!ObjectHelper.stringEquals(this.toString(), other.toString())) { + return false; + } + + return true; + } + + toDate(): Date { + return new Date(this.toString()); + } + + private padNumber(num: number, length: number = 2): string { + let numAsString = String(num); + + while (numAsString.length < length) { + numAsString = '0' + numAsString; + } + + return numAsString; + } +} + +export class InstantBuilder { + + year: number; + + month: number; + + day: number; + + hours: number; + + minutes: number; + + seconds: number; + + fractions: number; + + public setYear(value: number): InstantBuilder { + this.year = value; + return this; + } + + public setMonth(value: number): InstantBuilder { + this.month = value; + return this; + } + + public setDay(value: number): InstantBuilder { + this.day = value; + return this; + } + + public setHours(value: number): InstantBuilder { + this.hours = value; + return this; + } + + public setMinutes(value: number): InstantBuilder { + this.minutes = value; + return this; + } + + public setSeconds(value: number): InstantBuilder { + this.seconds = value; + return this; + } + + public setFractions(value: number): InstantBuilder { + if (value > 0) { + this.fractions = value; + } + return this; + } + + public build(): Instant { + return new Instant(this); + } +}