diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index bea65cdbf..952e85a29 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -340,6 +340,9 @@ "common.workflows": "Workflows", "common.yes": "Yes", "contents.arrayAddItem": "Add Item", + "contents.arrayClear": "Clear", + "contents.arrayClearConfirmText": "Do you really want to clear the array?", + "contents.arrayClearConfirmTitle": "Clear array", "contents.arrayCloneItem": "Clone this item", "contents.arrayCollapseAll": "Collapse all items", "contents.arrayCollapseItem": "Collapse this item", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 3b9a5e4b2..69f1c726e 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -340,6 +340,9 @@ "common.workflows": "Workflow", "common.yes": "Si", "contents.arrayAddItem": "Aggiungi un elemento", + "contents.arrayClear": "Clear", + "contents.arrayClearConfirmText": "Do you really want to clear the array?", + "contents.arrayClearConfirmTitle": "Clear array", "contents.arrayCloneItem": "Clona questo elemento", "contents.arrayCollapseAll": "Comprimi tutti gli elementi", "contents.arrayCollapseItem": "Comprimi l'elemento", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index b608d6b5d..0583d6d93 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -340,6 +340,9 @@ "common.workflows": "Workflows", "common.yes": "Ja", "contents.arrayAddItem": "Item toevoegen", + "contents.arrayClear": "Clear", + "contents.arrayClearConfirmText": "Do you really want to clear the array?", + "contents.arrayClearConfirmTitle": "Clear array", "contents.arrayCloneItem": "Kloon dit item", "contents.arrayCollapseAll": "Collapse all items", "contents.arrayCollapseItem": "Collapse this item", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index ac1ece3ff..8eb1fde46 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -172,6 +172,7 @@ "contents.validation.normalCharactersBetween": "Must have between {min} and {max} text character(s).", "contents.validation.notAllowed": "Not an allowed value.", "contents.validation.pattern": "Must follow the pattern.", + "contents.validation.reference": "Geolocation can only have latitude and longitude property.", "contents.validation.referenceNotFound": "Reference '{id}' not found.", "contents.validation.referenceToInvalidSchema": "Reference '{id}' has invalid schema.", "contents.validation.regexTooSlow": "Regex is too slow.", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index bea65cdbf..952e85a29 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -340,6 +340,9 @@ "common.workflows": "Workflows", "common.yes": "Yes", "contents.arrayAddItem": "Add Item", + "contents.arrayClear": "Clear", + "contents.arrayClearConfirmText": "Do you really want to clear the array?", + "contents.arrayClearConfirmTitle": "Clear array", "contents.arrayCloneItem": "Clone this item", "contents.arrayCollapseAll": "Collapse all items", "contents.arrayCollapseItem": "Collapse this item", diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index 056aa9eb2..1c5181353 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -601,6 +601,9 @@ Deve seguire il pattern. + + Geolocation can only have latitude and longitude property. + Contiene un collegamento '{id}' non valido. @@ -938,10 +941,10 @@ Sebastian Stehle and Contributors, 2016-2021 - With your setup, only admins can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=false</code> as environment variable. + With your setup, only admins can create new apps. If you want to change this set <code>UI__ONLYADMINSCANCREATEAPPS=false</code> as environment variable. - With your setup, every user can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=true</code> as environment variable. + With your setup, every user can create new apps. If you want to change this set <code>UI__ONLYADMINSCANCREATEAPPS=true</code> as environment variable. You are using the <strong>folder asset store</strong> where all assets are stored in the file system. Please remember to include the asset folder into your backup strategy and map it to a volume, if you are using Docker. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index d3fc592a5..5cc43d694 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -601,6 +601,9 @@ Moet het patroon volgen. + + Geolocation can only have latitude and longitude property. + Bevat ongeldige referentie '{id}'. @@ -938,10 +941,10 @@ Sebastian Stehle and Contributors, 2016-2021 - With your setup, only admins can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=false</code> as environment variable. + With your setup, only admins can create new apps. If you want to change this set <code>UI__ONLYADMINSCANCREATEAPPS=false</code> as environment variable. - With your setup, every user can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=true</code> as environment variable. + With your setup, every user can create new apps. If you want to change this set <code>UI__ONLYADMINSCANCREATEAPPS=true</code> as environment variable. You are using the <strong>folder asset store</strong> where all assets are stored in the file system. Please remember to include the asset folder into your backup strategy and map it to a volume, if you are using Docker. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index 77332a5be..dff65e73c 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -601,6 +601,9 @@ Must follow the pattern. + + Geolocation can only have latitude and longitude property. + Reference '{id}' not found. @@ -938,10 +941,10 @@ Sebastian Stehle and Contributors, 2016-2021 - With your setup, only admins can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=false</code> as environment variable. + With your setup, only admins can create new apps. If you want to change this set <code>UI__ONLYADMINSCANCREATEAPPS=false</code> as environment variable. - With your setup, every user can create new apps. If you want to change this set <code>UI__ONLYADMINCANCREATEAPPS=true</code> as environment variable. + With your setup, every user can create new apps. If you want to change this set <code>UI__ONLYADMINSCANCREATEAPPS=true</code> as environment variable. You are using the <strong>folder asset store</strong> where all assets are stored in the file system. Please remember to include the asset folder into your backup strategy and map it to a volume, if you are using Docker. diff --git a/frontend/app/features/content/shared/forms/array-editor.component.html b/frontend/app/features/content/shared/forms/array-editor.component.html index 83adc55cd..72aecaf0e 100644 --- a/frontend/app/features/content/shared/forms/array-editor.component.html +++ b/frontend/app/features/content/shared/forms/array-editor.component.html @@ -32,6 +32,14 @@ + +
diff --git a/frontend/app/features/content/shared/forms/array-editor.component.ts b/frontend/app/features/content/shared/forms/array-editor.component.ts index b5d1befa7..eac56c6df 100644 --- a/frontend/app/features/content/shared/forms/array-editor.component.ts +++ b/frontend/app/features/content/shared/forms/array-editor.component.ts @@ -77,6 +77,10 @@ export class ArrayEditorComponent implements OnChanges { this.formModel.addItem(value); } + public clear() { + this.formModel.reset(); + } + public sort(event: CdkDragDrop>) { this.formModel.sort(sorted(event)); diff --git a/frontend/app/features/content/shared/forms/field-editor.component.ts b/frontend/app/features/content/shared/forms/field-editor.component.ts index 5c750e855..409ddca95 100644 --- a/frontend/app/features/content/shared/forms/field-editor.component.ts +++ b/frontend/app/features/content/shared/forms/field-editor.component.ts @@ -88,6 +88,6 @@ export class FieldEditorComponent implements OnChanges { } public unset() { - this.formModel.form.setValue(undefined); + this.formModel.unset(); } } \ No newline at end of file diff --git a/frontend/app/framework/angular/forms/undefinable-form-array.spec.ts b/frontend/app/framework/angular/forms/undefinable-form-array.spec.ts new file mode 100644 index 000000000..99007962f --- /dev/null +++ b/frontend/app/framework/angular/forms/undefinable-form-array.spec.ts @@ -0,0 +1,89 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { FormArray, FormControl } from '@angular/forms'; +import { UndefinableFormArray } from './undefinable-form-array'; + +describe('UndefinableFormArray', () => { + const tests = [{ + name: 'undefined', + value: undefined + }, { + name: 'defined', + value: ['1'] + }]; + + tests.forEach(x => { + it(`should set value as ${x.name}`, () => { + const control = + new UndefinableFormArray([ + new FormControl('') + ]); + + assertValue(control, x.value, () => { + control.setValue(x.value); + }); + }); + }); + + tests.forEach(x => { + it(`should patch value as ${x.name}`, () => { + const control = + new UndefinableFormArray([ + new FormControl('') + ]); + + assertValue(control, x.value, () => { + control.patchValue(x.value); + }); + }); + }); + + tests.forEach(x => { + it(`should reset value as ${x.name}`, () => { + const control = + new UndefinableFormArray([ + new FormControl('') + ]); + + assertValue(control, x.value, () => { + control.reset(x.value); + }); + }); + }); + + it('should reset value back after push', () => { + const control = new UndefinableFormArray([]); + + assertValue(control, ['1'], () => { + control.setValue(undefined); + control.push(new FormControl('1')); + }); + }); + + it('should reset value back after insert', () => { + const control = new UndefinableFormArray([]); + + assertValue(control, ['1'], () => { + control.setValue(undefined); + control.insert(0, new FormControl('1')); + }); + }); + + function assertValue(control: FormArray, expected: any, action: () => void) { + let currentValue: any; + + control.valueChanges.subscribe(value => { + currentValue = value; + }); + + action(); + + expect(currentValue).toEqual(expected); + expect(control.getRawValue()).toEqual(expected); + } +}); \ No newline at end of file diff --git a/frontend/app/framework/angular/forms/undefinable-form-array.ts b/frontend/app/framework/angular/forms/undefinable-form-array.ts new file mode 100644 index 000000000..0cb113842 --- /dev/null +++ b/frontend/app/framework/angular/forms/undefinable-form-array.ts @@ -0,0 +1,97 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +// tslint:disable: readonly-array + +import { EventEmitter } from '@angular/core'; +import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, FormArray, ValidatorFn } from '@angular/forms'; +import { Types } from '@app/framework/internal'; + +export class UndefinableFormArray extends FormArray { + private isUndefined = false; + + constructor(controls: AbstractControl[], validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) { + super(controls, validatorOrOpts, asyncValidator); + + const reduce = this['_reduceValue']; + + this['_reduceValue'] = () => { + if (this.isUndefined) { + return undefined; + } else { + return reduce(); + } + }; + } + + public getRawValue() { + if (this.isUndefined) { + return undefined as any; + } else { + return super.getRawValue(); + } + } + + public push(control: AbstractControl) { + this.isUndefined = false; + + super.push(control); + } + + public insert(index: number, control: AbstractControl) { + this.isUndefined = false; + + super.insert(index, control); + } + + public setValue(value: any[] | undefined, options?: { onlySelf?: boolean; emitEvent?: boolean; }) { + this.isUndefined = Types.isUndefined(value); + + if (this.isUndefined) { + super.reset([], options); + } else { + super.setValue(value!, options); + } + } + + public patchValue(value: any[] | undefined, options?: { onlySelf?: boolean; emitEvent?: boolean; }) { + this.isUndefined = Types.isUndefined(value); + + if (this.isUndefined) { + super.reset([], options); + } else { + super.patchValue(value!, options); + } + } + + public reset(value: any[] | undefined, options?: { onlySelf?: boolean; emitEvent?: boolean; }) { + this.isUndefined = Types.isUndefined(value); + + super.reset(value || [], options); + } + + public updateValueAndValidity(opts: { onlySelf?: boolean; emitEvent?: boolean; } = {}) { + super.updateValueAndValidity({ emitEvent: false, onlySelf: true }); + + if (this.isUndefined) { + this.unsetValue(); + } + + if (opts.emitEvent !== false) { + (this.valueChanges as EventEmitter).emit(this.value); + (this.statusChanges as EventEmitter).emit(this.status); + } + + if (this.parent && !opts.onlySelf) { + this.parent.updateValueAndValidity(opts); + } + } + + private unsetValue() { + (this as { value: any }).value = undefined; + } +} \ No newline at end of file diff --git a/frontend/app/framework/declarations.ts b/frontend/app/framework/declarations.ts index 47c823986..758990950 100644 --- a/frontend/app/framework/declarations.ts +++ b/frontend/app/framework/declarations.ts @@ -33,6 +33,7 @@ export * from './angular/forms/indeterminate-value.directive'; export * from './angular/forms/model'; export * from './angular/forms/progress-bar.component'; export * from './angular/forms/transform-input.directive'; +export * from './angular/forms/undefinable-form-array'; export * from './angular/forms/validators'; export * from './angular/highlight.pipe'; export * from './angular/hover-background.directive'; diff --git a/frontend/app/shared/services/schemas.spec.ts b/frontend/app/shared/services/schemas.spec.ts new file mode 100644 index 000000000..be6e781f3 --- /dev/null +++ b/frontend/app/shared/services/schemas.spec.ts @@ -0,0 +1,119 @@ + /* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +// tslint:disable: max-line-length + +import { createProperties, MetaFields, SchemaPropertiesDto } from '@app/shared/internal'; +import { TestValues } from './../state/_test-helpers'; + +const { + createField, + createSchema} = TestValues; + +describe('SchemaDetailsDto', () => { + const field1 = createField({ properties: createProperties('Array'), id: 1 }); + const field2 = createField({ properties: createProperties('Array'), id: 2 }); + const field3 = createField({ properties: createProperties('Array'), id: 3 }); + + it('should return label as display name', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto('Label') }); + + expect(schema.displayName).toBe('Label'); + }); + + it('should return name as display name if label is undefined', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto(undefined) }); + + expect(schema.displayName).toBe('schema1'); + }); + + it('should return name as display name label is empty', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto('') }); + + expect(schema.displayName).toBe('schema1'); + }); + + it('should return configured fields as list fields if fields are declared', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3], fieldsInLists: ['field1', 'field3'] }); + + expect(schema.defaultListFields).toEqual([field1, field3]); + }); + + it('should return first fields as list fields if no field is declared', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] }); + + expect(schema.defaultListFields).toEqual([MetaFields.lastModifiedByAvatar, field1, MetaFields.statusColor, MetaFields.lastModified]); + }); + + it('should return preset with empty content field as list fields if fields is empty', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto() }); + + expect(schema.defaultListFields).toEqual([MetaFields.lastModifiedByAvatar, '', MetaFields.statusColor, MetaFields.lastModified]); + }); + + it('should return configured fields as references fields if fields are declared', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3], fieldsInReferences: ['field1', 'field3'] }); + + expect(schema.defaultReferenceFields).toEqual([field1, field3]); + }); + + it('should return first field as reference fields if no field is declared', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] }); + + expect(schema.defaultReferenceFields).toEqual([field1]); + }); + + it('should return noop field as reference field if list is empty', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto() }); + + expect(schema.defaultReferenceFields).toEqual(['']); + }); +}); + +describe('FieldDto', () => { + it('should return label as display name', () => { + const field = createField({ properties: createProperties('Array', { label: 'Label' }) }); + + expect(field.displayName).toBe('Label'); + }); + + it('should return name as display name if label is null', () => { + const field = createField({ properties: createProperties('Assets') }); + + expect(field.displayName).toBe('field1'); + }); + + it('should return name as display name label is empty', () => { + const field = createField({ properties: createProperties('Assets', { label: '' }) }); + + expect(field.displayName).toBe('field1'); + }); + + it('should return placeholder as display placeholder', () => { + const field = createField({ properties: createProperties('Assets', { placeholder: 'Placeholder' }) }); + + expect(field.displayPlaceholder).toBe('Placeholder'); + }); + + it('should return empty as display placeholder if placeholder is null', () => { + const field = createField({ properties: createProperties('Assets') }); + + expect(field.displayPlaceholder).toBe(''); + }); + + it('should return localizable if partitioning is language', () => { + const field = createField({ properties: createProperties('Assets'), partitioning: 'language' }); + + expect(field.isLocalizable).toBeTruthy(); + }); + + it('should not return localizable if partitioning is invariant', () => { + const field = createField({ properties: createProperties('Assets'), partitioning: 'invariant' }); + + expect(field.isLocalizable).toBeFalsy(); + }); +}); \ No newline at end of file diff --git a/frontend/app/shared/state/_test-helpers.ts b/frontend/app/shared/state/_test-helpers.ts index b8f86f770..135bf28cc 100644 --- a/frontend/app/shared/state/_test-helpers.ts +++ b/frontend/app/shared/state/_test-helpers.ts @@ -7,7 +7,7 @@ import { of } from 'rxjs'; import { Mock } from 'typemoq'; -import { AppsState, AuthService, DateTime, Version } from './../'; +import { AppsState, AuthService, DateTime, FieldPropertiesDto, FieldRule, NestedFieldDto, RootFieldDto, SchemaDetailsDto, SchemaPropertiesDto, Version } from './../'; const app = 'my-app'; const creation = DateTime.today().addDays(-2); @@ -30,10 +30,62 @@ const authService = Mock.ofType(); authService.setup(x => x.user) .returns(() => { id: modifier, token: modifier }); +type SchemaValues = { + id?: number; + fields?: ReadonlyArray; + fieldsInLists?: ReadonlyArray; + fieldsInReferences?: ReadonlyArray; + fieldRules?: ReadonlyArray; + properties?: SchemaPropertiesDto; +}; + +function createSchema({ properties, id, fields, fieldsInLists, fieldsInReferences, fieldRules }: SchemaValues = {}) { + id = id || 1; + + return new SchemaDetailsDto({}, + `schema${1}`, + `schema${1}`, + 'category', + properties || new SchemaPropertiesDto(), false, true, + creation, + creator, + modified, + modifier, + new Version('1'), + fields, + fieldsInLists || [], + fieldsInReferences || [], + fieldRules || []); +} + +type FieldValues = { + id?: number; + properties: FieldPropertiesDto; + isDisabled?: boolean, + isHidden?: boolean, + partitioning?: string; + nested?: ReadonlyArray +}; + +function createField({ properties, id, partitioning, isDisabled, nested }: FieldValues) { + id = id || 1; + + return new RootFieldDto({}, id, `field${id}`, properties, partitioning || 'language', false, false, isDisabled, nested); +} + +function createNestedField({ properties, id, isDisabled }: FieldValues) { + id = id || 1; + + return new NestedFieldDto({}, id, `nested${id}`, properties, 0, false, false, isDisabled); +} + export const TestValues = { app, appsState, authService, + createField, + createNestedField, + createSchema, creation, creator, modified, diff --git a/frontend/app/shared/state/contents.forms-helpers.ts b/frontend/app/shared/state/contents.forms-helpers.ts index ed7c89535..809844185 100644 --- a/frontend/app/shared/state/contents.forms-helpers.ts +++ b/frontend/app/shared/state/contents.forms-helpers.ts @@ -164,6 +164,10 @@ export abstract class AbstractContentForm { - const field1 = createField({ properties: createProperties('Array'), id: 1 }); - const field2 = createField({ properties: createProperties('Array'), id: 2 }); - const field3 = createField({ properties: createProperties('Array'), id: 3 }); - - it('should return label as display name', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto('Label') }); - - expect(schema.displayName).toBe('Label'); - }); - - it('should return name as display name if label is undefined', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto(undefined) }); - - expect(schema.displayName).toBe('schema1'); - }); - - it('should return name as display name label is empty', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto('') }); - - expect(schema.displayName).toBe('schema1'); - }); - - it('should return configured fields as list fields if fields are declared', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3], fieldsInLists: ['field1', 'field3'] }); - - expect(schema.defaultListFields).toEqual([field1, field3]); - }); - - it('should return first fields as list fields if no field is declared', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] }); - - expect(schema.defaultListFields).toEqual([MetaFields.lastModifiedByAvatar, field1, MetaFields.statusColor, MetaFields.lastModified]); - }); - - it('should return preset with empty content field as list fields if fields is empty', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto() }); - - expect(schema.defaultListFields).toEqual([MetaFields.lastModifiedByAvatar, '', MetaFields.statusColor, MetaFields.lastModified]); - }); - - it('should return configured fields as references fields if fields are declared', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3], fieldsInReferences: ['field1', 'field3'] }); - - expect(schema.defaultReferenceFields).toEqual([field1, field3]); - }); - - it('should return first field as reference fields if no field is declared', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] }); - - expect(schema.defaultReferenceFields).toEqual([field1]); - }); - - it('should return noop field as reference field if list is empty', () => { - const schema = createSchema({ properties: new SchemaPropertiesDto() }); - - expect(schema.defaultReferenceFields).toEqual(['']); - }); -}); - -describe('FieldDto', () => { - it('should return label as display name', () => { - const field = createField({ properties: createProperties('Array', { label: 'Label' }) }); - - expect(field.displayName).toBe('Label'); - }); - - it('should return name as display name if label is null', () => { - const field = createField({ properties: createProperties('Assets') }); - - expect(field.displayName).toBe('field1'); - }); - - it('should return name as display name label is empty', () => { - const field = createField({ properties: createProperties('Assets', { label: '' }) }); - - expect(field.displayName).toBe('field1'); - }); - - it('should return placeholder as display placeholder', () => { - const field = createField({ properties: createProperties('Assets', { placeholder: 'Placeholder' }) }); - - expect(field.displayPlaceholder).toBe('Placeholder'); - }); - - it('should return empty as display placeholder if placeholder is null', () => { - const field = createField({ properties: createProperties('Assets') }); - - expect(field.displayPlaceholder).toBe(''); - }); - - it('should return localizable if partitioning is language', () => { - const field = createField({ properties: createProperties('Assets'), partitioning: 'language' }); - - expect(field.isLocalizable).toBeTruthy(); - }); - - it('should not return localizable if partitioning is invarient', () => { - const field = createField({ properties: createProperties('Assets'), partitioning: 'invariant' }); - - expect(field.isLocalizable).toBeFalsy(); - }); -}); - -describe('ArrayField', () => { - const field = createField({ properties: createProperties('Array', { isRequired: true, minItems: 1, maxItems: 5 }) }); - - it('should create validators', () => { - expect(FieldsValidators.create(field, false).length).toBe(2); - }); - - it('should format to empty string if null', () => { - expect(FieldFormatter.format(field, null)).toBe(''); - }); - - it('should format to plural count for many items', () => { - expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 Items'); - }); - - it('should format to plural count for single item', () => { - expect(FieldFormatter.format(field, [1])).toBe('1 Item'); - }); - - it('should return zero formatting if other type', () => { - expect(FieldFormatter.format(field, 1)).toBe('0 Items'); - }); - - it('should return default value as null', () => { - expect(FieldDefaultValue.get(field, 'iv')).toBeNull(); - }); -}); - -describe('AssetsField', () => { - const field = createField({ properties: createProperties('Assets', { isRequired: true, minItems: 1, maxItems: 5 }) }); - - it('should create validators', () => { - expect(FieldsValidators.create(field, false).length).toBe(3); - }); - - it('should format to empty string if null', () => { - expect(FieldFormatter.format(field, null)).toBe(''); - }); - - it('should format to plural count for many items', () => { - expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 Assets'); - }); - - it('should format to plural count for single item', () => { - expect(FieldFormatter.format(field, [1])).toBe('1 Asset'); - }); - - it('should return zero formatting if other type', () => { - expect(FieldFormatter.format(field, 1)).toBe('0 Assets'); - }); - - it('should return default value from properties', () => { - const field2 = createField({ properties: createProperties('Assets', { defaultValue: ['1', '2'] }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toEqual(['1', '2']); - }); - - it('should override default value from localizable properties', () => { - const field2 = createField({ properties: createProperties('Assets', { defaultValue: ['1', '2'], defaultValues: { 'iv': null } }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); - }); -}); - -describe('TagsField', () => { - const field = createField({ properties: createProperties('Tags', { isRequired: true, minItems: 1, maxItems: 5 }) }); - - it('should create validators', () => { - expect(FieldsValidators.create(field, false).length).toBe(2); - }); - - it('should format to empty string if null', () => { - expect(FieldFormatter.format(field, null)).toBe(''); - }); - - it('should format to asset count', () => { - expect(FieldFormatter.format(field, ['hello', 'squidex', 'cms'])).toBe('hello, squidex, cms'); - }); - - it('should return zero formatting if other type', () => { - expect(FieldFormatter.format(field, 1)).toBe(''); - }); - - it('should return default value from properties', () => { - const field2 = createField({ properties: createProperties('Tags', { defaultValue: ['1', '2'] }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toEqual(['1', '2']); - }); - - it('should override default value from localizable properties', () => { - const field2 = createField({ properties: createProperties('Tags', { defaultValue: ['1', '2'], defaultValues: { 'iv': null } }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); - }); -}); - -describe('BooleanField', () => { - const field = createField({ properties: createProperties('Boolean', { editor: 'Checkbox', isRequired: true }) }); - - it('should create validators', () => { - expect(FieldsValidators.create(field, false).length).toBe(1); - }); - - it('should format to empty string if null', () => { - expect(FieldFormatter.format(field, null)).toBe(''); - }); - - it('should format to Yes if true', () => { - expect(FieldFormatter.format(field, true)).toBe('Yes'); - }); - - it('should format to No if false', () => { - expect(FieldFormatter.format(field, false)).toBe('No'); - }); - - it('should return default value from properties', () => { - const field2 = createField({ properties: createProperties('Boolean', { editor: 'Checkbox', defaultValue: true }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toBeTruthy(); - }); - - it('should override default value from localizable properties', () => { - const field2 = createField({ properties: createProperties('Boolean', { defaultValue: true, defaultValues: { 'iv': null } }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); - }); -}); - -describe('DateTimeField', () => { - const field = createField({ properties: createProperties('DateTime', { editor: 'DateTime', isRequired: true }) }); - - beforeEach(() => { - DateHelper.setlocale(null); - }); - - it('should create validators', () => { - expect(FieldsValidators.create(field, false).length).toBe(1); - }); - - it('should format to empty string if null', () => { - expect(FieldFormatter.format(field, null)).toBe(''); - }); - - it('should format to input if parsing failed', () => { - expect(FieldFormatter.format(field, true)).toBe(true); - }); - - it('should format old format to date', () => { - const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) }); - - expect(FieldFormatter.format(dateField, '2017-12-12')).toBe('12/12/2017'); - }); - - it('should format datetime to date', () => { - const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) }); - - expect(FieldFormatter.format(dateField, '2017-12-12T16:00:00Z')).toBe('12/12/2017'); - }); - - it('should format date to date', () => { - const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) }); - - expect(FieldFormatter.format(dateField, '2017-12-12T00:00:00Z')).toBe('12/12/2017'); - }); - - it('should format to date time', () => { - const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime' }) }); - - expect(FieldFormatter.format(field2, '2017-12-12T16:00:00Z')).toBe('12/12/2017, 4:00:00 PM'); - }); - - it('should return default from properties value', () => { - const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', defaultValue: '2017-10-12T16:00:00Z' }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toEqual('2017-10-12T16:00:00Z'); - }); - - it('should override default value from localizable properties', () => { - const field2 = createField({ properties: createProperties('DateTime', { defaultValue: '2017-10-12T16:00:00Z', defaultValues: { 'iv': null } }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); - }); - - it('should return default from Today', () => { - const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', calculatedDefaultValue: 'Today' }) }); - - expect(FieldDefaultValue.get(field2, 'iv', now)).toEqual('2017-10-12T00:00:00Z'); - }); - - it('should return default value from Today', () => { - const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', calculatedDefaultValue: 'Now' }) }); - - expect(FieldDefaultValue.get(field2, 'iv', now)).toEqual('2017-10-12T16:30:10Z'); - }); -}); - -describe('GeolocationField', () => { - const field = createField({ properties: createProperties('Geolocation', { isRequired: true }) }); - - it('should create validators', () => { - expect(FieldsValidators.create(field, false).length).toBe(1); - }); - - it('should format to empty string if null', () => { - expect(FieldFormatter.format(field, null)).toBe(''); - }); - - it('should format to latitude and longitude', () => { - expect(FieldFormatter.format(field, { latitude: 42, longitude: 3.14 })).toBe('3.14, 42'); - }); - - it('should return default value as null', () => { - expect(FieldDefaultValue.get(field, 'iv')).toBeNull(); - }); -}); - -describe('JsonField', () => { - const field = createField({ properties: createProperties('Json', { isRequired: true }) }); - - it('should create validators', () => { - expect(FieldsValidators.create(field, false).length).toBe(1); - }); - - it('should format to empty string if null', () => { - expect(FieldFormatter.format(field, null)).toBe(''); - }); - - it('should format to constant', () => { - expect(FieldFormatter.format(field, {})).toBe(''); - }); - - it('should return default value as null', () => { - expect(FieldDefaultValue.get(field, 'iv')).toBeNull(); - }); -}); - -describe('NumberField', () => { - const field = createField({ properties: createProperties('Number', { isRequired: true, minValue: 1, maxValue: 6, allowedValues: [1, 3] }) }); - - it('should create validators', () => { - expect(FieldsValidators.create(field, false).length).toBe(3); - }); - - it('should format to empty string if null', () => { - expect(FieldFormatter.format(field, null)).toBe(''); - }); - - it('should format to number', () => { - expect(FieldFormatter.format(field, 42)).toEqual('42'); - }); - - it('should format to stars if html allowed', () => { - const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) }); - - expect(FieldFormatter.format(field2, 3)).toEqual(new HtmlValue('★ ★ ★ ')); - }); - - it('should format to short star view for many stars', () => { - const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) }); - - expect(FieldFormatter.format(field2, 42)).toEqual(new HtmlValue('★ 42')); - }); - - it('should format to short star view for no stars', () => { - const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) }); - - expect(FieldFormatter.format(field2, 0)).toEqual(new HtmlValue('★ 0')); - }); - - it('should format to short star view for negative stars', () => { - const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) }); - - expect(FieldFormatter.format(field2, -13)).toEqual(new HtmlValue('★ -13')); - }); - - it('should not format to stars if html not allowed', () => { - const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) }); - - expect(FieldFormatter.format(field2, 3, false)).toEqual('3'); - }); - - it('should return default value from properties', () => { - const field2 = createField({ properties: createProperties('Number', { defaultValue: 13 }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toEqual(13); - }); - - it('should override default value from localizable properties', () => { - const field2 = createField({ properties: createProperties('Number', { defaultValue: 13, defaultValues: { 'iv': null } }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); - }); -}); - -describe('ReferencesField', () => { - const field = createField({ properties: createProperties('References', { editor: 'List', isRequired: true, minItems: 1, maxItems: 5 }) }); - - it('should create validators', () => { - expect(FieldsValidators.create(field, false).length).toBe(3); - }); - - it('should format to empty string if null', () => { - expect(FieldFormatter.format(field, null)).toBe(''); - }); - - it('should format to plural count for many items', () => { - expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 References'); - }); - - it('should format to plural count for single item', () => { - expect(FieldFormatter.format(field, [1])).toBe('1 Reference'); - }); - - it('should return zero formatting if other type', () => { - expect(FieldFormatter.format(field, 1)).toBe('0 References'); - }); - - it('should return default value from properties', () => { - const field2 = createField({ properties: createProperties('References', { defaultValue: ['1', '2'] }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toEqual(['1', '2']); - }); - - it('should override default value from localizable properties', () => { - const field2 = createField({ properties: createProperties('References', { defaultValue: ['1', '2'], defaultValues: { 'iv': null } }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); - }); -}); - -describe('StringField', () => { - const field = createField({ properties: createProperties('String', { isRequired: true, pattern: 'pattern', minLength: 1, maxLength: 5, allowedValues: ['a', 'b'] }) }); - - it('should create validators', () => { - expect(FieldsValidators.create(field, false).length).toBe(4); - }); - - it('should format to empty string if null', () => { - expect(FieldFormatter.format(field, null)).toBe(''); - }); - - it('should format to string', () => { - expect(FieldFormatter.format(field, 'hello')).toBe('hello'); - }); - - it('should format to preview image', () => { - const field2 = createField({ properties: createProperties('String', { editor: 'StockPhoto' }) }); - - expect(FieldFormatter.format(field2, 'https://images.unsplash.com/123?x', true)).toEqual(new HtmlValue('')); - }); - - it('should not format to preview image when html not allowed', () => { - const field2 = createField({ properties: createProperties('String', { editor: 'StockPhoto' }) }); - - expect(FieldFormatter.format(field2, 'https://images.unsplash.com/123?x', false)).toBe('https://images.unsplash.com/123?x'); - }); - - it('should not format to preview image when not unsplash image', () => { - const field2 = createField({ properties: createProperties('String', { editor: 'StockPhoto' }) }); - - expect(FieldFormatter.format(field2, 'https://images.com/123?x', true)).toBe('https://images.com/123?x'); - }); - - it('should return default value from properties', () => { - const field2 = createField({ properties: createProperties('String', { defaultValue: 'MyDefault' }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toEqual('MyDefault'); - }); - - it('should override default value from localizable properties', () => { - const field2 = createField({ properties: createProperties('String', { defaultValue: 'MyDefault', defaultValues: { 'iv': null } }) }); - - expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); - }); -}); - describe('GetContentValue', () => { const language = new LanguageDto('en', 'English'); const fieldInvariant = createField({ properties: createProperties('Number'), partitioning: 'invariant' }); @@ -910,12 +426,7 @@ describe('ContentForm', () => { }); it('should load with array and not enable disabled nested fields', () => { - const contentForm = createForm([ - createField({ id: 4, properties: createProperties('Array'), partitioning: 'invariant', nested: [ - createNestedField({ id: 41, properties: createProperties('String') }), - createNestedField({ id: 42, properties: createProperties('String'), isDisabled: true }) - ]}) - ]); + const { contentForm, array } = createArrayFormWith2Items(); contentForm.load({ field4: { @@ -925,64 +436,99 @@ describe('ContentForm', () => { } }); - const nestedForm = contentForm.form.get('field4.iv') as FormArray; - const nestedItem = nestedForm.get([0])!; - - expect(nestedForm.controls.length).toBe(1); + const nestedItem = array.form.get([0])!; + expectLength(array, 1); expectForm(nestedItem, 'nested41', { disabled: false, value: 'Text' }); expectForm(nestedItem, 'nested42', { disabled: true, value: null }); }); it('should add array item', () => { - const contentForm = createForm([ - createField({ id: 4, properties: createProperties('Array'), partitioning: 'invariant', nested: [ - createNestedField({ id: 41, properties: createProperties('String') }), - createNestedField({ id: 42, properties: createProperties('String', { defaultValue: 'Default' }), isDisabled: true }) - ]}) - ]); - - const array = contentForm.get(complexSchema.fields[3])!.get(languages[0]) as FieldArrayForm; + const { array } = createArrayFormWith2Items(); array.addItem(); - array.addItem(); - - const nestedForm = contentForm.form.get('field4.iv') as FormArray; - const nestedItem = nestedForm.get([0])!; - expect(nestedForm.controls.length).toBe(2); + const nestedItem = array.form.get([2])!; + expectLength(array, 3); expectForm(nestedItem, 'nested41', { disabled: false, value: null }); expectForm(nestedItem, 'nested42', { disabled: true, value: 'Default' }); }); - it('should remove array item', () => { - const contentForm = createForm([ - createField({ id: 4, properties: createProperties('Array'), partitioning: 'invariant', nested: [ - createNestedField({ id: 41, properties: createProperties('String') }) - ]}) - ]); + it('should sort array item', () => { + const { array } = createArrayFormWith2Items(); - const array = contentForm.get(complexSchema.fields[3])!.get(languages[0]) as FieldArrayForm; + array.sort([array.get(1), array.get(0)]); + + expectLength(array, 2); + expect(array.form.value).toEqual([{ nested41: 'Text2' }, { nested41: 'Text1' }]); + }); + + it('should remove array item', () => { + const { array } = createArrayFormWith2Items(); - array.addItem(); - array.addItem(); array.removeItemAt(0); - const nestedForm = contentForm.form.get('field4.iv') as FormArray; + expectLength(array, 1); + expect(array.form.value).toEqual([{ nested41: 'Text2' }]); + }); + + it('should reset array item', () => { + const { array } = createArrayFormWith2Items(); + + array.reset(); - expect(nestedForm.controls.length).toBe(1); + expectLength(array, 0); + expect(array.form.value).toEqual([]); + }); + + it('should unset array item', () => { + const { array } = createArrayFormWith2Items(); + + array.unset(); + + expectLength(array, 0); + expect(array.form.value).toEqual(undefined); }); it('should not array item if field has no nested fields', () => { const contentForm = createForm([ createField({ id: 4, properties: createProperties('Array'), partitioning: 'invariant' }) ]); + const nestedForm = contentForm.form.get('field4.iv') as FormArray; expect(nestedForm.controls.length).toBe(0); }); + function createArrayFormWith2Items() { + const contentForm = createForm([ + createField({ id: 4, properties: createProperties('Array'), partitioning: 'invariant', nested: [ + createNestedField({ id: 41, properties: createProperties('String') }), + createNestedField({ id: 42, properties: createProperties('String', { defaultValue: 'Default' }), isDisabled: true }) + ]}) + ]); + + const array = contentForm.get('field4')!.get('iv') as FieldArrayForm; + + contentForm.load({ + field4: { + iv: [{ + nested41: 'Text1' + }, { + nested41: 'Text2' + }] + } + }); + + return { contentForm, array }; + } + + function expectLength(array: FieldArrayForm, length: number) { + expect(array.form.controls.length).toBe(length); + expect(array.items.length).toBe(length); + } + function expectForm(parent: AbstractControl, path: string, test: { invalid?: boolean, disabled?: boolean, value?: any }) { const form = parent.get(path); @@ -1110,46 +656,4 @@ describe('ContentForm', () => { return new EditContentForm(languages, createSchema({ fields, fieldRules }), {}, 0); } -}); - -type SchemaValues = { - id?: number; - fields?: ReadonlyArray; - fieldsInLists?: ReadonlyArray; - fieldsInReferences?: ReadonlyArray; - fieldRules?: ReadonlyArray; - properties?: SchemaPropertiesDto; -}; - -function createSchema({ properties, id, fields, fieldsInLists, fieldsInReferences, fieldRules }: SchemaValues = {}) { - id = id || 1; - - return new SchemaDetailsDto({}, - `schema${1}`, - `schema${1}`, - 'category', - properties || new SchemaPropertiesDto(), false, true, - creation, - creator, - modified, - modifier, - new Version('1'), - fields, - fieldsInLists || [], - fieldsInReferences || [], - fieldRules || []); -} - -type FieldValues = { properties: FieldPropertiesDto; id?: number; partitioning?: string; isDisabled?: boolean, nested?: ReadonlyArray }; - -function createField({ properties, id, partitioning, isDisabled, nested }: FieldValues) { - id = id || 1; - - return new RootFieldDto({}, id, `field${id}`, properties, partitioning || 'language', false, false, isDisabled, nested); -} - -function createNestedField({ properties, id, isDisabled }: FieldValues) { - id = id || 1; - - return new NestedFieldDto({}, id, `nested${id}`, properties, 0, false, false, isDisabled); -} \ No newline at end of file +}); \ No newline at end of file diff --git a/frontend/app/shared/state/contents.forms.ts b/frontend/app/shared/state/contents.forms.ts index ed534bb32..32dcca927 100644 --- a/frontend/app/shared/state/contents.forms.ts +++ b/frontend/app/shared/state/contents.forms.ts @@ -7,8 +7,8 @@ // tslint:disable: readonly-array -import { FormArray, FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; -import { Form, Types, valueAll$ } from '@app/framework'; +import { FormBuilder, FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms'; +import { Form, Types, UndefinableFormArray, valueAll$ } from '@app/framework'; import { BehaviorSubject, Observable } from 'rxjs'; import { debounceTime, onErrorResumeNext } from 'rxjs/operators'; import { AppLanguageDto } from './../services/app-languages.service'; @@ -314,7 +314,7 @@ export class FieldValueForm extends AbstractContentForm { +export class FieldArrayForm extends AbstractContentForm { private readonly item$ = new BehaviorSubject>([]); public get itemChanges(): Observable> { @@ -349,6 +349,20 @@ export class FieldArrayForm extends AbstractContentForm this.form.push(child.form); } + public unset() { + this.items = []; + + super.unset(); + + this.form.clear(); + } + + public reset() { + this.items = []; + + this.form.clear(); + } + public removeItemAt(index: number) { this.items = this.items.filter((_, i) => i !== index); @@ -393,7 +407,7 @@ export class FieldArrayForm extends AbstractContentForm private static buildControl(field: RootFieldDto, isOptional: boolean) { const validators = FieldsValidators.create(field, isOptional); - return new FormArray([], validators); + return new UndefinableFormArray([], validators); } } diff --git a/frontend/app/shared/state/contents.forms.visitors.spec.ts b/frontend/app/shared/state/contents.forms.visitors.spec.ts new file mode 100644 index 000000000..811db467e --- /dev/null +++ b/frontend/app/shared/state/contents.forms.visitors.spec.ts @@ -0,0 +1,499 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +// tslint:disable: max-line-length + +import { DateHelper } from '@app/framework'; +import { createProperties, DateTime, FieldDefaultValue, FieldFormatter, FieldsValidators, HtmlValue, MetaFields, SchemaPropertiesDto } from '@app/shared/internal'; +import { TestValues } from './_test-helpers'; + +const { + createField, + createSchema +} = TestValues; + +const now = DateTime.parseISO('2017-10-12T16:30:10Z'); + +describe('SchemaDetailsDto', () => { + const field1 = createField({ properties: createProperties('Array'), id: 1 }); + const field2 = createField({ properties: createProperties('Array'), id: 2 }); + const field3 = createField({ properties: createProperties('Array'), id: 3 }); + + it('should return label as display name', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto('Label') }); + + expect(schema.displayName).toBe('Label'); + }); + + it('should return name as display name if label is undefined', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto(undefined) }); + + expect(schema.displayName).toBe('schema1'); + }); + + it('should return name as display name label is empty', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto('') }); + + expect(schema.displayName).toBe('schema1'); + }); + + it('should return configured fields as list fields if fields are declared', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3], fieldsInLists: ['field1', 'field3'] }); + + expect(schema.defaultListFields).toEqual([field1, field3]); + }); + + it('should return first fields as list fields if no field is declared', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] }); + + expect(schema.defaultListFields).toEqual([MetaFields.lastModifiedByAvatar, field1, MetaFields.statusColor, MetaFields.lastModified]); + }); + + it('should return preset with empty content field as list fields if fields is empty', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto() }); + + expect(schema.defaultListFields).toEqual([MetaFields.lastModifiedByAvatar, '', MetaFields.statusColor, MetaFields.lastModified]); + }); + + it('should return configured fields as references fields if fields are declared', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3], fieldsInReferences: ['field1', 'field3'] }); + + expect(schema.defaultReferenceFields).toEqual([field1, field3]); + }); + + it('should return first field as reference fields if no field is declared', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] }); + + expect(schema.defaultReferenceFields).toEqual([field1]); + }); + + it('should return noop field as reference field if list is empty', () => { + const schema = createSchema({ properties: new SchemaPropertiesDto() }); + + expect(schema.defaultReferenceFields).toEqual(['']); + }); +}); + +describe('FieldDto', () => { + it('should return label as display name', () => { + const field = createField({ properties: createProperties('Array', { label: 'Label' }) }); + + expect(field.displayName).toBe('Label'); + }); + + it('should return name as display name if label is null', () => { + const field = createField({ properties: createProperties('Assets') }); + + expect(field.displayName).toBe('field1'); + }); + + it('should return name as display name label is empty', () => { + const field = createField({ properties: createProperties('Assets', { label: '' }) }); + + expect(field.displayName).toBe('field1'); + }); + + it('should return placeholder as display placeholder', () => { + const field = createField({ properties: createProperties('Assets', { placeholder: 'Placeholder' }) }); + + expect(field.displayPlaceholder).toBe('Placeholder'); + }); + + it('should return empty as display placeholder if placeholder is null', () => { + const field = createField({ properties: createProperties('Assets') }); + + expect(field.displayPlaceholder).toBe(''); + }); + + it('should return localizable if partitioning is language', () => { + const field = createField({ properties: createProperties('Assets'), partitioning: 'language' }); + + expect(field.isLocalizable).toBeTruthy(); + }); + + it('should not return localizable if partitioning is invariant', () => { + const field = createField({ properties: createProperties('Assets'), partitioning: 'invariant' }); + + expect(field.isLocalizable).toBeFalsy(); + }); +}); + +describe('ArrayField', () => { + const field = createField({ properties: createProperties('Array', { isRequired: true, minItems: 1, maxItems: 5 }) }); + + it('should create validators', () => { + expect(FieldsValidators.create(field, false).length).toBe(2); + }); + + it('should format to empty string if null', () => { + expect(FieldFormatter.format(field, null)).toBe(''); + }); + + it('should format to plural count for many items', () => { + expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 Items'); + }); + + it('should format to plural count for single item', () => { + expect(FieldFormatter.format(field, [1])).toBe('1 Item'); + }); + + it('should return zero formatting if other type', () => { + expect(FieldFormatter.format(field, 1)).toBe('0 Items'); + }); + + it('should return default value as null', () => { + expect(FieldDefaultValue.get(field, 'iv')).toBeNull(); + }); +}); + +describe('AssetsField', () => { + const field = createField({ properties: createProperties('Assets', { isRequired: true, minItems: 1, maxItems: 5 }) }); + + it('should create validators', () => { + expect(FieldsValidators.create(field, false).length).toBe(3); + }); + + it('should format to empty string if null', () => { + expect(FieldFormatter.format(field, null)).toBe(''); + }); + + it('should format to plural count for many items', () => { + expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 Assets'); + }); + + it('should format to plural count for single item', () => { + expect(FieldFormatter.format(field, [1])).toBe('1 Asset'); + }); + + it('should return zero formatting if other type', () => { + expect(FieldFormatter.format(field, 1)).toBe('0 Assets'); + }); + + it('should return default value from properties', () => { + const field2 = createField({ properties: createProperties('Assets', { defaultValue: ['1', '2'] }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toEqual(['1', '2']); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('Assets', { defaultValue: ['1', '2'], defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); + }); +}); + +describe('TagsField', () => { + const field = createField({ properties: createProperties('Tags', { isRequired: true, minItems: 1, maxItems: 5 }) }); + + it('should create validators', () => { + expect(FieldsValidators.create(field, false).length).toBe(2); + }); + + it('should format to empty string if null', () => { + expect(FieldFormatter.format(field, null)).toBe(''); + }); + + it('should format to asset count', () => { + expect(FieldFormatter.format(field, ['hello', 'squidex', 'cms'])).toBe('hello, squidex, cms'); + }); + + it('should return zero formatting if other type', () => { + expect(FieldFormatter.format(field, 1)).toBe(''); + }); + + it('should return default value from properties', () => { + const field2 = createField({ properties: createProperties('Tags', { defaultValue: ['1', '2'] }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toEqual(['1', '2']); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('Tags', { defaultValue: ['1', '2'], defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); + }); +}); + +describe('BooleanField', () => { + const field = createField({ properties: createProperties('Boolean', { editor: 'Checkbox', isRequired: true }) }); + + it('should create validators', () => { + expect(FieldsValidators.create(field, false).length).toBe(1); + }); + + it('should format to empty string if null', () => { + expect(FieldFormatter.format(field, null)).toBe(''); + }); + + it('should format to Yes if true', () => { + expect(FieldFormatter.format(field, true)).toBe('Yes'); + }); + + it('should format to No if false', () => { + expect(FieldFormatter.format(field, false)).toBe('No'); + }); + + it('should return default value from properties', () => { + const field2 = createField({ properties: createProperties('Boolean', { editor: 'Checkbox', defaultValue: true }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeTruthy(); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('Boolean', { defaultValue: true, defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); + }); +}); + +describe('DateTimeField', () => { + const field = createField({ properties: createProperties('DateTime', { editor: 'DateTime', isRequired: true }) }); + + beforeEach(() => { + DateHelper.setlocale(null); + }); + + it('should create validators', () => { + expect(FieldsValidators.create(field, false).length).toBe(1); + }); + + it('should format to empty string if null', () => { + expect(FieldFormatter.format(field, null)).toBe(''); + }); + + it('should format to input if parsing failed', () => { + expect(FieldFormatter.format(field, true)).toBe(true); + }); + + it('should format old format to date', () => { + const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) }); + + expect(FieldFormatter.format(dateField, '2017-12-12')).toBe('12/12/2017'); + }); + + it('should format datetime to date', () => { + const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) }); + + expect(FieldFormatter.format(dateField, '2017-12-12T16:00:00Z')).toBe('12/12/2017'); + }); + + it('should format date to date', () => { + const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) }); + + expect(FieldFormatter.format(dateField, '2017-12-12T00:00:00Z')).toBe('12/12/2017'); + }); + + it('should format to date time', () => { + const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime' }) }); + + expect(FieldFormatter.format(field2, '2017-12-12T16:00:00Z')).toBe('12/12/2017, 4:00:00 PM'); + }); + + it('should return default from properties value', () => { + const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', defaultValue: '2017-10-12T16:00:00Z' }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toEqual('2017-10-12T16:00:00Z'); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('DateTime', { defaultValue: '2017-10-12T16:00:00Z', defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); + }); + + it('should return default from Today', () => { + const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', calculatedDefaultValue: 'Today' }) }); + + expect(FieldDefaultValue.get(field2, 'iv', now)).toEqual('2017-10-12T00:00:00Z'); + }); + + it('should return default value from Today', () => { + const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', calculatedDefaultValue: 'Now' }) }); + + expect(FieldDefaultValue.get(field2, 'iv', now)).toEqual('2017-10-12T16:30:10Z'); + }); +}); + +describe('GeolocationField', () => { + const field = createField({ properties: createProperties('Geolocation', { isRequired: true }) }); + + it('should create validators', () => { + expect(FieldsValidators.create(field, false).length).toBe(1); + }); + + it('should format to empty string if null', () => { + expect(FieldFormatter.format(field, null)).toBe(''); + }); + + it('should format to latitude and longitude', () => { + expect(FieldFormatter.format(field, { latitude: 42, longitude: 3.14 })).toBe('3.14, 42'); + }); + + it('should return default value as null', () => { + expect(FieldDefaultValue.get(field, 'iv')).toBeNull(); + }); +}); + +describe('JsonField', () => { + const field = createField({ properties: createProperties('Json', { isRequired: true }) }); + + it('should create validators', () => { + expect(FieldsValidators.create(field, false).length).toBe(1); + }); + + it('should format to empty string if null', () => { + expect(FieldFormatter.format(field, null)).toBe(''); + }); + + it('should format to constant', () => { + expect(FieldFormatter.format(field, {})).toBe(''); + }); + + it('should return default value as null', () => { + expect(FieldDefaultValue.get(field, 'iv')).toBeNull(); + }); +}); + +describe('NumberField', () => { + const field = createField({ properties: createProperties('Number', { isRequired: true, minValue: 1, maxValue: 6, allowedValues: [1, 3] }) }); + + it('should create validators', () => { + expect(FieldsValidators.create(field, false).length).toBe(3); + }); + + it('should format to empty string if null', () => { + expect(FieldFormatter.format(field, null)).toBe(''); + }); + + it('should format to number', () => { + expect(FieldFormatter.format(field, 42)).toEqual('42'); + }); + + it('should format to stars if html allowed', () => { + const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) }); + + expect(FieldFormatter.format(field2, 3)).toEqual(new HtmlValue('★ ★ ★ ')); + }); + + it('should format to short star view for many stars', () => { + const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) }); + + expect(FieldFormatter.format(field2, 42)).toEqual(new HtmlValue('★ 42')); + }); + + it('should format to short star view for no stars', () => { + const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) }); + + expect(FieldFormatter.format(field2, 0)).toEqual(new HtmlValue('★ 0')); + }); + + it('should format to short star view for negative stars', () => { + const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) }); + + expect(FieldFormatter.format(field2, -13)).toEqual(new HtmlValue('★ -13')); + }); + + it('should not format to stars if html not allowed', () => { + const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) }); + + expect(FieldFormatter.format(field2, 3, false)).toEqual('3'); + }); + + it('should return default value from properties', () => { + const field2 = createField({ properties: createProperties('Number', { defaultValue: 13 }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toEqual(13); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('Number', { defaultValue: 13, defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); + }); +}); + +describe('ReferencesField', () => { + const field = createField({ properties: createProperties('References', { editor: 'List', isRequired: true, minItems: 1, maxItems: 5 }) }); + + it('should create validators', () => { + expect(FieldsValidators.create(field, false).length).toBe(3); + }); + + it('should format to empty string if null', () => { + expect(FieldFormatter.format(field, null)).toBe(''); + }); + + it('should format to plural count for many items', () => { + expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 References'); + }); + + it('should format to plural count for single item', () => { + expect(FieldFormatter.format(field, [1])).toBe('1 Reference'); + }); + + it('should return zero formatting if other type', () => { + expect(FieldFormatter.format(field, 1)).toBe('0 References'); + }); + + it('should return default value from properties', () => { + const field2 = createField({ properties: createProperties('References', { defaultValue: ['1', '2'] }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toEqual(['1', '2']); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('References', { defaultValue: ['1', '2'], defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); + }); +}); + +describe('StringField', () => { + const field = createField({ properties: createProperties('String', { isRequired: true, pattern: 'pattern', minLength: 1, maxLength: 5, allowedValues: ['a', 'b'] }) }); + + it('should create validators', () => { + expect(FieldsValidators.create(field, false).length).toBe(4); + }); + + it('should format to empty string if null', () => { + expect(FieldFormatter.format(field, null)).toBe(''); + }); + + it('should format to string', () => { + expect(FieldFormatter.format(field, 'hello')).toBe('hello'); + }); + + it('should format to preview image', () => { + const field2 = createField({ properties: createProperties('String', { editor: 'StockPhoto' }) }); + + expect(FieldFormatter.format(field2, 'https://images.unsplash.com/123?x', true)).toEqual(new HtmlValue('')); + }); + + it('should not format to preview image when html not allowed', () => { + const field2 = createField({ properties: createProperties('String', { editor: 'StockPhoto' }) }); + + expect(FieldFormatter.format(field2, 'https://images.unsplash.com/123?x', false)).toBe('https://images.unsplash.com/123?x'); + }); + + it('should not format to preview image when not unsplash image', () => { + const field2 = createField({ properties: createProperties('String', { editor: 'StockPhoto' }) }); + + expect(FieldFormatter.format(field2, 'https://images.com/123?x', true)).toBe('https://images.com/123?x'); + }); + + it('should return default value from properties', () => { + const field2 = createField({ properties: createProperties('String', { defaultValue: 'MyDefault' }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toEqual('MyDefault'); + }); + + it('should override default value from localizable properties', () => { + const field2 = createField({ properties: createProperties('String', { defaultValue: 'MyDefault', defaultValues: { 'iv': null } }) }); + + expect(FieldDefaultValue.get(field2, 'iv')).toBeNull(); + }); +}); \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2383f89b7..70764b8a3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1569,7 +1569,7 @@ "dependencies": { "jsesc": { "version": "1.3.0", - "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", "dev": true } @@ -3053,7 +3053,7 @@ }, "css-color-names": { "version": "0.0.4", - "resolved": "http://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", + "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", "dev": true }, @@ -8755,7 +8755,7 @@ }, "onetime": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "dev": true }, @@ -12464,7 +12464,7 @@ }, "rgba-regex": { "version": "1.0.0", - "resolved": "http://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", "dev": true }, @@ -14412,7 +14412,7 @@ "dependencies": { "json5": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "dev": true, "requires": { diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 38c1498cd..a70a190c5 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -20,7 +20,7 @@ "sourceMap": true, "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, - "target": "es5", + "target": "es2015", "paths": { "@app*": [ "app*"