From 6f85c31d74668a41a6a7ff9626109ecf8c80774c Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Fri, 17 May 2019 20:56:35 +0200 Subject: [PATCH 1/6] Started with separators and some bug fixes for array lists. --- .../Schemas/Fields.cs | 20 + .../Schemas/IFieldPropertiesVisitor.cs | 2 + .../Schemas/IFieldVisitor.cs | 2 + .../Schemas/UIFieldEditor.cs | 14 + .../Schemas/UIFieldProperties.cs | 34 + .../EnrichContent/ContentEnricher.cs | 2 +- .../EnrichContent/DefaultValueFactory.cs | 5 + .../ExtractReferenceIds/ReferencesCleaner.cs | 5 + .../ReferencesExtractor.cs | 5 + .../GenerateEdmSchema/EdmTypeVisitor.cs | 5 + .../GenerateJsonSchema/Builder.cs | 64 ++ .../ContentSchemaBuilder.cs | 30 +- .../JsonSchemaExtensions.cs | 25 +- .../GenerateJsonSchema/JsonTypeVisitor.cs | 155 ++- .../ValidateContent/JsonValueConverter.cs | 5 + .../Validators/NoValueValidator.cs | 30 + .../ValidateContent/ValidatorsFactory.cs | 5 + .../GraphQL/Types/ContentDataGraphType.cs | 12 +- .../GraphQL/Types/QueryGraphTypeVisitor.cs | 5 + .../Guards/FieldPropertiesValidator.cs | 9 + .../Converters/FieldPropertiesDtoFactory.cs | 5 + .../Models/Fields/UIFieldPropertiesDto.cs | 26 + .../content/content-field.component.html | 28 +- .../pages/content/content-field.component.ts | 36 +- .../pages/content/content-page.component.ts | 10 +- .../shared/array-editor.component.html | 3 +- .../content/shared/array-item.component.html | 13 +- .../content/shared/array-item.component.scss | 8 + .../content/shared/array-item.component.ts | 5 +- .../shared/field-editor.component.html | 129 +-- .../shared/field-editor.component.scss | 16 + .../content/shared/field-editor.component.ts | 10 +- .../shared/references-dropdown.component.ts | 3 +- .../app/features/schemas/declarations.ts | 1 - src/Squidex/app/features/schemas/module.ts | 4 +- .../pages/schema/field-wizard.component.html | 2 +- .../schemas/pages/schema/field.component.html | 4 +- .../forms/field-form-common.component.html | 2 +- .../schema/forms/field-form-ui.component.html | 29 - .../schema/forms/field-form-ui.component.scss | 2 - .../schema/forms/field-form-ui.component.ts | 29 +- .../field-form-validation.component.html | 29 - .../field-form-validation.component.scss | 2 - .../forms/field-form-validation.component.ts | 32 +- .../pages/schema/schema-page.component.html | 8 +- .../schema/types/assets-ui.component.html | 3 - .../schema/types/assets-ui.component.scss | 2 - .../pages/schema/types/assets-ui.component.ts | 27 - .../angular/forms/form-alert.component.ts | 2 +- .../angular/forms/form-error.component.ts | 7 +- .../components/table-header.component.ts | 3 +- .../app/shared/services/schemas.service.ts | 2 +- .../app/shared/services/schemas.types.ts | 44 +- .../app/shared/state/contents.forms.ts | 71 +- src/Squidex/app/shared/state/schemas.forms.ts | 3 + src/Squidex/app/theme/_bootstrap-vars.scss | 1 - src/Squidex/app/theme/_bootstrap.scss | 6 + .../app/theme/icomoon/demo-files/demo.css | 2 +- src/Squidex/app/theme/icomoon/demo.html | 888 +++++++++--------- .../app/theme/icomoon/fonts/icomoon.eot | Bin 29424 -> 29780 bytes .../app/theme/icomoon/fonts/icomoon.svg | 89 +- .../app/theme/icomoon/fonts/icomoon.ttf | Bin 29260 -> 29616 bytes .../app/theme/icomoon/fonts/icomoon.woff | Bin 29336 -> 29692 bytes src/Squidex/app/theme/icomoon/selection.json | 2 +- src/Squidex/app/theme/icomoon/style.css | 245 ++--- .../GenerateJsonSchema/JsonSchemaTests.cs | 55 +- .../TestUtils.cs | 9 +- .../FieldProperties/UIFieldPropertiesTests.cs | 43 + 68 files changed, 1383 insertions(+), 991 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs create mode 100644 src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs create mode 100644 src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs create mode 100644 src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs delete mode 100644 src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.html delete mode 100644 src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.scss delete mode 100644 src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.html delete mode 100644 src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.scss delete mode 100644 src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.html delete mode 100644 src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.scss delete mode 100644 src/Squidex/app/features/schemas/pages/schema/types/assets-ui.component.ts create mode 100644 tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/UIFieldPropertiesTests.cs diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs index 2ae68dabe..1938ad663 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs @@ -66,6 +66,11 @@ namespace Squidex.Domain.Apps.Core.Schemas return new RootField(id, name, partitioning, properties, settings); } + public static RootField UI(long id, string name, Partitioning partitioning, UIFieldProperties properties = null, IFieldSettings settings = null) + { + return new RootField(id, name, partitioning, properties, settings); + } + public static NestedField Assets(long id, string name, AssetsFieldProperties properties = null, IFieldSettings settings = null) { return new NestedField(id, name, properties, settings); @@ -111,6 +116,11 @@ namespace Squidex.Domain.Apps.Core.Schemas return new NestedField(id, name, properties, settings); } + public static NestedField UI(long id, string name, UIFieldProperties properties = null, IFieldSettings settings = null) + { + return new NestedField(id, name, properties, settings); + } + public static Schema AddArray(this Schema schema, long id, string name, Partitioning partitioning, Func handler = null, ArrayFieldProperties properties = null, IFieldSettings settings = null) { var field = Array(id, name, partitioning, properties, settings); @@ -168,6 +178,11 @@ namespace Squidex.Domain.Apps.Core.Schemas return schema.AddField(Tags(id, name, partitioning, properties, settings)); } + public static Schema AddUI(this Schema schema, long id, string name, Partitioning partitioning, UIFieldProperties properties = null, IFieldSettings settings = null) + { + return schema.AddField(UI(id, name, partitioning, properties, settings)); + } + public static ArrayField AddAssets(this ArrayField field, long id, string name, AssetsFieldProperties properties = null, IFieldSettings settings = null) { return field.AddField(Assets(id, name, properties, settings)); @@ -212,5 +227,10 @@ namespace Squidex.Domain.Apps.Core.Schemas { return field.AddField(Tags(id, name, properties, settings)); } + + public static ArrayField AddUI(this ArrayField field, long id, string name, UIFieldProperties properties = null, IFieldSettings settings = null) + { + return field.AddField(UI(id, name, properties, settings)); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs index c4593a450..652d6173b 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs @@ -28,5 +28,7 @@ namespace Squidex.Domain.Apps.Core.Schemas T Visit(StringFieldProperties properties); T Visit(TagsFieldProperties properties); + + T Visit(UIFieldProperties properties); } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs index 67142acc4..710560f6d 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs @@ -28,5 +28,7 @@ namespace Squidex.Domain.Apps.Core.Schemas T Visit(IField field); T Visit(IField field); + + T Visit(IField field); } } diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs new file mode 100644 index 000000000..7e52418cd --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldEditor.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public enum UIFieldEditor + { + Separator + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs new file mode 100644 index 000000000..3fd109ce8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/UIFieldProperties.cs @@ -0,0 +1,34 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class UIFieldProperties : FieldProperties + { + public UIFieldEditor Editor { get; set; } + + public override T Accept(IFieldPropertiesVisitor visitor) + { + return visitor.Visit(this); + } + + public override T Accept(IFieldVisitor visitor, IField field) + { + return visitor.Visit((IField)field); + } + + public override NestedField CreateNestedField(long id, string name, IFieldSettings settings = null) + { + return new NestedField(id, name, this, settings); + } + + public override RootField CreateRootField(long id, string name, Partitioning partitioning, IFieldSettings settings = null) + { + return new RootField(id, name, partitioning, this, settings); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs index 22f88bb85..196225e64 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/ContentEnricher.cs @@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Core.EnrichContent var defaultValue = DefaultValueFactory.CreateDefaultValue(field, SystemClock.Instance.GetCurrentInstant()); - if (field.RawProperties.IsRequired || defaultValue.Type == JsonValueType.Null) + if (field.RawProperties.IsRequired || defaultValue == null || defaultValue.Type == JsonValueType.Null) { return; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs index 15c3b936b..d7a7a1f21 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/EnrichContent/DefaultValueFactory.cs @@ -74,6 +74,11 @@ namespace Squidex.Domain.Apps.Core.EnrichContent return JsonValue.Array(); } + public IJsonValue Visit(IField field) + { + return JsonValue.Null; + } + public IJsonValue Visit(IField field) { if (field.Properties.CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now) diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs index a7c459a13..b8c0e33e9 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs @@ -93,6 +93,11 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds return value; } + public IJsonValue Visit(IField field) + { + return value; + } + public IJsonValue Visit(IArrayField field) { return value; diff --git a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs index ed00417a0..b4373ac0d 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesExtractor.cs @@ -101,5 +101,10 @@ namespace Squidex.Domain.Apps.Core.ExtractReferenceIds { return Enumerable.Empty(); } + + public IEnumerable Visit(IField field) + { + return Enumerable.Empty(); + } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs index 388448792..1bcabde3a 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateEdmSchema/EdmTypeVisitor.cs @@ -90,6 +90,11 @@ namespace Squidex.Domain.Apps.Core.GenerateEdmSchema return CreatePrimitive(EdmPrimitiveTypeKind.String, field); } + public IEdmTypeReference Visit(IField field) + { + return null; + } + private static IEdmTypeReference CreatePrimitive(EdmPrimitiveTypeKind kind, IField field) { return EdmCoreModel.Instance.GetPrimitive(kind, !field.RawProperties.IsRequired); diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs new file mode 100644 index 000000000..203c393f1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/Builder.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NJsonSchema; + +namespace Squidex.Domain.Apps.Core.GenerateJsonSchema +{ + public static class Builder + { + public static JsonSchema4 Object() + { + return new JsonSchema4 { Type = JsonObjectType.Object, AllowAdditionalItems = false }; + } + + public static JsonSchema4 Guid() + { + return new JsonSchema4 { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid }; + } + + public static JsonSchema4 String() + { + return new JsonSchema4 { Type = JsonObjectType.String }; + } + + public static JsonProperty ArrayProperty(JsonSchema4 item) + { + return new JsonProperty { Type = JsonObjectType.Array, Item = item }; + } + + public static JsonProperty BooleanProperty() + { + return new JsonProperty { Type = JsonObjectType.Boolean }; + } + + public static JsonProperty DateTimeProperty(string description = null, bool isRequired = false) + { + return new JsonProperty { Type = JsonObjectType.Number, Format = JsonFormatStrings.DateTime, Description = description, IsRequired = isRequired }; + } + + public static JsonProperty GuidProperty(string description = null, bool isRequired = false) + { + return new JsonProperty { Type = JsonObjectType.String, Format = JsonFormatStrings.Guid, Description = description, IsRequired = isRequired }; + } + + public static JsonProperty NumberProperty(string description = null, bool isRequired = false) + { + return new JsonProperty { Type = JsonObjectType.Number, Description = description, IsRequired = isRequired }; + } + + public static JsonProperty ObjectProperty(JsonSchema4 item, string description = null, bool isRequired = false) + { + return new JsonProperty { Type = JsonObjectType.Object, Reference = item, Description = description, IsRequired = isRequired }; + } + + public static JsonProperty StringProperty(string description = null, bool isRequired = false) + { + return new JsonProperty { Type = JsonObjectType.String, Description = description, IsRequired = isRequired }; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs index 452e0386f..c4743f347 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/ContentSchemaBuilder.cs @@ -24,33 +24,19 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema { Properties = { - ["id"] = CreateProperty($"The id of the {schemaName} content."), - ["data"] = CreateProperty($"The data of the {schemaName}.", dataSchema), - ["version"] = CreateProperty($"The version of the {schemaName}.", JsonObjectType.Number), - ["created"] = CreateProperty($"The date and time when the {schemaName} content has been created.", "date-time"), - ["createdBy"] = CreateProperty($"The user that has created the {schemaName} content."), - ["lastModified"] = CreateProperty($"The date and time when the {schemaName} content has been modified last.", "date-time"), - ["lastModifiedBy"] = CreateProperty($"The user that has updated the {schemaName} content last.") + ["id"] = Builder.GuidProperty($"The id of the {schemaName} content.", true), + ["data"] = Builder.ObjectProperty(dataSchema, $"The data of the {schemaName}.", true), + ["dataDraft"] = Builder.ObjectProperty(dataSchema, $"The draft data of the {schemaName}.", false), + ["version"] = Builder.NumberProperty($"The version of the {schemaName}.", true), + ["created"] = Builder.DateTimeProperty($"The date and time when the {schemaName} content has been created.", true), + ["createdBy"] = Builder.StringProperty($"The user that has created the {schemaName} content.", true), + ["lastModified"] = Builder.DateTimeProperty($"The date and time when the {schemaName} content has been modified last.", true), + ["lastModifiedBy"] = Builder.StringProperty($"The user that has updated the {schemaName} content last.", true) }, Type = JsonObjectType.Object }; return contentSchema; } - - private static JsonProperty CreateProperty(string description, JsonSchema4 dataSchema) - { - return new JsonProperty { Description = description, IsRequired = true, Type = JsonObjectType.Object, Reference = dataSchema }; - } - - private static JsonProperty CreateProperty(string description, JsonObjectType type) - { - return new JsonProperty { Description = description, IsRequired = true, Type = type }; - } - - private static JsonProperty CreateProperty(string description, string format = null) - { - return new JsonProperty { Description = description, Format = format, IsRequired = true, Type = JsonObjectType.String }; - } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs index a11185325..2e3a041c2 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonSchemaExtensions.cs @@ -27,31 +27,36 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema foreach (var field in schema.Fields.Where(x => !x.IsHidden)) { - var partitionProperty = CreateProperty(field); - var partitionObject = new JsonSchema4 { Type = JsonObjectType.Object, AllowAdditionalProperties = false }; + var partitionObject = Builder.Object(); var partition = partitionResolver(field.Partitioning); foreach (var partitionItem in partition) { var partitionItemProperty = field.Accept(jsonTypeVisitor); - partitionItemProperty.Description = partitionItem.Name; - partitionItemProperty.IsRequired = field.RawProperties.IsRequired && !partitionItem.IsOptional; + if (partitionItemProperty != null) + { + partitionItemProperty.Description = partitionItem.Name; + partitionItemProperty.IsRequired = field.RawProperties.IsRequired && !partitionItem.IsOptional; - partitionObject.Properties.Add(partitionItem.Key, partitionItemProperty); + partitionObject.Properties.Add(partitionItem.Key, partitionItemProperty); + } } - partitionProperty.Reference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}Property", partitionObject); + if (partitionObject.Properties.Count > 0) + { + var propertyReference = schemaResolver($"{schemaName}{field.Name.ToPascalCase()}Property", partitionObject); - jsonSchema.Properties.Add(field.Name, partitionProperty); + jsonSchema.Properties.Add(field.Name, CreateProperty(field, propertyReference)); + } } return jsonSchema; } - public static JsonProperty CreateProperty(IField field) + public static JsonProperty CreateProperty(IField field, JsonSchema4 reference) { - var jsonProperty = new JsonProperty { IsRequired = field.RawProperties.IsRequired, Type = JsonObjectType.Object }; + var jsonProperty = Builder.ObjectProperty(reference); if (!string.IsNullOrWhiteSpace(field.RawProperties.Hints)) { @@ -62,6 +67,8 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema jsonProperty.Description = field.Name; } + jsonProperty.IsRequired = field.RawProperties.IsRequired; + return jsonProperty; } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs index 00ad5f60f..68bc679be 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs @@ -24,164 +24,125 @@ namespace Squidex.Domain.Apps.Core.GenerateJsonSchema public JsonProperty Visit(IArrayField field) { - return CreateProperty(field, jsonProperty => + var item = Builder.Object(); + + foreach (var nestedField in field.Fields.Where(x => !x.IsHidden)) { - var itemSchema = new JsonSchema4 - { - Type = JsonObjectType.Object - }; + var childProperty = nestedField.Accept(this); - foreach (var nestedField in field.Fields.Where(x => !x.IsHidden)) + if (childProperty != null) { - var childProperty = nestedField.Accept(this); - childProperty.Description = nestedField.RawProperties.Hints; childProperty.IsRequired = nestedField.RawProperties.IsRequired; - itemSchema.Properties.Add(nestedField.Name, childProperty); + item.Properties.Add(nestedField.Name, childProperty); } + } - jsonProperty.Type = JsonObjectType.Object; - jsonProperty.Item = itemSchema; - }); + return Builder.ArrayProperty(item); } public JsonProperty Visit(IField field) { - return CreateProperty(field, jsonProperty => - { - var itemSchema = schemaResolver("AssetItem", new JsonSchema4 { Type = JsonObjectType.String }); + var item = schemaResolver("AssetItem", Builder.Guid()); - jsonProperty.Type = JsonObjectType.Array; - jsonProperty.Item = itemSchema; - }); + return Builder.ArrayProperty(item); } public JsonProperty Visit(IField field) { - return CreateProperty(field, jsonProperty => - { - jsonProperty.Type = JsonObjectType.Boolean; - }); + return Builder.BooleanProperty(); } public JsonProperty Visit(IField field) { - return CreateProperty(field, jsonProperty => - { - jsonProperty.Type = JsonObjectType.String; - jsonProperty.Format = JsonFormatStrings.DateTime; - }); + return Builder.DateTimeProperty(); } public JsonProperty Visit(IField field) { - return CreateProperty(field, jsonProperty => - { - var geolocationSchema = new JsonSchema4 - { - AllowAdditionalProperties = false - }; + var geolocationSchema = Builder.Object(); - geolocationSchema.Properties.Add("latitude", new JsonProperty - { - Type = JsonObjectType.Number, - Minimum = -90, - Maximum = 90, - IsRequired = true - }); + geolocationSchema.Properties.Add("latitude", new JsonProperty + { + Type = JsonObjectType.Number, + Minimum = -90, + Maximum = 90, + IsRequired = true + }); - geolocationSchema.Properties.Add("longitude", new JsonProperty - { - Type = JsonObjectType.Number, - Minimum = -180, - Maximum = 180, - IsRequired = true - }); + geolocationSchema.Properties.Add("longitude", new JsonProperty + { + Type = JsonObjectType.Number, + Minimum = -180, + Maximum = 180, + IsRequired = true + }); - var schemaReference = schemaResolver("GeolocationDto", geolocationSchema); + var reference = schemaResolver("GeolocationDto", geolocationSchema); - jsonProperty.Type = JsonObjectType.Object; - jsonProperty.Reference = schemaReference; - }); + return Builder.ObjectProperty(reference); } public JsonProperty Visit(IField field) { - return CreateProperty(field, jsonProperty => - { - jsonProperty.Type = JsonObjectType.Object; - }); + return Builder.StringProperty(); } public JsonProperty Visit(IField field) { - return CreateProperty(field, jsonProperty => + var property = Builder.NumberProperty(); + + if (field.Properties.MinValue.HasValue) { - jsonProperty.Type = JsonObjectType.Number; + property.Minimum = (decimal)field.Properties.MinValue.Value; + } - if (field.Properties.MinValue.HasValue) - { - jsonProperty.Minimum = (decimal)field.Properties.MinValue.Value; - } + if (field.Properties.MaxValue.HasValue) + { + property.Maximum = (decimal)field.Properties.MaxValue.Value; + } - if (field.Properties.MaxValue.HasValue) - { - jsonProperty.Maximum = (decimal)field.Properties.MaxValue.Value; - } - }); + return property; } public JsonProperty Visit(IField field) { - return CreateProperty(field, jsonProperty => - { - var itemSchema = schemaResolver("ReferenceItem", new JsonSchema4 { Type = JsonObjectType.String }); + var item = schemaResolver("ReferenceItem", Builder.Guid()); - jsonProperty.Type = JsonObjectType.Array; - jsonProperty.Item = itemSchema; - }); + return Builder.ArrayProperty(item); } public JsonProperty Visit(IField field) { - return CreateProperty(field, jsonProperty => - { - jsonProperty.Type = JsonObjectType.String; + var property = Builder.StringProperty(); - jsonProperty.MinLength = field.Properties.MinLength; - jsonProperty.MaxLength = field.Properties.MaxLength; + property.MinLength = field.Properties.MinLength; + property.MaxLength = field.Properties.MaxLength; - if (field.Properties.AllowedValues != null) - { - var names = jsonProperty.EnumerationNames = jsonProperty.EnumerationNames ?? new Collection(); + if (field.Properties.AllowedValues != null) + { + var names = property.EnumerationNames = property.EnumerationNames ?? new Collection(); - foreach (var value in field.Properties.AllowedValues) - { - names.Add(value); - } + foreach (var value in field.Properties.AllowedValues) + { + names.Add(value); } - }); + } + + return property; } public JsonProperty Visit(IField field) { - return CreateProperty(field, jsonProperty => - { - var itemSchema = schemaResolver("TagsItem", new JsonSchema4 { Type = JsonObjectType.String }); + var item = schemaResolver("ReferenceItem", Builder.String()); - jsonProperty.Type = JsonObjectType.Array; - jsonProperty.Item = itemSchema; - }); + return Builder.ArrayProperty(item); } - private static JsonProperty CreateProperty(IField field, Action updater) + public JsonProperty Visit(IField field) { - var property = new JsonProperty { IsRequired = field.RawProperties.IsRequired }; - - updater(property); - - return property; + return null; } } } diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs index fff4b75e7..867dd79ba 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -78,6 +78,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent throw new InvalidCastException("Invalid json type, expected string."); } + public object Visit(IField field) + { + return value; + } + public object Visit(IField field) { if (value.Type == JsonValueType.String) diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs new file mode 100644 index 000000000..52e857d5b --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/NoValueValidator.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class NoValueValidator : IValidator + { + public static readonly NoValueValidator Instance = new NoValueValidator(); + + private NoValueValidator() + { + } + + public Task ValidateAsync(object value, ValidationContext context, AddError addError) + { + if (value != null) + { + addError(context.Path, "Field does not accept a value."); + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs index ae6351a91..3feff106e 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ValidatorsFactory.cs @@ -183,5 +183,10 @@ namespace Squidex.Domain.Apps.Core.ValidateContent yield return new CollectionItemValidator(new RequiredStringValidator(true)); } + + public IEnumerable Visit(IField field) + { + yield return NoValueValidator.Instance; + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs index 6c930f903..4ea0623a7 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentDataGraphType.cs @@ -28,9 +28,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types foreach (var field in schema.SchemaDef.Fields.Where(x => !x.IsHidden)) { - var fieldInfo = model.GetGraphType(schema, field); + var (resolvedType, valueResolver) = model.GetGraphType(schema, field); - if (fieldInfo.ResolveType != null) + if (valueResolver != null) { var fieldType = field.TypeName(); var fieldName = field.DisplayName(); @@ -46,11 +46,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { var key = partitionItem.Key; - var resolver = new FuncFieldResolver(c => + var partitionResolver = new FuncFieldResolver(c => { if (((ContentFieldData)c.Source).TryGetValue(key, out var value)) { - return fieldInfo.Resolver(value, c); + return valueResolver(value, c); } else { @@ -61,8 +61,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types fieldGraphType.AddField(new FieldType { Name = key.EscapePartition(), - Resolver = resolver, - ResolvedType = fieldInfo.ResolveType, + Resolver = partitionResolver, + ResolvedType = resolvedType, Description = field.RawProperties.Hints }); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs index 2ed69033c..32a9a308f 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/QueryGraphTypeVisitor.cs @@ -81,6 +81,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types return ResolveDefault(AllTypes.NoopTags); } + public (IGraphType ResolveType, ValueResolver Resolver) Visit(IField field) + { + return (null, null); + } + private static (IGraphType ResolveType, ValueResolver Resolver) ResolveDefault(IGraphType type) { return (type, NoopResolver); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs index fff833c57..857730c7a 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs @@ -268,5 +268,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards nameof(properties.MaxItems)); } } + + public IEnumerable Visit(UIFieldProperties properties) + { + if (!properties.Editor.IsEnumValue()) + { + yield return new ValidationError(Not.Valid("Editor"), + nameof(properties.Editor)); + } + } } } diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs index 31bc2cc01..c2cacdb89 100644 --- a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs @@ -55,6 +55,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Converters return SimpleMapper.Map(properties, new ReferencesFieldPropertiesDto()); } + public FieldPropertiesDto Visit(UIFieldProperties properties) + { + return SimpleMapper.Map(properties, new UIFieldPropertiesDto()); + } + public FieldPropertiesDto Visit(TagsFieldProperties properties) { var result = SimpleMapper.Map(properties, new TagsFieldPropertiesDto()); diff --git a/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs new file mode 100644 index 000000000..76040518a --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/UIFieldPropertiesDto.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Collections; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields +{ + public sealed class UIFieldPropertiesDto : FieldPropertiesDto + { + /// + /// The editor that is used to manage this field. + /// + public UIFieldEditor Editor { get; set; } + + public override FieldProperties ToProperties() + { + return SimpleMapper.Map(this, new UIFieldProperties()); + } + } +} 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 36cafd558..75c5816b5 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 @@ -1,6 +1,6 @@
-
+
+ + + + +
- -
+
+ + + + +
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 c949af5de..07be6e356 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 @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { Component, DoCheck, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { AbstractControl, FormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; import { combineLatest } from 'rxjs/operators'; @@ -29,7 +29,7 @@ import { styleUrls: ['./content-field.component.scss'], templateUrl: './content-field.component.html' }) -export class ContentFieldComponent implements OnChanges { +export class ContentFieldComponent implements DoCheck, OnChanges { @Input() public form: EditContentForm; @@ -75,7 +75,7 @@ export class ContentFieldComponent implements OnChanges { this.showAllControls = this.localStore.getBoolean(this.configKey()); } - if (changes['fieldForm']) { + if (changes['fieldForm'] && this.fieldForm) { this.isInvalid = invalid$(this.fieldForm); } @@ -89,26 +89,30 @@ export class ContentFieldComponent implements OnChanges { combineLatest(value$(this.fieldFormCompare), (lhs, rhs) => !Types.jsJsonEquals(lhs, rhs))); } + } + + public ngDoCheck() { + if (this.fieldForm) { + const control = this.findControl(this.fieldForm); - const control = this.findControl(this.fieldForm); + if (this.selectedFormControl !== control) { + if (this.selectedFormControl && Types.isFunction(this.selectedFormControl['_clearChangeFns'])) { + this.selectedFormControl['_clearChangeFns'](); + } - if (this.selectedFormControl !== control) { - if (this.selectedFormControl && Types.isFunction(this.selectedFormControl['_clearChangeFns'])) { - this.selectedFormControl['_clearChangeFns'](); + this.selectedFormControl = control; } - this.selectedFormControl = control; - } + if (this.fieldFormCompare) { + const controlCompare = this.findControl(this.fieldFormCompare); - if (this.fieldFormCompare) { - const controlCompare = this.findControl(this.fieldFormCompare); + if (this.selectedFormControlCompare !== controlCompare) { + if (this.selectedFormControlCompare && Types.isFunction(this.selectedFormControlCompare['_clearChangeFns'])) { + this.selectedFormControlCompare['_clearChangeFns'](); + } - if (this.selectedFormControlCompare !== controlCompare) { - if (this.selectedFormControlCompare && Types.isFunction(this.selectedFormControlCompare['_clearChangeFns'])) { - this.selectedFormControlCompare['_clearChangeFns'](); + this.selectedFormControlCompare = controlCompare; } - - this.selectedFormControlCompare = controlCompare; } } } 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 b607f2400..0488caf99 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 @@ -214,7 +214,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD if (!this.content || version === null || version.eq(this.content.version)) { this.contentFormCompare = null; this.contentVersion = null; - this.contentForm.load(this.content.dataDraft); + this.loadContent(this.content.dataDraft); } else { this.contentsState.loadVersion(this.content, version) .subscribe(dto => { @@ -224,14 +224,16 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD this.contentFormCompare.form.disable(); } - this.contentFormCompare.load(dto.payload); - this.contentForm.load(this.content.dataDraft); + const isArchive = this.content && this.content.status === 'Archived'; + + this.contentFormCompare.loadContent(dto.payload, true); + this.contentForm.loadContent(this.content.dataDraft, isArchive); } else { if (this.contentFormCompare) { this.contentFormCompare = null; } - this.contentForm.load(dto.payload); + this.loadContent(dto.payload); } this.contentVersion = version; diff --git a/src/Squidex/app/features/content/shared/array-editor.component.html b/src/Squidex/app/features/content/shared/array-editor.component.html index 9f94de23f..2632496dd 100644 --- a/src/Squidex/app/features/content/shared/array-editor.component.html +++ b/src/Squidex/app/features/content/shared/array-editor.component.html @@ -6,6 +6,7 @@ [form]="form" [field]="field" [isHidden]="snapshot.isHidden" + [isDisabled]="arrayControl.disabled" [isFirst]="i === 0" [isLast]="i === arrayControl.controls.length - 1" [index]="i" @@ -20,7 +21,7 @@
- diff --git a/src/Squidex/app/features/content/shared/array-item.component.html b/src/Squidex/app/features/content/shared/array-item.component.html index 658f6e19d..5f1b4f23d 100644 --- a/src/Squidex/app/features/content/shared/array-item.component.html +++ b/src/Squidex/app/features/content/shared/array-item.component.html @@ -5,16 +5,16 @@ Item #{{index + 1}} - - - - - -
diff --git a/src/Squidex/app/features/content/shared/array-item.component.scss b/src/Squidex/app/features/content/shared/array-item.component.scss index 11cb6b684..7b54dfb82 100644 --- a/src/Squidex/app/features/content/shared/array-item.component.scss +++ b/src/Squidex/app/features/content/shared/array-item.component.scss @@ -1,6 +1,14 @@ @import '_vars'; @import '_mixins'; +:host /deep/ { + .ui-separator { + border-color: $color-border !important; + font-size: 1.2rem; + font-weight: normal; + } +} + .card { & { border: 1px solid darken($color-border, 5%); diff --git a/src/Squidex/app/features/content/shared/array-item.component.ts b/src/Squidex/app/features/content/shared/array-item.component.ts index d6154dfbc..563cf9df4 100644 --- a/src/Squidex/app/features/content/shared/array-item.component.ts +++ b/src/Squidex/app/features/content/shared/array-item.component.ts @@ -51,6 +51,9 @@ export class ArrayItemComponent implements OnChanges { @Input() public isLast = false; + @Input() + public isDisabled = false; + @Input() public index: number; @@ -73,7 +76,7 @@ export class ArrayItemComponent implements OnChanges { } if (changes['itemForm'] || changes['field']) { - this.fieldControls = this.field.nested.map(field => ({ field, control: this.itemForm.get(field.name)! })).filter(x => !!x.control); + this.fieldControls = this.field.nested.map(field => ({ field, control: this.itemForm.get(field.name)! })).filter(x => !x.field.properties.isNormalField || !!x.control); } } diff --git a/src/Squidex/app/features/content/shared/field-editor.component.html b/src/Squidex/app/features/content/shared/field-editor.component.html index 21e87f6f7..9dff90fd7 100644 --- a/src/Squidex/app/features/content/shared/field-editor.component.html +++ b/src/Squidex/app/features/content/shared/field-editor.component.html @@ -1,11 +1,13 @@ - - - - Disabled - - +
+ + + + Disabled + + +
@@ -14,6 +16,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -38,6 +71,27 @@ + + + + + + + + + + + + @@ -77,28 +131,6 @@ - - - - - - - - - - - - - - - - - - - - - - @@ -113,37 +145,10 @@ - - - - - - - - - - - - - - - - - + + +

{{field.displayName}}

+
@@ -153,4 +158,4 @@ {{hints}} - +
diff --git a/src/Squidex/app/features/content/shared/field-editor.component.scss b/src/Squidex/app/features/content/shared/field-editor.component.scss index 8cb38413e..206e9cf54 100644 --- a/src/Squidex/app/features/content/shared/field-editor.component.scss +++ b/src/Squidex/app/features/content/shared/field-editor.component.scss @@ -11,4 +11,20 @@ font-size: .8rem; font-weight: normal; } +} + +.ui { + & { + margin-bottom: 1rem; + } + + &-separator { + color: lighten($color-text, 20%); + border-bottom: 1px solid darken($color-border, 5%); + font-size: 1.3rem; + font-weight: normal; + padding: .25rem 0; + margin-top: 1.5rem; + margin-bottom: 0; + } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/field-editor.component.ts b/src/Squidex/app/features/content/shared/field-editor.component.ts index d06292a19..69e129549 100644 --- a/src/Squidex/app/features/content/shared/field-editor.component.ts +++ b/src/Squidex/app/features/content/shared/field-editor.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnChanges } from '@angular/core'; import { FormControl } from '@angular/forms'; import { @@ -20,7 +20,7 @@ import { styleUrls: ['./field-editor.component.scss'], templateUrl: './field-editor.component.html' }) -export class FieldEditorComponent { +export class FieldEditorComponent implements OnChanges { @Input() public form: EditContentForm; @@ -43,4 +43,10 @@ export class FieldEditorComponent { public displaySuffix: string; public uniqueId = MathHelper.guid(); + + public ngOnChanges() { + let a = 0; + a++; + console.log(a); + } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/references-dropdown.component.ts b/src/Squidex/app/features/content/shared/references-dropdown.component.ts index 721edc3ec..ac9025e7e 100644 --- a/src/Squidex/app/features/content/shared/references-dropdown.component.ts +++ b/src/Squidex/app/features/content/shared/references-dropdown.component.ts @@ -45,8 +45,7 @@ type ContentName = { name: string, id: string }; - `, + `, providers: [SQX_REFERENCES_DROPDOWN_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) diff --git a/src/Squidex/app/features/schemas/declarations.ts b/src/Squidex/app/features/schemas/declarations.ts index 7082aa204..4f7ac0fb9 100644 --- a/src/Squidex/app/features/schemas/declarations.ts +++ b/src/Squidex/app/features/schemas/declarations.ts @@ -5,7 +5,6 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -export * from './pages/schema/types/assets-ui.component'; export * from './pages/schema/types/assets-validation.component'; export * from './pages/schema/types/boolean-ui.component'; export * from './pages/schema/types/boolean-validation.component'; diff --git a/src/Squidex/app/features/schemas/module.ts b/src/Squidex/app/features/schemas/module.ts index 0db0eef25..0207d7049 100644 --- a/src/Squidex/app/features/schemas/module.ts +++ b/src/Squidex/app/features/schemas/module.ts @@ -17,7 +17,6 @@ import { } from '@app/shared'; import { - AssetsUIComponent, AssetsValidationComponent, BooleanUIComponent, BooleanValidationComponent, @@ -81,13 +80,12 @@ const routes: Routes = [ SchemaMustExistGuard ], declarations: [ - FieldComponent, - AssetsUIComponent, AssetsValidationComponent, BooleanUIComponent, BooleanValidationComponent, DateTimeUIComponent, DateTimeValidationComponent, + FieldComponent, FieldFormCommonComponent, FieldFormUIComponent, FieldFormValidationComponent, diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html index 00dfdec04..ad414cce5 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html @@ -44,7 +44,7 @@ placeholder="Enter field name" sqxFocusOnInit />
-
+
-
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.html deleted file mode 100644 index 469ffb141..000000000 --- a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.scss b/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.scss deleted file mode 100644 index fbb752506..000000000 --- a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import '_vars'; -@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.ts b/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.ts index 806c84119..173d51b6e 100644 --- a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/forms/field-form-ui.component.ts @@ -12,8 +12,33 @@ import { FieldDto } from '@app/shared'; @Component({ selector: 'sqx-field-form-ui', - styleUrls: ['field-form-ui.component.scss'], - templateUrl: 'field-form-ui.component.html' + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + + ` }) export class FieldFormUIComponent { @Input() diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.html b/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.html deleted file mode 100644 index 1cd5d2a74..000000000 --- a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.scss b/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.scss deleted file mode 100644 index fbb752506..000000000 --- a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import '_vars'; -@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts index d02c495a5..886f68411 100644 --- a/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/forms/field-form-validation.component.ts @@ -16,8 +16,36 @@ import { @Component({ selector: 'sqx-field-form-validation', - styleUrls: ['field-form-validation.component.scss'], - templateUrl: 'field-form-validation.component.html' + template: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ` }) export class FieldFormValidationComponent { @Input() 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 7bb111314..69ccaa9fc 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 @@ -70,11 +70,9 @@
- - +
+ +
- -
+
({ field, control: this.itemForm.get(field.name)! })).filter(x => !x.field.properties.isNormalField || !!x.control); + this.fieldControls = this.field.nested.map(field => ({ field, control: this.itemForm.get(field.name)! })).filter(x => !x.field.properties.isContentField || !!x.control); } } diff --git a/src/Squidex/app/features/content/shared/field-editor.component.html b/src/Squidex/app/features/content/shared/field-editor.component.html index 9dff90fd7..506a43f4b 100644 --- a/src/Squidex/app/features/content/shared/field-editor.component.html +++ b/src/Squidex/app/features/content/shared/field-editor.component.html @@ -1,5 +1,5 @@ -
- +
+ diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html index ad414cce5..b47989637 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html @@ -44,7 +44,7 @@ placeholder="Enter field name" sqxFocusOnInit />
-
+
-
+
diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index bf23df6a0..11172cf8b 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -71,7 +71,7 @@ export class SchemaDetailsDto extends SchemaDto { protected onCloned() { if (this.fields) { - let fields = this.fields.filter(x => x.properties.isListField && x.properties.isNormalField); + let fields = this.fields.filter(x => x.properties.isListField && x.properties.isContentField); if (fields.length === 0 && this.fields.length > 0) { fields = [this.fields[0]]; diff --git a/src/Squidex/app/shared/services/schemas.types.ts b/src/Squidex/app/shared/services/schemas.types.ts index e1a4e6a6b..c9df2017c 100644 --- a/src/Squidex/app/shared/services/schemas.types.ts +++ b/src/Squidex/app/shared/services/schemas.types.ts @@ -158,7 +158,7 @@ export abstract class FieldPropertiesDto { return true; } - public get isNormalField() { + public get isContentField() { return true; } @@ -407,7 +407,7 @@ export class UIFieldPropertiesDto extends FieldPropertiesDto { return false; } - public get isNormalField() { + public get isContentField() { return false; } diff --git a/src/Squidex/app/shared/state/schemas.forms.ts b/src/Squidex/app/shared/state/schemas.forms.ts index b434ae8e9..5442beb61 100644 --- a/src/Squidex/app/shared/state/schemas.forms.ts +++ b/src/Squidex/app/shared/state/schemas.forms.ts @@ -182,7 +182,7 @@ export class EditSchemaForm extends Form { - public isNormalField = value$(this.form.get('type')!).pipe(map(x => x !== 'UI')); + public isContentField = value$(this.form.get('type')!).pipe(map(x => x !== 'UI')); constructor(formBuilder: FormBuilder) { super(formBuilder.group({ diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs index 8c87279ec..3fcacafd0 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/GenerateJsonSchema/JsonSchemaTests.cs @@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Operations.GenerateJsonSchema void CheckField(IField field) { - if (field is IField || field.IsHidden) + if (!field.IsForApi()) { Assert.DoesNotContain(field.Name, jsonProperties); } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs index 1c1601934..496fc96ca 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs @@ -122,6 +122,24 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent }); } + [Fact] + public async Task Should_add_error_if_required_data_string_field_is_not_in_bag() + { + schema = schema.AddString(1, "my-field", Partitioning.Invariant, + new StringFieldProperties { IsRequired = true }); + + var data = + new NamedContentData(); + + await data.ValidateAsync(context, schema, languagesConfig.ToResolver(), errors); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Field is required.", "my-field") + }); + } + [Fact] public async Task Should_add_error_if_data_contains_invalid_language() { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs index f7f46592e..deb78c479 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs @@ -5,10 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.ValidateContent; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -27,23 +31,24 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent } [Fact] - public async Task Should_not_add_error_if_value_is_null() + public async Task Should_not_add_error_if_value_is_undefined() { var sut = Field(new UIFieldProperties()); - await sut.ValidateAsync(null, errors, ValidationTestExtensions.ValidContext); + await sut.ValidateAsync(Undefined.Value, errors, ValidationTestExtensions.ValidContext); Assert.Empty(errors); } [Fact] - public async Task Should_not_add_error_if_value_is_json_null() + public async Task Should_add_error_if_value_is_json_null() { var sut = Field(new UIFieldProperties()); await sut.ValidateAsync(JsonValue.Null, errors); - Assert.Empty(errors); + errors.Should().BeEquivalentTo( + new[] { "Value must not be defined." }); } [Fact] @@ -57,9 +62,66 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { "Value must not be defined." }); } - private static RootField Field(UIFieldProperties properties) + [Fact] + public async Task Should_add_error_if_field_object_is_defined() + { + var schema = + new Schema("my-schema") + .AddUI(1, "my-ui1", Partitioning.Invariant) + .AddUI(2, "my-ui2", Partitioning.Invariant); + + var data = + new NamedContentData() + .AddField("my-ui1", new ContentFieldData()) + .AddField("my-ui2", new ContentFieldData() + .AddValue("iv", null)); + + var validationContext = ValidationTestExtensions.ValidContext; + var validator = new ContentValidator(schema, new PartitionResolver(x => InvariantPartitioning.Instance), validationContext); + + await validator.ValidateAsync(data); + + validator.Errors.Should().BeEquivalentTo( + new[] { + new ValidationError("Value must not be defined.", "my-ui1"), + new ValidationError("Value must not be defined.", "my-ui2") + }); + } + + [Fact] + public async Task Should_add_error_if_array_item_field_is_defined() + { + var schema = + new Schema("my-schema") + .AddArray(1, "my-array", Partitioning.Invariant, array => array + .AddUI(101, "my-ui")); + + var data = + new NamedContentData() + .AddField("my-array", new ContentFieldData() + .AddValue("iv", + JsonValue.Array( + JsonValue.Object() + .Add("my-ui", null)))); + + var validationContext = + new ValidationContext( + Guid.NewGuid(), + Guid.NewGuid(), + (c, s) => null, + (c) => null); + + var validator = new ContentValidator(schema, new PartitionResolver(x => InvariantPartitioning.Instance), validationContext); + + await validator.ValidateAsync(data); + + validator.Errors.Should().BeEquivalentTo( + new[] { new ValidationError("Value must not be defined.", "my-array[1].my-ui") }); + } + + private static NestedField Field(UIFieldProperties properties) { - return Fields.UI(1, "my-ui", Partitioning.Invariant, properties); + return new NestedField(1, "my-ui", properties); } } } diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs index ccd2aa9ee..52dc64bce 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ValidationTestExtensions.cs @@ -12,7 +12,6 @@ using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent.Validators; -using Squidex.Infrastructure.Json.Objects; namespace Squidex.Domain.Apps.Core.Operations.ValidateContent { @@ -38,18 +37,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent CreateFormatter(errors)); } - public static Task ValidateAsync(this IField field, IJsonValue value, IList errors, ValidationContext context = null) + public static Task ValidateAsync(this IField field, object value, IList errors, ValidationContext context = null) { - return new FieldValidator(ValidatorsFactory.CreateValidators(field).ToArray(), field) + return new FieldValidator(FieldValueValidatorsFactory.CreateValidators(field).ToArray(), field) .ValidateAsync( value, CreateContext(context), CreateFormatter(errors)); } - public static Task ValidateOptionalAsync(this IField field, IJsonValue value, IList errors, ValidationContext context = null) + public static Task ValidateOptionalAsync(this IField field, object value, IList errors, ValidationContext context = null) { - return new FieldValidator(ValidatorsFactory.CreateValidators(field).ToArray(), field) + return new FieldValidator(FieldValueValidatorsFactory.CreateValidators(field).ToArray(), field) .ValidateAsync( value, CreateContext(context).Optional(true), diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs index 428924d69..4c4e0c028 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/NoValueValidatorTests.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent.Validators; using Squidex.Infrastructure.Json.Objects; using Xunit; @@ -19,11 +20,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators private readonly List errors = new List(); [Fact] - public async Task Should_not_add_error_if_value_is_null() + public async Task Should_not_add_error_if_value_is_undefined() { var sut = NoValueValidator.Instance; - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(Undefined.Value, errors); Assert.Empty(errors); } From c843f82f488141b2564b3cd35b09223ea8197ca4 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 20 May 2019 18:06:48 +0200 Subject: [PATCH 4/6] Validation for UI fields. --- .../Schemas/FieldExtensions.cs | 7 +- .../Guards/FieldPropertiesValidator.cs | 13 ++- .../Schemas/Guards/GuardSchema.cs | 15 ++++ .../Schemas/Guards/GuardSchemaField.cs | 10 +++ .../schemas/pages/schema/field.component.html | 4 +- src/Squidex/app/theme/_bootstrap.scss | 14 ++++ .../Schemas/Guards/GuardSchemaFieldTests.cs | 81 +++++++++++++++++-- .../Schemas/Guards/GuardSchemaTests.cs | 39 ++++++++- 8 files changed, 173 insertions(+), 10 deletions(-) diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs index 1d817dc1e..88a11c699 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldExtensions.cs @@ -31,7 +31,12 @@ namespace Squidex.Domain.Apps.Core.Schemas public static bool IsForApi(this T field, bool withHidden = false) where T : IField { - return (withHidden || !field.IsHidden) && !(field is IField); + return (withHidden || !field.IsHidden) && field.RawProperties.IsForApi(); + } + + public static bool IsForApi(this T properties) where T : FieldProperties + { + return !(properties is UIFieldProperties); } public static Schema ReorderFields(this Schema schema, List ids, long? parentId = null) diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs index 857730c7a..7eb970464 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs @@ -21,7 +21,18 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards public static IEnumerable Validate(FieldProperties properties) { - return properties?.Accept(Instance); + if (properties != null) + { + if (!properties.IsForApi() && properties.IsListField) + { + yield return new ValidationError("UI field cannot be a list field.", nameof(properties.IsListField)); + } + + foreach (var error in properties.Accept(Instance)) + { + yield return error; + } + } } public IEnumerable Visit(ArrayFieldProperties properties) diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs index 65b22cbe9..ab4851731 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs @@ -13,6 +13,8 @@ using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Infrastructure; +#pragma warning disable IDE0060 // Remove unused parameter + namespace Squidex.Domain.Apps.Entities.Schemas.Guards { public static class GuardSchema @@ -199,6 +201,19 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards } else { + if (!field.Properties.IsForApi()) + { + if (field.IsHidden) + { + e("UI field cannot be hidden.", $"{prefix}.{nameof(field.IsHidden)}"); + } + + if (field.IsDisabled) + { + e("UI field cannot be disabled.", $"{prefix}.{nameof(field.IsDisabled)}"); + } + } + var errors = FieldPropertiesValidator.Validate(field.Properties); errors.Foreach(x => x.WithPrefix($"{prefix}.{nameof(field.Properties)}").AddTo(e)); diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs index 83853330d..ed93453bc 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchemaField.cs @@ -101,6 +101,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { throw new DomainException("Schema field is already hidden."); } + + if (!field.IsForApi()) + { + throw new DomainException("UI field cannot be hidden."); + } } public static void CanDisable(Schema schema, DisableField command) @@ -113,6 +118,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards { throw new DomainException("Schema field is already disabled."); } + + if (!field.IsForApi()) + { + throw new DomainException("UI field cannot be disabled."); + } } public static void CanDelete(Schema schema, DeleteField command) diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.html b/src/Squidex/app/features/schemas/pages/schema/field.component.html index 27ce3bc1c..772319a9a 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field.component.html @@ -25,7 +25,7 @@