From 24b7b8af20519e743d63524b100c1e048c5a59d8 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 1 Jun 2018 16:11:29 +0200 Subject: [PATCH] Tests fixed. --- .../ConvertContent/FieldConverters.cs | 85 +++++++++---------- .../framework/angular/forms/forms-helper.ts | 44 ++++++++++ src/Squidex/app/framework/declarations.ts | 1 + src/Squidex/app/framework/internal.ts | 1 + src/Squidex/app/framework/state.ts | 40 ++------- src/Squidex/app/framework/utils/lazy.spec.ts | 42 +++++++++ src/Squidex/app/framework/utils/lazy.ts | 24 ++++++ .../shared/services/schemas.service.spec.ts | 6 +- .../app/shared/services/schemas.service.ts | 48 +++++------ .../app/shared/state/contents.forms.spec.ts | 10 +-- .../app/shared/state/contents.forms.ts | 75 ++++++++++------ .../ConvertContent/FieldConvertersTests.cs | 12 --- 12 files changed, 242 insertions(+), 146 deletions(-) create mode 100644 src/Squidex/app/framework/angular/forms/forms-helper.ts create mode 100644 src/Squidex/app/framework/utils/lazy.spec.ts create mode 100644 src/Squidex/app/framework/utils/lazy.ts diff --git a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs index 854009f5c..381a2d8aa 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs @@ -233,54 +233,61 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { if (field is IArrayField arrayField) { + var result = new ContentFieldData(); + foreach (var partition in data) { - if (partition.Value is JArray jArray) + if (!(partition.Value is JArray jArray)) { - for (var i = 0; i < jArray.Count; i++) - { - if (jArray[i] is JObject item) - { - var result = new JObject(); + continue; + } - foreach (var kvp in item) - { - var nestedField = fieldResolver(arrayField, kvp.Key); + var newArray = new JArray(); - if (nestedField == null) - { - continue; - } + foreach (JObject item in jArray.OfType()) + { + var newItem = new JObject(); + + foreach (var kvp in item) + { + var nestedField = fieldResolver(arrayField, kvp.Key); - var newValue = kvp.Value; + if (nestedField == null) + { + continue; + } - var isUnset = false; + var newValue = kvp.Value; - if (converters != null) - { - foreach (var converter in converters) - { - newValue = converter(newValue, nestedField); - - if (ReferenceEquals(newValue, Value.Unset)) - { - isUnset = true; - break; - } - } - } + var isUnset = false; - if (!isUnset) + if (converters != null) + { + foreach (var converter in converters) + { + newValue = converter(newValue, nestedField); + + if (ReferenceEquals(newValue, Value.Unset)) { - result.Add(keyResolver(nestedField), newValue); + isUnset = true; + break; } } + } - jArray[i] = result; + if (!isUnset) + { + newItem.Add(keyResolver(nestedField), newValue); } } + + newArray.Add(newItem); } + + result.Add(partition.Key, newArray); } + + return result; } return data; @@ -293,7 +300,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { if (!(field is IArrayField)) { - ContentFieldData result = null; + var result = new ContentFieldData(); foreach (var partition in data) { @@ -315,21 +322,13 @@ namespace Squidex.Domain.Apps.Core.ConvertContent } } - if (result != null || isUnset || !ReferenceEquals(newValue, partition.Value)) + if (!isUnset) { - if (result == null) - { - result = new ContentFieldData(); - } - - if (!isUnset) - { - result.Add(partition.Key, newValue); - } + result.Add(partition.Key, newValue); } } - return result ?? data; + return result; } return data; diff --git a/src/Squidex/app/framework/angular/forms/forms-helper.ts b/src/Squidex/app/framework/angular/forms/forms-helper.ts new file mode 100644 index 000000000..d38f8125e --- /dev/null +++ b/src/Squidex/app/framework/angular/forms/forms-helper.ts @@ -0,0 +1,44 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + + import { AbstractControl, FormArray, FormGroup } from '@angular/forms'; + +import { Types } from '@app/framework/internal'; + +export const formControls = (form: AbstractControl): AbstractControl[] => { + if (Types.is(form, FormGroup)) { + return Object.values(form.controls); + } else if (Types.is(form, FormArray)) { + return form.controls; + } else { + return []; + } +}; + +export const fullValue = (form: AbstractControl): any => { + if (Types.is(form, FormGroup)) { + const groupValue = {}; + + for (let key in form.controls) { + if (form.controls.hasOwnProperty(key)) { + groupValue[key] = fullValue(form.controls[key]); + } + } + + return groupValue; + } else if (Types.is(form, FormArray)) { + const arrayValue = []; + + for (let child of form.controls) { + arrayValue.push(fullValue(child)); + } + + return arrayValue; + } else { + return form.value; + } +}; \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 56fce1cfc..d18af77bf 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -14,6 +14,7 @@ export * from './angular/forms/dropdown.component'; export * from './angular/forms/file-drop.directive'; export * from './angular/forms/focus-on-init.directive'; export * from './angular/forms/form-error.component'; +export * from './angular/forms/forms-helper'; export * from './angular/forms/iframe-editor.component'; export * from './angular/forms/indeterminate-value.directive'; export * from './angular/forms/jscript-editor.component'; diff --git a/src/Squidex/app/framework/internal.ts b/src/Squidex/app/framework/internal.ts index b6dca6cbc..bb5d86e99 100644 --- a/src/Squidex/app/framework/internal.ts +++ b/src/Squidex/app/framework/internal.ts @@ -23,6 +23,7 @@ export * from './utils/date-time'; export * from './utils/duration'; export * from './utils/error'; export * from './utils/immutable-array'; +export * from './utils/lazy'; export * from './utils/math-helper'; export * from './utils/modal-view'; export * from './utils/pager'; diff --git a/src/Squidex/app/framework/state.ts b/src/Squidex/app/framework/state.ts index 28ff48cbc..4a3cf67dd 100644 --- a/src/Squidex/app/framework/state.ts +++ b/src/Squidex/app/framework/state.ts @@ -5,11 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AbstractControl, FormArray, FormGroup } from '@angular/forms'; +import { AbstractControl } from '@angular/forms'; import { BehaviorSubject, Observable } from 'rxjs'; -import { ErrorDto } from './utils/error'; -import { Types } from './utils/types'; +import { ErrorDto, Types } from '@app/framework/internal'; +import { fullValue} from './angular/forms/forms-helper'; export interface FormState { submitted: boolean; @@ -17,34 +17,6 @@ export interface FormState { error?: string; } -export class Lazy { - private valueSet = false; - private valueField: T; - - public get value(): T { - if (!this.valueSet) { - this.valueField = this.factory(); - this.valueSet = true; - } - - return this.valueField; - } - constructor( - private readonly factory: () => T - ) { - } -} - -export const formControls = (form: AbstractControl): AbstractControl[] => { - if (Types.is(form, FormGroup)) { - return Object.values(form.controls); - } else if (Types.is(form, FormArray)) { - return form.controls; - } else { - return []; - } -}; - export class Form { private readonly state = new State({ submitted: false }); @@ -85,7 +57,7 @@ export class Form { this.state.next({ submitted: true }); if (this.form.valid) { - const value = this.form.value; + const value = fullValue(this.form); this.disable(); @@ -133,6 +105,10 @@ export class Model { const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this, values); + if (Types.isFunction(clone.onCloned)) { + clone.onCloned(); + } + return clone; } } diff --git a/src/Squidex/app/framework/utils/lazy.spec.ts b/src/Squidex/app/framework/utils/lazy.spec.ts new file mode 100644 index 000000000..60d720ab1 --- /dev/null +++ b/src/Squidex/app/framework/utils/lazy.spec.ts @@ -0,0 +1,42 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Lazy } from './lazy'; + +describe('Lazy', () => { + it('should provider value', () => { + const lazy = new Lazy(() => 1); + + expect(lazy.value).toBe(1); + }); + + it('should call delegate once', () => { + let called = 0; + + const lazy = new Lazy(() => { + called++; + return 13; + }); + + expect(lazy.value).toBe(13); + expect(lazy.value).toBe(13); + expect(called).toBe(1); + }); + + it('should call delegate once when returned undefined', () => { + let called = 0; + + const lazy = new Lazy(() => { + called++; + return undefined; + }); + + expect(lazy.value).toBeUndefined(); + expect(lazy.value).toBeUndefined(); + expect(called).toBe(1); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/framework/utils/lazy.ts b/src/Squidex/app/framework/utils/lazy.ts new file mode 100644 index 000000000..4227aed92 --- /dev/null +++ b/src/Squidex/app/framework/utils/lazy.ts @@ -0,0 +1,24 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +export class Lazy { + private valueSet = false; + private valueField: T; + + public get value(): T { + if (!this.valueSet) { + this.valueField = this.factory(); + this.valueSet = true; + } + + return this.valueField; + } + constructor( + private readonly factory: () => T + ) { + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts index 7dc597698..01a0099d3 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -158,6 +158,7 @@ describe('SchemasService', () => { { fieldId: 101, name: 'field101', + isLocked: true, isHidden: true, isDisabled: true, properties: { @@ -167,6 +168,7 @@ describe('SchemasService', () => { { fieldId: 102, name: 'field102', + isLocked: true, isHidden: true, isDisabled: true, properties: { @@ -293,8 +295,8 @@ describe('SchemasService', () => { new Version('2'), [ new RootFieldDto(11, 'field11', createProperties('Array'), 'language', true, true, true, [ - new NestedFieldDto(101, 'field101', createProperties('String'), 11, true, true), - new NestedFieldDto(102, 'field102', createProperties('Number'), 11, true, true) + new NestedFieldDto(101, 'field101', createProperties('String'), 11, true, true, true), + new NestedFieldDto(102, 'field102', createProperties('Number'), 11, true, true, true) ]), new RootFieldDto(12, 'field12', createProperties('Assets'), 'language', true, true, true), new RootFieldDto(13, 'field13', createProperties('Boolean'), 'language', true, true, true), diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index 6deb838c2..ac168234d 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -16,7 +16,6 @@ import { ApiUrlConfig, DateTime, HTTP, - Lazy, Model, StringHelper, Version, @@ -26,10 +25,8 @@ import { import { createProperties, FieldPropertiesDto } from './schemas.types'; export class SchemaDto extends Model { - private readonly displayNameValue = new Lazy((() => StringHelper.firstNonEmpty(this.properties.label, this.name))); - public get displayName() { - return this.displayNameValue.value; + return StringHelper.firstNonEmpty(this.properties.label, this.name); } constructor( @@ -53,28 +50,8 @@ export class SchemaDto extends Model { } export class SchemaDetailsDto extends SchemaDto { - private inlineEditableFieldsValue = new Lazy(() => this.listFields.filter(x => x.isInlineEditable)); - private listFieldsValue = new Lazy(() => { - let fields = this.fields.filter(x => x.properties.isListField); - - if (fields.length === 0 && this.fields.length > 0) { - fields = [this.fields[0]]; - } - - if (fields.length === 0) { - fields = [{ properties: {} }]; - } - - return fields; - }); - - public get inlineEditableFields() { - return this.inlineEditableFieldsValue.value; - } - - public get listFields() { - return this.listFieldsValue.value; - } + public listFields: RootFieldDto[]; + public listFieldsEditable: RootFieldDto[]; constructor(id: string, name: string, category: string, properties: SchemaPropertiesDto, isPublished: boolean, created: DateTime, createdBy: string, lastModified: DateTime, lastModifiedBy: string, version: Version, public readonly fields: RootFieldDto[], @@ -85,6 +62,25 @@ export class SchemaDetailsDto extends SchemaDto { public readonly scriptChange?: string ) { super(id, name, category, properties, isPublished, created, createdBy, lastModified, lastModifiedBy, version); + + this.onCloned(); + } + + protected onCloned() { + if (this.fields) { + let fields = this.fields.filter(x => x.properties.isListField); + + if (fields.length === 0 && this.fields.length > 0) { + fields = [this.fields[0]]; + } + + if (fields.length === 0) { + fields = [{ properties: {} }]; + } + + this.listFields = fields; + this.listFieldsEditable = fields.filter(x => x.isInlineEditable); + } } public with(value: Partial): SchemaDetailsDto { diff --git a/src/Squidex/app/shared/state/contents.forms.spec.ts b/src/Squidex/app/shared/state/contents.forms.spec.ts index 6d4eb7952..e64cea3a6 100644 --- a/src/Squidex/app/shared/state/contents.forms.spec.ts +++ b/src/Squidex/app/shared/state/contents.forms.spec.ts @@ -129,7 +129,7 @@ describe('ArrayField', () => { }); it('should format to asset count', () => { - expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 Items(s)'); + expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 Item(s)'); }); it('should return zero formatting if other type', () => { @@ -250,15 +250,15 @@ describe('DateTimeField', () => { }); it('should return calculated date when Today for DateFieldProperties', () => { - Object.assign(field.properties, { calculatedFieldDefaultValue: 'Today' }); + Object.assign(field.properties, { calculatedDefaultValue: 'Today' }); - expect((field).properties.getFieldDefaultValue(now)).toEqual('2017-10-12'); + expect(FieldDefaultValue.get(field, now)).toEqual('2017-10-12'); }); it('should return calculated date when Now for DateFieldProperties', () => { - Object.assign(field.properties, { calculatedFieldDefaultValue: 'Now' }); + Object.assign(field.properties, { calculatedDefaultValue: 'Now' }); - expect((field).properties.getFieldDefaultValue(now)).toEqual('2017-10-12T16:30:10Z'); + expect(FieldDefaultValue.get(field, now)).toEqual('2017-10-12T16:30:10Z'); }); }); diff --git a/src/Squidex/app/shared/state/contents.forms.ts b/src/Squidex/app/shared/state/contents.forms.ts index fdbdb0ab7..cad79f703 100644 --- a/src/Squidex/app/shared/state/contents.forms.ts +++ b/src/Squidex/app/shared/state/contents.forms.ts @@ -43,7 +43,7 @@ export class FieldFormatter implements FieldPropertiesVisitor { } public static format(field: FieldDto, value: any) { - if (!value) { + if (value === null || value === undefined) { return ''; } @@ -259,8 +259,25 @@ export class FieldValidatorsFactory implements FieldPropertiesVisitor { - public static get(field: FieldDto) { - return field.properties.accept(new FieldDefaultValue()); + constructor( + private readonly now?: DateTime + ) { + } + + public visitDateTime(properties: DateTimeFieldPropertiesDto): any { + const now = this.now || DateTime.now(); + + if (properties.calculatedDefaultValue === 'Now') { + return now.toUTCStringFormat('YYYY-MM-DDTHH:mm:ss') + 'Z'; + } else if (properties.calculatedDefaultValue === 'Today') { + return now.toUTCStringFormat('YYYY-MM-DD'); + } else { + return properties.defaultValue; + } + } + + public static get(field: FieldDto, now?: DateTime) { + return field.properties.accept(new FieldDefaultValue(now)); } public visitArray(properties: ArrayFieldPropertiesDto): any { @@ -275,10 +292,6 @@ export class FieldDefaultValue implements FieldPropertiesVisitor { return properties.defaultValue; } - public visitDateTime(properties: DateTimeFieldPropertiesDto): any { - return null; - } - public visitGeolocation(properties: GeolocationFieldPropertiesDto): any { return null; } @@ -388,16 +401,24 @@ export class EditContentForm extends Form { const fieldValue = value ? value[field.name] || {} : {}; const addControls = (key: string, language: AppLanguageDto | null) => { - const languageValue = fieldValue[key]; - const languageForm = new FormArray([]); + const partitionValue = fieldValue[key]; - if (Types.isArray(languageValue)) { - for (let i = 0; i < languageValue.length; i++) { - this.addArrayItem(field, language, languageForm); - } + let partitionForm = fieldForm.controls[key]; + + if (!partitionForm) { + partitionForm = new FormArray([]); + + fieldForm.setControl(key, partitionForm); } - fieldForm.setControl(key, languageForm); + const length = Types.isArray(partitionValue) ? partitionValue.length : 0; + + while (partitionForm.controls.length < length) { + this.addArrayItem(field, language, partitionForm); + } + while (partitionForm.controls.length > length) { + partitionForm.removeAt(partitionForm.length - 1); + } }; if (field.isLocalizable) { @@ -432,19 +453,21 @@ export class EditContentForm extends Form { continue; } - if (field.properties.fieldType === 'Array') { + if (field.isArray) { + fieldForm.enable(); + for (let partitionForm of formControls(fieldForm)) { - for (let nested of field.nested) { - const nestedForm = partitionForm.get(nested.name); + for (let itemForm of formControls(partitionForm)) { + for (let nested of field.nested) { + const nestedForm = itemForm.get(nested.name); - if (!nestedForm) { - continue; - } + if (!nestedForm) { + continue; + } - if (nested.isDisabled) { - nestedForm.disable(); - } else { - nestedForm.enable(); + if (nested.isDisabled) { + nestedForm.disable({ onlySelf: true }); + } } } } @@ -464,7 +487,7 @@ export class PatchContentForm extends Form { ) { super(new FormGroup({})); - for (let field of this.schema.inlineEditableFields) { + for (let field of this.schema.listFieldsEditable) { const validators = FieldValidatorsFactory.createValidators(field, this.language.isOptional); this.form.setControl(field.name, new FormControl(undefined, validators)); @@ -477,7 +500,7 @@ export class PatchContentForm extends Form { if (result) { const request = {}; - for (let field of this.schema.inlineEditableFields) { + for (let field of this.schema.listFieldsEditable) { const value = result[field.name]; if (field.isLocalizable) { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs index 09afbd233..65a638810 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs @@ -27,18 +27,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent Fields.Number(1, "field1"), Fields.Number(2, "field2").Hide()); - [Fact] - public void Should_return_same_object_for_value_conversion_if_nothing_converted() - { - var input = - new ContentFieldData() - .AddValue("iv", new JObject()); - - var actual = FieldConverters.ForValues()(input, stringInvariantField); - - Assert.Same(input, actual); - } - [Fact] public void Should_filter_for_value_conversion() {