From 07328dfe2168502bd3c162b3fe1e977d03305e92 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 23 Sep 2019 10:42:27 +0200 Subject: [PATCH 1/3] Bug with unique checkbox fixed. (#419) --- .../pages/schema/field-wizard.component.ts | 2 +- .../schemas/pages/schema/field.component.ts | 2 +- .../types/string-validation.component.html | 2 +- .../app/shared/services/schemas.service.ts | 6 +- .../app/shared/services/schemas.types.ts | 179 +++++++----------- .../app/shared/state/contents.forms.spec.ts | 86 ++++----- 6 files changed, 112 insertions(+), 165 deletions(-) diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts index 0ce144904..c3f57e844 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.ts @@ -94,7 +94,7 @@ export class FieldWizardComponent implements OnInit { const value = this.editForm.submit(); if (value) { - const properties = createProperties(this.field.properties['fieldType'], value); + const properties = createProperties(this.field.properties.fieldType, value); this.schemasState.updateField(this.schema, this.field as RootFieldDto, { properties }) .subscribe(() => { diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.ts b/src/Squidex/app/features/schemas/pages/schema/field.component.ts index 9984f9935..43f64e4f0 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/field.component.ts @@ -115,7 +115,7 @@ export class FieldComponent implements OnChanges { const value = this.editForm.submit(); if (value) { - const properties = createProperties(this.field.properties['fieldType'], value); + const properties = createProperties(this.field.properties.fieldType, value); this.schemasState.updateField(this.schema, this.field, { properties }) .subscribe(() => { diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html index 884fd8c85..4d17ab9f4 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html @@ -1,5 +1,5 @@
-
+
diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index c87b1f16b..cf4b819ce 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -222,12 +222,8 @@ export class RootFieldDto extends FieldDto { return this.properties.fieldType === 'Array'; } - public get isString() { - return this.properties.fieldType === 'String'; - } - public get isTranslatable() { - return this.isLocalizable && this.isString && (this.properties.editor === 'Input' || this.properties.editor === 'Textarea'); + return this.isLocalizable && this.properties.isTranslateable; } constructor(links: ResourceLinks, fieldId: number, name: string, properties: FieldPropertiesDto, diff --git a/src/Squidex/app/shared/services/schemas.types.ts b/src/Squidex/app/shared/services/schemas.types.ts index 74da1a758..91e5d7531 100644 --- a/src/Squidex/app/shared/services/schemas.types.ts +++ b/src/Squidex/app/shared/services/schemas.types.ts @@ -62,42 +62,46 @@ export function createProperties(fieldType: FieldType, values?: any): FieldPrope switch (fieldType) { case 'Array': - properties = new ArrayFieldPropertiesDto(values); + properties = new ArrayFieldPropertiesDto(); break; case 'Assets': - properties = new AssetsFieldPropertiesDto(values); + properties = new AssetsFieldPropertiesDto(); break; case 'Boolean': - properties = new BooleanFieldPropertiesDto('Checkbox', values); + properties = new BooleanFieldPropertiesDto(); break; case 'DateTime': - properties = new DateTimeFieldPropertiesDto('DateTime', values); + properties = new DateTimeFieldPropertiesDto(); break; case 'Geolocation': - properties = new GeolocationFieldPropertiesDto(values); + properties = new GeolocationFieldPropertiesDto(); break; case 'Json': - properties = new JsonFieldPropertiesDto(values); + properties = new JsonFieldPropertiesDto(); break; case 'Number': - properties = new NumberFieldPropertiesDto('Input', values); + properties = new NumberFieldPropertiesDto(); break; case 'References': - properties = new ReferencesFieldPropertiesDto('List', values); + properties = new ReferencesFieldPropertiesDto(); break; case 'String': - properties = new StringFieldPropertiesDto('Input', values); + properties = new StringFieldPropertiesDto(); break; case 'Tags': - properties = new TagsFieldPropertiesDto(values); + properties = new TagsFieldPropertiesDto(); break; case 'UI': - properties = new UIFieldPropertiesDto(values); + properties = new UIFieldPropertiesDto(); break; default: throw 'Invalid properties type'; } + if (values) { + Object.assign(properties, values); + } + return properties; } @@ -129,19 +133,15 @@ export abstract class FieldPropertiesDto { public abstract fieldType: FieldType; public readonly editorUrl?: string; - public readonly label?: string; public readonly hints?: string; - public readonly placeholder?: string; - public readonly isRequired: boolean = false; public readonly isListField: boolean = false; public readonly isReferenceField: boolean = false; + public readonly isRequired: boolean = false; + public readonly label?: string; + public readonly placeholder?: string; - constructor(public readonly editor: string, - props?: Partial - ) { - if (props) { - Object.assign(this, props); - } + public get isTranslateable() { + return false; } public get isComplexUI() { @@ -162,14 +162,8 @@ export abstract class FieldPropertiesDto { export class ArrayFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Array'; - public readonly minItems?: number; public readonly maxItems?: number; - - constructor( - props?: Partial - ) { - super('Default', props); - } + public readonly minItems?: number; public accept(visitor: FieldPropertiesVisitor): T { return visitor.visitArray(this); @@ -179,92 +173,78 @@ export class ArrayFieldPropertiesDto extends FieldPropertiesDto { export class AssetsFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Assets'; - public readonly minItems?: number; + public readonly allowDuplicates?: boolean; + public readonly allowedExtensions?: string[]; + public readonly aspectHeight?: number; + public readonly aspectWidth?: number; + public readonly maxHeight?: number; public readonly maxItems?: number; - public readonly minSize?: number; public readonly maxSize?: number; - public readonly allowedExtensions?: string[]; - public readonly mustBeImage?: boolean; - public readonly minWidth?: number; public readonly maxWidth?: number; public readonly minHeight?: number; - public readonly maxHeight?: number; - public readonly aspectWidth?: number; - public readonly aspectHeight?: number; - public readonly allowDuplicates?: boolean; + public readonly minItems?: number; + public readonly minSize?: number; + public readonly minWidth?: number; + public readonly mustBeImage?: boolean; public get isSortable() { return false; } - constructor( - props?: Partial - ) { - super('Default', props); - } - public accept(visitor: FieldPropertiesVisitor): T { return visitor.visitAssets(this); } } +export type BooleanFieldEditor = 'Checkbox' | 'Toggle'; + export class BooleanFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Boolean'; - public readonly inlineEditable: boolean = false; public readonly defaultValue?: boolean; + public readonly editor: BooleanFieldEditor = 'Checkbox'; + public readonly inlineEditable: boolean = false; public get isComplexUI() { return false; } - constructor(editor: string, - props?: Partial - ) { - super(editor, props); - } - public accept(visitor: FieldPropertiesVisitor): T { return visitor.visitBoolean(this); } } +export type DateTimeFieldEditor = 'DateTime' | 'Date'; + export class DateTimeFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'DateTime'; + public readonly calculatedDefaultValue?: string; public readonly defaultValue?: string; + public readonly editor: DateTimeFieldEditor = 'DateTime'; public readonly maxValue?: string; public readonly minValue?: string; - public readonly calculatedDefaultValue?: string; public get isComplexUI() { return false; } - constructor(editor: string, - props?: Partial - ) { - super(editor, props); - } - public accept(visitor: FieldPropertiesVisitor): T { return visitor.visitDateTime(this); } } +export type GeolocationFieldEditor = 'Map'; + export class GeolocationFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Geolocation'; + public readonly editor: GeolocationFieldEditor = 'Map'; + public get isSortable() { return false; } - constructor( - props?: Partial - ) { - super('Map', props); - } - public accept(visitor: FieldPropertiesVisitor): T { return visitor.visitGeolocation(this); } @@ -277,87 +257,75 @@ export class JsonFieldPropertiesDto extends FieldPropertiesDto { return false; } - constructor( - props?: Partial - ) { - super('Default', props); - } - public accept(visitor: FieldPropertiesVisitor): T { return visitor.visitJson(this); } } +export type NumberFieldEditor = 'Input' | 'Radio' | 'Dropdown' | 'Stars'; + export class NumberFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Number'; + public readonly allowedValues?: number[]; + public readonly defaultValue?: number; + public readonly editor: NumberFieldEditor = 'Input'; public readonly inlineEditable: boolean = false; public readonly isUnique: boolean = false; - public readonly defaultValue?: number; public readonly maxValue?: number; public readonly minValue?: number; - public readonly allowedValues?: number[]; public get isComplexUI() { return false; } - constructor(editor: string, - props?: Partial - ) { - super(editor, props); - } - public accept(visitor: FieldPropertiesVisitor): T { return visitor.visitNumber(this); } } +export type ReferencesFieldEditor = 'List' | 'Dropdown'; + export class ReferencesFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'References'; - public readonly minItems?: number; + public readonly allowDuplicates?: boolean; + public readonly editor: ReferencesFieldEditor = 'List'; public readonly maxItems?: number; - public readonly editor: string; - public readonly schemaId?: string; + public readonly minItems?: number; public readonly resolveReference?: boolean; - public readonly allowDuplicates?: boolean; + public readonly schemaId?: string; public get isSortable() { return false; } - constructor(editor: string, - props?: Partial - ) { - super(editor, props); - } - public accept(visitor: FieldPropertiesVisitor): T { return visitor.visitReferences(this); } } +export type StringEditor = 'Color' | 'Dropdown' | 'Html' | 'Input' | 'Markdown' | 'Radio' | 'RichText' | 'Slug' | 'TextArea'; + export class StringFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'String'; - public readonly inlineEditable = false; - public readonly isUnique: boolean = false; + public readonly allowedValues?: string[]; public readonly defaultValue?: string; + public readonly editor: StringEditor = 'Input'; + public readonly inlineEditable: boolean = false; + public readonly isUnique: boolean = false; + public readonly maxLength?: number; + public readonly minLength?: number; public readonly pattern?: string; public readonly patternMessage?: string; - public readonly minLength?: number; - public readonly maxLength?: number; - public readonly allowedValues?: string[]; public get isComplexUI() { return this.editor !== 'Input' && this.editor !== 'Color' && this.editor !== 'Radio' && this.editor !== 'Slug' && this.editor !== 'TextArea'; } - constructor(editor: string, - props?: Partial - ) { - super(editor, props); + public get isTranslateable() { + return this.editor === 'Input' || this.editor === 'TextArea'; } public accept(visitor: FieldPropertiesVisitor): T { @@ -365,12 +333,15 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto { } } +export type TagsFieldEditor = 'Tags' | 'Checkboxes' | 'Dropdown'; + export class TagsFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Tags'; - public readonly minItems?: number; - public readonly maxItems?: number; public readonly allowedValues?: string[]; + public readonly editor: TagsFieldEditor = 'Tags'; + public readonly maxItems?: number; + public readonly minItems?: number; public get isComplexUI() { return false; @@ -380,12 +351,6 @@ export class TagsFieldPropertiesDto extends FieldPropertiesDto { return false; } - constructor( - props?: Partial - ) { - super('Tags', props); - } - public accept(visitor: FieldPropertiesVisitor): T { return visitor.visitTags(this); } @@ -394,6 +359,8 @@ export class TagsFieldPropertiesDto extends FieldPropertiesDto { export class UIFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'UI'; + public readonly editor = 'Separator'; + public get isComplexUI() { return false; } @@ -406,12 +373,6 @@ export class UIFieldPropertiesDto extends FieldPropertiesDto { return false; } - constructor( - props?: Partial - ) { - super('Separator', props); - } - public accept(visitor: FieldPropertiesVisitor): T { return visitor.visitUI(this); } diff --git a/src/Squidex/app/shared/state/contents.forms.spec.ts b/src/Squidex/app/shared/state/contents.forms.spec.ts index 31e246045..00fa72c93 100644 --- a/src/Squidex/app/shared/state/contents.forms.spec.ts +++ b/src/Squidex/app/shared/state/contents.forms.spec.ts @@ -9,32 +9,22 @@ import { AbstractControl, FormArray } from '@angular/forms'; import { AppLanguageDto, - ArrayFieldPropertiesDto, - AssetsFieldPropertiesDto, - BooleanFieldPropertiesDto, createProperties, DateTime, - DateTimeFieldPropertiesDto, EditContentForm, FieldDefaultValue, FieldFormatter, FieldPropertiesDto, FieldsValidators, - GeolocationFieldPropertiesDto, getContentValue, HtmlValue, ImmutableArray, - JsonFieldPropertiesDto, LanguageDto, NestedFieldDto, - NumberFieldPropertiesDto, PartitionConfig, - ReferencesFieldPropertiesDto, RootFieldDto, SchemaDetailsDto, SchemaPropertiesDto, - StringFieldPropertiesDto, - TagsFieldPropertiesDto, Version } from '@app/shared/internal'; @@ -67,9 +57,9 @@ describe('SchemaDetailsDto', () => { }); it('should return configured fields as list fields if no schema field are declared', () => { - const field1 = createField({ properties: new ArrayFieldPropertiesDto({ isListField: true }) }); - const field2 = createField({ properties: new ArrayFieldPropertiesDto({ isListField: false }), id: 2 }); - const field3 = createField({ properties: new ArrayFieldPropertiesDto({ isListField: true }), id: 3 }); + const field1 = createField({ properties: createProperties('Array', { isListField: true }) }); + const field2 = createField({ properties: createProperties('Array', { isListField: false }), id: 2 }); + const field3 = createField({ properties: createProperties('Array', { isListField: true }), id: 3 }); const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] }); @@ -77,9 +67,9 @@ describe('SchemaDetailsDto', () => { }); it('should return first fields as list fields if no schema field is declared', () => { - const field1 = createField({ properties: new ArrayFieldPropertiesDto() }); - const field2 = createField({ properties: new ArrayFieldPropertiesDto(), id: 2 }); - const field3 = createField({ properties: new ArrayFieldPropertiesDto(), id: 3 }); + const field1 = createField({ properties: createProperties('Array') }); + const field2 = createField({ properties: createProperties('Array'), id: 2 }); + const field3 = createField({ properties: createProperties('Array'), id: 3 }); const schema = createSchema({ properties: new SchemaPropertiesDto(''), fields: [field1, field2, field3] }); @@ -95,50 +85,50 @@ describe('SchemaDetailsDto', () => { describe('FieldDto', () => { it('should return label as display name', () => { - const field = createField({ properties: new AssetsFieldPropertiesDto({ label: 'Label' }) }); + 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: new AssetsFieldPropertiesDto() }); + 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: new AssetsFieldPropertiesDto({ label: '' }) }); + const field = createField({ properties: createProperties('Assets', { label: '' }) }); expect(field.displayName).toBe('field1'); }); it('should return placeholder as display placeholder', () => { - const field = createField({ properties: new AssetsFieldPropertiesDto({ placeholder: '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: new AssetsFieldPropertiesDto() }); + const field = createField({ properties: createProperties('Assets') }); expect(field.displayPlaceholder).toBe(''); }); it('should return localizable if partitioning is language', () => { - const field = createField({ properties: new AssetsFieldPropertiesDto(), partitioning: '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: new AssetsFieldPropertiesDto(), partitioning: 'invariant' }); + const field = createField({ properties: createProperties('Assets'), partitioning: 'invariant' }); expect(field.isLocalizable).toBeFalsy(); }); }); describe('ArrayField', () => { - const field = createField({ properties: new ArrayFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }) }); + const field = createField({ properties: createProperties('Array', { isRequired: true, minItems: 1, maxItems: 5 }) }); it('should create validators', () => { expect(FieldsValidators.create(field, false).length).toBe(2); @@ -162,7 +152,7 @@ describe('ArrayField', () => { }); describe('AssetsField', () => { - const field = createField({ properties: new AssetsFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }) }); + const field = createField({ properties: createProperties('Assets', { isRequired: true, minItems: 1, maxItems: 5 }) }); it('should create validators', () => { expect(FieldsValidators.create(field, false).length).toBe(3); @@ -186,7 +176,7 @@ describe('AssetsField', () => { }); describe('TagsField', () => { - const field = createField({ properties: new TagsFieldPropertiesDto({ isRequired: true, minItems: 1, maxItems: 5 }) }); + const field = createField({ properties: createProperties('Tags', { isRequired: true, minItems: 1, maxItems: 5 }) }); it('should create validators', () => { expect(FieldsValidators.create(field, false).length).toBe(2); @@ -210,7 +200,7 @@ describe('TagsField', () => { }); describe('BooleanField', () => { - const field = createField({ properties: new BooleanFieldPropertiesDto('Checkbox', { isRequired: true }) }); + const field = createField({ properties: createProperties('Boolean', { editor: 'Checkbox', isRequired: true }) }); it('should create validators', () => { expect(FieldsValidators.create(field, false).length).toBe(1); @@ -229,7 +219,7 @@ describe('BooleanField', () => { }); it('should return default value for default properties', () => { - const field2 = createField({ properties: new BooleanFieldPropertiesDto('Checkbox', { defaultValue: true }) }); + const field2 = createField({ properties: createProperties('Boolean', { editor: 'Checkbox', defaultValue: true }) }); expect(FieldDefaultValue.get(field2)).toBeTruthy(); }); @@ -237,7 +227,7 @@ describe('BooleanField', () => { describe('DateTimeField', () => { const now = DateTime.parseISO_UTC('2017-10-12T16:30:10Z'); - const field = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { isRequired: true }) }); + const field = createField({ properties: createProperties('DateTime', { editor: 'DateTime', isRequired: true }) }); it('should create validators', () => { expect(FieldsValidators.create(field, false).length).toBe(1); @@ -252,38 +242,38 @@ describe('DateTimeField', () => { }); it('should format to date', () => { - const dateField = createField({ properties: new DateTimeFieldPropertiesDto('Date') }); + const dateField = createField({ properties: createProperties('DateTime', { editor: 'Date' }) }); expect(FieldFormatter.format(dateField, '2017-12-12T16:00:00Z')).toBe('2017-12-12'); }); it('should format to date time', () => { - const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime') }); + const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime' }) }); expect(FieldFormatter.format(field2, '2017-12-12T16:00:00Z')).toBe('2017-12-12 16:00:00'); }); it('should return default for DateFieldProperties', () => { - const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { defaultValue: '2017-10-12T16:00:00Z' }) }); + const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', defaultValue: '2017-10-12T16:00:00Z' }) }); expect(FieldDefaultValue.get(field2)).toEqual('2017-10-12T16:00:00Z'); }); it('should return calculated date when Today for DateFieldProperties', () => { - const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { calculatedDefaultValue: 'Today' }) }); + const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', calculatedDefaultValue: 'Today' }) }); expect(FieldDefaultValue.get(field2, now)).toEqual('2017-10-12T00:00:00Z'); }); it('should return calculated date when Now for DateFieldProperties', () => { - const field2 = createField({ properties: new DateTimeFieldPropertiesDto('DateTime', { calculatedDefaultValue: 'Now' }) }); + const field2 = createField({ properties: createProperties('DateTime', { editor: 'DateTime', calculatedDefaultValue: 'Now' }) }); expect(FieldDefaultValue.get(field2, now)).toEqual('2017-10-12T16:30:10Z'); }); }); describe('GeolocationField', () => { - const field = createField({ properties: new GeolocationFieldPropertiesDto({ isRequired: true }) }); + const field = createField({ properties: createProperties('Geolocation', { isRequired: true }) }); it('should create validators', () => { expect(FieldsValidators.create(field, false).length).toBe(1); @@ -303,7 +293,7 @@ describe('GeolocationField', () => { }); describe('JsonField', () => { - const field = createField({ properties: new JsonFieldPropertiesDto({ isRequired: true }) }); + const field = createField({ properties: createProperties('Json', { isRequired: true }) }); it('should create validators', () => { expect(FieldsValidators.create(field, false).length).toBe(1); @@ -323,7 +313,7 @@ describe('JsonField', () => { }); describe('NumberField', () => { - const field = createField({ properties: new NumberFieldPropertiesDto('Input', { isRequired: true, minValue: 1, maxValue: 6, allowedValues: [1, 3] }) }); + 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); @@ -338,44 +328,44 @@ describe('NumberField', () => { }); it('should format to stars if html allowed', () => { - const field2 = createField({ properties: new NumberFieldPropertiesDto('Stars') }); + 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: new NumberFieldPropertiesDto('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: new NumberFieldPropertiesDto('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: new NumberFieldPropertiesDto('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: new NumberFieldPropertiesDto('Stars') }); + const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) }); expect(FieldFormatter.format(field2, 3, false)).toEqual('3'); }); it('should return default value for default properties', () => { - const field2 = createField({ properties: new NumberFieldPropertiesDto('Input', { defaultValue: 13 }) }); + const field2 = createField({ properties: createProperties('Number', { defaultValue: 13 }) }); expect(FieldDefaultValue.get(field2)).toEqual(13); }); }); describe('ReferencesField', () => { - const field = createField({ properties: new ReferencesFieldPropertiesDto('List', { isRequired: true, minItems: 1, maxItems: 5 }) }); + 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); @@ -399,7 +389,7 @@ describe('ReferencesField', () => { }); describe('StringField', () => { - const field = createField({ properties: new StringFieldPropertiesDto('Input', { isRequired: true, pattern: 'pattern', minLength: 1, maxLength: 5, allowedValues: ['a', 'b'] }) }); + 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); @@ -414,7 +404,7 @@ describe('StringField', () => { }); it('should return default value for default properties', () => { - const field2 = createField({ properties: new StringFieldPropertiesDto('Input', { defaultValue: 'MyDefault' }) }); + const field2 = createField({ properties: createProperties('String', { defaultValue: 'MyDefault' }) }); expect(FieldDefaultValue.get(field2)).toEqual('MyDefault'); }); @@ -422,8 +412,8 @@ describe('StringField', () => { describe('GetContentValue', () => { const language = new LanguageDto('en', 'English'); - const fieldInvariant = createField({ properties: new NumberFieldPropertiesDto('Input'), partitioning: 'invariant' }); - const fieldLocalized = createField({ properties: new NumberFieldPropertiesDto('Input') }); + const fieldInvariant = createField({ properties: createProperties('Number'), partitioning: 'invariant' }); + const fieldLocalized = createField({ properties: createProperties('Number') }); it('should resolve invariant field from references value', () => { const content: any = { From fce1c9558b82002567d5bb620c4868153a4ee970 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 23 Sep 2019 13:11:43 +0200 Subject: [PATCH 2/3] Better cancellation for migration. (#418) * Better cancellation for migration. --- src/Squidex.Infrastructure/Migrations/Migrator.cs | 5 +++-- src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs | 1 + src/Squidex/Config/Startup/MigratorHost.cs | 2 +- .../Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Squidex.Infrastructure/Migrations/Migrator.cs b/src/Squidex.Infrastructure/Migrations/Migrator.cs index 63c801339..0748e5a1a 100644 --- a/src/Squidex.Infrastructure/Migrations/Migrator.cs +++ b/src/Squidex.Infrastructure/Migrations/Migrator.cs @@ -7,6 +7,7 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Squidex.Infrastructure.Log; @@ -32,7 +33,7 @@ namespace Squidex.Infrastructure.Migrations this.log = log; } - public async Task MigrateAsync() + public async Task MigrateAsync(CancellationToken ct = default) { var version = 0; @@ -49,7 +50,7 @@ namespace Squidex.Infrastructure.Migrations version = await migrationStatus.GetVersionAsync(); - while (true) + while (!ct.IsCancellationRequested) { var (newVersion, migrations) = migrationPath.GetNext(version); diff --git a/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs b/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs index ae47858a5..cebe9623f 100644 --- a/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs +++ b/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs @@ -28,6 +28,7 @@ namespace Squidex.Infrastructure.Orleans { for (var i = 1; i <= NumTries; i++) { + ct.ThrowIfCancellationRequested(); try { var grain = grainFactory.GetGrain(SingleGrain.Id); diff --git a/src/Squidex/Config/Startup/MigratorHost.cs b/src/Squidex/Config/Startup/MigratorHost.cs index f65867da4..53ff2305e 100644 --- a/src/Squidex/Config/Startup/MigratorHost.cs +++ b/src/Squidex/Config/Startup/MigratorHost.cs @@ -25,7 +25,7 @@ namespace Squidex.Config.Startup protected override Task StartAsync(ISemanticLog log, CancellationToken ct) { - return migrator.MigrateAsync(); + return migrator.MigrateAsync(ct); } } } diff --git a/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs b/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs index 45a5559e7..6237b3b04 100644 --- a/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs @@ -108,7 +108,7 @@ namespace Squidex.Infrastructure.Migrations A.CallTo(() => migrator_1_2.UpdateAsync()).Throws(new ArgumentException()); - await Assert.ThrowsAsync(sut.MigrateAsync); + await Assert.ThrowsAsync(() => sut.MigrateAsync()); A.CallTo(() => migrator_0_1.UpdateAsync()).MustHaveHappened(); A.CallTo(() => migrator_1_2.UpdateAsync()).MustHaveHappened(); @@ -147,7 +147,7 @@ namespace Squidex.Infrastructure.Migrations var sut = new Migrator(new InMemoryStatus(), path, log) { LockWaitMs = 2 }; - await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(sut.MigrateAsync))); + await Task.WhenAll(Enumerable.Repeat(0, 10).Select(x => Task.Run(() => sut.MigrateAsync()))); A.CallTo(() => migrator_0_1.UpdateAsync()) .MustHaveHappened(1, Times.Exactly); From 0997858fc154b524df9dda1870f58e4a771316d4 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 23 Sep 2019 13:12:08 +0200 Subject: [PATCH 3/3] Feature/sync schema (#417) * Sync schemas in UI --- src/Squidex/Config/Web/WebServices.cs | 6 +++ .../content/content-field.component.html | 4 +- .../pages/content/content-field.component.ts | 4 ++ .../content/shared/content.component.html | 2 +- .../content/shared/content.component.ts | 6 ++- .../schema/schema-export-form.component.html | 40 ++++++++++++++---- .../schema/schema-export-form.component.scss | 6 ++- .../schema/schema-export-form.component.ts | 42 ++++++++++++++++--- .../pages/schema/schema-page.component.html | 2 +- .../workflows/workflow-step.component.html | 8 ++-- .../workflows/workflow-step.component.ts | 16 +++++++ .../angular/forms/json-editor.component.scss | 2 +- .../framework/angular/panel.component.html | 2 +- .../app/framework/angular/panel.component.ts | 4 ++ .../framework/services/dialog.service.spec.ts | 4 +- .../app/framework/services/dialog.service.ts | 2 +- .../queries/filter-comparison.component.html | 2 +- .../queries/filter-comparison.component.ts | 6 ++- .../queries/filter-logical.component.html | 4 +- .../queries/filter-logical.component.ts | 6 ++- .../shared/services/schemas.service.spec.ts | 27 ++++++++++++ .../app/shared/services/schemas.service.ts | 26 +++++++++++- src/Squidex/app/shared/state/schemas.forms.ts | 10 +++++ .../app/shared/state/schemas.state.spec.ts | 16 +++++++ src/Squidex/app/shared/state/schemas.state.ts | 8 ++++ 25 files changed, 218 insertions(+), 37 deletions(-) diff --git a/src/Squidex/Config/Web/WebServices.cs b/src/Squidex/Config/Web/WebServices.cs index 7cbcc446d..97ef2eb56 100644 --- a/src/Squidex/Config/Web/WebServices.cs +++ b/src/Squidex/Config/Web/WebServices.cs @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -52,6 +53,11 @@ namespace Squidex.Config.Web services.AddSingletonAs() .AsOptional(); + services.Configure(options => + { + options.SuppressModelStateInvalidFilter = true; + }); + services.AddMvc(options => { options.Filters.Add(); diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 970faac5e..034c25fdb 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -9,7 +9,7 @@ @@ -67,7 +67,7 @@ diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.ts b/src/Squidex/app/features/content/pages/content/content-field.component.ts index 8355376dc..40f587cdf 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-field.component.ts @@ -184,6 +184,10 @@ export class ContentFieldComponent implements DoCheck, OnChanges { } } + public emitLanguageChange(language: AppLanguageDto) { + this.languageChange.emit(language); + } + public prefix(language: AppLanguageDto) { return `(${language.iso2Code})`; } diff --git a/src/Squidex/app/features/content/shared/content.component.html b/src/Squidex/app/features/content/shared/content.component.html index a43854b26..509a397f6 100644 --- a/src/Squidex/app/features/content/shared/content.component.html +++ b/src/Squidex/app/features/content/shared/content.component.html @@ -4,7 +4,7 @@ + (ngModelChange)="emitSelectedChange($event)" /> diff --git a/src/Squidex/app/features/content/shared/content.component.ts b/src/Squidex/app/features/content/shared/content.component.ts index dc42ed5ad..56ad23dd7 100644 --- a/src/Squidex/app/features/content/shared/content.component.ts +++ b/src/Squidex/app/features/content/shared/content.component.ts @@ -42,7 +42,7 @@ export class ContentComponent implements OnChanges { public statusChange = new EventEmitter(); @Output() - public selectedChange = new EventEmitter(); + public selectedChange = new EventEmitter(); @Input() public selected = false; @@ -140,6 +140,10 @@ export class ContentComponent implements OnChanges { this.updateValues(); } + public emitSelectedChange(isSelected: boolean) { + this.selectedChange.emit(isSelected); + } + public emitDelete() { this.delete.emit(); } diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html index 56dec217d..b6361cefb 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.html @@ -1,9 +1,31 @@ - - - Export Schema - - - - - - \ No newline at end of file +
+ + + Export Schema + + + + + + + +
+
+ + +
+ +
+ + +
+
+ + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss index fbb752506..1adbc54e4 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss +++ b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.scss @@ -1,2 +1,6 @@ @import '_vars'; -@import '_mixins'; \ No newline at end of file +@import '_mixins'; + +.json { + min-height: 500px; +} \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts index dcf003200..abdfea678 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-export-form.component.ts @@ -5,26 +5,56 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; -import { SchemaDetailsDto } from '@app/shared'; +import { + SchemaDetailsDto, + SchemasState, + SynchronizeSchemaForm +} from '@app/shared'; @Component({ selector: 'sqx-schema-export-form', styleUrls: ['./schema-export-form.component.scss'], templateUrl: './schema-export-form.component.html' }) -export class SchemaExportFormComponent implements OnInit { +export class SchemaExportFormComponent implements OnChanges { @Output() public complete = new EventEmitter(); @Input() public schema: SchemaDetailsDto; - public export: any; + public synchronizeForm = new SynchronizeSchemaForm(this.formBuilder); - public ngOnInit() { - this.export = this.schema.export(); + constructor( + private readonly formBuilder: FormBuilder, + private readonly schemasState: SchemasState + ) { + } + + public ngOnChanges() { + this.synchronizeForm.form.get('json')!.setValue(this.schema.export()); + } + + public synchronizeSchema() { + const value = this.synchronizeForm.submit(); + + if (value) { + const request = { + ...value.json, + noFieldDeletion: !value.fieldsDelete, + noFieldRecreation: !value.fieldsDelete + }; + + this.schemasState.synchronize(this.schema, request) + .subscribe(() => { + this.synchronizeForm.submitCompleted(); + }, error => { + this.synchronizeForm.submitFailed(error); + }); + } } public emitComplete() { diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index a4689d3e4..d3bebe6cc 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -7,7 +7,7 @@
diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html index 7a899def9..52ae9dba2 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html +++ b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.html @@ -1,7 +1,7 @@
-
-
@@ -40,7 +40,7 @@ [transition]="transition" [disabled]="disabled" [roles]="roles" - (remove)="transitionRemove.emit(transition)" + (remove)="emitTransitionRemove(transition)" (update)="changeTransition(transition, $event)"> @@ -56,7 +56,7 @@
-
diff --git a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts index c33fe36f7..c73fab4c7 100644 --- a/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts +++ b/src/Squidex/app/features/settings/pages/workflows/workflow-step.component.ts @@ -88,6 +88,22 @@ export class WorkflowStepComponent implements OnChanges { this.update.emit({ noUpdate }); } + public emitMakeInitial() { + this.makeInitial.emit(); + } + + public emitTransitionAdd(transition: WorkflowStep) { + this.transitionAdd.emit(transition); + } + + public emitTransitionRemove(transition: WorkflowTransition) { + this.transitionRemove.emit(transition); + } + + public emitRemove() { + this.remove.emit(); + } + public trackByTransition(index: number, transition: WorkflowTransition) { return transition.to; } diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.scss b/src/Squidex/app/framework/angular/forms/json-editor.component.scss index 9a7b03689..ebf88817e 100644 --- a/src/Squidex/app/framework/angular/forms/json-editor.component.scss +++ b/src/Squidex/app/framework/angular/forms/json-editor.component.scss @@ -3,7 +3,7 @@ // sass-lint:disable class-name-format -$editor-height: 20rem; +$editor-height: 30rem; :host ::ng-deep { .ace_editor { diff --git a/src/Squidex/app/framework/angular/panel.component.html b/src/Squidex/app/framework/angular/panel.component.html index 11dc4494d..2223810c6 100644 --- a/src/Squidex/app/framework/angular/panel.component.html +++ b/src/Squidex/app/framework/angular/panel.component.html @@ -18,7 +18,7 @@ - + diff --git a/src/Squidex/app/framework/angular/panel.component.ts b/src/Squidex/app/framework/angular/panel.component.ts index b4f593f49..8f7015f50 100644 --- a/src/Squidex/app/framework/angular/panel.component.ts +++ b/src/Squidex/app/framework/angular/panel.component.ts @@ -124,4 +124,8 @@ export class PanelComponent implements AfterViewInit, OnDestroy, OnInit { } } } + + public emitClose() { + this.close.emit(); + } } \ No newline at end of file diff --git a/src/Squidex/app/framework/services/dialog.service.spec.ts b/src/Squidex/app/framework/services/dialog.service.spec.ts index 376841628..5337b2684 100644 --- a/src/Squidex/app/framework/services/dialog.service.spec.ts +++ b/src/Squidex/app/framework/services/dialog.service.spec.ts @@ -29,7 +29,7 @@ describe('DialogService', () => { it('should create error notification', () => { const notification = Notification.error('MyError'); - expect(notification.displayTime).toBe(5000); + expect(notification.displayTime).toBe(10000); expect(notification.message).toBe('MyError'); expect(notification.messageType).toBe('danger'); }); @@ -37,7 +37,7 @@ describe('DialogService', () => { it('should create info notification', () => { const notification = Notification.info('MyInfo'); - expect(notification.displayTime).toBe(5000); + expect(notification.displayTime).toBe(10000); expect(notification.message).toBe('MyInfo'); expect(notification.messageType).toBe('info'); }); diff --git a/src/Squidex/app/framework/services/dialog.service.ts b/src/Squidex/app/framework/services/dialog.service.ts index 087f09626..1def51649 100644 --- a/src/Squidex/app/framework/services/dialog.service.ts +++ b/src/Squidex/app/framework/services/dialog.service.ts @@ -47,7 +47,7 @@ export class Notification { constructor( public readonly message: string, public readonly messageType: string, - public readonly displayTime: number = 5000 + public readonly displayTime: number = 10000 ) { } diff --git a/src/Squidex/app/shared/components/queries/filter-comparison.component.html b/src/Squidex/app/shared/components/queries/filter-comparison.component.html index 626f506a6..33c65524b 100644 --- a/src/Squidex/app/shared/components/queries/filter-comparison.component.html +++ b/src/Squidex/app/shared/components/queries/filter-comparison.component.html @@ -60,7 +60,7 @@
-
diff --git a/src/Squidex/app/shared/components/queries/filter-comparison.component.ts b/src/Squidex/app/shared/components/queries/filter-comparison.component.ts index ca18b65e5..af8497792 100644 --- a/src/Squidex/app/shared/components/queries/filter-comparison.component.ts +++ b/src/Squidex/app/shared/components/queries/filter-comparison.component.ts @@ -100,7 +100,11 @@ export class FilterComparisonComponent implements OnChanges { this.fieldModel = newModel; } - private emitChange() { + public emitRemove() { + this.remove.emit(); + } + + public emitChange() { this.change.emit(); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/components/queries/filter-logical.component.html b/src/Squidex/app/shared/components/queries/filter-logical.component.html index aa7e1eae9..5f04ae50d 100644 --- a/src/Squidex/app/shared/components/queries/filter-logical.component.html +++ b/src/Squidex/app/shared/components/queries/filter-logical.component.html @@ -13,7 +13,7 @@
-
@@ -26,7 +26,7 @@ + (remove)="removeFilter(filter)" (change)="emitChange()">
diff --git a/src/Squidex/app/shared/components/queries/filter-logical.component.ts b/src/Squidex/app/shared/components/queries/filter-logical.component.ts index 40c494749..6bc74b4fa 100644 --- a/src/Squidex/app/shared/components/queries/filter-logical.component.ts +++ b/src/Squidex/app/shared/components/queries/filter-logical.component.ts @@ -94,7 +94,11 @@ export class FilterLogicalComponent { } } - private emitChange() { + public emitRemove() { + this.remove.emit(); + } + + public emitChange() { this.change.emit(); } } \ 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 f01968ff5..0af51cbeb 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -181,6 +181,33 @@ describe('SchemasService', () => { expect(schema!).toEqual(createSchemaDetails(12)); })); + it('should make put request to synchronize schema', + inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { + + const dto = {}; + + const resource: Resource = { + _links: { + ['update/sync']: { method: 'PUT', href: '/api/apps/my-app/schemas/my-schema/sync' } + } + }; + + let schema: SchemaDetailsDto; + + schemasService.putSchemaSync('my-app', resource, dto, version).subscribe(result => { + schema = result; + }); + + const req = httpMock.expectOne('http://service/p/api/apps/my-app/schemas/my-schema/sync'); + + expect(req.request.method).toEqual('PUT'); + expect(req.request.headers.get('If-Match')).toBe(version.value); + + req.flush(schemaDetailsResponse(12)); + + expect(schema!).toEqual(createSchemaDetails(12)); + })); + it('should make put request to update category', inject([SchemasService, HttpTestingController], (schemasService: SchemasService, httpMock: HttpTestingController) => { diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index cf4b819ce..6726d945d 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -41,6 +41,7 @@ export class SchemaDto { public readonly canOrderFields: boolean; public readonly canPublish: boolean; public readonly canReadContents: boolean; + public readonly canSynchronize: boolean; public readonly canUnpublish: boolean; public readonly canUpdate: boolean; public readonly canUpdateCategory: boolean; @@ -69,6 +70,7 @@ export class SchemaDto { this.canOrderFields = hasAnyLink(links, 'fields/order'); this.canPublish = hasAnyLink(links, 'publish'); this.canReadContents = hasAnyLink(links, 'contents'); + this.canSynchronize = hasAnyLink(this, 'update/sync'); this.canUnpublish = hasAnyLink(links, 'unpublish'); this.canUpdate = hasAnyLink(links, 'update'); this.canUpdateCategory = hasAnyLink(links, 'update/category'); @@ -125,7 +127,7 @@ export class SchemaDetailsDto extends SchemaDto { const clone = {}; for (const key in source) { - if (source.hasOwnProperty(key) && exclude.indexOf(key) < 0) { + if (source.hasOwnProperty(key) && exclude.indexOf(key) < 0 && key.indexOf('can') !== 0) { const value = source[key]; if (value) { @@ -139,7 +141,7 @@ export class SchemaDetailsDto extends SchemaDto { const result: any = { fields: this.fields.map(field => { - const copy = cleanup(field, 'fieldId'); + const copy = cleanup(field, 'fieldId', '_links'); copy.properties = cleanup(field.properties); @@ -280,6 +282,11 @@ export interface UpdateFieldDto { readonly properties: FieldPropertiesDto; } +export interface SynchronizeSchemaDto { + noFieldDeletiong?: boolean; + noFieldRecreation?: boolean; +} + export interface UpdateSchemaDto { readonly label?: string; readonly hints?: string; @@ -342,6 +349,21 @@ export class SchemasService { pretifyError('Failed to update schema scripts. Please reload.')); } + public putSchemaSync(appName: string, resource: Resource, dto: SynchronizeSchemaDto & any, version: Version): Observable { + const link = resource._links['update/sync']; + + const url = this.apiUrl.buildUrl(link.href); + + return HTTP.requestVersioned(this.http, link.method, url, version, dto).pipe( + map(({ payload }) => { + return parseSchemaWithDetails(payload.body); + }), + tap(() => { + this.analytics.trackEvent('Schema', 'Updated', appName); + }), + pretifyError('Failed to synchronize schema. Please reload.')); + } + public putSchema(appName: string, resource: Resource, dto: UpdateSchemaDto, version: Version): Observable { const link = resource._links['update']; diff --git a/src/Squidex/app/shared/state/schemas.forms.ts b/src/Squidex/app/shared/state/schemas.forms.ts index 5442beb61..85e837918 100644 --- a/src/Squidex/app/shared/state/schemas.forms.ts +++ b/src/Squidex/app/shared/state/schemas.forms.ts @@ -65,6 +65,16 @@ export class AddPreviewUrlForm extends Form { + constructor(formBuilder: FormBuilder) { + super(formBuilder.group({ + json: {}, + fieldsDelete: false, + fieldsRecreate: false + })); + } +} + export class ConfigurePreviewUrlsForm extends Form { constructor( private readonly formBuilder: FormBuilder diff --git a/src/Squidex/app/shared/state/schemas.state.spec.ts b/src/Squidex/app/shared/state/schemas.state.spec.ts index 9607e8372..8529366d9 100644 --- a/src/Squidex/app/shared/state/schemas.state.spec.ts +++ b/src/Squidex/app/shared/state/schemas.state.spec.ts @@ -302,6 +302,22 @@ describe('SchemasState', () => { expect(schemasState.snapshot.selectedSchema).toEqual(updated); }); + it('should update schema and selected schema when schema synced', () => { + const request = {}; + + const updated = createSchemaDetails(1, '_new'); + + schemasService.setup(x => x.putSchemaSync(app, schema1, It.isAny(), version)) + .returns(() => of(updated)).verifiable(); + + schemasState.synchronize(schema1, request).subscribe(); + + const schema1New = schemasState.snapshot.schemas.at(0); + + expect(schema1New).toEqual(updated); + expect(schemasState.snapshot.selectedSchema).toEqual(updated); + }); + it('should update schema and selected schema when scripts configured', () => { const request = { query: '' }; diff --git a/src/Squidex/app/shared/state/schemas.state.ts b/src/Squidex/app/shared/state/schemas.state.ts index e921befe6..0da3fad96 100644 --- a/src/Squidex/app/shared/state/schemas.state.ts +++ b/src/Squidex/app/shared/state/schemas.state.ts @@ -219,6 +219,14 @@ export class SchemasState extends State { shareSubscribed(this.dialogs)); } + public synchronize(schema: SchemaDto, request: {}): Observable { + return this.schemasService.putSchemaSync(this.appName, schema, request, schema.version).pipe( + tap(updated => { + this.replaceSchema(updated); + }), + shareSubscribed(this.dialogs)); + } + public update(schema: SchemaDto, request: UpdateSchemaDto): Observable { return this.schemasService.putSchema(this.appName, schema, request, schema.version).pipe( tap(updated => {