From a6b5318bedbe2c33f3330dcf544fe49560fa80e6 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 18 Feb 2018 18:14:56 +0100 Subject: [PATCH 01/11] API extended --- .../Schemas/NumberFieldProperties.cs | 2 ++ .../Schemas/StringFieldProperties.cs | 2 ++ .../Guards/FieldPropertiesValidator.cs | 14 +++++++++ .../Fields/BooleanFieldPropertiesDto.cs | 5 +++ .../Models/Fields/NumberFieldPropertiesDto.cs | 5 +++ .../Models/Fields/StringFieldPropertiesDto.cs | 5 +++ .../NumberFieldPropertiesTests.cs | 28 +++++++++++++++++ .../StringFieldPropertiesTests.cs | 31 +++++++++++++++++++ 8 files changed, 92 insertions(+) diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs index bd86a2a74..5732601a9 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/NumberFieldProperties.cs @@ -21,6 +21,8 @@ namespace Squidex.Domain.Apps.Core.Schemas public double? DefaultValue { get; set; } + public bool InlineEditable { get; set; } + public NumberFieldEditor Editor { get; set; } public override T Accept(IFieldPropertiesVisitor visitor) diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs index 008c18c58..df46bdf6d 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs @@ -19,6 +19,8 @@ namespace Squidex.Domain.Apps.Core.Schemas public int? MaxLength { get; set; } + public bool InlineEditable { get; set; } + public string DefaultValue { get; set; } public string Pattern { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs index 44becd941..28ddfda3f 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs @@ -170,6 +170,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards nameof(properties.MinValue), nameof(properties.MaxValue)); } + + if (properties.InlineEditable && properties.Editor != NumberFieldEditor.Input && properties.Editor != NumberFieldEditor.Dropdown) + { + yield return new ValidationError("Inline editing is only allowed for dropdowns and input fields.", + nameof(properties.InlineEditable), + nameof(properties.Editor)); + } } public IEnumerable Visit(ReferencesFieldProperties properties) @@ -216,6 +223,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards nameof(properties.MinLength), nameof(properties.MaxLength)); } + + if (properties.InlineEditable && properties.Editor != StringFieldEditor.Input && properties.Editor != StringFieldEditor.Input && properties.Editor != StringFieldEditor.Slug) + { + yield return new ValidationError("Inline editing is only allowed for dropdowns, slugs and input fields.", + nameof(properties.InlineEditable), + nameof(properties.Editor)); + } } public IEnumerable Visit(TagsFieldProperties properties) diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs index 2f7552b87..45964540e 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/BooleanFieldPropertiesDto.cs @@ -21,6 +21,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public bool? DefaultValue { get; set; } + /// + /// Indicates that the inline editor is enabled for this field. + /// + public bool InlineEditable { get; set; } + /// /// The editor that is used to manage this field. /// diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs index 4efdf18e1..3a255891c 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/NumberFieldPropertiesDto.cs @@ -37,6 +37,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public double[] AllowedValues { get; set; } + /// + /// Indicates that the inline editor is enabled for this field. + /// + public bool InlineEditable { get; set; } + /// /// The editor that is used to manage this field. /// diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs index 5cae9dfb9..4451846ca 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs @@ -47,6 +47,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public string[] AllowedValues { get; set; } + /// + /// Indicates that the inline editor is enabled for this field. + /// + public bool InlineEditable { get; set; } + /// /// The editor that is used to manage this field. /// diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs index 26fb5be47..7b2dd9448 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs @@ -129,5 +129,33 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties new ValidationError("Editor is not a valid value.", "Editor") }); } + + [Theory] + [InlineData(NumberFieldEditor.Radio)] + [InlineData(NumberFieldEditor.Stars)] + public void Should_add_error_if_inline_editing_is_not_allowed_for_editor(NumberFieldEditor editor) + { + var sut = new NumberFieldProperties { InlineEditable = true, Editor = editor }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Inline editing is only allowed for dropdowns and input fields.", "InlineEditable", "Editor") + }); + } + + [Theory] + [InlineData(NumberFieldEditor.Input)] + [InlineData(NumberFieldEditor.Dropdown)] + public void Should_not_add_error_if_inline_editing_is_allowed_for_editor(NumberFieldEditor editor) + { + var sut = new NumberFieldProperties { InlineEditable = true, Editor = editor }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } } } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs index 31c922809..1169c2088 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs @@ -100,5 +100,36 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties new ValidationError("Pattern is not a valid expression.", "Pattern") }); } + + [Theory] + [InlineData(StringFieldEditor.Markdown)] + [InlineData(StringFieldEditor.Radio)] + [InlineData(StringFieldEditor.RichText)] + [InlineData(StringFieldEditor.TextArea)] + public void Should_add_error_if_inline_editing_is_not_allowed_for_editor(StringFieldEditor editor) + { + var sut = new StringFieldProperties { InlineEditable = true, Editor = editor }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Inline editing is only allowed for dropdowns, slugs and input fields.", "InlineEditable", "Editor") + }); + } + + [Theory] + [InlineData(StringFieldEditor.Dropdown)] + [InlineData(StringFieldEditor.Input)] + [InlineData(StringFieldEditor.Slug)] + public void Should_not_add_error_if_inline_editing_is_allowed_for_editor(StringFieldEditor editor) + { + var sut = new StringFieldProperties { InlineEditable = true, Editor = editor }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } } } From 2eed29f7d9a7bfb42ff5b79054b76c0631b9bad3 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 18 Feb 2018 18:29:58 +0100 Subject: [PATCH 02/11] API client extended. --- src/Squidex/app/shared/services/schemas.fields.spec.ts | 6 +++--- src/Squidex/app/shared/services/schemas.service.ts | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Squidex/app/shared/services/schemas.fields.spec.ts b/src/Squidex/app/shared/services/schemas.fields.spec.ts index 791e25799..55de4799b 100644 --- a/src/Squidex/app/shared/services/schemas.fields.spec.ts +++ b/src/Squidex/app/shared/services/schemas.fields.spec.ts @@ -117,7 +117,7 @@ describe('TagsField', () => { }); describe('BooleanField', () => { - const field = createField(new BooleanFieldPropertiesDto(null, null, null, true, false, 'Checkbox')); + const field = createField(new BooleanFieldPropertiesDto(null, null, null, true, false, false, 'Checkbox')); it('should create validators', () => { expect(field.createValidators(false).length).toBe(1); @@ -230,7 +230,7 @@ describe('JsonField', () => { }); describe('NumberField', () => { - const field = createField(new NumberFieldPropertiesDto(null, null, null, true, false, 'Input', undefined, 3, 1, [1, 2, 3])); + const field = createField(new NumberFieldPropertiesDto(null, null, null, true, false, false, 'Input', undefined, 3, 1, [1, 2, 3])); it('should create validators', () => { expect(field.createValidators(false).length).toBe(4); @@ -276,7 +276,7 @@ describe('ReferencesField', () => { }); describe('StringField', () => { - const field = createField(new StringFieldPropertiesDto(null, null, null, true, false, 'Input', undefined, 'pattern', undefined, 3, 1, ['1', '2'])); + const field = createField(new StringFieldPropertiesDto(null, null, null, true, false, false, 'Input', undefined, 'pattern', undefined, 3, 1, ['1', '2'])); it('should create validators', () => { expect(field.createValidators(false).length).toBe(5); diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index a9964164e..b6f2a684c 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -40,13 +40,13 @@ export function createProperties(fieldType: string, values: Object | null = null switch (fieldType) { case 'Number': - properties = new NumberFieldPropertiesDto(null, null, null, false, false, 'Input'); + properties = new NumberFieldPropertiesDto(null, null, null, false, false, false, 'Input'); break; case 'String': - properties = new StringFieldPropertiesDto(null, null, null, false, false, 'Input'); + properties = new StringFieldPropertiesDto(null, null, null, false, false, false, 'Input'); break; case 'Boolean': - properties = new BooleanFieldPropertiesDto(null, null, null, false, false, 'Checkbox'); + properties = new BooleanFieldPropertiesDto(null, null, null, false, false, false, 'Checkbox'); break; case 'DateTime': properties = new DateTimeFieldPropertiesDto(null, null, null, false, false, 'DateTime'); @@ -355,6 +355,7 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto { constructor(label: string | null, hints: string | null, placeholder: string | null, isRequired: boolean, isListField: boolean, + public readonly isInlineEditable: boolean, public readonly editor: string, public readonly defaultValue?: string, public readonly pattern?: string, @@ -415,6 +416,7 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto { constructor(label: string | null, hints: string | null, placeholder: string | null, isRequired: boolean, isListField: boolean, + public readonly isInlineEditable: boolean, public readonly editor: string, public readonly defaultValue?: number, public readonly maxValue?: number, @@ -523,6 +525,7 @@ export class BooleanFieldPropertiesDto extends FieldPropertiesDto { constructor(label: string | null, hints: string | null, placeholder: string | null, isRequired: boolean, isListField: boolean, + public readonly isInlineEditable: boolean, public readonly editor: string, public readonly defaultValue?: boolean ) { From c7c3f5b33f65f3f77c277d659cbf5717b78dff02 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 18 Feb 2018 18:51:41 +0100 Subject: [PATCH 03/11] UI to manage inline editing. --- .../schema/types/boolean-ui.component.html | 10 +++++ .../schema/types/boolean-ui.component.ts | 8 +++- .../types/boolean-validation.component.html | 13 ++++--- .../types/boolean-validation.component.ts | 5 ++- .../schema/types/date-time-ui.component.ts | 5 ++- .../types/date-time-validation.component.ts | 8 ++-- .../schema/types/geolocation-ui.component.ts | 2 +- .../schema/types/number-ui.component.html | 10 +++++ .../pages/schema/types/number-ui.component.ts | 34 ++++++++++++---- .../types/number-validation.component.ts | 6 +-- .../schema/types/string-ui.component.html | 10 +++++ .../pages/schema/types/string-ui.component.ts | 39 +++++++++++++------ .../types/string-validation.component.ts | 6 +-- .../app/shared/services/schemas.service.ts | 6 +-- 14 files changed, 120 insertions(+), 42 deletions(-) diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html index ef8db15a7..7d99dc6ce 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.html @@ -30,4 +30,14 @@ +
+
+
+ + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts index 5eb3aadcd..7336ef71a 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/boolean-ui.component.ts @@ -23,13 +23,17 @@ export class BooleanUIComponent implements OnInit { public properties: BooleanFieldPropertiesDto; public ngOnInit() { - this.editForm.addControl('editor', + this.editForm.setControl('editor', new FormControl(this.properties.editor, [ Validators.required ])); - this.editForm.addControl('placeholder', + + this.editForm.setControl('placeholder', new FormControl(this.properties.placeholder, [ Validators.maxLength(100) ])); + + this.editForm.setControl('inlineEditable', + new FormControl(this.properties.inlineEditable)); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html b/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html index 6a68e0180..f90e71d7f 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.html @@ -10,11 +10,14 @@ -
- - -
- +
+
+
+ + +
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts index b6839be87..7dbc279be 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts @@ -26,9 +26,12 @@ export class BooleanValidationComponent implements OnInit { public showDefaultValue: Observable; public ngOnInit() { - this.editForm.addControl('defaultValue', + this.editForm.setControl('defaultValue', new FormControl(this.properties.defaultValue)); + this.editForm.setControl('inlineEditable', + new FormControl(this.properties.inlineEditable)); + this.showDefaultValue = this.editForm.controls['isRequired'].valueChanges .startWith(this.properties.isRequired) diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts index 5a5152f6e..97a0855fd 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/date-time-ui.component.ts @@ -28,11 +28,12 @@ export class DateTimeUIComponent implements OnInit { public hideAllowedValues: Observable; public ngOnInit() { - this.editForm.addControl('editor', + this.editForm.setControl('editor', new FormControl(this.properties.editor, [ Validators.required ])); - this.editForm.addControl('placeholder', + + this.editForm.setControl('placeholder', new FormControl(this.properties.placeholder, [ Validators.maxLength(100) ])); diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts index 15f133f45..4ff7d6be2 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts @@ -29,20 +29,20 @@ export class DateTimeValidationComponent implements OnInit { public calculatedDefaultValues = ['Now', 'Today']; public ngOnInit() { - this.editForm.addControl('calculatedDefaultValue', + this.editForm.setControl('calculatedDefaultValue', new FormControl(this.properties.calculatedDefaultValue)); - this.editForm.addControl('maxValue', + this.editForm.setControl('maxValue', new FormControl(this.properties.maxValue, [ ValidatorsEx.validDateTime() ])); - this.editForm.addControl('minValue', + this.editForm.setControl('minValue', new FormControl(this.properties.minValue, [ ValidatorsEx.validDateTime() ])); - this.editForm.addControl('defaultValue', + this.editForm.setControl('defaultValue', new FormControl(this.properties.defaultValue, [ ValidatorsEx.validDateTime() ])); diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts index 14883cf52..a5de1bf68 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts @@ -23,7 +23,7 @@ export class GeolocationUIComponent implements OnInit { public properties: GeolocationFieldPropertiesDto; public ngOnInit() { - this.editForm.addControl('editor', + this.editForm.setControl('editor', new FormControl(this.properties.editor, [ Validators.required ])); diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html index 3dc5ae6cf..fba63c8d1 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.html @@ -51,4 +51,14 @@
+
+
+
+ + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts index f63f9dac5..30342c35d 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts @@ -17,7 +17,8 @@ import { FloatConverter, NumberFieldPropertiesDto } from 'shared'; templateUrl: 'number-ui.component.html' }) export class NumberUIComponent implements OnDestroy, OnInit { - private editorSubscription: Subscription; + private hideAllowedValuesSubscription: Subscription; + private hideInlineEditableSubscription: Subscription; @Input() public editForm: FormGroup; @@ -28,33 +29,52 @@ export class NumberUIComponent implements OnDestroy, OnInit { public converter = new FloatConverter(); public hideAllowedValues: Observable; + public hideInlineEditable: Observable; public ngOnDestroy() { - this.editorSubscription.unsubscribe(); + this.hideAllowedValuesSubscription.unsubscribe(); + this.hideInlineEditableSubscription.unsubscribe(); } public ngOnInit() { - this.editForm.addControl('editor', + this.editForm.setControl('editor', new FormControl(this.properties.editor, [ Validators.required ])); - this.editForm.addControl('placeholder', + + this.editForm.setControl('placeholder', new FormControl(this.properties.placeholder, [ Validators.maxLength(100) ])); - this.editForm.addControl('allowedValues', + + this.editForm.setControl('allowedValues', new FormControl(this.properties.allowedValues, [])); + this.editForm.setControl('inlineEditable', + new FormControl(this.properties.inlineEditable)); + this.hideAllowedValues = this.editForm.controls['editor'].valueChanges .startWith(this.properties.editor) - .map(x => !x || x === 'Input' || x === 'Textarea'); + .map(x => !(x && (x === 'Radio' || x === 'Dropdown'))); + + this.hideInlineEditable = + this.editForm.controls['editor'].valueChanges + .startWith(this.properties.editor) + .map(x => !(x && (x === 'Input' || x === 'Dropdown'))); - this.editorSubscription = + this.hideAllowedValuesSubscription = this.hideAllowedValues.subscribe(isSelection => { if (isSelection) { this.editForm.controls['allowedValues'].setValue(undefined); } }); + + this.hideInlineEditableSubscription = + this.hideInlineEditable.subscribe(isSelection => { + if (isSelection) { + this.editForm.controls['inlineEditable'].setValue(false); + } + }); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts index e8801a593..6e076e127 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts @@ -26,13 +26,13 @@ export class NumberValidationComponent implements OnInit { public showDefaultValue: Observable; public ngOnInit() { - this.editForm.addControl('maxValue', + this.editForm.setControl('maxValue', new FormControl(this.properties.maxValue)); - this.editForm.addControl('minValue', + this.editForm.setControl('minValue', new FormControl(this.properties.minValue)); - this.editForm.addControl('defaultValue', + this.editForm.setControl('defaultValue', new FormControl(this.properties.defaultValue)); this.showDefaultValue = diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html index 4114db13d..d64264d0d 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.html @@ -72,4 +72,14 @@ +
+
+
+ + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts index d11c4effd..157b66cfe 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts @@ -17,7 +17,8 @@ import { StringFieldPropertiesDto } from 'shared'; templateUrl: 'string-ui.component.html' }) export class StringUIComponent implements OnDestroy, OnInit { - private editorSubscription: Subscription; + private hideAllowedValuesSubscription: Subscription; + private hideInlineEditableSubscription: Subscription; @Input() public editForm: FormGroup; @@ -26,9 +27,11 @@ export class StringUIComponent implements OnDestroy, OnInit { public properties: StringFieldPropertiesDto; public hideAllowedValues: Observable; + public hideInlineEditable: Observable; public ngOnDestroy() { - this.editorSubscription.unsubscribe(); + this.hideAllowedValuesSubscription.unsubscribe(); + this.hideInlineEditableSubscription.unsubscribe(); } public ngOnInit() { @@ -45,17 +48,31 @@ export class StringUIComponent implements OnDestroy, OnInit { this.editForm.setControl('allowedValues', new FormControl(this.properties.allowedValues)); + this.editForm.setControl('inlineEditable', + new FormControl(this.properties.inlineEditable)); + this.hideAllowedValues = this.editForm.controls['editor'].valueChanges .startWith(this.properties.editor) - .map(x => !x || x === 'Input' || x === 'TextArea' || x === 'RichText' || x === 'Markdown'); - - this.editorSubscription = - this.hideAllowedValues - .subscribe(isSelection => { - if (isSelection) { - this.editForm.controls['allowedValues'].setValue(undefined); - } - }); + .map(x => !(x && (x === 'Radio' || x === 'Dropdown'))); + + this.hideInlineEditable = + this.editForm.controls['editor'].valueChanges + .startWith(this.properties.editor) + .map(x => !(x && (x === 'Input' || x === 'Dropdown' || x === 'Slug'))); + + this.hideAllowedValuesSubscription = + this.hideAllowedValues.subscribe(isSelection => { + if (isSelection) { + this.editForm.controls['allowedValues'].setValue(undefined); + } + }); + + this.hideInlineEditableSubscription = + this.hideInlineEditable.subscribe(isSelection => { + if (isSelection) { + this.editForm.controls['inlineEditable'].setValue(false); + } + }); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts index c7e593132..fde5f8edf 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts @@ -64,14 +64,14 @@ export class StringValidationComponent implements OnDestroy, OnInit { .startWith(this.properties.isRequired) .map(x => !x); - this.showPatternMessage = - this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim().length > 0; - this.showPatternSuggestions = this.editForm.controls['pattern'].valueChanges .startWith('') .map(x => !x || x.trim().length === 0); + this.showPatternMessage = + this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim().length > 0; + this.patternSubscription = this.editForm.controls['pattern'].valueChanges .subscribe((value: string) => { diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index b6f2a684c..08bdbd7a6 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -355,7 +355,7 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto { constructor(label: string | null, hints: string | null, placeholder: string | null, isRequired: boolean, isListField: boolean, - public readonly isInlineEditable: boolean, + public readonly inlineEditable: boolean, public readonly editor: string, public readonly defaultValue?: string, public readonly pattern?: string, @@ -416,7 +416,7 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto { constructor(label: string | null, hints: string | null, placeholder: string | null, isRequired: boolean, isListField: boolean, - public readonly isInlineEditable: boolean, + public readonly inlineEditable: boolean, public readonly editor: string, public readonly defaultValue?: number, public readonly maxValue?: number, @@ -525,7 +525,7 @@ export class BooleanFieldPropertiesDto extends FieldPropertiesDto { constructor(label: string | null, hints: string | null, placeholder: string | null, isRequired: boolean, isListField: boolean, - public readonly isInlineEditable: boolean, + public readonly inlineEditable: boolean, public readonly editor: string, public readonly defaultValue?: boolean ) { From ec37f2fcecd86c3a9ef76a03a54581bb4dcf2de8 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 18 Feb 2018 22:35:35 +0100 Subject: [PATCH 04/11] UI improvements. --- .../Schemas/BooleanFieldProperties.cs | 2 + src/Squidex/app/app.routes.ts | 4 +- .../content/content-field.component.html | 12 +- .../pages/content/content-field.component.ts | 39 ++++--- .../pages/content/content-page.component.ts | 34 +++--- .../contents/contents-page.component.html | 7 +- .../pages/contents/contents-page.component.ts | 19 +++- .../shared/content-item.component.html | 79 ++++++++++++-- .../shared/content-item.component.scss | 4 + .../content/shared/content-item.component.ts | 103 +++++++++++++++--- .../shared/references-editor.component.html | 2 +- .../shared/references-editor.component.ts | 3 +- .../angular/control-errors.component.html | 2 +- .../angular/control-errors.component.ts | 98 ++++++++++++----- .../framework/angular/http-extensions-impl.ts | 6 + .../framework/angular/modal-view.directive.ts | 62 ++++++----- .../app/framework/utils/string-helper.ts | 2 +- .../language-selector.component.html | 4 +- .../shared/services/contents.service.spec.ts | 15 +++ .../app/shared/services/contents.service.ts | 15 +++ .../app/shared/services/schemas.service.ts | 12 +- 21 files changed, 385 insertions(+), 139 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs index 2785eceba..531507e37 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/BooleanFieldProperties.cs @@ -14,6 +14,8 @@ namespace Squidex.Domain.Apps.Core.Schemas { public bool? DefaultValue { get; set; } + public bool InlineEditable { get; set; } + public BooleanFieldEditor Editor { get; set; } public override T Accept(IFieldPropertiesVisitor visitor) diff --git a/src/Squidex/app/app.routes.ts b/src/Squidex/app/app.routes.ts index 00674bff9..c6ca613b6 100644 --- a/src/Squidex/app/app.routes.ts +++ b/src/Squidex/app/app.routes.ts @@ -6,7 +6,7 @@ */ import { ModuleWithProviders } from '@angular/core'; -import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; +import { RouterModule, Routes } from '@angular/router'; import { AppAreaComponent, @@ -96,4 +96,4 @@ export const routes: Routes = [ } ]; -export const routing: ModuleWithProviders = RouterModule.forRoot(routes, { useHash: false, preloadingStrategy: PreloadAllModules }); \ No newline at end of file +export const routing: ModuleWithProviders = RouterModule.forRoot(routes, { useHash: false }); \ No newline at end of file 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 7a05664f4..44784c00c 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 @@ -6,7 +6,7 @@ Disabled
-
+
@@ -24,7 +24,7 @@
- +
@@ -48,13 +48,13 @@
- +
- +
- +
@@ -106,7 +106,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 4152da206..b3f36db2a 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 @@ -7,9 +7,9 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; import { AppLanguageDto, FieldDto } from 'shared'; -import { ActivatedRoute, Router } from '@angular/router'; @Component({ selector: 'sqx-content-field', @@ -17,10 +17,6 @@ import { ActivatedRoute, Router } from '@angular/router'; templateUrl: './content-field.component.html' }) export class ContentFieldComponent implements OnInit { - constructor(private readonly router: Router, private readonly route: ActivatedRoute) { - } - private masterLanguageCode: string; - @Input() public field: FieldDto; @@ -33,35 +29,38 @@ export class ContentFieldComponent implements OnInit { @Input() public contentFormSubmitted: boolean; - public fieldPartitions: string[]; - public fieldPartition: string; + public selectedFormControl: string; + public selectedLanguage: AppLanguageDto; - public selectLanguage(language: AppLanguageDto) { - this.fieldPartition = language.iso2Code; + constructor( + private readonly router: Router, + private readonly route: ActivatedRoute + ) { } public ngOnInit() { - this.masterLanguageCode = this.languages.find(l => l.isMaster)!.iso2Code; - if (this.field.isDisabled) { this.fieldForm.disable(); } - if (this.field.partitioning === 'language') { - this.fieldPartitions = this.languages.map(t => t.iso2Code); - this.fieldPartition = this.fieldPartitions[0]; + const masterLanguage = this.languages.find(l => l.isMaster)!; + + if (this.field.isLocalizable) { + this.selectedFormControl = masterLanguage.iso2Code; } else { - this.fieldPartitions = ['iv']; - this.fieldPartition = 'iv'; + this.selectedFormControl = 'iv'; } + + this.selectedLanguage = masterLanguage; } - public assetPluginClicked() { - this.router.navigate(['assets'], { relativeTo: this.route }); + public selectLanguage(language: AppLanguageDto) { + this.selectedFormControl = language.iso2Code; + this.selectedLanguage = language; } - public selectFieldLanguage(partition: string) { - return partition === 'iv' ? this.masterLanguageCode : partition; + public assetPluginClicked() { + this.router.navigate(['assets'], { relativeTo: this.route }); } } diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index 9ba93a9e9..c67137e86 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -40,6 +40,7 @@ import { export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit { private contentStatusChangedSubscription: Subscription; private contentDeletedSubscription: Subscription; + private contentUpdatedSubscription: Subscription; private contentVersionSelectedSubscription: Subscription; public schema: SchemaDetailsDto; @@ -62,6 +63,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, public ngOnDestroy() { this.contentVersionSelectedSubscription.unsubscribe(); this.contentStatusChangedSubscription.unsubscribe(); + this.contentUpdatedSubscription.unsubscribe(); this.contentDeletedSubscription.unsubscribe(); } @@ -80,6 +82,14 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, } }); + this.contentUpdatedSubscription = + this.ctx.bus.of(ContentUpdated) + .subscribe(message => { + if (this.content && message.content.id === this.content.id) { + this.reloadContentForm(message.content); + } + }); + this.contentStatusChangedSubscription = this.ctx.bus.of(ContentStatusChanged) .subscribe(message => { @@ -102,9 +112,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, this.ctx.route.data.map(d => d.content) .subscribe((content: ContentDto) => { - this.content = content; - - this.populateContentForm(); + this.reloadContentForm(content); }); } @@ -122,7 +130,7 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, this.contentOld = null; this.emitContentUpdated(this.content); - this.populateContentForm(); + this.reloadContentForm(this.content); } } @@ -159,13 +167,13 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, } else { this.contentsService.putContent(this.ctx.appName, this.schema.name, this.content.id, requestDto, this.content.version) .subscribe(dto => { - this.content = this.content.update(dto.payload, this.ctx.userToken, dto.version); + const content = this.content.update(dto.payload, this.ctx.userToken, dto.version); this.ctx.notifyInfo('Content saved successfully.'); this.emitContentUpdated(this.content); this.enableContentForm(); - this.populateContentForm(); + this.reloadContentForm(content); }, error => { this.ctx.notifyError(error); @@ -187,12 +195,9 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, this.contentOld = null; } - this.content = this.content.setData(dto); - this.ctx.notifyInfo('Content version loaded successfully.'); - this.emitContentUpdated(this.content); - this.populateContentForm(); + this.reloadContentForm(this.content.setData(dto)); }, error => { this.ctx.notifyError(error); }); @@ -235,12 +240,12 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, for (const field of schema.fields) { const group = new FormGroup({}); - if (field.partitioning === 'language') { + if (field.isLocalizable) { for (let language of this.languages) { - group.addControl(language.iso2Code, new FormControl(undefined, field.createValidators(language.isOptional))); + group.setControl(language.iso2Code, new FormControl(undefined, field.createValidators(language.isOptional))); } } else { - group.addControl('iv', new FormControl(undefined, field.createValidators(false))); + group.setControl('iv', new FormControl(undefined, field.createValidators(false))); } controls[field.name] = group; @@ -249,7 +254,8 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, this.contentForm = new FormGroup(controls); } - private populateContentForm() { + private reloadContentForm(content: ContentDto) { + this.content = content; this.contentForm.markAsPristine(); this.isNewMode = !this.content; diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-page.component.html index f00ddc40a..a52a910c7 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.html @@ -124,7 +124,7 @@ + (deleting)="deleteContent(content)" + (saved)="onContentSaved(content, $event)"> @@ -141,7 +142,7 @@ diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index 689d817ec..217265983 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -27,7 +27,8 @@ import { ImmutableArray, ModalView, Pager, - SchemaDetailsDto + SchemaDetailsDto, + Versioned } from 'shared'; @Component({ @@ -203,7 +204,9 @@ export class ContentsPageComponent implements OnDestroy, OnInit { DateTime.parseISO_UTC(dueTime) : null; - this.contentItems = this.contentItems.replaceBy('id', content.changeStatus(status, dt, this.ctx.userToken, dto.version)); + content = content.changeStatus(status, dt, this.ctx.userToken, dto.version); + + this.contentItems = this.contentItems.replaceBy('id', content); this.emitContentStatusChanged(content); } @@ -240,6 +243,14 @@ export class ContentsPageComponent implements OnDestroy, OnInit { }); } + public onContentSaved(content: ContentDto, update: Versioned) { + content = content.update(update.payload, this.ctx.userToken, update.version); + + this.contentItems = this.contentItems.replaceBy('id', content); + + this.emitContentUpdated(content); + } + public load(showInfo = false) { this.contentsService.getContents(this.ctx.appName, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, undefined, this.isArchive) .finally(() => { @@ -342,6 +353,10 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.ctx.bus.emit(new ContentStatusChanged(content)); } + private emitContentUpdated(content: ContentDto) { + this.ctx.bus.emit(new ContentUpdated(content)); + } + private emitContentRemoved(content: ContentDto) { this.ctx.bus.emit(new ContentRemoved(content)); } diff --git a/src/Squidex/app/features/content/shared/content-item.component.html b/src/Squidex/app/features/content/shared/content-item.component.html index 051ed7162..c19c1d417 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.html +++ b/src/Squidex/app/features/content/shared/content-item.component.html @@ -1,15 +1,61 @@ - - + - - - {{value}} - + + +
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
+
+ {{values[i]}} +
- + @@ -28,10 +74,23 @@ {{content.lastModified | sqxFromNow}} - + + + + + + + + + - + +
- + diff --git a/src/Squidex/app/features/content/shared/content-item.component.scss b/src/Squidex/app/features/content/shared/content-item.component.scss index d87c980ec..a9239ad91 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.scss +++ b/src/Squidex/app/features/content/shared/content-item.component.scss @@ -1,6 +1,10 @@ @import '_vars'; @import '_mixins'; +.truncate { + @include truncate; +} + .content-status { & { vertical-align: middle; diff --git a/src/Squidex/app/features/content/shared/content-item.component.ts b/src/Squidex/app/features/content/shared/content-item.component.ts index d5f667beb..a2fb8aea0 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.ts +++ b/src/Squidex/app/features/content/shared/content-item.component.ts @@ -5,15 +5,20 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; import { AppContext, + AppLanguageDto, ContentDto, + ContentsService, fadeAnimation, FieldDto, ModalView, - SchemaDto + SchemaDto, + Types, + Versioned } from 'shared'; /* tslint:disable:component-selector */ @@ -27,8 +32,7 @@ import { ], animations: [ fadeAnimation - ], - changeDetection: ChangeDetectionStrategy.OnPush + ] }) export class ContentItemComponent implements OnInit, OnChanges { @Output() @@ -46,6 +50,9 @@ export class ContentItemComponent implements OnInit, OnChanges { @Output() public deleting = new EventEmitter(); + @Output() + public saved = new EventEmitter>(); + @Output() public selectedChange = new EventEmitter(); @@ -53,10 +60,7 @@ export class ContentItemComponent implements OnInit, OnChanges { public selected = false; @Input() - public columnWidth: number; - - @Input() - public languageCode: string; + public language: AppLanguageDto; @Input() public schemaFields: FieldDto[]; @@ -73,11 +77,15 @@ export class ContentItemComponent implements OnInit, OnChanges { @Input('sqxContent') public content: ContentDto; + public formSubmitted = false; + public form: FormGroup = new FormGroup({}); + public dropdown = new ModalView(false, true); public values: any[] = []; - constructor(public readonly ctx: AppContext + constructor(public readonly ctx: AppContext, + private readonly contentsService: ContentsService ) { } @@ -86,31 +94,96 @@ export class ContentItemComponent implements OnInit, OnChanges { } public ngOnInit() { + for (let field of this.schemaFields) { + if (field.properties['inlineEditable']) { + this.form.setControl(field.name, new FormControl(undefined, field.createValidators(this.language.isOptional))); + } + } + this.updateValues(); } + public shouldStop(event: Event) { + if (this.form.dirty) { + event.stopPropagation(); + event.stopImmediatePropagation(); + } + } + + public save() { + this.formSubmitted = true; + + if (this.form.dirty && this.form.valid) { + this.form.disable(); + + const request = {}; + + for (let field of this.schemaFields) { + if (field.properties['inlineEditable']) { + const value = this.form.controls[field.name].value; + + if (field.partitioning === 'invariant') { + request[field.name] = { iv: value }; + } else { + request[field.name] = { [this.language.iso2Code]: value }; + } + } + } + + this.contentsService.patchContent(this.ctx.appName, this.schema.name, this.content.id, request, this.content.version) + .finally(() => { + this.form.enable(); + }) + .subscribe(dto => { + this.form.markAsPristine(); + + this.emitSaved(dto); + }, error => { + this.ctx.notifyError(error); + }); + } + } + + private emitSaved(data: Versioned) { + this.saved.emit(data); + } + private updateValues() { this.values = []; if (this.schemaFields) { for (let field of this.schemaFields) { - this.values.push(this.getValue(field)); + const value = this.getRawValue(field); + + if (Types.isUndefined(value)) { + this.values.push(''); + } else { + this.values.push(field.formatValue(value)); + } + + if (this.form) { + const formControl = this.form.controls[field.name]; + + if (formControl) { + formControl.setValue(value); + } + } } } } - private getValue(field: FieldDto): any { + private getRawValue(field: FieldDto): any { const contentField = this.content.data[field.name]; if (contentField) { if (field.partitioning === 'language') { - return field.formatValue(contentField[this.languageCode]); + return contentField[this.language.iso2Code]; } else { - return field.formatValue(contentField['iv']); + return contentField['iv']; } - } else { - return ''; } + + return undefined; } } diff --git a/src/Squidex/app/features/content/shared/references-editor.component.html b/src/Squidex/app/features/content/shared/references-editor.component.html index 7e2a68453..07147915e 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.html +++ b/src/Squidex/app/features/content/shared/references-editor.component.html @@ -13,7 +13,7 @@ +
{{message}} diff --git a/src/Squidex/app/framework/angular/control-errors.component.ts b/src/Squidex/app/framework/angular/control-errors.component.ts index 9f1f9c972..d37f51b3f 100644 --- a/src/Squidex/app/framework/angular/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/control-errors.component.ts @@ -5,8 +5,9 @@ * Copyright (c) Sebastian Stehle. All rights r vbeserved */ -import { ChangeDetectionStrategy, Component, Host, Input, OnChanges, Optional } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnChanges, OnDestroy, Optional } from '@angular/core'; import { AbstractControl, FormGroupDirective } from '@angular/forms'; +import { Observable, Subscription } from 'rxjs'; import { fadeAnimation } from './animations'; @@ -33,9 +34,11 @@ const DEFAULT_ERRORS: { [key: string]: string } = { ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class ControlErrorsComponent implements OnChanges { +export class ControlErrorsComponent implements OnChanges, OnDestroy { private displayFieldName: string; private control: AbstractControl; + private controlSubscription: Subscription | null = null; + private originalMarkAsTouched: any; @Input() public for: string; @@ -52,17 +55,74 @@ export class ControlErrorsComponent implements OnChanges { @Input() public submitOnly = false; - public get errorMessages(): string[] | null { - if (!this.control) { - return null; + public errorMessages: string[] = []; + + constructor( + @Optional() @Host() private readonly formGroupDirective: FormGroupDirective, + private readonly changeDetector: ChangeDetectorRef + ) { + if (!this.formGroupDirective) { + throw new Error('control-errors must be used with a parent formGroup directive'); } + } - if (this.control.invalid && ((this.control.touched && !this.submitOnly) || this.submitted) && this.control.errors) { - const errors: string[] = []; + public ngOnDestroy() { + this.unsubscribe(); + } + + public ngOnChanges() { + if (this.fieldName) { + this.displayFieldName = this.fieldName; + } else if (this.for) { + this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1); + } + + const control = this.formGroupDirective.form.controls[this.for]; + + if (this.control !== control) { + this.unsubscribe(); + + this.control = control; + + if (control) { + const self = this; + + this.controlSubscription = + Observable.merge(control.valueChanges, control.statusChanges) + .subscribe(() => { + this.createMessages(); + }); + + this.originalMarkAsTouched = this.control.markAsTouched; + + this.control['markAsTouched'] = function () { + self.originalMarkAsTouched.apply(this, arguments); + + self.createMessages(); + }; + } + } + this.createMessages(); + } + + private unsubscribe() { + if (this.controlSubscription) { + this.controlSubscription.unsubscribe(); + } + + if (this.control && this.originalMarkAsTouched) { + this.control['markAsTouched'] = this.originalMarkAsTouched; + } + } + + private createMessages() { + const errors: string[] = []; + + if (this.control.invalid && ((this.control.touched && !this.submitOnly) || this.submitted) && this.control.errors) { for (let key in this.control.errors) { if (this.control.errors.hasOwnProperty(key)) { - let message = (this.errors ? this.errors[key] : null) || DEFAULT_ERRORS[key]; + let message = (this.errors ? this.errors[key] : null) || DEFAULT_ERRORS[key.toLowerCase()]; if (!message) { continue; @@ -81,28 +141,10 @@ export class ControlErrorsComponent implements OnChanges { errors.push(message); } } - - return errors.length > 0 ? errors : null; } - return null; - } - - constructor( - @Optional() @Host() private readonly formGroupDirective: FormGroupDirective - ) { - if (!this.formGroupDirective) { - throw new Error('control-errors must be used with a parent formGroup directive'); - } - } - - public ngOnChanges() { - if (this.fieldName) { - this.displayFieldName = this.fieldName; - } else if (this.for) { - this.displayFieldName = this.for.substr(0, 1).toUpperCase() + this.for.substr(1); - } + this.errorMessages = errors; - this.control = this.formGroupDirective.form.controls[this.for]; + this.changeDetector.detectChanges(); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/http-extensions-impl.ts b/src/Squidex/app/framework/angular/http-extensions-impl.ts index 3b96117ec..f99f383b7 100644 --- a/src/Squidex/app/framework/angular/http-extensions-impl.ts +++ b/src/Squidex/app/framework/angular/http-extensions-impl.ts @@ -87,6 +87,12 @@ export module HTTP { return handleVersion(http.put(url, body, { observe: 'response', headers }), version); } + export function patchVersioned(http: HttpClient, url: string, body: any, version?: Version): Observable>> { + const headers = createHeaders(version); + + return handleVersion(http.request('PATCH', url, { body, observe: 'response', headers }), version); + } + export function deleteVersioned(http: HttpClient, url: string, version?: Version): Observable>> { const headers = createHeaders(version); diff --git a/src/Squidex/app/framework/angular/modal-view.directive.ts b/src/Squidex/app/framework/angular/modal-view.directive.ts index a8df8263b..fea01ae0b 100644 --- a/src/Squidex/app/framework/angular/modal-view.directive.ts +++ b/src/Squidex/app/framework/angular/modal-view.directive.ts @@ -21,7 +21,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy { private renderedView: EmbeddedViewRef | null = null; @Input('sqxModalView') - public modalView: ModalView; + public modalView: ModalView | any; @Input('sqxModalViewOnRoot') public placeOnRoot = false; @@ -40,7 +40,9 @@ export class ModalViewDirective implements OnChanges, OnDestroy { public ngOnDestroy() { this.stopListening(); - this.modalView.hide(); + if (this.modalView instanceof ModalView) { + this.modalView.hide(); + } } public ngOnChanges(changes: SimpleChanges) { @@ -53,36 +55,42 @@ export class ModalViewDirective implements OnChanges, OnDestroy { this.subscription = null; } - if (this.modalView) { + if (this.modalView instanceof ModalView) { this.subscription = this.modalView.isOpen.subscribe(isOpen => { - if (isOpen === (this.renderedView !== null)) { - return; - } + this.update(isOpen); + }); + } else { + this.update(!!this.modalView); + } + } - if (isOpen && !this.renderedView) { - if (this.placeOnRoot) { - this.renderedView = this.rootContainer.createEmbeddedView(this.templateRef); - } else { - this.renderedView = this.viewContainer.createEmbeddedView(this.templateRef); - } - this.renderer.setElementStyle(this.renderedView.rootNodes[0], 'display', 'block'); - - setTimeout(() => { - this.startListening(); - }); - } else if (!isOpen && this.renderedView) { - this.renderedView = null; - - if (this.placeOnRoot) { - this.rootContainer.clear(); - } else { - this.viewContainer.clear(); - } + private update(isOpen: boolean) { + if (isOpen === (this.renderedView !== null)) { + return; + } - this.stopListening(); - } + if (isOpen && !this.renderedView) { + if (this.placeOnRoot) { + this.renderedView = this.rootContainer.createEmbeddedView(this.templateRef); + } else { + this.renderedView = this.viewContainer.createEmbeddedView(this.templateRef); + } + this.renderer.setElementStyle(this.renderedView.rootNodes[0], 'display', 'block'); + + setTimeout(() => { + this.startListening(); }); + } else if (!isOpen && this.renderedView) { + this.renderedView = null; + + if (this.placeOnRoot) { + this.rootContainer.clear(); + } else { + this.viewContainer.clear(); + } + + this.stopListening(); } } diff --git a/src/Squidex/app/framework/utils/string-helper.ts b/src/Squidex/app/framework/utils/string-helper.ts index 06cd2cee8..778da4c2a 100644 --- a/src/Squidex/app/framework/utils/string-helper.ts +++ b/src/Squidex/app/framework/utils/string-helper.ts @@ -6,7 +6,7 @@ */ export module StringHelper { - export function firstNonEmpty(...values: string[]) { + export function firstNonEmpty(...values: (string | undefined | null)[]) { for (let value of values) { if (value) { value = value.trim(); diff --git a/src/Squidex/app/shared/components/language-selector.component.html b/src/Squidex/app/shared/components/language-selector.component.html index 880e57172..466bc2802 100644 --- a/src/Squidex/app/shared/components/language-selector.component.html +++ b/src/Squidex/app/shared/components/language-selector.component.html @@ -1,11 +1,11 @@
-