diff --git a/Dockerfile.build b/Dockerfile.build index 680882fd9..d9057ded8 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -1,4 +1,4 @@ -FROM microsoft/aspnetcore-build +FROM microsoft/aspnetcore-build:1.1.1 # Install runtime dependencies diff --git a/Squidex.sln b/Squidex.sln index d75d2e1af..7c456fe9f 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26020.0 +VisualStudioVersion = 15.0.26228.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex", "src\Squidex\Squidex.csproj", "{61F6BBCE-A080-4400-B194-70E2F5D2096E}" EndProject @@ -152,16 +152,16 @@ Global {D7166C56-178A-4457-B56A-C615C7450DEE}.Release|x86.ActiveCfg = Release|Any CPU {927E1F1C-95F0-4991-B33F-603977204B02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {927E1F1C-95F0-4991-B33F-603977204B02}.Debug|Any CPU.Build.0 = Debug|Any CPU - {927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x64.ActiveCfg = Debug|x64 - {927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x64.Build.0 = Debug|x64 - {927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x86.ActiveCfg = Debug|x86 - {927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x86.Build.0 = Debug|x86 + {927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x64.ActiveCfg = Debug|Any CPU + {927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x64.Build.0 = Debug|Any CPU + {927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x86.ActiveCfg = Debug|Any CPU + {927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x86.Build.0 = Debug|Any CPU {927E1F1C-95F0-4991-B33F-603977204B02}.Release|Any CPU.ActiveCfg = Release|Any CPU {927E1F1C-95F0-4991-B33F-603977204B02}.Release|Any CPU.Build.0 = Release|Any CPU - {927E1F1C-95F0-4991-B33F-603977204B02}.Release|x64.ActiveCfg = Release|x64 - {927E1F1C-95F0-4991-B33F-603977204B02}.Release|x64.Build.0 = Release|x64 - {927E1F1C-95F0-4991-B33F-603977204B02}.Release|x86.ActiveCfg = Release|x86 - {927E1F1C-95F0-4991-B33F-603977204B02}.Release|x86.Build.0 = Release|x86 + {927E1F1C-95F0-4991-B33F-603977204B02}.Release|x64.ActiveCfg = Release|Any CPU + {927E1F1C-95F0-4991-B33F-603977204B02}.Release|x64.Build.0 = Release|Any CPU + {927E1F1C-95F0-4991-B33F-603977204B02}.Release|x86.ActiveCfg = Release|Any CPU + {927E1F1C-95F0-4991-B33F-603977204B02}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Squidex.sln.DotSettings b/Squidex.sln.DotSettings index 0adccb11b..010c5837d 100644 --- a/Squidex.sln.DotSettings +++ b/Squidex.sln.DotSettings @@ -14,41 +14,27 @@ True False True - - True - - - - ExplicitlyExcluded - DO_NOT_SHOW + + True DO_NOT_SHOW - - DO_NOT_SHOW - DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW - - - DO_NOT_SHOW - DO_NOT_SHOW DO_NOT_SHOW - DO_NOT_SHOW DO_NOT_SHOW - SUGGESTION - SUGGESTION - SUGGESTION - DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW DO_NOT_SHOW + False + True + + ExplicitlyExcluded TypeScript16 <?xml version="1.0" encoding="utf-16"?><Profile name="Header"><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile> <?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile> - False SingleQuoted ========================================================================== $FILENAME$ @@ -99,9 +85,9 @@ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> - True True True True True - True \ No newline at end of file + True + \ No newline at end of file diff --git a/media/logo-white.png b/media/logo-white.png new file mode 100644 index 000000000..35960bba6 Binary files /dev/null and b/media/logo-white.png differ diff --git a/media/logo-white.svg b/media/logo-white.svg new file mode 100644 index 000000000..8ed41e69a --- /dev/null +++ b/media/logo-white.svg @@ -0,0 +1,112 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/src/Squidex.Core/ContentEnricher.cs b/src/Squidex.Core/ContentEnricher.cs new file mode 100644 index 000000000..387b13c32 --- /dev/null +++ b/src/Squidex.Core/ContentEnricher.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// ContentEnricher.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json.Linq; +using Squidex.Core.Contents; +using Squidex.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; +using System.Collections.Generic; + +namespace Squidex.Core +{ + public sealed class ContentEnricher + { + private readonly Schema schema; + private readonly HashSet languages; + + public ContentEnricher(HashSet languages, Schema schema) + { + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(languages, nameof(languages)); + + this.schema = schema; + + this.languages = languages; + } + + public void Enrich(ContentData data) + { + Guard.NotNull(data, nameof(data)); + Guard.NotEmpty(languages, nameof(languages)); + + foreach (var field in schema.FieldsByName.Values) + { + var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); + + if (field.RawProperties.IsLocalizable) + { + foreach (var language in languages) + { + Enrich(field, fieldData, language); + } + } + else + { + Enrich(field, fieldData, Language.Invariant); + } + + if (fieldData.Count > 0) + { + data.AddField(field.Name, fieldData); + } + } + } + + private static void Enrich(Field field, ContentFieldData fieldData, Language language) + { + Guard.NotNull(fieldData, nameof(fieldData)); + Guard.NotNull(language, nameof(language)); + + var defaultValue = field.RawProperties.GetDefaultValue(); + + if (field.RawProperties.IsRequired || defaultValue.IsNull()) + { + return; + } + + if (!fieldData.TryGetValue(language.Iso2Code, out JToken value) || value == null || value.Type == JTokenType.Null) + { + fieldData.AddValue(language.Iso2Code, defaultValue); + } + } + } +} diff --git a/src/Squidex.Core/ContentExtensions.cs b/src/Squidex.Core/ContentExtensions.cs new file mode 100644 index 000000000..3decba2bf --- /dev/null +++ b/src/Squidex.Core/ContentExtensions.cs @@ -0,0 +1,52 @@ +// ========================================================================== +// ContentExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Squidex.Core.Contents; +using Squidex.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Core +{ + public static class ContentExtensions + { + public static ContentData Enrich(this ContentData data, Schema schema, HashSet languages) + { + var validator = new ContentEnricher(languages, schema); + + validator.Enrich(data); + + return data; + } + + public static async Task ValidateAsync(this ContentData data, Schema schema, HashSet languages, IList errors) + { + var validator = new ContentValidator(schema, languages); + + await validator.ValidateAsync(data); + + foreach (var error in validator.Errors) + { + errors.Add(error); + } + } + + public static async Task ValidatePartialAsync(this ContentData data, Schema schema, HashSet languages, IList errors) + { + var validator = new ContentValidator(schema, languages); + + await validator.ValidatePartialAsync(data); + + foreach (var error in validator.Errors) + { + errors.Add(error); + } + } + } +} diff --git a/src/Squidex.Core/ContentValidator.cs b/src/Squidex.Core/ContentValidator.cs new file mode 100644 index 000000000..39fc32f2a --- /dev/null +++ b/src/Squidex.Core/ContentValidator.cs @@ -0,0 +1,190 @@ +// ========================================================================== +// ContentValidator.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Squidex.Core.Contents; +using Squidex.Core.Schemas; +using Squidex.Infrastructure; + +namespace Squidex.Core +{ + public sealed class ContentValidator + { + private readonly Schema schema; + private readonly HashSet languages; + private readonly List errors = new List(); + + public ContentValidator(Schema schema, HashSet languages) + { + Guard.NotNull(schema, nameof(schema)); + Guard.NotNull(languages, nameof(languages)); + + this.schema = schema; + + this.languages = languages; + } + + public IReadOnlyList Errors + { + get { return errors; } + } + + public async Task ValidatePartialAsync(ContentData data) + { + Guard.NotNull(data, nameof(data)); + + foreach (var fieldData in data) + { + var fieldName = fieldData.Key; + + if (!schema.FieldsByName.TryGetValue(fieldData.Key, out Field field)) + { + AddError(" is not a known field", fieldName); + } + else + { + if (field.RawProperties.IsLocalizable) + { + await ValidateLocalizableFieldPartialAsync(field, fieldData.Value); + } + else + { + await ValidateNonLocalizableFieldPartialAsync(field, fieldData.Value); + } + } + } + } + + private async Task ValidateLocalizableFieldPartialAsync(Field field, ContentFieldData fieldData) + { + foreach (var languageValue in fieldData) + { + if (!Language.TryGetLanguage(languageValue.Key, out Language language)) + { + AddError($" has an invalid language '{languageValue.Key}'", field); + } + else if (!languages.Contains(language)) + { + AddError($" has an unsupported language '{languageValue.Key}'", field); + } + else + { + await ValidateAsync(field, languageValue.Value, language); + } + } + } + + private async Task ValidateNonLocalizableFieldPartialAsync(Field field, ContentFieldData fieldData) + { + if (fieldData.Keys.Any(x => x != Language.Invariant.Iso2Code)) + { + AddError($" can only contain a single entry for invariant language ({Language.Invariant.Iso2Code})", field); + } + + if (fieldData.TryGetValue(Language.Invariant.Iso2Code, out JToken value)) + { + await ValidateAsync(field, value); + } + } + + public async Task ValidateAsync(ContentData data) + { + Guard.NotNull(data, nameof(data)); + + ValidateUnknownFields(data); + + foreach (var field in schema.FieldsByName.Values) + { + var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); + + if (field.RawProperties.IsLocalizable) + { + await ValidateLocalizableFieldAsync(field, fieldData); + } + else + { + await ValidateNonLocalizableField(field, fieldData); + } + } + } + + private void ValidateUnknownFields(ContentData data) + { + foreach (var fieldData in data) + { + if (!schema.FieldsByName.ContainsKey(fieldData.Key)) + { + AddError(" is not a known field", fieldData.Key); + } + } + } + + private async Task ValidateLocalizableFieldAsync(Field field, ContentFieldData fieldData) + { + foreach (var valueLanguage in fieldData.Keys) + { + if (!Language.TryGetLanguage(valueLanguage, out Language language)) + { + AddError($" has an invalid language '{valueLanguage}'", field); + } + else if (!languages.Contains(language)) + { + AddError($" has an unsupported language '{valueLanguage}'", field); + } + } + + foreach (var language in languages) + { + var value = fieldData.GetOrCreate(language.Iso2Code, k => JValue.CreateNull()); + + await ValidateAsync(field, value, language); + } + } + + private async Task ValidateNonLocalizableField(Field field, ContentFieldData fieldData) + { + if (fieldData.Keys.Any(x => x != Language.Invariant.Iso2Code)) + { + AddError($" can only contain a single entry for invariant language ({Language.Invariant.Iso2Code})", field); + } + + var value = fieldData.GetOrCreate(Language.Invariant.Iso2Code, k => JValue.CreateNull()); + + await ValidateAsync(field, value); + } + + private Task ValidateAsync(Field field, JToken value, Language language = null) + { + return field.ValidateAsync(value, m => AddError(m, field, language)); + } + + private void AddError(string message, Field field, Language language = null) + { + var displayName = !string.IsNullOrWhiteSpace(field.RawProperties.Label) ? field.RawProperties.Label : field.Name; + + if (language != null) + { + displayName += $" ({language.Iso2Code})"; + } + + message = message.Replace("", displayName); + + errors.Add(new ValidationError(message, field.Name)); + } + + private void AddError(string message, string fieldName) + { + message = message.Replace("", fieldName); + + errors.Add(new ValidationError(message, fieldName)); + } + } +} diff --git a/src/Squidex.Core/Contents/ContentData.cs b/src/Squidex.Core/Contents/ContentData.cs index 2766b6251..283dd52a4 100644 --- a/src/Squidex.Core/Contents/ContentData.cs +++ b/src/Squidex.Core/Contents/ContentData.cs @@ -62,6 +62,28 @@ namespace Squidex.Core.Contents return result; } + public ContentData ToCleaned() + { + var result = new ContentData(); + + foreach (var fieldValue in this.Where(x => x.Value != null)) + { + var resultValue = new ContentFieldData(); + + foreach (var languageValue in fieldValue.Value.Where(x => x.Value != null && x.Value.Type != JTokenType.Null)) + { + resultValue[languageValue.Key] = languageValue.Value; + } + + if (resultValue.Count > 0) + { + result[fieldValue.Key] = resultValue; + } + } + + return result; + } + public ContentData ToIdModel(Schema schema) { Guard.NotNull(schema, nameof(schema)); @@ -130,6 +152,10 @@ namespace Squidex.Core.Contents { fieldResult.Add(languageCode, value); } + else if (language.Equals(masterLanguage) && fieldValues.TryGetValue(invariantCode, out value)) + { + fieldResult.Add(languageCode, value); + } } } else diff --git a/src/Squidex.Core/FieldExtensions.cs b/src/Squidex.Core/FieldExtensions.cs new file mode 100644 index 000000000..1128a2190 --- /dev/null +++ b/src/Squidex.Core/FieldExtensions.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// FieldExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Squidex.Core.Schemas; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Json; + +namespace Squidex.Core +{ + public static class FieldExtensions + { + public static async Task ValidateAsync(this Field field, JToken value, Action addError) + { + Guard.NotNull(value, nameof(value)); + + try + { + var typedValue = value.IsNull() ? null : field.ConvertValue(value); + + foreach (var validator in field.Validators) + { + await validator.ValidateAsync(typedValue, addError); + } + } + catch + { + addError(" is not a valid value"); + } + } + } +} diff --git a/src/Squidex.Core/Identity/SquidexClaimTypes.cs b/src/Squidex.Core/Identity/SquidexClaimTypes.cs index bbdb0877a..c5b77417e 100644 --- a/src/Squidex.Core/Identity/SquidexClaimTypes.cs +++ b/src/Squidex.Core/Identity/SquidexClaimTypes.cs @@ -8,7 +8,7 @@ namespace Squidex.Core.Identity { - public class SquidexClaimTypes + public static class SquidexClaimTypes { public static readonly string SquidexDisplayName = "urn:squidex:name"; diff --git a/src/Squidex.Core/Schemas/BooleanField.cs b/src/Squidex.Core/Schemas/BooleanField.cs index 5944b4a72..428be63ce 100644 --- a/src/Squidex.Core/Schemas/BooleanField.cs +++ b/src/Squidex.Core/Schemas/BooleanField.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using Microsoft.OData.Edm; using Microsoft.OData.Edm.Library; @@ -30,12 +31,12 @@ namespace Squidex.Core.Schemas } } - protected override object ConvertValue(JToken value) + public override object ConvertValue(JToken value) { return (bool?)value; } - protected override void PrepareJsonSchema(JsonProperty jsonProperty) + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) { jsonProperty.Type = JsonObjectType.Boolean; } diff --git a/src/Squidex.Core/Schemas/BooleanFieldEditor.cs b/src/Squidex.Core/Schemas/BooleanFieldEditor.cs index 15105e5c7..fd6d7fe5f 100644 --- a/src/Squidex.Core/Schemas/BooleanFieldEditor.cs +++ b/src/Squidex.Core/Schemas/BooleanFieldEditor.cs @@ -10,6 +10,7 @@ namespace Squidex.Core.Schemas { public enum BooleanFieldEditor { - Checkbox + Checkbox, + Toggle } } diff --git a/src/Squidex.Core/Schemas/Cloneable.cs b/src/Squidex.Core/Schemas/CloneableBase.cs similarity index 89% rename from src/Squidex.Core/Schemas/Cloneable.cs rename to src/Squidex.Core/Schemas/CloneableBase.cs index 98908d4c6..6b1eecefe 100644 --- a/src/Squidex.Core/Schemas/Cloneable.cs +++ b/src/Squidex.Core/Schemas/CloneableBase.cs @@ -1,5 +1,5 @@ // ========================================================================== -// Cloneable.cs +// CloneableBase.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -10,9 +10,9 @@ using System; namespace Squidex.Core.Schemas { - public abstract class Cloneable + public abstract class CloneableBase { - protected T Clone(Action updater) where T : Cloneable + protected T Clone(Action updater) where T : CloneableBase { var clone = (T)MemberwiseClone(); diff --git a/src/Squidex.Core/Schemas/DateTimeField.cs b/src/Squidex.Core/Schemas/DateTimeField.cs index 7670cfd66..af3ed662d 100644 --- a/src/Squidex.Core/Schemas/DateTimeField.cs +++ b/src/Squidex.Core/Schemas/DateTimeField.cs @@ -17,6 +17,7 @@ using NodaTime.Text; using Squidex.Core.Schemas.Validators; using Squidex.Infrastructure; +// ReSharper disable InvertIf // ReSharper disable ConvertIfStatementToConditionalTernaryExpression // ReSharper disable ConvertIfStatementToSwitchStatement @@ -43,7 +44,7 @@ namespace Squidex.Core.Schemas } } - protected override object ConvertValue(JToken value) + public override object ConvertValue(JToken value) { if (value.Type == JTokenType.String) { @@ -57,15 +58,10 @@ namespace Squidex.Core.Schemas return parseResult.Value; } - if (value.Type == JTokenType.Null) - { - return null; - } - throw new InvalidCastException("Invalid json type, expected string."); } - protected override void PrepareJsonSchema(JsonProperty jsonProperty) + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) { jsonProperty.Type = JsonObjectType.String; diff --git a/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs b/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs index eefcca87d..189911847 100644 --- a/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs +++ b/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs @@ -67,7 +67,7 @@ namespace Squidex.Core.Schemas public override JToken GetDefaultValue() { - return DefaultValue != null ? DefaultValue.ToString() : null; + return DefaultValue?.ToString(); } protected override IEnumerable ValidateCore() diff --git a/src/Squidex.Core/Schemas/Field.cs b/src/Squidex.Core/Schemas/Field.cs index 5f5636b94..60d6d2b2a 100644 --- a/src/Squidex.Core/Schemas/Field.cs +++ b/src/Squidex.Core/Schemas/Field.cs @@ -8,12 +8,11 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; using Microsoft.OData.Edm; using Microsoft.OData.Edm.Library; using Newtonsoft.Json.Linq; using NJsonSchema; -using Squidex.Core.Contents; +using Squidex.Core.Schemas.Validators; using Squidex.Infrastructure; // ReSharper disable InvertIf @@ -22,7 +21,7 @@ using Squidex.Infrastructure; namespace Squidex.Core.Schemas { - public abstract class Field : Cloneable + public abstract class Field : CloneableBase { private readonly Lazy> validators; private readonly long id; @@ -50,6 +49,11 @@ namespace Squidex.Core.Schemas get { return isDisabled; } } + public IReadOnlyList Validators + { + get { return validators.Value; } + } + public abstract FieldProperties RawProperties { get; } protected Field(long id, string name) @@ -66,53 +70,7 @@ namespace Squidex.Core.Schemas public abstract Field Update(FieldProperties newProperties); - public void Enrich(ContentFieldData fieldData, Language language) - { - Guard.NotNull(fieldData, nameof(fieldData)); - Guard.NotNull(language, nameof(language)); - - var defaultValue = RawProperties.GetDefaultValue(); - - if (!RawProperties.IsRequired && defaultValue != null && fieldData.GetOrDefault(language.Iso2Code) == null) - { - fieldData.AddValue(language.Iso2Code, defaultValue); - } - } - - public async Task ValidateAsync(JToken value, ICollection errors, Language language = null) - { - Guard.NotNull(value, nameof(value)); - - var rawErrors = new List(); - try - { - var typedValue = value.Type == JTokenType.Null ? null : ConvertValue(value); - - foreach (var validator in validators.Value) - { - await validator.ValidateAsync(typedValue, rawErrors); - } - } - catch - { - rawErrors.Add(" is not a valid value"); - } - - if (rawErrors.Count > 0) - { - var displayName = !string.IsNullOrWhiteSpace(RawProperties.Label) ? RawProperties.Label : name; - - if (language != null) - { - displayName += $" ({language.Iso2Code})"; - } - - foreach (var error in rawErrors) - { - errors.Add(error.Replace("", displayName)); - } - } - } + public abstract object ConvertValue(JToken value); public Field Hide() { @@ -174,7 +132,7 @@ namespace Squidex.Core.Schemas edmType.AddStructuralProperty(Name, new EdmComplexTypeReference(languageType, false)); } - public void AddToSchema(JsonSchema4 schema, IEnumerable languages, string schemaName, Func schemaResolver) + public void AddToJsonSchema(JsonSchema4 schema, IEnumerable languages, string schemaName, Func schemaResolver) { Guard.NotNull(schema, nameof(schema)); Guard.NotNull(languages, nameof(languages)); @@ -190,14 +148,14 @@ namespace Squidex.Core.Schemas foreach (var language in languages) { - var languageProperty = new JsonProperty { Description = language.EnglishName }; + var languageProperty = new JsonProperty { Description = language.EnglishName, IsRequired = RawProperties.IsRequired }; - PrepareJsonSchema(languageProperty); + PrepareJsonSchema(languageProperty, schemaResolver); languagesObject.Properties.Add(language.Iso2Code, languageProperty); } - languagesProperty.AllOf.Add(schemaResolver($"{schemaName}{Name.ToPascalCase()}Property", languagesObject)); + languagesProperty.SchemaReference = schemaResolver($"{schemaName}{Name.ToPascalCase()}Property", languagesObject); schema.Properties.Add(Name, languagesProperty); } @@ -206,9 +164,9 @@ namespace Squidex.Core.Schemas { var jsonProperty = new JsonProperty { IsRequired = RawProperties.IsRequired, Type = JsonObjectType.Object }; - if (!string.IsNullOrWhiteSpace(RawProperties.Label)) + if (!string.IsNullOrWhiteSpace(RawProperties.Hints)) { - jsonProperty.Description = RawProperties.Label; + jsonProperty.Description = RawProperties.Hints; } else { @@ -227,8 +185,6 @@ namespace Squidex.Core.Schemas protected abstract IEdmTypeReference CreateEdmType(); - protected abstract void PrepareJsonSchema(JsonProperty jsonProperty); - - protected abstract object ConvertValue(JToken value); + protected abstract void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver); } } \ No newline at end of file diff --git a/src/Squidex.Core/Schemas/FieldProperties.cs b/src/Squidex.Core/Schemas/FieldProperties.cs index 0fbbea7f1..42413b2be 100644 --- a/src/Squidex.Core/Schemas/FieldProperties.cs +++ b/src/Squidex.Core/Schemas/FieldProperties.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure; namespace Squidex.Core.Schemas { - public abstract class FieldProperties : NamedElementProperties, IValidatable + public abstract class FieldProperties : NamedElementPropertiesBase, IValidatable { private bool isRequired; private bool isLocalizable; diff --git a/src/Squidex.Core/Schemas/FieldRegistry.cs b/src/Squidex.Core/Schemas/FieldRegistry.cs index 51613499a..6b4d00d60 100644 --- a/src/Squidex.Core/Schemas/FieldRegistry.cs +++ b/src/Squidex.Core/Schemas/FieldRegistry.cs @@ -70,9 +70,12 @@ namespace Squidex.Core.Schemas Add( (id, name, p) => new JsonField(id, name, (JsonFieldProperties)p)); + + Add( + (id, name, p) => new GeolocationField(id, name, (GeolocationFieldProperties)p)); } - public void Add(FactoryFunction fieldFactory) + private void Add(FactoryFunction fieldFactory) { Guard.NotNull(fieldFactory, nameof(fieldFactory)); diff --git a/src/Squidex.Core/Schemas/GeolocationField.cs b/src/Squidex.Core/Schemas/GeolocationField.cs new file mode 100644 index 000000000..4e79d8df6 --- /dev/null +++ b/src/Squidex.Core/Schemas/GeolocationField.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// GeolocationField.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Microsoft.OData.Edm; +using Newtonsoft.Json.Linq; +using NJsonSchema; +using Squidex.Core.Schemas.Validators; +using Squidex.Infrastructure; + +namespace Squidex.Core.Schemas +{ + public sealed class GeolocationField : Field + { + public GeolocationField(long id, string name, GeolocationFieldProperties properties) + : base(id, name, properties) + { + } + + protected override IEnumerable CreateValidators() + { + if (Properties.IsRequired) + { + yield return new RequiredValidator(); + } + } + + public override object ConvertValue(JToken value) + { + var geolocation = (JObject)value; + + foreach (var property in geolocation.Properties()) + { + if (!string.Equals(property.Name, "latitude", StringComparison.OrdinalIgnoreCase) && + !string.Equals(property.Name, "longitude", StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidCastException("Geolocation can only have latitude and longitude property."); + } + } + + var lat = (double)geolocation["latitude"]; + var lon = (double)geolocation["longitude"]; + + Guard.Between(lat, -90, 90, "latitude"); + Guard.Between(lon, -180, 180, "longitude"); + + return value; + } + + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) + { + jsonProperty.Type = JsonObjectType.Object; + + var geolocationSchema = new JsonSchema4(); + + 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.AllowAdditionalProperties = false; + + var schemaReference = schemaResolver("GeolocationDto", geolocationSchema); + + jsonProperty.SchemaReference = schemaReference; + } + + protected override IEdmTypeReference CreateEdmType() + { + return null; + } + } +} diff --git a/src/Squidex.Core/Schemas/GeolocationFieldEditor.cs b/src/Squidex.Core/Schemas/GeolocationFieldEditor.cs new file mode 100644 index 000000000..dd2a347a6 --- /dev/null +++ b/src/Squidex.Core/Schemas/GeolocationFieldEditor.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// GeolocationFieldEditor.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Core.Schemas +{ + public enum GeolocationFieldEditor + { + Map + } +} diff --git a/src/Squidex.Core/Schemas/GeolocationFieldProperties.cs b/src/Squidex.Core/Schemas/GeolocationFieldProperties.cs new file mode 100644 index 000000000..ffb4cbca4 --- /dev/null +++ b/src/Squidex.Core/Schemas/GeolocationFieldProperties.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// GeolocationFieldProperties.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using Squidex.Infrastructure; + +namespace Squidex.Core.Schemas +{ + [TypeName("GeolocationField")] + public sealed class GeolocationFieldProperties : FieldProperties + { + private GeolocationFieldEditor editor; + + public GeolocationFieldEditor Editor + { + get { return editor; } + set + { + ThrowIfFrozen(); + + editor = value; + } + } + + public override JToken GetDefaultValue() + { + return null; + } + + protected override IEnumerable ValidateCore() + { + if (!Editor.IsEnumValue()) + { + yield return new ValidationError("Editor ist not a valid value", nameof(Editor)); + } + } + } +} diff --git a/src/Squidex.Core/Schemas/JsonField.cs b/src/Squidex.Core/Schemas/JsonField.cs index f12703c9b..ad9dbab35 100644 --- a/src/Squidex.Core/Schemas/JsonField.cs +++ b/src/Squidex.Core/Schemas/JsonField.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using Microsoft.OData.Edm; using Newtonsoft.Json.Linq; @@ -29,12 +30,12 @@ namespace Squidex.Core.Schemas } } - protected override object ConvertValue(JToken value) + public override object ConvertValue(JToken value) { return value; } - protected override void PrepareJsonSchema(JsonProperty jsonProperty) + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) { jsonProperty.Type = JsonObjectType.Object; } diff --git a/src/Squidex.Core/Schemas/NamedElementProperties.cs b/src/Squidex.Core/Schemas/NamedElementPropertiesBase.cs similarity index 92% rename from src/Squidex.Core/Schemas/NamedElementProperties.cs rename to src/Squidex.Core/Schemas/NamedElementPropertiesBase.cs index 11a9b52aa..ac5d9cf2e 100644 --- a/src/Squidex.Core/Schemas/NamedElementProperties.cs +++ b/src/Squidex.Core/Schemas/NamedElementPropertiesBase.cs @@ -1,5 +1,5 @@ // ========================================================================== -// NamedElementProperties.cs +// NamedElementPropertiesBase.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -10,7 +10,7 @@ using System; namespace Squidex.Core.Schemas { - public abstract class NamedElementProperties + public abstract class NamedElementPropertiesBase { private string label; private string hints; diff --git a/src/Squidex.Core/Schemas/NumberField.cs b/src/Squidex.Core/Schemas/NumberField.cs index aeae44b63..dd2579664 100644 --- a/src/Squidex.Core/Schemas/NumberField.cs +++ b/src/Squidex.Core/Schemas/NumberField.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Linq; using Microsoft.OData.Edm; @@ -41,7 +42,7 @@ namespace Squidex.Core.Schemas } } - protected override void PrepareJsonSchema(JsonProperty jsonProperty) + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) { jsonProperty.Type = JsonObjectType.Number; @@ -61,7 +62,7 @@ namespace Squidex.Core.Schemas return EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.Double, !Properties.IsRequired); } - protected override object ConvertValue(JToken value) + public override object ConvertValue(JToken value) { return (double?)value; } diff --git a/src/Squidex.Core/Schemas/NumberFieldEditor.cs b/src/Squidex.Core/Schemas/NumberFieldEditor.cs index 8e54de235..5ab5deed9 100644 --- a/src/Squidex.Core/Schemas/NumberFieldEditor.cs +++ b/src/Squidex.Core/Schemas/NumberFieldEditor.cs @@ -12,6 +12,7 @@ namespace Squidex.Core.Schemas { Input, Radio, - Dropdown + Dropdown, + Stars } } diff --git a/src/Squidex.Core/Schemas/Schema.cs b/src/Squidex.Core/Schemas/Schema.cs index dba1f78b9..9a3ac2b26 100644 --- a/src/Squidex.Core/Schemas/Schema.cs +++ b/src/Squidex.Core/Schemas/Schema.cs @@ -10,11 +10,8 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using System.Threading.Tasks; using Microsoft.OData.Edm.Library; -using Newtonsoft.Json.Linq; using NJsonSchema; -using Squidex.Core.Contents; using Squidex.Infrastructure; // ReSharper disable ConvertIfStatementToConditionalTernaryExpression @@ -22,7 +19,7 @@ using Squidex.Infrastructure; namespace Squidex.Core.Schemas { - public sealed class Schema : Cloneable + public sealed class Schema : CloneableBase { private readonly string name; private readonly SchemaProperties properties; @@ -189,177 +186,21 @@ namespace Squidex.Core.Schemas return edmType; } - public JsonSchema4 BuildSchema(HashSet languages, Func schemaResolver) + public JsonSchema4 BuildJsonSchema(HashSet languages, Func schemaResolver) { Guard.NotEmpty(languages, nameof(languages)); Guard.NotNull(schemaResolver, nameof(schemaResolver)); var schemaName = Name.ToPascalCase(); - var schema = new JsonSchema4 { Id = schemaName, Type = JsonObjectType.Object }; + var schema = new JsonSchema4 { Type = JsonObjectType.Object }; foreach (var field in fieldsByName.Values.Where(x => !x.IsHidden)) { - field.AddToSchema(schema, languages, schemaName, schemaResolver); + field.AddToJsonSchema(schema, languages, schemaName, schemaResolver); } return schema; } - - public async Task ValidatePartialAsync(ContentData data, IList errors, HashSet languages) - { - Guard.NotNull(data, nameof(data)); - Guard.NotNull(errors, nameof(errors)); - - foreach (var fieldData in data) - { - if (!fieldsByName.TryGetValue(fieldData.Key, out Field field)) - { - errors.Add(new ValidationError($"{fieldData.Key} is not a known field", fieldData.Key)); - } - else - { - var fieldErrors = new List(); - - if (field.RawProperties.IsLocalizable) - { - foreach (var languageValue in fieldData.Value) - { - if (!Language.TryGetLanguage(languageValue.Key, out Language language)) - { - fieldErrors.Add($"{field.Name} has an invalid language '{languageValue.Key}'"); - } - else if (!languages.Contains(language)) - { - fieldErrors.Add($"{field.Name} has an unsupported language '{languageValue.Key}'"); - } - else - { - await field.ValidateAsync(languageValue.Value, fieldErrors, language); - } - } - } - else - { - if (fieldData.Value.Keys.Any(x => x != Language.Invariant.Iso2Code)) - { - fieldErrors.Add($"{field.Name} can only contain a single entry for invariant language ({Language.Invariant.Iso2Code})"); - } - - if (fieldData.Value.TryGetValue(Language.Invariant.Iso2Code, out JToken value)) - { - await field.ValidateAsync(value, fieldErrors); - } - } - - foreach (var error in fieldErrors) - { - errors.Add(new ValidationError(error, field.Name)); - } - } - } - } - - public async Task ValidateAsync(ContentData data, IList errors, HashSet languages) - { - Guard.NotNull(data, nameof(data)); - Guard.NotNull(errors, nameof(errors)); - Guard.NotEmpty(languages, nameof(languages)); - - ValidateUnknownFields(data, errors); - - foreach (var field in fieldsByName.Values) - { - var fieldErrors = new List(); - var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); - - if (field.RawProperties.IsLocalizable) - { - await ValidateLocalizableFieldAsync(languages, fieldData, fieldErrors, field); - } - else - { - await ValidateNonLocalizableField(fieldData, fieldErrors, field); - } - - foreach (var error in fieldErrors) - { - errors.Add(new ValidationError(error, field.Name)); - } - } - } - - private void ValidateUnknownFields(ContentData data, IList errors) - { - foreach (var fieldData in data) - { - if (!fieldsByName.ContainsKey(fieldData.Key)) - { - errors.Add(new ValidationError($"{fieldData.Key} is not a known field", fieldData.Key)); - } - } - } - - private static async Task ValidateLocalizableFieldAsync(HashSet languages, ContentFieldData fieldData, List fieldErrors, Field field) - { - foreach (var valueLanguage in fieldData.Keys) - { - if (!Language.TryGetLanguage(valueLanguage, out Language language)) - { - fieldErrors.Add($"{field.Name} has an invalid language '{valueLanguage}'"); - } - else if (!languages.Contains(language)) - { - fieldErrors.Add($"{field.Name} has an unsupported language '{valueLanguage}'"); - } - } - - foreach (var language in languages) - { - var value = fieldData.GetOrCreate(language.Iso2Code, k => JValue.CreateNull()); - - await field.ValidateAsync(value, fieldErrors, language); - } - } - - private static async Task ValidateNonLocalizableField(ContentFieldData fieldData, List fieldErrors, Field field) - { - if (fieldData.Keys.Any(x => x != Language.Invariant.Iso2Code)) - { - fieldErrors.Add($"{field.Name} can only contain a single entry for invariant language ({Language.Invariant.Iso2Code})"); - } - - var value = fieldData.GetOrCreate(Language.Invariant.Iso2Code, k => JValue.CreateNull()); - - await field.ValidateAsync(value, fieldErrors); - } - - public void Enrich(ContentData data, HashSet languages) - { - Guard.NotNull(data, nameof(data)); - Guard.NotEmpty(languages, nameof(languages)); - - foreach (var field in fieldsByName.Values) - { - var fieldData = data.GetOrCreate(field.Name, k => new ContentFieldData()); - - if (field.RawProperties.IsLocalizable) - { - foreach (var language in languages) - { - field.Enrich(fieldData, language); - } - } - else - { - field.Enrich(fieldData, Language.Invariant); - } - - if (fieldData.Count > 0) - { - data.AddField(field.Name, fieldData); - } - } - } } } \ No newline at end of file diff --git a/src/Squidex.Core/Schemas/SchemaProperties.cs b/src/Squidex.Core/Schemas/SchemaProperties.cs index 25ba80c67..16e3a552f 100644 --- a/src/Squidex.Core/Schemas/SchemaProperties.cs +++ b/src/Squidex.Core/Schemas/SchemaProperties.cs @@ -8,7 +8,7 @@ namespace Squidex.Core.Schemas { - public sealed class SchemaProperties : NamedElementProperties + public sealed class SchemaProperties : NamedElementPropertiesBase { } } \ No newline at end of file diff --git a/src/Squidex.Core/Schemas/StringField.cs b/src/Squidex.Core/Schemas/StringField.cs index 5114bf439..cbbd99307 100644 --- a/src/Squidex.Core/Schemas/StringField.cs +++ b/src/Squidex.Core/Schemas/StringField.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -47,7 +48,7 @@ namespace Squidex.Core.Schemas } } - protected override void PrepareJsonSchema(JsonProperty jsonProperty) + protected override void PrepareJsonSchema(JsonProperty jsonProperty, Func schemaResolver) { jsonProperty.Type = JsonObjectType.String; @@ -65,7 +66,7 @@ namespace Squidex.Core.Schemas return EdmCoreModel.Instance.GetPrimitive(EdmPrimitiveTypeKind.String, !Properties.IsRequired); } - protected override object ConvertValue(JToken value) + public override object ConvertValue(JToken value) { return value.ToString(); } diff --git a/src/Squidex.Core/Schemas/StringFieldEditor.cs b/src/Squidex.Core/Schemas/StringFieldEditor.cs index fb92c7f71..6a222e401 100644 --- a/src/Squidex.Core/Schemas/StringFieldEditor.cs +++ b/src/Squidex.Core/Schemas/StringFieldEditor.cs @@ -10,9 +10,9 @@ namespace Squidex.Core.Schemas { public enum StringFieldEditor { - Dropdown, Input, Markdown, + Dropdown, Radio, RichText, TextArea diff --git a/src/Squidex.Core/Schemas/Validators/AllowedValuesValidator.cs b/src/Squidex.Core/Schemas/Validators/AllowedValuesValidator.cs index 1179d56da..0a7402e32 100644 --- a/src/Squidex.Core/Schemas/Validators/AllowedValuesValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/AllowedValuesValidator.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; +using System; using System.Linq; using System.Threading.Tasks; using Squidex.Infrastructure; @@ -25,7 +25,7 @@ namespace Squidex.Core.Schemas.Validators this.allowedValues = allowedValues; } - public Task ValidateAsync(object value, ICollection errors) + public Task ValidateAsync(object value, Action addError) { if (value == null) { @@ -36,7 +36,7 @@ namespace Squidex.Core.Schemas.Validators if (!allowedValues.Contains(typedValue)) { - errors.Add(" is not an allowed value"); + addError(" is not an allowed value"); } return TaskHelper.Done; diff --git a/src/Squidex.Core/Schemas/IValidator.cs b/src/Squidex.Core/Schemas/Validators/IValidator.cs similarity index 75% rename from src/Squidex.Core/Schemas/IValidator.cs rename to src/Squidex.Core/Schemas/Validators/IValidator.cs index 405c0bd01..a604e9a70 100644 --- a/src/Squidex.Core/Schemas/IValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/IValidator.cs @@ -6,13 +6,13 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; +using System; using System.Threading.Tasks; -namespace Squidex.Core.Schemas +namespace Squidex.Core.Schemas.Validators { public interface IValidator { - Task ValidateAsync(object value, ICollection errors); + Task ValidateAsync(object value, Action addError); } } diff --git a/src/Squidex.Core/Schemas/Validators/PatternValidator.cs b/src/Squidex.Core/Schemas/Validators/PatternValidator.cs index 45401d8f7..aa806be54 100644 --- a/src/Squidex.Core/Schemas/Validators/PatternValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/PatternValidator.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; +using System; using System.Text.RegularExpressions; using System.Threading.Tasks; using Squidex.Infrastructure.Tasks; @@ -28,24 +28,20 @@ namespace Squidex.Core.Schemas.Validators regex = new Regex("^" + pattern + "$"); } - public Task ValidateAsync(object value, ICollection errors) + public Task ValidateAsync(object value, Action addError) { - var stringValue = value as string; - - if (stringValue == null) - { - return TaskHelper.Done; - } - - if (!regex.IsMatch(stringValue)) + if (value is string stringValue) { - if (string.IsNullOrWhiteSpace(errorMessage)) - { - errors.Add(" is not valid"); - } - else + if (!regex.IsMatch(stringValue)) { - errors.Add(errorMessage); + if (string.IsNullOrWhiteSpace(errorMessage)) + { + addError(" is not valid"); + } + else + { + addError(errorMessage); + } } } diff --git a/src/Squidex.Core/Schemas/Validators/RangeValidator.cs b/src/Squidex.Core/Schemas/Validators/RangeValidator.cs index 2c512b02c..0b5669228 100644 --- a/src/Squidex.Core/Schemas/Validators/RangeValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/RangeValidator.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure.Tasks; @@ -29,7 +28,7 @@ namespace Squidex.Core.Schemas.Validators this.max = max; } - public Task ValidateAsync(object value, ICollection errors) + public Task ValidateAsync(object value, Action addError) { if (value == null) { @@ -40,12 +39,12 @@ namespace Squidex.Core.Schemas.Validators if (min.HasValue && typedValue.CompareTo(min.Value) < 0) { - errors.Add($" must be greater than '{min}'"); + addError($" must be greater than '{min}'"); } if (max.HasValue && typedValue.CompareTo(max.Value) > 0) { - errors.Add($" must be less than '{max}'"); + addError($" must be less than '{max}'"); } return TaskHelper.Done; diff --git a/src/Squidex.Core/Schemas/Validators/RequiredStringValidator.cs b/src/Squidex.Core/Schemas/Validators/RequiredStringValidator.cs index ca3941f2a..f64d61c9a 100644 --- a/src/Squidex.Core/Schemas/Validators/RequiredStringValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/RequiredStringValidator.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; +using System; using System.Threading.Tasks; using Squidex.Infrastructure.Tasks; @@ -21,7 +21,7 @@ namespace Squidex.Core.Schemas.Validators this.validateEmptyStrings = validateEmptyStrings; } - public Task ValidateAsync(object value, ICollection errors) + public Task ValidateAsync(object value, Action addError) { if (value != null && !(value is string)) { @@ -32,7 +32,7 @@ namespace Squidex.Core.Schemas.Validators if (valueAsString == null || (validateEmptyStrings && string.IsNullOrWhiteSpace(valueAsString))) { - errors.Add(" is required"); + addError(" is required"); } return TaskHelper.Done; diff --git a/src/Squidex.Core/Schemas/Validators/RequiredValidator.cs b/src/Squidex.Core/Schemas/Validators/RequiredValidator.cs index 1938726d9..5e86ad0cb 100644 --- a/src/Squidex.Core/Schemas/Validators/RequiredValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/RequiredValidator.cs @@ -6,7 +6,7 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; +using System; using System.Threading.Tasks; using Squidex.Infrastructure.Tasks; @@ -14,11 +14,11 @@ namespace Squidex.Core.Schemas.Validators { public class RequiredValidator : IValidator { - public Task ValidateAsync(object value, ICollection errors) + public Task ValidateAsync(object value, Action addError) { if (value == null) { - errors.Add(" is required"); + addError(" is required"); } return TaskHelper.Done; diff --git a/src/Squidex.Core/Schemas/Validators/StringLengthValidator.cs b/src/Squidex.Core/Schemas/Validators/StringLengthValidator.cs index b1e01fb61..73360646c 100644 --- a/src/Squidex.Core/Schemas/Validators/StringLengthValidator.cs +++ b/src/Squidex.Core/Schemas/Validators/StringLengthValidator.cs @@ -7,10 +7,11 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure.Tasks; +// ReSharper disable InvertIf + namespace Squidex.Core.Schemas.Validators { public class StringLengthValidator : IValidator @@ -29,23 +30,19 @@ namespace Squidex.Core.Schemas.Validators this.maxLength = maxLength; } - public Task ValidateAsync(object value, ICollection errors) + public Task ValidateAsync(object value, Action addError) { - var stringValue = value as string; - - if (stringValue == null) - { - return TaskHelper.Done; - } - - if (minLength.HasValue && stringValue.Length < minLength.Value) - { - errors.Add($" must have more than '{minLength}' characters"); - } - - if (maxLength.HasValue && stringValue.Length > maxLength.Value) + if (value is string stringValue) { - errors.Add($" must have less than '{maxLength}' characters"); + if (minLength.HasValue && stringValue.Length < minLength.Value) + { + addError($" must have more than '{minLength}' characters"); + } + + if (maxLength.HasValue && stringValue.Length > maxLength.Value) + { + addError($" must have less than '{maxLength}' characters"); + } } return TaskHelper.Done; diff --git a/src/Squidex.Core/Squidex.Core.csproj b/src/Squidex.Core/Squidex.Core.csproj index 8080fb0ce..05847ae26 100644 --- a/src/Squidex.Core/Squidex.Core.csproj +++ b/src/Squidex.Core/Squidex.Core.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Squidex.Events/.gitignore b/src/Squidex.Events/.gitignore deleted file mode 100644 index 0ca27f04e..000000000 --- a/src/Squidex.Events/.gitignore +++ /dev/null @@ -1,234 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Microsoft Azure ApplicationInsights config file -ApplicationInsights.config - -# Windows Store app package directory -AppPackages/ -BundleArtifacts/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe - -# FAKE - F# Make -.fake/ diff --git a/src/Squidex.Events/Schemas/Utils/SchemaEventDispatcher.cs b/src/Squidex.Events/Schemas/Utils/SchemaEventDispatcher.cs index 20b3f1b3e..c66e8caac 100644 --- a/src/Squidex.Events/Schemas/Utils/SchemaEventDispatcher.cs +++ b/src/Squidex.Events/Schemas/Utils/SchemaEventDispatcher.cs @@ -12,7 +12,7 @@ using Squidex.Core.Schemas; namespace Squidex.Events.Schemas.Utils { - public class SchemaEventDispatcher + public static class SchemaEventDispatcher { public static Schema Dispatch(SchemaCreated @event) { diff --git a/src/Squidex.Events/SquidexEvent.cs b/src/Squidex.Events/SquidexEvent.cs index e84658bef..3b121958e 100644 --- a/src/Squidex.Events/SquidexEvent.cs +++ b/src/Squidex.Events/SquidexEvent.cs @@ -1,5 +1,5 @@ // ========================================================================== -// SquidexEvent.cs +// SquidexEventBase.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group diff --git a/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs index d51cfc9c7..b7d3ff079 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs @@ -69,15 +69,17 @@ namespace Squidex.Infrastructure.MongoDb.EventStore { await Collection.Find(x => x.EventStream == streamName).ForEachAsync(commit => { - var position = commit.EventStreamOffset; + var eventNumber = commit.EventsOffset; + var eventStreamNumber = commit.EventStreamOffset; foreach (var @event in commit.Events) { - var eventData = SimpleMapper.Map(@event, new EventData()); + eventNumber++; + eventStreamNumber++; - observer.OnNext(new StoredEvent(position, eventData)); + var eventData = SimpleMapper.Map(@event, new EventData()); - position++; + observer.OnNext(new StoredEvent(eventNumber, eventStreamNumber, eventData)); } }, ct); }); @@ -92,16 +94,18 @@ namespace Squidex.Infrastructure.MongoDb.EventStore await Collection.Find(x => x.EventsOffset >= commitOffset).SortBy(x => x.EventsOffset).ForEachAsync(commit => { var eventNumber = commit.EventsOffset; + var eventStreamNumber = commit.EventStreamOffset; foreach (var @event in commit.Events) { eventNumber++; + eventStreamNumber++; if (eventNumber > lastReceivedEventNumber) { var eventData = SimpleMapper.Map(@event, new EventData()); - observer.OnNext(new StoredEvent(eventNumber, eventData)); + observer.OnNext(new StoredEvent(eventNumber, eventStreamNumber, eventData)); } } }, ct); diff --git a/src/Squidex.Infrastructure.MongoDb/InstantSerializer.cs b/src/Squidex.Infrastructure.MongoDb/InstantSerializer.cs index bc9046d87..c5de2f735 100644 --- a/src/Squidex.Infrastructure.MongoDb/InstantSerializer.cs +++ b/src/Squidex.Infrastructure.MongoDb/InstantSerializer.cs @@ -9,6 +9,7 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; using NodaTime; +using System; // ReSharper disable InvertIf @@ -16,26 +17,16 @@ namespace Squidex.Infrastructure.MongoDb { public sealed class InstantSerializer : SerializerBase, IBsonPolymorphicSerializer { - private static bool isRegistered; - private static readonly object LockObject = new object(); + private static readonly Lazy Registerer = new Lazy(() => + { + BsonSerializer.RegisterSerializer(new InstantSerializer()); + + return true; + }); public static bool Register() { - if (!isRegistered) - { - lock (LockObject) - { - if (!isRegistered) - { - BsonSerializer.RegisterSerializer(new InstantSerializer()); - - isRegistered = true; - return true; - } - } - } - - return false; + return !Registerer.IsValueCreated && Registerer.Value; } public bool IsDiscriminatorCompatibleWithObjectSerializer diff --git a/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs index 481e55b2f..b0ec9002a 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs @@ -11,6 +11,7 @@ using System.Globalization; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; +using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.MongoDb { @@ -29,7 +30,7 @@ namespace Squidex.Infrastructure.MongoDb } } - protected ProjectionDefinitionBuilder Projection + protected static ProjectionDefinitionBuilder Projection { get { @@ -37,7 +38,7 @@ namespace Squidex.Infrastructure.MongoDb } } - protected SortDefinitionBuilder Sort + protected static SortDefinitionBuilder Sort { get { @@ -45,7 +46,7 @@ namespace Squidex.Infrastructure.MongoDb } } - protected UpdateDefinitionBuilder Update + protected static UpdateDefinitionBuilder Update { get { @@ -53,7 +54,7 @@ namespace Squidex.Infrastructure.MongoDb } } - protected FilterDefinitionBuilder Filter + protected static FilterDefinitionBuilder Filter { get { @@ -61,7 +62,7 @@ namespace Squidex.Infrastructure.MongoDb } } - protected IndexKeysDefinitionBuilder IndexKeys + protected static IndexKeysDefinitionBuilder IndexKeys { get { @@ -127,7 +128,7 @@ namespace Squidex.Infrastructure.MongoDb protected virtual Task SetupCollectionAsync(IMongoCollection collection) { - return Task.FromResult(true); + return TaskHelper.Done; } public virtual Task ClearAsync() @@ -135,7 +136,7 @@ namespace Squidex.Infrastructure.MongoDb return Collection.DeleteManyAsync(new BsonDocument()); } - public async Task TryDropCollectionAsync() + public async Task DropCollectionIfExistsAsync() { try { diff --git a/src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs b/src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs index 553a6a728..723e9d2ab 100644 --- a/src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs +++ b/src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; @@ -15,26 +16,16 @@ namespace Squidex.Infrastructure.MongoDb { public class RefTokenSerializer : SerializerBase { - private static bool isRegistered; - private static readonly object LockObject = new object(); + private static readonly Lazy Registerer = new Lazy(() => + { + BsonSerializer.RegisterSerializer(new RefTokenSerializer()); + + return true; + }); public static bool Register() { - if (!isRegistered) - { - lock (LockObject) - { - if (!isRegistered) - { - BsonSerializer.RegisterSerializer(new RefTokenSerializer()); - - isRegistered = true; - return true; - } - } - } - - return false; + return !Registerer.IsValueCreated && Registerer.Value; } public override RefToken Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) diff --git a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index 2d95e66be..a2f2b7df3 100644 --- a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -10,6 +10,6 @@ - + diff --git a/src/Squidex.Infrastructure.Redis/RedisInfrastructureErrors.cs b/src/Squidex.Infrastructure.Redis/RedisInfrastructureErrors.cs index ca7ad3a3b..1fca8ec32 100644 --- a/src/Squidex.Infrastructure.Redis/RedisInfrastructureErrors.cs +++ b/src/Squidex.Infrastructure.Redis/RedisInfrastructureErrors.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging; namespace Squidex.Infrastructure.Redis { - public class RedisInfrastructureErrors + public static class RedisInfrastructureErrors { public static readonly EventId InvalidatingReceivedFailed = new EventId(50001, "InvalidingReceivedFailed"); diff --git a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj b/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj index 68d07fc7b..98a461896 100644 --- a/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj +++ b/src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj @@ -10,7 +10,7 @@ - - + + diff --git a/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs index 795695484..ecab8169c 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/AggregateHandler.cs @@ -7,7 +7,6 @@ // ========================================================================== using System; -using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure.CQRS.Events; @@ -17,7 +16,6 @@ namespace Squidex.Infrastructure.CQRS.Commands { private readonly IDomainObjectRepository domainObjectRepository; private readonly IDomainObjectFactory domainObjectFactory; - private readonly IEnumerable eventProcessors; public IDomainObjectRepository Repository { @@ -31,55 +29,72 @@ namespace Squidex.Infrastructure.CQRS.Commands public AggregateHandler( IDomainObjectFactory domainObjectFactory, - IDomainObjectRepository domainObjectRepository, - IEnumerable eventProcessors) + IDomainObjectRepository domainObjectRepository) { - Guard.NotNull(eventProcessors, nameof(eventProcessors)); Guard.NotNull(domainObjectFactory, nameof(domainObjectFactory)); Guard.NotNull(domainObjectRepository, nameof(domainObjectRepository)); this.domainObjectFactory = domainObjectFactory; this.domainObjectRepository = domainObjectRepository; - - this.eventProcessors = eventProcessors; } - public async Task CreateAsync(IAggregateCommand command, Func creator) where T : class, IAggregate + public async Task CreateAsync(CommandContext context, Func creator) where T : class, IAggregate { Guard.NotNull(creator, nameof(creator)); - Guard.NotNull(command, nameof(command)); - Guard.NotEmpty(command.AggregateId, nameof(command.AggregateId)); + Guard.NotNull(context, nameof(context)); - var aggregate = domainObjectFactory.CreateNew(command.AggregateId); + var aggregateCommand = GetCommand(context); + var aggregate = (T)domainObjectFactory.CreateNew(typeof(T), aggregateCommand.AggregateId); await creator(aggregate); - await Save(command, aggregate); + await SaveAsync(aggregate); + + if (!context.IsHandled) + { + context.Succeed(new EntityCreatedResult(aggregate.Id, aggregate.Version)); + } } - public async Task UpdateAsync(IAggregateCommand command, Func updater) where T : class, IAggregate + public async Task UpdateAsync(CommandContext context, Func updater) where T : class, IAggregate { Guard.NotNull(updater, nameof(updater)); - Guard.NotNull(command, nameof(command)); - Guard.NotEmpty(command.AggregateId, nameof(command.AggregateId)); + Guard.NotNull(context, nameof(context)); - var aggregate = await domainObjectRepository.GetByIdAsync(command.AggregateId); + var aggregateCommand = GetCommand(context); + var aggregate = await domainObjectRepository.GetByIdAsync(aggregateCommand.AggregateId, aggregateCommand.ExpectedVersion); await updater(aggregate); - await Save(command, aggregate); + await SaveAsync(aggregate); + + if (!context.IsHandled) + { + context.Succeed(new EntitySavedResult(aggregate.Version)); + } + } + + private static IAggregateCommand GetCommand(CommandContext context) + { + var command = context.Command as IAggregateCommand; + + if (command == null) + { + throw new ArgumentException("Context must have an aggregate command.", nameof(context)); + } + + Guard.NotEmpty(command.AggregateId, "context.Command.AggregateId"); + + return command; } - private async Task Save(ICommand command, IAggregate aggregate) + private async Task SaveAsync(IAggregate aggregate) { var events = aggregate.GetUncomittedEvents(); foreach (var @event in events) { - foreach (var eventProcessor in eventProcessors) - { - await eventProcessor.ProcessEventAsync(@event, aggregate, command); - } + @event.SetAggregateId(aggregate.Id); } await domainObjectRepository.SaveAsync(aggregate, events, Guid.NewGuid()); diff --git a/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs b/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs index 07b57e503..c56f7599a 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/CommandingExtensions.cs @@ -14,14 +14,9 @@ namespace Squidex.Infrastructure.CQRS.Commands { public static class CommandingExtensions { - public static T CreateNew(this IDomainObjectFactory factory, Guid id) where T : IAggregate + public static Task CreateAsync(this IAggregateHandler handler, CommandContext context, Action creator) where T : class, IAggregate { - return (T)factory.CreateNew(typeof(T), id); - } - - public static Task CreateAsync(this IAggregateHandler handler, IAggregateCommand command, Action creator) where T : class, IAggregate - { - return handler.CreateAsync(command, x => + return handler.CreateAsync(context, x => { creator(x); @@ -29,12 +24,12 @@ namespace Squidex.Infrastructure.CQRS.Commands }); } - public static Task UpdateAsync(this IAggregateHandler handler, IAggregateCommand command, Action creator) where T : class, IAggregate + public static Task UpdateAsync(this IAggregateHandler handler, CommandContext context, Action updater) where T : class, IAggregate { - return handler.UpdateAsync(command, x => + return handler.UpdateAsync(context, x => { - creator(x); - + updater(x); + return TaskHelper.Done; }); } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs index 24efb3956..1308de330 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectFactory.cs @@ -28,7 +28,14 @@ namespace Squidex.Infrastructure.CQRS.Commands var factoryFunctionType = typeof(DomainObjectFactoryFunction<>).MakeGenericType(type); var factoryFunction = (Delegate)serviceProvider.GetService(factoryFunctionType); - return (IAggregate)factoryFunction.DynamicInvoke(id); + var aggregate = (IAggregate)factoryFunction.DynamicInvoke(id); + + if (aggregate.Version != -1) + { + throw new InvalidOperationException("Must have a version of -1"); + } + + return aggregate; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs index 7c657d2e6..3c163beb2 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs @@ -39,10 +39,8 @@ namespace Squidex.Infrastructure.CQRS.Commands this.nameResolver = nameResolver; } - public async Task GetByIdAsync(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate + public async Task GetByIdAsync(Guid id, long? expectedVersion = null) where TDomainObject : class, IAggregate { - Guard.GreaterThan(version, 0, nameof(version)); - var streamName = nameResolver.GetStreamName(typeof(TDomainObject), id); var events = await eventStore.GetEventsAsync(streamName).ToList(); @@ -61,9 +59,9 @@ namespace Squidex.Infrastructure.CQRS.Commands domainObject.ApplyEvent(envelope); } - if (domainObject.Version != version && version < int.MaxValue) + if (expectedVersion != null && domainObject.Version != expectedVersion.Value) { - throw new DomainObjectVersionException(id.ToString(), typeof(TDomainObject), domainObject.Version, version); + throw new DomainObjectVersionException(id.ToString(), typeof(TDomainObject), domainObject.Version, expectedVersion.Value); } return domainObject; @@ -76,8 +74,7 @@ namespace Squidex.Infrastructure.CQRS.Commands var streamName = nameResolver.GetStreamName(domainObject.GetType(), domainObject.Id); var versionCurrent = domainObject.Version; - var versionBefore = versionCurrent - events.Count; - var versionExpected = versionBefore == 0 ? -1 : versionBefore - 1; + var versionExpected = versionCurrent - events.Count; var eventsToSave = events.Select(x => formatter.ToEventData(x, commitId)).ToList(); diff --git a/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs index 1f8bc892b..855c11865 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/EnrichWithTimestampHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using NodaTime; +using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Commands { @@ -24,14 +25,12 @@ namespace Squidex.Infrastructure.CQRS.Commands public Task HandleAsync(CommandContext context) { - var timestampCommand = context.Command as ITimestampCommand; - - if (timestampCommand != null) + if (context.Command is ITimestampCommand timestampCommand) { timestampCommand.Timestamp = clock.GetCurrentInstant(); } - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs b/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs new file mode 100644 index 000000000..259b836df --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// EntityCreatedResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.CQRS.Commands +{ + public static class EntityCreatedResult + { + public static EntityCreatedResult Create(T idOrValue, long version) + { + return new EntityCreatedResult(idOrValue, version); + } + } +} diff --git a/src/Squidex/Config/Web/WebpackServices.cs b/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult_T.cs similarity index 51% rename from src/Squidex/Config/Web/WebpackServices.cs rename to src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult_T.cs index 728fe4c90..7b56d620c 100644 --- a/src/Squidex/Config/Web/WebpackServices.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/EntityCreatedResult_T.cs @@ -1,23 +1,21 @@ // ========================================================================== -// WebpackServices.cs +// EntityCreatedResult_T.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using Microsoft.Extensions.DependencyInjection; -using Squidex.Pipeline; - -namespace Squidex.Config.Web +namespace Squidex.Infrastructure.CQRS.Commands { - public static class WebpackServices + public sealed class EntityCreatedResult : EntitySavedResult { - public static IServiceCollection AddWebpackBuilder(this IServiceCollection services) - { - services.AddSingleton(); + public T IdOrValue { get; } - return services; + public EntityCreatedResult(T idOrValue, long version) + : base(version) + { + IdOrValue = idOrValue; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/EntitySavedResult.cs b/src/Squidex.Infrastructure/CQRS/Commands/EntitySavedResult.cs new file mode 100644 index 000000000..deef50b7c --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Commands/EntitySavedResult.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// EntitySavedResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.CQRS.Commands +{ + public class EntitySavedResult + { + public long Version { get; } + + public EntitySavedResult(long version) + { + Version = version; + } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs index 0ebafa736..5d7ca75bb 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/IAggregateHandler.cs @@ -13,8 +13,8 @@ namespace Squidex.Infrastructure.CQRS.Commands { public interface IAggregateHandler { - Task CreateAsync(IAggregateCommand command, Func creator) where T : class, IAggregate; + Task CreateAsync(CommandContext context, Func creator) where T : class, IAggregate; - Task UpdateAsync(IAggregateCommand command, Func updater) where T : class, IAggregate; + Task UpdateAsync(CommandContext context, Func updater) where T : class, IAggregate; } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs b/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs index c1834662e..fb3516b3b 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/ICommand.cs @@ -10,5 +10,6 @@ namespace Squidex.Infrastructure.CQRS.Commands { public interface ICommand { + long? ExpectedVersion { get; set; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs b/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs index 925076952..2aa0ee679 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/IDomainObjectRepository.cs @@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { public interface IDomainObjectRepository { - Task GetByIdAsync(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate; + Task GetByIdAsync(Guid id, long? expectedVersion = null) where TDomainObject : class, IAggregate; Task SaveAsync(IAggregate domainObject, ICollection> events, Guid commitId); } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs index e75660e1d..0e31d7548 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf @@ -36,7 +37,7 @@ namespace Squidex.Infrastructure.CQRS.Commands logger.LogCritical(InfrastructureErrors.CommandUnknown, exception, "Unknown command {0}", context.Command); } - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs b/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs index 37a4e6d00..d95a02cd6 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.CQRS.Commands { @@ -24,7 +25,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { logger.LogInformation("Handling {0} command", context.Command); - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex.Infrastructure/CQRS/DomainObject.cs b/src/Squidex.Infrastructure/CQRS/DomainObjectBase.cs similarity index 91% rename from src/Squidex.Infrastructure/CQRS/DomainObject.cs rename to src/Squidex.Infrastructure/CQRS/DomainObjectBase.cs index b9130fedf..b8e1ceb30 100644 --- a/src/Squidex.Infrastructure/CQRS/DomainObject.cs +++ b/src/Squidex.Infrastructure/CQRS/DomainObjectBase.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Infrastructure.CQRS { - public abstract class DomainObject : IAggregate, IEquatable + public abstract class DomainObjectBase : IAggregate, IEquatable { private readonly List> uncomittedEvents = new List>(); private readonly Guid id; @@ -28,10 +28,10 @@ namespace Squidex.Infrastructure.CQRS get { return id; } } - protected DomainObject(Guid id, int version) + protected DomainObjectBase(Guid id, int version) { Guard.NotEmpty(id, nameof(id)); - Guard.GreaterEquals(version, 0, nameof(version)); + Guard.GreaterEquals(version, -1, nameof(version)); this.id = id; diff --git a/src/Squidex.Infrastructure/CQRS/CommonHeaders.cs b/src/Squidex.Infrastructure/CQRS/Events/CommonHeaders.cs similarity index 81% rename from src/Squidex.Infrastructure/CQRS/CommonHeaders.cs rename to src/Squidex.Infrastructure/CQRS/Events/CommonHeaders.cs index 9274e62e2..617123666 100644 --- a/src/Squidex.Infrastructure/CQRS/CommonHeaders.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/CommonHeaders.cs @@ -6,9 +6,9 @@ // All rights reserved. // ========================================================================== -namespace Squidex.Infrastructure.CQRS +namespace Squidex.Infrastructure.CQRS.Events { - public sealed class CommonHeaders + public static class CommonHeaders { public static readonly string AggregateId = "AggregateId"; @@ -18,6 +18,8 @@ namespace Squidex.Infrastructure.CQRS public static readonly string EventNumber = "EventNumber"; + public static readonly string EventStreamNumber = "EventStreamNumber"; + public static readonly string Timestamp = "Timestamp"; public static readonly string Actor = "Actor"; diff --git a/src/Squidex.Infrastructure/CQRS/Events/DefaultMemoryEventNotifier.cs b/src/Squidex.Infrastructure/CQRS/Events/DefaultMemoryEventNotifier.cs index 46d0945ea..afd527615 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/DefaultMemoryEventNotifier.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/DefaultMemoryEventNotifier.cs @@ -12,7 +12,7 @@ namespace Squidex.Infrastructure.CQRS.Events { public sealed class DefaultMemoryEventNotifier : IEventNotifier { - private readonly string ChannelName = typeof(DefaultMemoryEventNotifier).Name; + private static readonly string ChannelName = typeof(DefaultMemoryEventNotifier).Name; private readonly IPubSub invalidator; diff --git a/src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs b/src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs deleted file mode 100644 index e8b07466d..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/EnrichWithAggregateIdProcessor.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// EnrichWithAggregateIdProcessor.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.CQRS.Events -{ - public sealed class EnrichWithAggregateIdProcessor : IEventProcessor - { - public Task ProcessEventAsync(Envelope @event, IAggregate aggregate, ICommand command) - { - var aggregateCommand = command as IAggregateCommand; - - if (aggregateCommand != null) - { - @event.SetAggregateId(aggregateCommand.AggregateId); - } - - return TaskHelper.Done; - } - } -} diff --git a/src/Squidex.Infrastructure/CQRS/Envelope.cs b/src/Squidex.Infrastructure/CQRS/Events/Envelope.cs similarity index 97% rename from src/Squidex.Infrastructure/CQRS/Envelope.cs rename to src/Squidex.Infrastructure/CQRS/Events/Envelope.cs index 79588727f..e2bf2aa45 100644 --- a/src/Squidex.Infrastructure/CQRS/Envelope.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/Envelope.cs @@ -9,7 +9,7 @@ using System; using NodaTime; -namespace Squidex.Infrastructure.CQRS +namespace Squidex.Infrastructure.CQRS.Events { public class Envelope where TPayload : class { diff --git a/src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs b/src/Squidex.Infrastructure/CQRS/Events/EnvelopeExtensions.cs similarity index 83% rename from src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs rename to src/Squidex.Infrastructure/CQRS/Events/EnvelopeExtensions.cs index 27a757517..80886571a 100644 --- a/src/Squidex.Infrastructure/CQRS/EnvelopeExtensions.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/EnvelopeExtensions.cs @@ -10,7 +10,7 @@ using System; using System.Globalization; using NodaTime; -namespace Squidex.Infrastructure.CQRS +namespace Squidex.Infrastructure.CQRS.Events { public static class EnvelopeExtensions { @@ -26,6 +26,18 @@ namespace Squidex.Infrastructure.CQRS return envelope; } + public static long EventStreamNumber(this EnvelopeHeaders headers) + { + return headers[CommonHeaders.EventStreamNumber].ToInt32(CultureInfo.InvariantCulture); + } + + public static Envelope SetEventStreamNumber(this Envelope envelope, long value) where T : class + { + envelope.Headers.Set(CommonHeaders.EventStreamNumber, value); + + return envelope; + } + public static Guid CommitId(this EnvelopeHeaders headers) { return headers[CommonHeaders.CommitId].ToGuid(CultureInfo.InvariantCulture); diff --git a/src/Squidex.Infrastructure/CQRS/EnvelopeHeaders.cs b/src/Squidex.Infrastructure/CQRS/Events/EnvelopeHeaders.cs similarity index 95% rename from src/Squidex.Infrastructure/CQRS/EnvelopeHeaders.cs rename to src/Squidex.Infrastructure/CQRS/Events/EnvelopeHeaders.cs index 3747ddbbb..f95580f9d 100644 --- a/src/Squidex.Infrastructure/CQRS/EnvelopeHeaders.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/EnvelopeHeaders.cs @@ -5,7 +5,8 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Infrastructure.CQRS + +namespace Squidex.Infrastructure.CQRS.Events { public sealed class EnvelopeHeaders : PropertiesBag { diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs b/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs index d693f040f..aca57463c 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs @@ -18,7 +18,7 @@ using Squidex.Infrastructure.Timers; namespace Squidex.Infrastructure.CQRS.Events { - public sealed class EventReceiver : DisposableObject + public sealed class EventReceiver : DisposableObjectBase { private readonly EventDataFormatter formatter; private readonly IEventStore eventStore; @@ -176,6 +176,7 @@ namespace Squidex.Infrastructure.CQRS.Events var @event = formatter.Parse(storedEvent.Data); @event.SetEventNumber(storedEvent.EventNumber); + @event.SetEventStreamNumber(storedEvent.EventStreamNumber); return @event; } diff --git a/src/Squidex.Infrastructure/CQRS/Events/StoredEvent.cs b/src/Squidex.Infrastructure/CQRS/Events/StoredEvent.cs index 4f8f04d6e..547956c13 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/StoredEvent.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/StoredEvent.cs @@ -11,6 +11,7 @@ namespace Squidex.Infrastructure.CQRS.Events public sealed class StoredEvent { private readonly long eventNumber; + private readonly long eventStreamNumber; private readonly EventData data; public long EventNumber @@ -18,18 +19,23 @@ namespace Squidex.Infrastructure.CQRS.Events get { return eventNumber; } } + public long EventStreamNumber + { + get { return eventStreamNumber; } + } + public EventData Data { get { return data; } } - public StoredEvent(long eventNumber, EventData data) + public StoredEvent(long eventNumber, long eventStreamNumber, EventData data) { Guard.NotNull(data, nameof(data)); this.data = data; - this.eventNumber = eventNumber; + this.eventStreamNumber = eventStreamNumber; } } } diff --git a/src/Squidex.Infrastructure/CollectionExtensions.cs b/src/Squidex.Infrastructure/CollectionExtensions.cs index 1050e4a68..0b177c816 100644 --- a/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -30,7 +30,7 @@ namespace Squidex.Infrastructure { if (item != null) { - hashCode = hashCode * 23 + item.GetHashCode(); + hashCode = hashCode * 23 + comparer.GetHashCode(item); } } diff --git a/src/Squidex.Infrastructure/Dispatching/ActionContextDispatcherFactory.cs b/src/Squidex.Infrastructure/Dispatching/ActionContextDispatcherFactory.cs index ad64b3f78..19f76be73 100644 --- a/src/Squidex.Infrastructure/Dispatching/ActionContextDispatcherFactory.cs +++ b/src/Squidex.Infrastructure/Dispatching/ActionContextDispatcherFactory.cs @@ -13,7 +13,7 @@ using System.Reflection; namespace Squidex.Infrastructure.Dispatching { - internal class ActionContextDispatcherFactory + internal static class ActionContextDispatcherFactory { public static Tuple> CreateActionHandler(MethodInfo methodInfo) { diff --git a/src/Squidex.Infrastructure/Dispatching/ActionDispatcherFactory.cs b/src/Squidex.Infrastructure/Dispatching/ActionDispatcherFactory.cs index 35d5113aa..04de5fe19 100644 --- a/src/Squidex.Infrastructure/Dispatching/ActionDispatcherFactory.cs +++ b/src/Squidex.Infrastructure/Dispatching/ActionDispatcherFactory.cs @@ -13,7 +13,7 @@ using System.Reflection; namespace Squidex.Infrastructure.Dispatching { - internal class ActionDispatcherFactory + internal static class ActionDispatcherFactory { public static Tuple> CreateActionHandler(MethodInfo methodInfo) { diff --git a/src/Squidex.Infrastructure/DisposableObject.cs b/src/Squidex.Infrastructure/DisposableObjectBase.cs similarity index 94% rename from src/Squidex.Infrastructure/DisposableObject.cs rename to src/Squidex.Infrastructure/DisposableObjectBase.cs index 98da1ace5..a8ead1cbd 100644 --- a/src/Squidex.Infrastructure/DisposableObject.cs +++ b/src/Squidex.Infrastructure/DisposableObjectBase.cs @@ -1,5 +1,5 @@ // ========================================================================== -// EnumExtensions.cs +// DisposableObjectBase.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -10,7 +10,7 @@ using System; namespace Squidex.Infrastructure { - public abstract class DisposableObject : IDisposable + public abstract class DisposableObjectBase : IDisposable { private readonly object disposeLock = new object(); private bool isDisposed; diff --git a/src/Squidex.Infrastructure/DomainObjectVersionException.cs b/src/Squidex.Infrastructure/DomainObjectVersionException.cs index 9a269cbbc..19599594d 100644 --- a/src/Squidex.Infrastructure/DomainObjectVersionException.cs +++ b/src/Squidex.Infrastructure/DomainObjectVersionException.cs @@ -12,20 +12,20 @@ namespace Squidex.Infrastructure { public class DomainObjectVersionException : DomainObjectException { - private readonly int currentVersion; - private readonly int expectedVersion; + private readonly long currentVersion; + private readonly long expectedVersion; - public int CurrentVersion + public long CurrentVersion { get { return currentVersion; } } - public int ExpectedVersion + public long ExpectedVersion { get { return expectedVersion; } } - public DomainObjectVersionException(string id, Type type, int currentVersion, int expectedVersion) + public DomainObjectVersionException(string id, Type type, long currentVersion, long expectedVersion) : base(FormatMessage(id, type, currentVersion, expectedVersion), id, type) { this.currentVersion = currentVersion; @@ -33,7 +33,7 @@ namespace Squidex.Infrastructure this.expectedVersion = expectedVersion; } - private static string FormatMessage(string id, Type type, int currentVersion, int expectedVersion) + private static string FormatMessage(string id, Type type, long currentVersion, long expectedVersion) { return $"Requested version {expectedVersion} for object '{id}' (type {type}), but found {currentVersion}."; } diff --git a/src/Squidex.Infrastructure/Json/ConverterContractResolver.cs b/src/Squidex.Infrastructure/Json/ConverterContractResolver.cs new file mode 100644 index 000000000..625d36fa1 --- /dev/null +++ b/src/Squidex.Infrastructure/Json/ConverterContractResolver.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// ConverterContractResolver.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Squidex.Infrastructure.Json +{ + public sealed class ConverterContractResolver : CamelCasePropertyNamesContractResolver + { + private readonly JsonConverter[] converters; + + public ConverterContractResolver(params JsonConverter[] converters) + { + this.converters = converters; + } + + protected override JsonConverter ResolveContractConverter(Type objectType) + { + var result = base.ResolveContractConverter(objectType); + + if (result != null) + { + return result; + } + + foreach (var converter in converters) + { + if (converter.CanConvert(objectType)) + { + return converter; + } + } + + return null; + } + } +} diff --git a/src/Squidex.Infrastructure/Json/JsonExtension.cs b/src/Squidex.Infrastructure/Json/JsonExtension.cs new file mode 100644 index 000000000..5b3125cc3 --- /dev/null +++ b/src/Squidex.Infrastructure/Json/JsonExtension.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// JsonExtension.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json.Linq; + +namespace Squidex.Infrastructure.Json +{ + public static class JsonExtension + { + public static bool IsNull(this JToken token) + { + if (token == null) + { + return true; + } + if (token.Type == JTokenType.Null) + { + return true; + } + if (token is JValue value) + { + return value.Value == null; + } + + return false; + } + } +} diff --git a/src/Squidex.Infrastructure/Language.cs b/src/Squidex.Infrastructure/Language.cs index 641d9fd82..b369332cc 100644 --- a/src/Squidex.Infrastructure/Language.cs +++ b/src/Squidex.Infrastructure/Language.cs @@ -19,7 +19,7 @@ namespace Squidex.Infrastructure private static readonly Regex CultureRegex = new Regex("([a-z]{2})(\\-[a-z]{2})?"); private readonly string iso2Code; private readonly string englishName; - private static readonly Dictionary allLanguages = new Dictionary(); + private static readonly Dictionary AllLanguagesField = new Dictionary(); public static readonly Language Invariant = AddLanguage("iv", "Invariant"); @@ -27,7 +27,7 @@ namespace Squidex.Infrastructure { var language = new Language(iso2Code, englishName); - allLanguages[iso2Code] = language; + AllLanguagesField[iso2Code] = language; return language; } @@ -38,7 +38,7 @@ namespace Squidex.Infrastructure try { - return allLanguages[iso2Code]; + return AllLanguagesField[iso2Code]; } catch (KeyNotFoundException) { @@ -48,7 +48,7 @@ namespace Squidex.Infrastructure public static IEnumerable AllLanguages { - get { return allLanguages.Values; } + get { return AllLanguagesField.Values; } } public string EnglishName @@ -72,17 +72,17 @@ namespace Squidex.Infrastructure { Guard.NotNullOrEmpty(iso2Code, nameof(iso2Code)); - return allLanguages.ContainsKey(iso2Code); + return AllLanguagesField.ContainsKey(iso2Code); } public static bool TryGetLanguage(string iso2Code, out Language language) { Guard.NotNullOrEmpty(iso2Code, nameof(iso2Code)); - return allLanguages.TryGetValue(iso2Code, out language); + return AllLanguagesField.TryGetValue(iso2Code, out language); } - public static Language TryParse(string input) + public static Language ParseOrNull(string input) { if (string.IsNullOrWhiteSpace(input)) { diff --git a/src/Squidex.Infrastructure/Languages.cs b/src/Squidex.Infrastructure/Languages.cs index 5fb87a0b1..0e03ff9fd 100644 --- a/src/Squidex.Infrastructure/Languages.cs +++ b/src/Squidex.Infrastructure/Languages.cs @@ -5,9 +5,13 @@ // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== +// + +using System.CodeDom.Compiler; namespace Squidex.Infrastructure { + [GeneratedCode("LanguagesGenerator", "1.0")] partial class Language { public static readonly Language AA = AddLanguage("aa", "Afar"); diff --git a/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs b/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs index 1bdeccfe0..355dd088d 100644 --- a/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs +++ b/src/Squidex.Infrastructure/Reflection/PropertyAccessor.cs @@ -26,7 +26,7 @@ namespace Squidex.Infrastructure.Reflection } else { - getMethod = x => throw new NotSupportedException(); + getMethod = x => { throw new NotSupportedException(); }; } if (propertyInfo.CanWrite) @@ -35,7 +35,7 @@ namespace Squidex.Infrastructure.Reflection } else { - setMethod = (x, y) => throw new NotSupportedException(); + setMethod = (x, y) => { throw new NotSupportedException(); }; } } diff --git a/src/Squidex.Infrastructure/Security/OpenIdClaims.cs b/src/Squidex.Infrastructure/Security/OpenIdClaims.cs index 1fe650780..e01fb6d88 100644 --- a/src/Squidex.Infrastructure/Security/OpenIdClaims.cs +++ b/src/Squidex.Infrastructure/Security/OpenIdClaims.cs @@ -8,7 +8,7 @@ namespace Squidex.Infrastructure.Security { - public class OpenIdClaims + public static class OpenIdClaims { /// /// Unique Identifier for the End-User at the Issuer. diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 64fe379bd..b9208aeb0 100644 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -7,8 +7,8 @@ True - - + + diff --git a/src/Squidex.Infrastructure/Tasks/TaskHelper.cs b/src/Squidex.Infrastructure/Tasks/TaskHelper.cs index c0ad8286c..eb2e1c1e0 100644 --- a/src/Squidex.Infrastructure/Tasks/TaskHelper.cs +++ b/src/Squidex.Infrastructure/Tasks/TaskHelper.cs @@ -13,6 +13,8 @@ namespace Squidex.Infrastructure.Tasks public static class TaskHelper { public static readonly Task Done = CreateDoneTask(); + public static readonly Task False = CreateResultTask(false); + public static readonly Task True = CreateResultTask(true); private static Task CreateDoneTask() { @@ -22,5 +24,14 @@ namespace Squidex.Infrastructure.Tasks return result.Task; } + + private static Task CreateResultTask(bool value) + { + var result = new TaskCompletionSource(); + + result.SetResult(value); + + return result.Task; + } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/Timers/CompletionTimer.cs b/src/Squidex.Infrastructure/Timers/CompletionTimer.cs index 94b94b640..b4dc061bf 100644 --- a/src/Squidex.Infrastructure/Timers/CompletionTimer.cs +++ b/src/Squidex.Infrastructure/Timers/CompletionTimer.cs @@ -14,7 +14,7 @@ using System.Threading.Tasks; namespace Squidex.Infrastructure.Timers { - public sealed class CompletionTimer : DisposableObject + public sealed class CompletionTimer : DisposableObjectBase { private readonly CancellationTokenSource disposeToken = new CancellationTokenSource(); private readonly Task runTask; diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs index 6d280253c..7c4e34fe3 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppEntity.cs @@ -13,6 +13,8 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; using Squidex.Read.Apps; +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global + namespace Squidex.Read.MongoDb.Apps { public sealed class MongoAppEntity : MongoEntity, IAppEntity @@ -25,6 +27,10 @@ namespace Squidex.Read.MongoDb.Apps [BsonElement] public string MasterLanguage { get; set; } + [BsonRequired] + [BsonElement] + public long Version { get; set; } + [BsonRequired] [BsonElement] public HashSet Languages { get; set; } = new HashSet(); diff --git a/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs index de55df60f..606a28e8c 100644 --- a/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs @@ -11,7 +11,6 @@ using System.Threading.Tasks; using Squidex.Events; using Squidex.Events.Apps; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs index 6d9d4dfcf..729356ef9 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentEntity.cs @@ -36,6 +36,10 @@ namespace Squidex.Read.MongoDb.Contents [BsonElement] public string Text { get; set; } + [BsonRequired] + [BsonElement] + public long Version { get; set; } + [BsonRequired] [BsonElement] public Guid AppId { get; set; } diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs index a29daffa5..99bcb9223 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository.cs @@ -12,12 +12,13 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.OData.Core; using MongoDB.Driver; -using Squidex.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Read.Contents; +using Squidex.Read.Contents.Builders; using Squidex.Read.Contents.Repositories; using Squidex.Read.MongoDb.Contents.Visitors; +using Squidex.Read.Schemas; using Squidex.Read.Schemas.Services; namespace Squidex.Read.MongoDb.Contents @@ -26,7 +27,8 @@ namespace Squidex.Read.MongoDb.Contents { private const string Prefix = "Projections_Content_"; private readonly IMongoDatabase database; - private readonly ISchemaProvider schemaProvider; + private readonly ISchemaProvider schemas; + private readonly EdmModelBuilder modelBuilder; protected static IndexKeysDefinitionBuilder IndexKeys { @@ -36,28 +38,31 @@ namespace Squidex.Read.MongoDb.Contents } } - public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemaProvider) + public MongoContentRepository(IMongoDatabase database, ISchemaProvider schemas, EdmModelBuilder modelBuilder) { Guard.NotNull(database, nameof(database)); - Guard.NotNull(schemaProvider, nameof(schemaProvider)); + Guard.NotNull(modelBuilder, nameof(modelBuilder)); + Guard.NotNull(schemas, nameof(schemas)); + this.schemas = schemas; this.database = database; - - this.schemaProvider = schemaProvider; + this.modelBuilder = modelBuilder; } public async Task> QueryAsync(Guid schemaId, bool nonPublished, string odataQuery, HashSet languages) { List result = null; - await ForSchemaAsync(schemaId, async (collection, schema) => + await ForSchemaAsync(schemaId, async (collection, schemaEntity) => { IFindFluent cursor; try { - var parser = schema.ParseQuery(languages, odataQuery); + var model = modelBuilder.BuildEdmModel(schemaEntity, languages); + + var parser = model.ParseQuery(odataQuery); - cursor = collection.Find(parser, schema, nonPublished).Take(parser).Skip(parser).Sort(parser, schema); + cursor = collection.Find(parser, schemaEntity.Schema, nonPublished).Take(parser).Skip(parser).Sort(parser, schemaEntity.Schema); } catch (NotSupportedException) { @@ -76,7 +81,7 @@ namespace Squidex.Read.MongoDb.Contents foreach (var entity in entities) { - entity.ParseData(schema); + entity.ParseData(schemaEntity.Schema); } result = entities.OfType().ToList(); @@ -89,14 +94,16 @@ namespace Squidex.Read.MongoDb.Contents { var result = 0L; - await ForSchemaAsync(schemaId, async (collection, schema) => + await ForSchemaAsync(schemaId, async (collection, schemaEntity) => { IFindFluent cursor; try { - var parser = schema.ParseQuery(languages, odataQuery); + var model = modelBuilder.BuildEdmModel(schemaEntity, languages); + + var parser = model.ParseQuery(odataQuery); - cursor = collection.Find(parser, schema, nonPublished); + cursor = collection.Find(parser, schemaEntity.Schema, nonPublished); } catch (NotSupportedException) { @@ -121,28 +128,28 @@ namespace Squidex.Read.MongoDb.Contents { MongoContentEntity result = null; - await ForSchemaAsync(schemaId, async (collection, schema) => + await ForSchemaAsync(schemaId, async (collection, schemaEntity) => { result = await collection.Find(x => x.Id == id).FirstOrDefaultAsync(); - result?.ParseData(schema); + result?.ParseData(schemaEntity.Schema); }); return result; } - private async Task ForSchemaAsync(Guid schemaId, Func, Schema, Task> action) + private async Task ForSchemaAsync(Guid schemaId, Func, ISchemaEntityWithSchema, Task> action) { var collection = GetCollection(schemaId); - var schemaEntity = await schemaProvider.FindSchemaByIdAsync(schemaId); + var schemaEntity = await schemas.FindSchemaByIdAsync(schemaId, true); if (schemaEntity == null) { return; } - await action(collection, schemaEntity.Schema); + await action(collection, schemaEntity); } } } diff --git a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs index 8574dd475..d2abc0e59 100644 --- a/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs @@ -12,7 +12,6 @@ using MongoDB.Bson; using MongoDB.Driver; using Squidex.Events.Contents; using Squidex.Events.Schemas; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; @@ -68,24 +67,24 @@ namespace Squidex.Read.MongoDb.Contents protected Task On(ContentCreated @event, EnvelopeHeaders headers) { - return ForSchemaAsync(@event.SchemaId.Id, (collection, schema) => + return ForSchemaAsync(@event.SchemaId.Id, (collection, schemaEntity) => { return collection.CreateAsync(@event, headers, x => { SimpleMapper.Map(@event, x); - x.SetData(schema, @event.Data); + x.SetData(schemaEntity.Schema, @event.Data); }); }); } protected Task On(ContentUpdated @event, EnvelopeHeaders headers) { - return ForSchemaAsync(@event.SchemaId.Id, (collection, schema) => + return ForSchemaAsync(@event.SchemaId.Id, (collection, schemaEntity) => { return collection.UpdateAsync(@event, headers, x => { - x.SetData(schema, @event.Data); + x.SetData(schemaEntity.Schema, @event.Data); }); }); } @@ -112,19 +111,19 @@ namespace Squidex.Read.MongoDb.Contents }); } - protected Task On(FieldDeleted @event, EnvelopeHeaders headers) + protected Task On(ContentDeleted @event, EnvelopeHeaders headers) { return ForSchemaIdAsync(@event.SchemaId.Id, collection => { - return collection.UpdateManyAsync(new BsonDocument(), Update.Unset(new StringFieldDefinition($"Data.{@event.FieldId}"))); + return collection.DeleteOneAsync(x => x.Id == headers.AggregateId()); }); } - protected Task On(ContentDeleted @event, EnvelopeHeaders headers) + protected Task On(FieldDeleted @event, EnvelopeHeaders headers) { return ForSchemaIdAsync(@event.SchemaId.Id, collection => { - return collection.DeleteOneAsync(x => x.Id == headers.AggregateId()); + return collection.UpdateManyAsync(new BsonDocument(), Update.Unset(new StringFieldDefinition($"Data.{@event.FieldId}"))); }); } diff --git a/src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs b/src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs new file mode 100644 index 000000000..d3b992635 --- /dev/null +++ b/src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// SchemaExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using Microsoft.OData.Core.UriParser; +using Microsoft.OData.Edm; + +namespace Squidex.Read.MongoDb.Contents.Visitors +{ + public static class EdmModelExtensions + { + public static ODataUriParser ParseQuery(this IEdmModel model, string query) + { + var path = model.EntityContainer.EntitySets().First().Path.Path.Last().Split('.').Last(); + + var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); + + return parser; + } + } +} diff --git a/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs b/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs index b3e7dec12..7a2ff5a2f 100644 --- a/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs +++ b/src/Squidex.Read.MongoDb/History/MongoHistoryEventEntity.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure.MongoDb; namespace Squidex.Read.MongoDb.History { - public sealed class MongoHistoryEventEntity : MongoEntity, IAppRefEntity, ITrackCreatedByEntity + public sealed class MongoHistoryEventEntity : MongoEntity, IAppRefEntity, IEntityWithCreatedBy { [BsonRequired] [BsonElement] @@ -40,7 +40,7 @@ namespace Squidex.Read.MongoDb.History [BsonElement] public Dictionary Parameters { get; set; } - RefToken ITrackCreatedByEntity.CreatedBy + RefToken IEntityWithCreatedBy.CreatedBy { get { diff --git a/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs index 0b57267b1..874764150 100644 --- a/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs @@ -13,7 +13,6 @@ using System.Threading; using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Events; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; using Squidex.Read.History; diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs index ddbced902..8e34fb03c 100644 --- a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaEntity.cs @@ -33,6 +33,10 @@ namespace Squidex.Read.MongoDb.Schemas [BsonElement] public string Schema { get; set; } + [BsonRequired] + [BsonElement] + public long Version { get; set; } + [BsonRequired] [BsonElement] public Guid AppId { get; set; } @@ -49,6 +53,10 @@ namespace Squidex.Read.MongoDb.Schemas [BsonElement] public bool IsPublished { get; set; } + [BsonRequired] + [BsonElement] + public bool IsDeleted { get; set; } + Schema ISchemaEntityWithSchema.Schema { get { return schema.Value; } diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository.cs index 58db8d1dc..c3767a308 100644 --- a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository.cs @@ -49,14 +49,14 @@ namespace Squidex.Read.MongoDb.Schemas public async Task> QueryAllAsync(Guid appId) { - var entities = await Collection.Find(s => s.AppId == appId).ToListAsync(); + var entities = await Collection.Find(s => s.AppId == appId && !s.IsDeleted).ToListAsync(); return entities.OfType().ToList(); } public async Task> QueryAllWithSchemaAsync(Guid appId) { - var entities = await Collection.Find(s => s.AppId == appId).ToListAsync(); + var entities = await Collection.Find(s => s.AppId == appId && !s.IsDeleted).ToListAsync(); entities.ForEach(x => x.DeserializeSchema(serializer)); @@ -66,7 +66,7 @@ namespace Squidex.Read.MongoDb.Schemas public async Task FindSchemaAsync(Guid appId, string name) { var entity = - await Collection.Find(s => s.Name == name && s.AppId == appId) + await Collection.Find(s => s.Name == name && s.AppId == appId && !s.IsDeleted) .FirstOrDefaultAsync(); entity?.DeserializeSchema(serializer); @@ -84,14 +84,5 @@ namespace Squidex.Read.MongoDb.Schemas return entity; } - - public async Task FindSchemaIdAsync(Guid appId, string name) - { - var entity = - await Collection.Find(s => s.Name == name & s.AppId == appId) - .Project(Projection.Include(x => x.Id)).FirstOrDefaultAsync(); - - return entity?.Id; - } } } diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs index c72f76f9c..f0a94b350 100644 --- a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs @@ -14,7 +14,6 @@ using Squidex.Events; using Squidex.Events.Schemas; using Squidex.Events.Schemas.Utils; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Reflection; @@ -92,7 +91,7 @@ namespace Squidex.Read.MongoDb.Schemas protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers) { - await Collection.DeleteOneAsync(x => x.Id == headers.AggregateId()); + await Collection.UpdateAsync(@event, headers, e => e.IsDeleted = true); SchemaSaved?.Invoke(@event.AppId, @event.SchemaId); } diff --git a/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj b/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj index ab4e5369b..b6da4c68e 100644 --- a/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj +++ b/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj @@ -15,10 +15,10 @@ - - + + - + diff --git a/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs b/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs index 0e628b70e..f9b6c005d 100644 --- a/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs +++ b/src/Squidex.Read.MongoDb/Utils/EntityMapper.cs @@ -7,7 +7,7 @@ // ========================================================================== using Squidex.Events; -using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; // ReSharper disable ConvertIfStatementToConditionalTernaryExpression @@ -23,6 +23,7 @@ namespace Squidex.Read.MongoDb.Utils SetId(headers, entity); + SetVersion(headers, entity); SetCreated(headers, entity); SetCreatedBy(@event, entity); @@ -33,6 +34,7 @@ namespace Squidex.Read.MongoDb.Utils public static T Update(SquidexEvent @event, EnvelopeHeaders headers, T entity) where T : MongoEntity, new() { + SetVersion(headers, entity); SetLastModified(headers, entity); SetLastModifiedBy(@event, entity); @@ -54,32 +56,33 @@ namespace Squidex.Read.MongoDb.Utils entity.LastModified = headers.Timestamp(); } - private static void SetCreatedBy(SquidexEvent @event, MongoEntity entity) + private static void SetVersion(EnvelopeHeaders headers, MongoEntity entity) { - var createdBy = entity as ITrackCreatedByEntity; + if (entity is IEntityWithVersion withVersion) + { + withVersion.Version = headers.EventStreamNumber(); + } + } - if (createdBy != null) + private static void SetCreatedBy(SquidexEvent @event, MongoEntity entity) + { + if (entity is IEntityWithCreatedBy withCreatedBy) { - createdBy.CreatedBy = @event.Actor; + withCreatedBy.CreatedBy = @event.Actor; } } private static void SetLastModifiedBy(SquidexEvent @event, MongoEntity entity) { - var modifiedBy = entity as ITrackLastModifiedByEntity; - - if (modifiedBy != null) + if (entity is IEntityWithLastModifiedBy withModifiedBy) { - modifiedBy.LastModifiedBy = @event.Actor; + withModifiedBy.LastModifiedBy = @event.Actor; } } private static void SetAppId(SquidexEvent @event, MongoEntity entity) { - var appEntity = entity as IAppRefEntity; - var appEvent = @event as AppEvent; - - if (appEntity != null && appEvent != null) + if (entity is IAppRefEntity appEntity && @event is AppEvent appEvent) { appEntity.AppId = appEvent.AppId.Id; } diff --git a/src/Squidex.Read.MongoDb/Utils/MongoCollectionExtensions.cs b/src/Squidex.Read.MongoDb/Utils/MongoCollectionExtensions.cs index a3fee940e..4bdd27df8 100644 --- a/src/Squidex.Read.MongoDb/Utils/MongoCollectionExtensions.cs +++ b/src/Squidex.Read.MongoDb/Utils/MongoCollectionExtensions.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using MongoDB.Driver; using Squidex.Events; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.MongoDb; namespace Squidex.Read.MongoDb.Utils diff --git a/src/Squidex.Read/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Read/Apps/AppHistoryEventsCreator.cs index a6f0360dc..e561c70e2 100644 --- a/src/Squidex.Read/Apps/AppHistoryEventsCreator.cs +++ b/src/Squidex.Read/Apps/AppHistoryEventsCreator.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Squidex.Events.Apps; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; using Squidex.Read.History; diff --git a/src/Squidex.Read/Apps/IAppEntity.cs b/src/Squidex.Read/Apps/IAppEntity.cs index 79b7aa401..455aa00dd 100644 --- a/src/Squidex.Read/Apps/IAppEntity.cs +++ b/src/Squidex.Read/Apps/IAppEntity.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure; namespace Squidex.Read.Apps { - public interface IAppEntity : IEntity + public interface IAppEntity : IEntity, IEntityWithVersion { string Name { get; } diff --git a/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs b/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs index 69c3f1493..3537e873b 100644 --- a/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs +++ b/src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs @@ -18,7 +18,7 @@ using Squidex.Read.Utils; namespace Squidex.Read.Apps.Services.Implementations { - public class CachingAppProvider : CachingProvider, IAppProvider + public class CachingAppProvider : CachingProviderBase, IAppProvider { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30); private readonly IAppRepository repository; diff --git a/src/Squidex.Read.MongoDb/Contents/Visitors/SchemaExtensions.cs b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs similarity index 57% rename from src/Squidex.Read.MongoDb/Contents/Visitors/SchemaExtensions.cs rename to src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs index 7a9cf44ca..3acb4e287 100644 --- a/src/Squidex.Read.MongoDb/Contents/Visitors/SchemaExtensions.cs +++ b/src/Squidex.Read/Contents/Builders/EdmModelBuilder.cs @@ -1,5 +1,5 @@ // ========================================================================== -// SchemaExtensions.cs +// EdmModelBuilder.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,19 +8,45 @@ using System; using System.Collections.Generic; -using Microsoft.OData.Core.UriParser; +using System.Linq; +using Microsoft.Extensions.Caching.Memory; using Microsoft.OData.Edm; using Microsoft.OData.Edm.Library; using Squidex.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Read.Schemas; +using Squidex.Read.Utils; -namespace Squidex.Read.MongoDb.Contents.Visitors +namespace Squidex.Read.Contents.Builders { - public static class SchemaExtensions + public sealed class EdmModelBuilder : CachingProviderBase { - public static EdmModel BuildEdmModel(this Schema schema, HashSet languages) + public EdmModelBuilder(IMemoryCache cache) + : base(cache) + { + } + + public IEdmModel BuildEdmModel(ISchemaEntityWithSchema schemaEntity, HashSet languages) + { + Guard.NotNull(languages, nameof(languages)); + Guard.NotNull(schemaEntity, nameof(schemaEntity)); + + var cacheKey = $"{schemaEntity.Id}_{schemaEntity.Version}_{string.Join(",", languages.Select(x => x.Iso2Code).OrderBy(x => x))}"; + + var result = Cache.GetOrCreate(cacheKey, entry => + { + entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(60); + + return BuildEdmModel(schemaEntity.Schema, languages); + }); + + return result; + } + + private static EdmModel BuildEdmModel(Schema schema, HashSet languages) { var model = new EdmModel(); + var container = new EdmEntityContainer("Squidex", "Container"); var schemaType = schema.BuildEdmType(languages, x => @@ -41,18 +67,9 @@ namespace Squidex.Read.MongoDb.Contents.Visitors model.AddElement(schemaType); model.AddElement(entityType); - container.AddEntitySet($"{schema.Name}_Set", entityType); + container.AddEntitySet("ContentSet", entityType); return model; } - - public static ODataUriParser ParseQuery(this Schema schema, HashSet languages, string query) - { - var model = schema.BuildEdmModel(languages); - - var parser = new ODataUriParser(model, new Uri($"{schema.Name}_Set?{query}", UriKind.Relative)); - - return parser; - } } } diff --git a/src/Squidex.Read/Contents/ContentHistoryEventsCreator.cs b/src/Squidex.Read/Contents/ContentHistoryEventsCreator.cs index 29e9aa32d..3e62195a3 100644 --- a/src/Squidex.Read/Contents/ContentHistoryEventsCreator.cs +++ b/src/Squidex.Read/Contents/ContentHistoryEventsCreator.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Squidex.Events.Contents; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Read.History; diff --git a/src/Squidex.Read/Contents/IContentEntity.cs b/src/Squidex.Read/Contents/IContentEntity.cs index 78460b80e..f672b129c 100644 --- a/src/Squidex.Read/Contents/IContentEntity.cs +++ b/src/Squidex.Read/Contents/IContentEntity.cs @@ -10,7 +10,7 @@ using Squidex.Core.Contents; namespace Squidex.Read.Contents { - public interface IContentEntity : IAppRefEntity, ITrackCreatedByEntity, ITrackLastModifiedByEntity + public interface IContentEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { bool IsPublished { get; } diff --git a/src/Squidex.Read/History/HistoryEventsCreatorBase.cs b/src/Squidex.Read/History/HistoryEventsCreatorBase.cs index 824a39140..c41c8a7b3 100644 --- a/src/Squidex.Read/History/HistoryEventsCreatorBase.cs +++ b/src/Squidex.Read/History/HistoryEventsCreatorBase.cs @@ -9,7 +9,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; // ReSharper disable ConvertIfStatementToReturnStatement diff --git a/src/Squidex.Read/History/IHistoryEventsCreator.cs b/src/Squidex.Read/History/IHistoryEventsCreator.cs index bae86c240..6fafe77cb 100644 --- a/src/Squidex.Read/History/IHistoryEventsCreator.cs +++ b/src/Squidex.Read/History/IHistoryEventsCreator.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Read.History diff --git a/src/Squidex.Read/ITrackCreatedByEntity.cs b/src/Squidex.Read/IEntityWithCreatedBy.cs similarity index 91% rename from src/Squidex.Read/ITrackCreatedByEntity.cs rename to src/Squidex.Read/IEntityWithCreatedBy.cs index 6d79a3f7f..5aad9c2f9 100644 --- a/src/Squidex.Read/ITrackCreatedByEntity.cs +++ b/src/Squidex.Read/IEntityWithCreatedBy.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Read { - public interface ITrackCreatedByEntity + public interface IEntityWithCreatedBy { RefToken CreatedBy { get; set; } } diff --git a/src/Squidex.Read/ITrackLastModifiedByEntity.cs b/src/Squidex.Read/IEntityWithLastModifiedBy.cs similarity index 90% rename from src/Squidex.Read/ITrackLastModifiedByEntity.cs rename to src/Squidex.Read/IEntityWithLastModifiedBy.cs index d19b36266..11976ab9a 100644 --- a/src/Squidex.Read/ITrackLastModifiedByEntity.cs +++ b/src/Squidex.Read/IEntityWithLastModifiedBy.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Read { - public interface ITrackLastModifiedByEntity + public interface IEntityWithLastModifiedBy { RefToken LastModifiedBy { get; set; } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventProcessor.cs b/src/Squidex.Read/IEntityWithVersion.cs similarity index 54% rename from src/Squidex.Infrastructure/CQRS/Events/IEventProcessor.cs rename to src/Squidex.Read/IEntityWithVersion.cs index 9a94a51fe..195e53bf2 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/IEventProcessor.cs +++ b/src/Squidex.Read/IEntityWithVersion.cs @@ -1,18 +1,15 @@ // ========================================================================== -// IEventProcessor.cs +// IEntityWithVersion.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS.Commands; - -namespace Squidex.Infrastructure.CQRS.Events +namespace Squidex.Read { - public interface IEventProcessor + public interface IEntityWithVersion { - Task ProcessEventAsync(Envelope @event, IAggregate aggregate, ICommand command); + long Version { get; set; } } } diff --git a/src/Squidex.Read/Schemas/ISchemaEntity.cs b/src/Squidex.Read/Schemas/ISchemaEntity.cs index 33c5479ce..d8ae691da 100644 --- a/src/Squidex.Read/Schemas/ISchemaEntity.cs +++ b/src/Squidex.Read/Schemas/ISchemaEntity.cs @@ -8,12 +8,14 @@ namespace Squidex.Read.Schemas { - public interface ISchemaEntity : IAppRefEntity, ITrackCreatedByEntity, ITrackLastModifiedByEntity + public interface ISchemaEntity : IAppRefEntity, IEntityWithCreatedBy, IEntityWithLastModifiedBy, IEntityWithVersion { string Name { get; } string Label { get; } bool IsPublished { get; } + + bool IsDeleted { get; } } } diff --git a/src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs b/src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs index ddb4c743e..d540c343c 100644 --- a/src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs +++ b/src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs @@ -24,7 +24,5 @@ namespace Squidex.Read.Schemas.Repositories Task FindSchemaAsync(Guid appId, string name); Task FindSchemaAsync(Guid schemaId); - - Task FindSchemaIdAsync(Guid appId, string name); } } diff --git a/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs b/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs index dcbb779b7..9c395f9a3 100644 --- a/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs +++ b/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs @@ -10,7 +10,6 @@ using System.Threading.Tasks; using Squidex.Events; using Squidex.Events.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Read.History; @@ -65,25 +64,21 @@ namespace Squidex.Read.Schemas protected override Task CreateEventCoreAsync(Envelope @event) { - var schemaEvent = @event.Payload as SchemaEvent; - - if (schemaEvent == null) + if (@event.Payload is SchemaEvent schemaEvent) { - return Task.FromResult(null); - } + string channel = $"schemas.{schemaEvent.SchemaId.Name}"; - string channel = $"schemas.{schemaEvent.SchemaId.Name}"; + var result = ForEvent(@event.Payload, channel).AddParameter("Name", schemaEvent.SchemaId.Name); - var result = ForEvent(@event.Payload, channel).AddParameter("Name", schemaEvent.SchemaId.Name); + if (schemaEvent is FieldEvent fieldEvent) + { + result.AddParameter("Field", fieldEvent.FieldId.Name); + } - var fieldEvent = schemaEvent as FieldEvent; - - if (fieldEvent != null) - { - result.AddParameter("Field", fieldEvent.FieldId.Name); + return Task.FromResult(result); } - return Task.FromResult(result); + return Task.FromResult(null); } } } \ No newline at end of file diff --git a/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs b/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs index 02dfcf03b..73b8a1df8 100644 --- a/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs +++ b/src/Squidex.Read/Schemas/Services/ISchemaProvider.cs @@ -14,7 +14,7 @@ namespace Squidex.Read.Schemas.Services { public interface ISchemaProvider { - Task FindSchemaByIdAsync(Guid id); + Task FindSchemaByIdAsync(Guid id, bool provideDeleted = false); Task FindSchemaByNameAsync(Guid appId, string name); diff --git a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs index 85cd97588..1d600f32f 100644 --- a/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs +++ b/src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs @@ -19,7 +19,7 @@ using Squidex.Read.Utils; namespace Squidex.Read.Schemas.Services.Implementations { - public class CachingSchemaProvider : CachingProvider, ISchemaProvider + public class CachingSchemaProvider : CachingProviderBase, ISchemaProvider { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); private readonly ISchemaRepository repository; @@ -32,7 +32,7 @@ namespace Squidex.Read.Schemas.Services.Implementations this.repository = repository; } - public async Task FindSchemaByIdAsync(Guid id) + public async Task FindSchemaByIdAsync(Guid id, bool provideDeleted = false) { var cacheKey = BuildIdCacheKey(id); @@ -48,6 +48,11 @@ namespace Squidex.Read.Schemas.Services.Implementations } } + if (result != null && result.IsDeleted && !provideDeleted) + { + result = null; + } + return result; } @@ -69,6 +74,11 @@ namespace Squidex.Read.Schemas.Services.Implementations } } + if (result != null && result.IsDeleted) + { + result = null; + } + return result; } diff --git a/src/Squidex.Read/Squidex.Read.csproj b/src/Squidex.Read/Squidex.Read.csproj index 911f23a17..fca27e890 100644 --- a/src/Squidex.Read/Squidex.Read.csproj +++ b/src/Squidex.Read/Squidex.Read.csproj @@ -13,7 +13,12 @@ - + + + + C:\Users\mail2\.nuget\packages\identityserver4\1.1.1\lib\netstandard1.4\IdentityServer4.dll + + diff --git a/src/Squidex.Read/Utils/CachingProvider.cs b/src/Squidex.Read/Utils/CachingProviderBase.cs similarity index 83% rename from src/Squidex.Read/Utils/CachingProvider.cs rename to src/Squidex.Read/Utils/CachingProviderBase.cs index 84f12e4ff..00a3491c6 100644 --- a/src/Squidex.Read/Utils/CachingProvider.cs +++ b/src/Squidex.Read/Utils/CachingProviderBase.cs @@ -1,5 +1,5 @@ // ========================================================================== -// CachingProvider.cs +// CachingProviderBase.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -11,7 +11,7 @@ using Squidex.Infrastructure; namespace Squidex.Read.Utils { - public abstract class CachingProvider + public abstract class CachingProviderBase { private readonly IMemoryCache cache; @@ -20,7 +20,7 @@ namespace Squidex.Read.Utils get { return cache; } } - protected CachingProvider(IMemoryCache cache) + protected CachingProviderBase(IMemoryCache cache) { Guard.NotNull(cache, nameof(cache)); diff --git a/src/Squidex.Write/Apps/AppCommandHandler.cs b/src/Squidex.Write/Apps/AppCommandHandler.cs index d77de95f9..6657528e2 100644 --- a/src/Squidex.Write/Apps/AppCommandHandler.cs +++ b/src/Squidex.Write/Apps/AppCommandHandler.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Tasks; using Squidex.Read.Apps.Repositories; using Squidex.Read.Users.Repositories; using Squidex.Write.Apps.Commands; @@ -51,11 +52,11 @@ namespace Squidex.Write.Apps throw new ValidationException("Cannot create a new app", error); } - await handler.CreateAsync(command, x => + await handler.CreateAsync(context, a => { - x.Create(command); + a.Create(command); - context.Succeed(command.AggregateId); + context.Succeed(EntityCreatedResult.Create(a.Id, a.Version)); }); } @@ -70,55 +71,52 @@ namespace Squidex.Write.Apps throw new ValidationException("Cannot assign contributor to app", error); } - await handler.UpdateAsync(command, x => - { - x.AssignContributor(command); - }); + await handler.UpdateAsync(context, a => a.AssignContributor(command)); } protected Task On(AttachClient command, CommandContext context) { - return handler.UpdateAsync(command, x => + return handler.UpdateAsync(context, a => { - x.AttachClient(command, keyGenerator.GenerateKey()); + a.AttachClient(command, keyGenerator.GenerateKey()); - context.Succeed(x.Clients[command.Id]); + context.Succeed(EntityCreatedResult.Create(a.Clients[command.Id], a.Version)); }); } protected Task On(RemoveContributor command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RemoveContributor(command)); + return handler.UpdateAsync(context, a => a.RemoveContributor(command)); } protected Task On(RenameClient command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RenameClient(command)); + return handler.UpdateAsync(context, a => a.RenameClient(command)); } protected Task On(RevokeClient command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RevokeClient(command)); + return handler.UpdateAsync(context, a => a.RevokeClient(command)); } protected Task On(AddLanguage command, CommandContext context) { - return handler.UpdateAsync(command, x => x.AddLanguage(command)); + return handler.UpdateAsync(context, a => a.AddLanguage(command)); } protected Task On(RemoveLanguage command, CommandContext context) { - return handler.UpdateAsync(command, x => x.RemoveLanguage(command)); + return handler.UpdateAsync(context, a => a.RemoveLanguage(command)); } protected Task On(SetMasterLanguage command, CommandContext context) { - return handler.UpdateAsync(command, x => x.SetMasterLanguage(command)); + return handler.UpdateAsync(context, a => a.SetMasterLanguage(command)); } public Task HandleAsync(CommandContext context) { - return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); + return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); } } } diff --git a/src/Squidex.Write/Apps/AppDomainObject.cs b/src/Squidex.Write/Apps/AppDomainObject.cs index c2ee22d62..b1d8a1aae 100644 --- a/src/Squidex.Write/Apps/AppDomainObject.cs +++ b/src/Squidex.Write/Apps/AppDomainObject.cs @@ -22,7 +22,7 @@ using Squidex.Write.Apps.Commands; namespace Squidex.Write.Apps { - public class AppDomainObject : DomainObject + public class AppDomainObject : DomainObjectBase { private static readonly Language DefaultLanguage = Language.EN; private readonly AppContributors contributors = new AppContributors(); diff --git a/src/Squidex.Write/Apps/ClientKeyGenerator.cs b/src/Squidex.Write/Apps/ClientKeyGenerator.cs index 965dc676e..a316b9061 100644 --- a/src/Squidex.Write/Apps/ClientKeyGenerator.cs +++ b/src/Squidex.Write/Apps/ClientKeyGenerator.cs @@ -16,14 +16,21 @@ namespace Squidex.Write.Apps { public class ClientKeyGenerator { + private readonly Func algorithmFactory; + + public ClientKeyGenerator() + { + algorithmFactory = SHA256.Create; + } + public virtual string GenerateKey() { - return Sha256(Guid.NewGuid().ToString()); + return Hash(Guid.NewGuid().ToString()); } - private static string Sha256(string input) + private string Hash(string input) { - using (var sha = SHA256.Create()) + using (var sha = algorithmFactory()) { var bytes = Encoding.UTF8.GetBytes(input); var hash = sha.ComputeHash(bytes); diff --git a/src/Squidex.Write/Contents/ContentCommandHandler.cs b/src/Squidex.Write/Contents/ContentCommandHandler.cs index e63785ca8..8b6e74739 100644 --- a/src/Squidex.Write/Contents/ContentCommandHandler.cs +++ b/src/Squidex.Write/Contents/ContentCommandHandler.cs @@ -9,9 +9,11 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Squidex.Core; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Tasks; using Squidex.Read.Apps.Services; using Squidex.Read.Schemas.Services; using Squidex.Write.Contents.Commands; @@ -22,31 +24,32 @@ namespace Squidex.Write.Contents { private readonly IAggregateHandler handler; private readonly IAppProvider appProvider; - private readonly ISchemaProvider schemaProvider; + private readonly ISchemaProvider schemas; public ContentCommandHandler( IAggregateHandler handler, IAppProvider appProvider, - ISchemaProvider schemaProvider) + ISchemaProvider schemas) { Guard.NotNull(handler, nameof(handler)); + Guard.NotNull(schemas, nameof(schemas)); Guard.NotNull(appProvider, nameof(appProvider)); - Guard.NotNull(schemaProvider, nameof(schemaProvider)); this.handler = handler; + this.schemas = schemas; + this.appProvider = appProvider; - this.schemaProvider = schemaProvider; } protected async Task On(CreateContent command, CommandContext context) { - await ValidateAsync(command, () => "Failed to create content"); + await ValidateAsync(command, () => "Failed to create content", true); - await handler.CreateAsync(command, s => + await handler.CreateAsync(context, c => { - s.Create(command); + c.Create(command); - context.Succeed(command.ContentId); + context.Succeed(EntityCreatedResult.Create(command.Data, c.Version)); }); } @@ -54,45 +57,43 @@ namespace Squidex.Write.Contents { await ValidateAsync(command, () => "Failed to update content"); - await handler.UpdateAsync(command, s => s.Update(command)); + await handler.UpdateAsync(context, c => c.Update(command)); } protected async Task On(PatchContent command, CommandContext context) { await ValidateAsync(command, () => "Failed to patch content"); - await handler.UpdateAsync(command, s => s.Patch(command)); + await handler.UpdateAsync(context, c => c.Patch(command)); } protected Task On(PublishContent command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Publish(command)); + return handler.UpdateAsync(context, c => c.Publish(command)); } protected Task On(UnpublishContent command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Unpublish(command)); + return handler.UpdateAsync(context, c => c.Unpublish(command)); } protected Task On(DeleteContent command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Delete(command)); + return handler.UpdateAsync(context, c => c.Delete(command)); } public Task HandleAsync(CommandContext context) { - return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); + return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); } - private async Task ValidateAsync(ContentDataCommand command, Func message) + private async Task ValidateAsync(ContentDataCommand command, Func message, bool enrich = false) { Guard.Valid(command, nameof(command), message); - var taskForApp = - appProvider.FindAppByIdAsync(command.AppId.Id); + var taskForApp = appProvider.FindAppByIdAsync(command.AppId.Id); - var taskForSchema = - schemaProvider.FindSchemaByIdAsync(command.SchemaId.Id); + var taskForSchema = schemas.FindSchemaByIdAsync(command.SchemaId.Id); await Task.WhenAll(taskForApp, taskForSchema); @@ -101,14 +102,17 @@ namespace Squidex.Write.Contents var schemaObject = taskForSchema.Result.Schema; var schemaErrors = new List(); - await schemaObject.ValidateAsync(command.Data, schemaErrors, languages); - - schemaObject.Enrich(command.Data, languages); + await command.Data.ValidateAsync(schemaObject, languages, schemaErrors); if (schemaErrors.Count > 0) { throw new ValidationException(message(), schemaErrors); } + + if (enrich) + { + command.Data.Enrich(schemaObject, languages); + } } } } diff --git a/src/Squidex.Write/Contents/ContentDomainObject.cs b/src/Squidex.Write/Contents/ContentDomainObject.cs index 269fef8ee..f56eda13e 100644 --- a/src/Squidex.Write/Contents/ContentDomainObject.cs +++ b/src/Squidex.Write/Contents/ContentDomainObject.cs @@ -18,7 +18,7 @@ using Squidex.Write.Contents.Commands; namespace Squidex.Write.Contents { - public class ContentDomainObject : DomainObject + public class ContentDomainObject : DomainObjectBase { private bool isDeleted; private bool isCreated; diff --git a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs index 2e8af2d13..d4af27a0f 100644 --- a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs +++ b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.Tasks; using Squidex.Read.Schemas.Services; using Squidex.Write.Schemas.Commands; @@ -41,77 +42,77 @@ namespace Squidex.Write.Schemas throw new ValidationException("Cannot create a new schema", error); } - await handler.CreateAsync(command, s => + await handler.CreateAsync(context, s => { s.Create(command); - context.Succeed(command.Name); + context.Succeed(EntityCreatedResult.Create(s.Id, s.Version)); }); } protected Task On(AddField command, CommandContext context) { - return handler.UpdateAsync(command, s => + return handler.UpdateAsync(context, s => { s.AddField(command); - context.Succeed(s.Schema.Fields.Values.First(x => x.Name == command.Name).Id); + context.Succeed(EntityCreatedResult.Create(s.Schema.Fields.Values.First(x => x.Name == command.Name).Id, s.Version)); }); } protected Task On(DeleteSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Delete(command)); + return handler.UpdateAsync(context, s => s.Delete(command)); } protected Task On(DeleteField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.DeleteField(command)); + return handler.UpdateAsync(context, s => s.DeleteField(command)); } protected Task On(DisableField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.DisableField(command)); + return handler.UpdateAsync(context, s => s.DisableField(command)); } protected Task On(EnableField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.EnableField(command)); + return handler.UpdateAsync(context, s => s.EnableField(command)); } protected Task On(HideField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.HideField(command)); + return handler.UpdateAsync(context, s => s.HideField(command)); } protected Task On(ShowField command, CommandContext context) { - return handler.UpdateAsync(command, s => s.ShowField(command)); + return handler.UpdateAsync(context, s => s.ShowField(command)); } protected Task On(UpdateSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => s.Update(command)); + return handler.UpdateAsync(context, s => s.Update(command)); } protected Task On(UpdateField command, CommandContext context) { - return handler.UpdateAsync(command, s => { s.UpdateField(command); }); + return handler.UpdateAsync(context, s => s.UpdateField(command)); } protected Task On(PublishSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => { s.Publish(command); }); + return handler.UpdateAsync(context, s => s.Publish(command)); } protected Task On(UnpublishSchema command, CommandContext context) { - return handler.UpdateAsync(command, s => { s.Unpublish(command); }); + return handler.UpdateAsync(context, s => s.Unpublish(command)); } public Task HandleAsync(CommandContext context) { - return context.IsHandled ? Task.FromResult(false) : this.DispatchActionAsync(context.Command, context); + return context.IsHandled ? TaskHelper.False : this.DispatchActionAsync(context.Command, context); } } } diff --git a/src/Squidex.Write/Schemas/SchemaDomainObject.cs b/src/Squidex.Write/Schemas/SchemaDomainObject.cs index edaf8d9f3..bda7fdab9 100644 --- a/src/Squidex.Write/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Write/Schemas/SchemaDomainObject.cs @@ -19,7 +19,7 @@ using Squidex.Write.Schemas.Commands; namespace Squidex.Write.Schemas { - public class SchemaDomainObject : DomainObject + public class SchemaDomainObject : DomainObjectBase { private readonly FieldRegistry registry; private bool isDeleted; diff --git a/src/Squidex.Write/SquidexCommand.cs b/src/Squidex.Write/SquidexCommand.cs index ae3637166..aed4baa52 100644 --- a/src/Squidex.Write/SquidexCommand.cs +++ b/src/Squidex.Write/SquidexCommand.cs @@ -14,5 +14,7 @@ namespace Squidex.Write public abstract class SquidexCommand : ICommand { public RefToken Actor { get; set; } + + public long? ExpectedVersion { get; set; } } } diff --git a/src/Squidex/Config/Constants.cs b/src/Squidex/Config/Constants.cs index 0c994fb2a..f1a9f9385 100644 --- a/src/Squidex/Config/Constants.cs +++ b/src/Squidex/Config/Constants.cs @@ -8,8 +8,10 @@ namespace Squidex.Config { - public class Constants + public static class Constants { + public static readonly string SecurityDefinition = "oauth-client-auth"; + public static readonly string ApiPrefix = "/api"; public static readonly string ApiScope = "squidex-api"; diff --git a/src/Squidex/Config/Domain/ClusterModule.cs b/src/Squidex/Config/Domain/ClusterModule.cs index 6087526a4..1e220f1d5 100644 --- a/src/Squidex/Config/Domain/ClusterModule.cs +++ b/src/Squidex/Config/Domain/ClusterModule.cs @@ -18,7 +18,7 @@ namespace Squidex.Config.Domain { public class ClusterModule : Module { - public IConfiguration Configuration { get; } + private IConfiguration Configuration { get; } public ClusterModule(IConfiguration configuration) { diff --git a/src/Squidex/Config/Domain/EventStoreModule.cs b/src/Squidex/Config/Domain/EventStoreModule.cs index 9e6f0f4fd..ccc10e035 100644 --- a/src/Squidex/Config/Domain/EventStoreModule.cs +++ b/src/Squidex/Config/Domain/EventStoreModule.cs @@ -19,7 +19,7 @@ namespace Squidex.Config.Domain { public class EventStoreModule : Module { - public IConfiguration Configuration { get; } + private IConfiguration Configuration { get; } public EventStoreModule(IConfiguration configuration) { diff --git a/src/Squidex/Config/Domain/InfrastructureModule.cs b/src/Squidex/Config/Domain/InfrastructureModule.cs index b430e5f15..68e3851af 100644 --- a/src/Squidex/Config/Domain/InfrastructureModule.cs +++ b/src/Squidex/Config/Domain/InfrastructureModule.cs @@ -20,11 +20,13 @@ using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Events; +// ReSharper disable UnusedAutoPropertyAccessor.Local + namespace Squidex.Config.Domain { public class InfrastructureModule : Module { - public IConfiguration Configuration { get; } + private IConfiguration Configuration { get; } public InfrastructureModule(IConfiguration configuration) { diff --git a/src/Squidex/Config/Domain/ReadModule.cs b/src/Squidex/Config/Domain/ReadModule.cs index 980b63975..eec94620a 100644 --- a/src/Squidex/Config/Domain/ReadModule.cs +++ b/src/Squidex/Config/Domain/ReadModule.cs @@ -12,16 +12,19 @@ using Squidex.Read.Apps; using Squidex.Read.Apps.Services; using Squidex.Read.Apps.Services.Implementations; using Squidex.Read.Contents; +using Squidex.Read.Contents.Builders; using Squidex.Read.History; using Squidex.Read.Schemas; using Squidex.Read.Schemas.Services; using Squidex.Read.Schemas.Services.Implementations; +// ReSharper disable UnusedAutoPropertyAccessor.Local + namespace Squidex.Config.Domain { public sealed class ReadModule : Module { - public IConfiguration Configuration { get; } + private IConfiguration Configuration { get; } public ReadModule(IConfiguration configuration) { @@ -49,6 +52,10 @@ namespace Squidex.Config.Domain builder.RegisterType() .As() .SingleInstance(); + + builder.RegisterType() + .AsSelf() + .SingleInstance(); } } } diff --git a/src/Squidex/Config/Domain/Serializers.cs b/src/Squidex/Config/Domain/Serializers.cs index 3f23d2323..29c3b5733 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/src/Squidex/Config/Domain/Serializers.cs @@ -10,7 +10,6 @@ using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; using NodaTime; using NodaTime.Serialization.JsonNet; using Squidex.Events; @@ -21,22 +20,21 @@ namespace Squidex.Config.Domain { public static class Serializers { - private static readonly TypeNameRegistry typeNameRegistry = new TypeNameRegistry(); + private static readonly TypeNameRegistry TypeNameRegistry = new TypeNameRegistry(); private static JsonSerializerSettings ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) { - settings.SerializationBinder = new TypeNameSerializationBinder(typeNameRegistry); - - settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - - settings.Converters.Add(new InstantConverter()); - settings.Converters.Add(new LanguageConverter()); - settings.Converters.Add(new NamedGuidIdConverter()); - settings.Converters.Add(new NamedLongIdConverter()); - settings.Converters.Add(new NamedStringIdConverter()); - settings.Converters.Add(new PropertiesBagConverter()); - settings.Converters.Add(new RefTokenConverter()); - settings.Converters.Add(new StringEnumConverter()); + settings.SerializationBinder = new TypeNameSerializationBinder(TypeNameRegistry); + + settings.ContractResolver = new ConverterContractResolver( + new InstantConverter(), + new LanguageConverter(), + new NamedGuidIdConverter(), + new NamedLongIdConverter(), + new NamedStringIdConverter(), + new PropertiesBagConverter(), + new RefTokenConverter(), + new StringEnumConverter()); settings.NullValueHandling = NullValueHandling.Ignore; @@ -52,7 +50,7 @@ namespace Squidex.Config.Domain static Serializers() { - typeNameRegistry.Map(typeof(SquidexEvent).GetTypeInfo().Assembly); + TypeNameRegistry.Map(typeof(SquidexEvent).GetTypeInfo().Assembly); } private static JsonSerializerSettings CreateSettings() @@ -67,7 +65,7 @@ namespace Squidex.Config.Domain public static IServiceCollection AddMyEventFormatter(this IServiceCollection services) { - services.AddSingleton(t => typeNameRegistry); + services.AddSingleton(t => TypeNameRegistry); services.AddSingleton(t => CreateSettings()); services.AddSingleton(t => CreateSerializer(t.GetRequiredService())); diff --git a/src/Squidex/Config/Domain/StoreModule.cs b/src/Squidex/Config/Domain/StoreModule.cs index 6da92ede5..194aff43a 100644 --- a/src/Squidex/Config/Domain/StoreModule.cs +++ b/src/Squidex/Config/Domain/StoreModule.cs @@ -15,7 +15,7 @@ namespace Squidex.Config.Domain { public class StoreModule : Module { - public IConfiguration Configuration { get; } + private IConfiguration Configuration { get; } public StoreModule(IConfiguration configuration) { diff --git a/src/Squidex/Config/Domain/StoreMongoDbModule.cs b/src/Squidex/Config/Domain/StoreMongoDbModule.cs index 4a152f644..5999a9215 100644 --- a/src/Squidex/Config/Domain/StoreMongoDbModule.cs +++ b/src/Squidex/Config/Domain/StoreMongoDbModule.cs @@ -32,9 +32,10 @@ namespace Squidex.Config.Domain { public class StoreMongoDbModule : Module { - private const string MongoDatabaseName = "string"; + private const string MongoDatabaseName = "MongoDatabaseName"; + private const string MongoDatabaseNameContent = "MongoDatabaseNameContent"; - public IConfiguration Configuration { get; } + private IConfiguration Configuration { get; } public StoreMongoDbModule(IConfiguration configuration) { @@ -57,13 +58,24 @@ namespace Squidex.Config.Domain throw new ConfigurationException("You must specify the MongoDB connection string in the 'squidex:stores:mongoDb:connectionString' configuration section."); } - builder.Register(c => + var databaseNameContent = Configuration.GetValue("squidex:stores:mongoDb:databaseNameContent"); + + if (string.IsNullOrWhiteSpace(databaseNameContent)) { - var mongoDbClient = new MongoClient(connectionString); - var mongoDatabase = mongoDbClient.GetDatabase(databaseName); + databaseNameContent = databaseName; + } + + builder.Register(c => new MongoClient(connectionString)) + .As() + .SingleInstance(); - return mongoDatabase; - }).Named(MongoDatabaseName).SingleInstance(); + builder.Register(c => c.Resolve().GetDatabase(databaseName)) + .Named(MongoDatabaseName) + .SingleInstance(); + + builder.Register(c => c.Resolve().GetDatabase(databaseNameContent)) + .Named(MongoDatabaseNameContent) + .SingleInstance(); builder.Register>(c => { @@ -100,7 +112,7 @@ namespace Squidex.Config.Domain .SingleInstance(); builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseName)) + .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseNameContent)) .As() .As() .AsSelf() diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs index 767a48e86..66c169c90 100644 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ b/src/Squidex/Config/Domain/WriteModule.cs @@ -10,17 +10,18 @@ using Autofac; using Microsoft.Extensions.Configuration; using Squidex.Core.Schemas; using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.CQRS.Events; using Squidex.Pipeline.CommandHandlers; using Squidex.Write.Apps; using Squidex.Write.Contents; using Squidex.Write.Schemas; +// ReSharper disable UnusedAutoPropertyAccessor.Local + namespace Squidex.Config.Domain { public class WriteModule : Module { - public IConfiguration Configuration { get; } + private IConfiguration Configuration { get; } public WriteModule(IConfiguration configuration) { @@ -29,6 +30,10 @@ namespace Squidex.Config.Domain protected override void Load(ContainerBuilder builder) { + builder.RegisterType() + .As() + .SingleInstance(); + builder.RegisterType() .As() .SingleInstance(); @@ -45,10 +50,6 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() - .As() - .SingleInstance(); - builder.RegisterType() .AsSelf() .SingleInstance(); @@ -69,11 +70,15 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.Register>(c => (id => new AppDomainObject(id, 0))) + builder.RegisterType() + .As() + .SingleInstance(); + + builder.Register>(c => (id => new AppDomainObject(id, -1))) .AsSelf() .SingleInstance(); - builder.Register>(c => (id => new ContentDomainObject(id, 0))) + builder.Register>(c => (id => new ContentDomainObject(id, -1))) .AsSelf() .SingleInstance(); @@ -81,7 +86,7 @@ namespace Squidex.Config.Domain { var fieldRegistry = c.Resolve(); - return (id => new SchemaDomainObject(id, 0, fieldRegistry)); + return (id => new SchemaDomainObject(id, -1, fieldRegistry)); }) .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Config/Identity/IdentityServices.cs b/src/Squidex/Config/Identity/IdentityServices.cs index 8be829c7d..1333075b4 100644 --- a/src/Squidex/Config/Identity/IdentityServices.cs +++ b/src/Squidex/Config/Identity/IdentityServices.cs @@ -15,7 +15,6 @@ using IdentityModel; using IdentityServer4.Models; using IdentityServer4.Stores; using Microsoft.AspNetCore.DataProtection; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity.MongoDB; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -59,7 +58,7 @@ namespace Squidex.Config.Identity return services; } - public static IServiceCollection AddMyIdentityServer(this IServiceCollection services, IHostingEnvironment env) + public static IServiceCollection AddMyIdentityServer(this IServiceCollection services) { X509Certificate2 certificate; diff --git a/src/Squidex/Config/Identity/IdentityUsage.cs b/src/Squidex/Config/Identity/IdentityUsage.cs index c83eb7a51..1fef70487 100644 --- a/src/Squidex/Config/Identity/IdentityUsage.cs +++ b/src/Squidex/Config/Identity/IdentityUsage.cs @@ -19,6 +19,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; using Squidex.Core.Identity; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf @@ -140,7 +141,7 @@ namespace Squidex.Config.Identity { context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); - return Task.FromResult(true); + return TaskHelper.Done; } public override async Task CreatingTicket(OAuthCreatingTicketContext context) diff --git a/src/Squidex/Config/Swagger/SwaggerServices.cs b/src/Squidex/Config/Swagger/SwaggerServices.cs index 5a0c5ee28..8eb5836fc 100644 --- a/src/Squidex/Config/Swagger/SwaggerServices.cs +++ b/src/Squidex/Config/Swagger/SwaggerServices.cs @@ -26,13 +26,13 @@ namespace Squidex.Config.Swagger { services.AddSingleton(typeof(SwaggerOwinSettings), s => { - var options = s.GetService>().Value; + var urlOptions = s.GetService>().Value; var settings = new SwaggerOwinSettings { Title = "Squidex API Specification", IsAspNetCore = false } - .ConfigurePaths() + .ConfigurePaths(urlOptions) .ConfigureSchemaSettings() - .ConfigureIdentity(options); + .ConfigureIdentity(urlOptions); return settings; }); @@ -43,20 +43,24 @@ namespace Squidex.Config.Swagger private static SwaggerOwinSettings ConfigureIdentity(this SwaggerOwinSettings settings, MyUrlsOptions urlOptions) { settings.DocumentProcessors.Add( - new SecurityDefinitionAppender("OAuth2", SwaggerHelper.CreateOAuthSchema(urlOptions))); + new SecurityDefinitionAppender(Constants.SecurityDefinition, SwaggerHelper.CreateOAuthSchema(urlOptions))); - settings.OperationProcessors.Add(new OperationSecurityScopeProcessor("roles")); + settings.OperationProcessors.Add(new OperationSecurityScopeProcessor(Constants.SecurityDefinition)); return settings; } - private static SwaggerOwinSettings ConfigurePaths(this SwaggerOwinSettings settings) + private static SwaggerOwinSettings ConfigurePaths(this SwaggerOwinSettings settings, MyUrlsOptions urlOptions) { settings.SwaggerRoute = $"{Constants.ApiPrefix}/swagger/v1/swagger.json"; settings.PostProcess = document => { document.BasePath = Constants.ApiPrefix; + document.Info.ExtensionData = new Dictionary + { + ["x-logo"] = new { url = urlOptions.BuildUrl("images/logo-white.png", false), backgroundColor = "#3f83df" } + }; }; settings.MiddlewareBasePath = Constants.ApiPrefix; diff --git a/src/Squidex/Config/Swagger/XmlTagProcessor.cs b/src/Squidex/Config/Swagger/XmlTagProcessor.cs index b104971fb..19847d1bb 100644 --- a/src/Squidex/Config/Swagger/XmlTagProcessor.cs +++ b/src/Squidex/Config/Swagger/XmlTagProcessor.cs @@ -12,6 +12,7 @@ using NJsonSchema.Infrastructure; using NSwag.Annotations; using NSwag.SwaggerGeneration.Processors; using NSwag.SwaggerGeneration.Processors.Contexts; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf @@ -57,7 +58,7 @@ namespace Squidex.Config.Swagger context.OperationDescription.Operation.Tags.Add(tagAttribute.Name); } - return Task.FromResult(true); + return TaskHelper.True; } } } diff --git a/src/Squidex/Config/Web/WebModule.cs b/src/Squidex/Config/Web/WebModule.cs index 0602845a1..23e1e5083 100644 --- a/src/Squidex/Config/Web/WebModule.cs +++ b/src/Squidex/Config/Web/WebModule.cs @@ -10,11 +10,13 @@ using Autofac; using Microsoft.Extensions.Configuration; using Squidex.Pipeline; +// ReSharper disable UnusedAutoPropertyAccessor.Local + namespace Squidex.Config.Web { public class WebModule : Module { - public IConfiguration Configuration { get; } + private IConfiguration Configuration { get; } public WebModule(IConfiguration configuration) { diff --git a/src/Squidex/Config/Web/WebpackUsage.cs b/src/Squidex/Config/Web/WebpackUsages.cs similarity index 65% rename from src/Squidex/Config/Web/WebpackUsage.cs rename to src/Squidex/Config/Web/WebpackUsages.cs index a4d218631..d8470b47c 100644 --- a/src/Squidex/Config/Web/WebpackUsage.cs +++ b/src/Squidex/Config/Web/WebpackUsages.cs @@ -1,5 +1,5 @@ // ========================================================================== -// WebpackUsage.cs +// WebpackUsages.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -7,20 +7,12 @@ // ========================================================================== using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; using Squidex.Pipeline; namespace Squidex.Config.Web { - public static class WebpackExtensions + public static class WebpackUsages { - public static IApplicationBuilder UseWebpackBuilder(this IApplicationBuilder app) - { - app.ApplicationServices.GetRequiredService().Execute(); - - return app; - } - public static IApplicationBuilder UseWebpackProxy(this IApplicationBuilder app) { app.UseMiddleware(); diff --git a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs index fd5237a82..90807b329 100644 --- a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Controllers.Api.Apps.Models; using Squidex.Core.Identity; @@ -64,6 +65,8 @@ namespace Squidex.Controllers.Api.Apps var response = entity.Clients.Select(x => SimpleMapper.Map(x, new ClientDto())).ToList(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(response); } @@ -82,15 +85,15 @@ namespace Squidex.Controllers.Api.Apps /// [HttpPost] [Route("apps/{app}/clients/")] - [ProducesResponseType(typeof(ClientDto[]), 201)] + [ProducesResponseType(typeof(ClientDto), 201)] public async Task PostClient(string app, [FromBody] CreateAppClientDto request) { var context = await CommandBus.PublishAsync(SimpleMapper.Map(request, new AttachClient())); - var result = context.Result(); + var result = context.Result>().IdOrValue; var response = SimpleMapper.Map(result, new ClientDto()); - return StatusCode(201, response); + return CreatedAtAction(nameof(GetClients), new { app }, response); } /// @@ -105,7 +108,6 @@ namespace Squidex.Controllers.Api.Apps /// [HttpPut] [Route("apps/{app}/clients/{clientId}/")] - [ProducesResponseType(typeof(ClientDto[]), 201)] public async Task PutClient(string app, string clientId, [FromBody] UpdateAppClientDto request) { await CommandBus.PublishAsync(SimpleMapper.Map(request, new RenameClient { Id = clientId })); diff --git a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs index d94e47244..bd9133b3e 100644 --- a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; @@ -60,6 +61,8 @@ namespace Squidex.Controllers.Api.Apps var response = entity.Contributors.Select(x => SimpleMapper.Map(x, new ContributorDto())).ToList(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(response); } diff --git a/src/Squidex/Controllers/Api/Apps/AppController.cs b/src/Squidex/Controllers/Api/Apps/AppController.cs index e34195ab2..4e6a90b51 100644 --- a/src/Squidex/Controllers/Api/Apps/AppController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppController.cs @@ -92,9 +92,11 @@ namespace Squidex.Controllers.Api.Apps var command = SimpleMapper.Map(request, new CreateApp()); var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - return CreatedAtAction(nameof(GetApps), new EntityCreatedDto { Id = result.ToString() }); + var result = context.Result>(); + var response = new EntityCreatedDto { Id = result.ToString(), Version = result.Version }; + + return CreatedAtAction(nameof(GetApps), response); } } } diff --git a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs index 3dcc070b9..b1f37a5da 100644 --- a/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppLanguagesController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; @@ -66,6 +67,8 @@ namespace Squidex.Controllers.Api.Apps return SimpleMapper.Map(x, new AppLanguageDto { IsMasterLanguage = isMasterLanguage }); }).ToList(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(model); } @@ -89,7 +92,7 @@ namespace Squidex.Controllers.Api.Apps var response = SimpleMapper.Map(request.Language, new AppLanguageDto()); - return StatusCode(201, response); + return CreatedAtAction(nameof(GetLanguages), new { app }, response); } /// diff --git a/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs b/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs index 2321855ea..9e6a4b929 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs +++ b/src/Squidex/Controllers/Api/Apps/Models/AppDto.cs @@ -24,6 +24,11 @@ namespace Squidex.Controllers.Api.Apps.Models [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] public string Name { get; set; } + /// + /// The version of the app. + /// + public long Version { get; set; } + /// /// The name of the app. /// diff --git a/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs b/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs index 63ac305e0..86ca96ae2 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs +++ b/src/Squidex/Controllers/Api/Apps/Models/ClientDto.cs @@ -10,7 +10,7 @@ using System.ComponentModel.DataAnnotations; namespace Squidex.Controllers.Api.Apps.Models { - public sealed class ClientDto + public class ClientDto { /// /// The client id. diff --git a/src/Squidex/Controllers/Api/EntityCreatedDto.cs b/src/Squidex/Controllers/Api/EntityCreatedDto.cs index fad9e6c60..b60d8dbb4 100644 --- a/src/Squidex/Controllers/Api/EntityCreatedDto.cs +++ b/src/Squidex/Controllers/Api/EntityCreatedDto.cs @@ -10,12 +10,17 @@ using System.ComponentModel.DataAnnotations; namespace Squidex.Controllers.Api { - public class EntityCreatedDto + public sealed class EntityCreatedDto { /// /// Id of the created entity. /// [Required] public string Id { get; set; } + + /// + /// The new version of the entity. + /// + public long Version { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Languages/LanguagesController.cs b/src/Squidex/Controllers/Api/Languages/LanguagesController.cs index bed7d44b1..8f6ab922e 100644 --- a/src/Squidex/Controllers/Api/Languages/LanguagesController.cs +++ b/src/Squidex/Controllers/Api/Languages/LanguagesController.cs @@ -38,7 +38,7 @@ namespace Squidex.Controllers.Api.Languages [ProducesResponseType(typeof(string[]), 200)] public IActionResult GetLanguages() { - var response = Language.AllLanguages.Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList(); + var response = Language.AllLanguages.Where(x => x != Language.Invariant).Select(x => SimpleMapper.Map(x, new LanguageDto())).ToList(); return Ok(response); } diff --git a/src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs index 878968f2b..2345c38f8 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/AddFieldDto.cs @@ -18,7 +18,7 @@ namespace Squidex.Controllers.Api.Schemas.Models /// The name of the field. Must be unique within the schema. /// [Required] - [RegularExpression("^[a-z0-9]+(\\-[a-z0-9]+)*$")] + [RegularExpression("^[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*$")] public string Name { get; set; } /// diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs b/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs index 98df82247..d0c4e0a2c 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs @@ -38,6 +38,10 @@ namespace Squidex.Controllers.Api.Schemas.Models.Converters { typeof(BooleanFieldProperties), p => Convert((BooleanFieldProperties)p) + }, + { + typeof(GeolocationFieldProperties), + p => Convert((GeolocationFieldProperties)p) } }; @@ -85,6 +89,13 @@ namespace Squidex.Controllers.Api.Schemas.Models.Converters return result; } + private static FieldPropertiesDto Convert(GeolocationFieldProperties source) + { + var result = SimpleMapper.Map(source, new GeolocationFieldPropertiesDto()); + + return result; + } + private static FieldPropertiesDto Convert(StringFieldProperties source) { var result = SimpleMapper.Map(source, new StringFieldPropertiesDto()); diff --git a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs index 1bd51e93f..1c8009a8a 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/FieldPropertiesDto.cs @@ -17,6 +17,7 @@ namespace Squidex.Controllers.Api.Schemas.Models [JsonConverter(typeof(JsonInheritanceConverter), "fieldType")] [KnownType(typeof(BooleanFieldPropertiesDto))] [KnownType(typeof(DateTimeFieldPropertiesDto))] + [KnownType(typeof(GeolocationFieldPropertiesDto))] [KnownType(typeof(JsonFieldPropertiesDto))] [KnownType(typeof(NumberFieldPropertiesDto))] [KnownType(typeof(StringFieldPropertiesDto))] diff --git a/src/Squidex/Controllers/Api/Schemas/Models/GeolocationPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/GeolocationPropertiesDto.cs new file mode 100644 index 000000000..a6f0333a9 --- /dev/null +++ b/src/Squidex/Controllers/Api/Schemas/Models/GeolocationPropertiesDto.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// GeolocationFieldPropertiesDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using NJsonSchema.Annotations; +using Squidex.Core.Schemas; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Controllers.Api.Schemas.Models +{ + [JsonSchema("Geolocation")] + public sealed class GeolocationFieldPropertiesDto : FieldPropertiesDto + { + /// + /// The default value for the field value. + /// + public bool? DefaultValue { get; set; } + + /// + /// The editor that is used to manage this field. + /// + [JsonConverter(typeof(StringEnumConverter))] + public GeolocationFieldEditor Editor { get; set; } + + public override FieldProperties ToProperties() + { + var result = SimpleMapper.Map(this, new GeolocationFieldProperties()); + + return result; + } + } +} diff --git a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs index 6636348e9..6d0e1b28c 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDetailsDto.cs @@ -72,5 +72,10 @@ namespace Squidex.Controllers.Api.Schemas.Models /// The date and time when the schema has been modified last. /// public Instant LastModified { get; set; } + + /// + /// The version of the schema. + /// + public int Version { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs index aa1e50b75..f01171462 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/SchemaDto.cs @@ -59,5 +59,10 @@ namespace Squidex.Controllers.Api.Schemas.Models /// The date and time when the schema has been modified last. /// public Instant LastModified { get; set; } + + /// + /// The version of the schema. + /// + public int Version { get; set; } } } diff --git a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs b/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs index 47b9d56ba..c44c6ba69 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs @@ -42,7 +42,7 @@ namespace Squidex.Controllers.Api.Schemas /// 201 => Schema field created. /// 409 => Schema field name already in use. /// 404 => App or schema not found. - /// 404 => Schema field properties not valid. + /// 400 => Schema field properties not valid. /// [HttpPost] [Route("apps/{app}/schemas/{name}/fields/")] @@ -54,9 +54,11 @@ namespace Squidex.Controllers.Api.Schemas var command = new AddField { Name = request.Name, Properties = request.Properties.ToProperties() }; var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - return StatusCode(201, new EntityCreatedDto { Id = result.ToString() }); + var result = context.Result>().IdOrValue; + var response = new EntityCreatedDto { Id = result.ToString() }; + + return StatusCode(201, response); } /// @@ -70,7 +72,7 @@ namespace Squidex.Controllers.Api.Schemas /// 204 => Schema field created. /// 409 => Schema field name already in use. /// 404 => App, schema or field not found. - /// 404 => Schema field properties not valid. + /// 400 => Schema field properties not valid. /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/")] diff --git a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs index 25bfbfd9f..a1e933331 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; using NSwag.Annotations; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; @@ -41,7 +42,7 @@ namespace Squidex.Controllers.Api.Schemas } /// - /// Get app schemas. + /// Get schemas. /// /// /// 200 => Schemas returned. @@ -80,6 +81,8 @@ namespace Squidex.Controllers.Api.Schemas var model = entity.ToModel(); + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(model); } diff --git a/src/Squidex/Controllers/ContentApi/ContentsController.cs b/src/Squidex/Controllers/ContentApi/ContentsController.cs index 4599cc4ae..cc002f906 100644 --- a/src/Squidex/Controllers/ContentApi/ContentsController.cs +++ b/src/Squidex/Controllers/ContentApi/ContentsController.cs @@ -12,7 +12,8 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Squidex.Controllers.Api; +using Microsoft.Extensions.Primitives; +using NSwag.Annotations; using Squidex.Controllers.ContentApi.Models; using Squidex.Core.Contents; using Squidex.Core.Identity; @@ -29,15 +30,17 @@ namespace Squidex.Controllers.ContentApi [Authorize(Roles = SquidexRoles.AppEditor)] [ApiExceptionFilter] [ServiceFilter(typeof(AppFilterAttribute))] + [SwaggerIgnore] public class ContentsController : ControllerBase { - private readonly ISchemaProvider schemaProvider; + private readonly ISchemaProvider schemas; private readonly IContentRepository contentRepository; - public ContentsController(ICommandBus commandBus, ISchemaProvider schemaProvider, IContentRepository contentRepository) + public ContentsController(ICommandBus commandBus, ISchemaProvider schemas, IContentRepository contentRepository) : base(commandBus) { - this.schemaProvider = schemaProvider; + this.schemas = schemas; + this.contentRepository = contentRepository; } @@ -45,7 +48,7 @@ namespace Squidex.Controllers.ContentApi [Route("content/{app}/{name}")] public async Task GetContents(string name, [FromQuery] bool nonPublished = false, [FromQuery] bool hidden = false) { - var schemaEntity = await schemaProvider.FindSchemaByNameAsync(AppId, name); + var schemaEntity = await schemas.FindSchemaByNameAsync(AppId, name); if (schemaEntity == null) { @@ -84,27 +87,29 @@ namespace Squidex.Controllers.ContentApi [Route("content/{app}/{name}/{id}")] public async Task GetContent(string name, Guid id, bool hidden = false) { - var schemaEntity = await schemaProvider.FindSchemaByNameAsync(AppId, name); + var schemaEntity = await schemas.FindSchemaByNameAsync(AppId, name); if (schemaEntity == null) { return NotFound(); } - var content = await contentRepository.FindContentAsync(schemaEntity.Id, id); + var entity = await contentRepository.FindContentAsync(schemaEntity.Id, id); - if (content == null) + if (entity == null) { return NotFound(); } - var model = SimpleMapper.Map(content, new ContentDto()); + var model = SimpleMapper.Map(entity, new ContentDto()); - if (content.Data != null) + if (entity.Data != null) { - model.Data = content.Data.ToApiModel(schemaEntity.Schema, App.Languages, App.MasterLanguage, hidden); + model.Data = entity.Data.ToApiModel(schemaEntity.Schema, App.Languages, App.MasterLanguage, hidden); } + Response.Headers["ETag"] = new StringValues(entity.Version.ToString()); + return Ok(model); } @@ -112,19 +117,23 @@ namespace Squidex.Controllers.ContentApi [Route("content/{app}/{name}/")] public async Task PostContent([FromBody] ContentData request) { - var command = new CreateContent { Data = request, ContentId = Guid.NewGuid() }; + var command = new CreateContent { ContentId = Guid.NewGuid(), Data = request.ToCleaned() }; var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - return CreatedAtAction(nameof(GetContent), new { id = result }, new EntityCreatedDto { Id = result.ToString() }); + var result = context.Result>(); + var response = ContentDto.Create(command, result); + + Response.Headers["ETag"] = new StringValues(response.Version.ToString()); + + return CreatedAtAction(nameof(GetContent), new { id = response.Id }, response); } [HttpPut] [Route("content/{app}/{name}/{id}")] public async Task PutContent(Guid id, [FromBody] ContentData request) { - var command = new UpdateContent { ContentId = id, Data = request }; + var command = new UpdateContent { ContentId = id, Data = request.ToCleaned() }; await CommandBus.PublishAsync(command); @@ -135,7 +144,7 @@ namespace Squidex.Controllers.ContentApi [Route("content/{app}/{name}/{id}")] public async Task PatchContent(Guid id, [FromBody] ContentData request) { - var command = new PatchContent { ContentId = id, Data = request }; + var command = new PatchContent { ContentId = id, Data = request.ToCleaned() }; await CommandBus.PublishAsync(command); diff --git a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs index 89f76fa9f..fc487d0a1 100644 --- a/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs +++ b/src/Squidex/Controllers/ContentApi/Generator/SchemasSwaggerGenerator.cs @@ -44,7 +44,6 @@ namespace Squidex.Controllers.ContentApi.Generator private readonly string schemaBodyDescription; private HashSet languages; private JsonSchema4 errorDtoSchema; - private JsonSchema4 entityCreatedDtoSchema; private string appBasePath; private IAppEntity app; @@ -80,6 +79,7 @@ namespace Squidex.Controllers.ContentApi.Generator GenerateSecurityDefinitions(); GenerateSecurityRequirements(); GenerateDefaultErrors(); + GeneratePing(); return document; } @@ -104,6 +104,10 @@ namespace Squidex.Controllers.ContentApi.Generator { document.Info = new SwaggerInfo { + ExtensionData = new Dictionary + { + ["x-logo"] = new { url = urlOptions.BuildUrl("images/logo-white.png", false), backgroundColor = "#3f83df" } + }, Title = $"Suidex API for {app.Name} App" }; } @@ -132,11 +136,6 @@ namespace Squidex.Controllers.ContentApi.Generator var errorSchema = JsonObjectTypeDescription.FromType(errorType, new Attribute[0], EnumHandling.String); errorDtoSchema = await swaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(errorType, errorSchema.IsNullable, null); - - var entityCreatedType = typeof(EntityCreatedDto); - var entityCreatedSchema = JsonObjectTypeDescription.FromType(entityCreatedType, new Attribute[0], EnumHandling.String); - - entityCreatedDtoSchema = await swaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(entityCreatedType, entityCreatedSchema.IsNullable, null); } private void GenerateSecurityRequirements() @@ -145,7 +144,7 @@ namespace Squidex.Controllers.ContentApi.Generator { new SwaggerSecurityRequirement { - { "roles", new List { SquidexRoles.AppOwner, SquidexRoles.AppDeveloper, SquidexRoles.AppEditor } } + { Constants.SecurityDefinition, new List { SquidexRoles.AppOwner, SquidexRoles.AppDeveloper, SquidexRoles.AppEditor } } } }; @@ -165,7 +164,7 @@ namespace Squidex.Controllers.ContentApi.Generator private void GenerateSchemasOperations(IEnumerable schemas) { - foreach (var schema in schemas.Select(x => x.Schema)) + foreach (var schema in schemas.Where(x => x.IsPublished).Select(x => x.Schema)) { GenerateSchemaOperations(schema); } @@ -173,26 +172,27 @@ namespace Squidex.Controllers.ContentApi.Generator private void GenerateSchemaOperations(Schema schema) { - var schemaName = schema.Properties.Label ?? schema.Name; + var schemaIdentifier = schema.Name.ToPascalCase(); + var schemaName = !string.IsNullOrWhiteSpace(schema.Properties.Label) ? schema.Properties.Label.Trim() : schema.Name; document.Tags.Add( new SwaggerTag { - Name = schemaName, Description = $"API to managed {schemaName} content." + Name = schemaName, Description = $"API to managed {schemaName} contents." }); - var dataSchem = AppendSchema($"{schema.Name}Dto", schema.BuildSchema(languages, AppendSchema)); + var dataSchema = AppendSchema($"{schemaIdentifier}Dto", schema.BuildJsonSchema(languages, AppendSchema)); var schemaOperations = new List { - GenerateSchemaQueryOperation(schema, schemaName, dataSchem), - GenerateSchemaCreateOperation(schema, schemaName, dataSchem), - GenerateSchemaGetOperation(schema, schemaName, dataSchem), - GenerateSchemaUpdateOperation(schema, schemaName, dataSchem), - GenerateSchemaPatchOperation(schema, schemaName, dataSchem), - GenerateSchemaPublishOperation(schema, schemaName), - GenerateSchemaUnpublishOperation(schema, schemaName), - GenerateSchemaDeleteOperation(schema, schemaName) + GenerateSchemaQueryOperation(schema, schemaName, schemaIdentifier, dataSchema), + GenerateSchemaCreateOperation(schema, schemaName, schemaIdentifier, dataSchema), + GenerateSchemaGetOperation(schema, schemaName, schemaIdentifier, dataSchema), + GenerateSchemaUpdateOperation(schema, schemaName, schemaIdentifier, dataSchema), + GenerateSchemaPatchOperation(schema, schemaName, schemaIdentifier, dataSchema), + GenerateSchemaPublishOperation(schema, schemaName, schemaIdentifier), + GenerateSchemaUnpublishOperation(schema, schemaName, schemaIdentifier), + GenerateSchemaDeleteOperation(schema, schemaName, schemaIdentifier) }; foreach (var operation in schemaOperations.SelectMany(x => x.Values).Distinct()) @@ -201,11 +201,31 @@ namespace Squidex.Controllers.ContentApi.Generator } } - private SwaggerOperations GenerateSchemaQueryOperation(Schema schema, string schemaName, JsonSchema4 dataSchema) + private void GeneratePing() + { + var swaggerOperation = AddOperation(SwaggerOperationMethod.Get, null, $"ping/{app.Name}", operation => + { + operation.OperationId = "MakePingTest"; + + operation.Description = "Make a simple request, e.g. to test credentials."; + + operation.Summary = "Make Test"; + + }); + + foreach (var operation in swaggerOperation.Values) + { + operation.Tags = new List { "PingTest" }; + } + } + + private SwaggerOperations GenerateSchemaQueryOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Get, null, $"{appBasePath}/{schema.Name}", operation => { - operation.Summary = $"Queries {schemaName} content."; + operation.OperationId = $"Query{schemaIdentifier}Contents"; + + operation.Summary = $"Queries {schemaName} contents."; operation.Description = schemaQueryDescription; @@ -221,78 +241,91 @@ namespace Squidex.Controllers.ContentApi.Generator }); } - private SwaggerOperations GenerateSchemaGetOperation(Schema schema, string schemaName, JsonSchema4 dataSchema) + private SwaggerOperations GenerateSchemaGetOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Get, schemaName, $"{appBasePath}/{schema.Name}/{{id}}", operation => { + operation.OperationId = $"Get{schemaIdentifier}Content"; + operation.Summary = $"Get a {schemaName} content."; - var responseSchema = CreateContentSchema(schemaName, schema.Name, dataSchema); + var responseSchema = CreateContentSchema(schemaName, schemaIdentifier, dataSchema); operation.AddResponse("200", $"{schemaName} content found.", responseSchema); }); } - private SwaggerOperations GenerateSchemaCreateOperation(Schema schema, string schemaName, JsonSchema4 dataSchema) + private SwaggerOperations GenerateSchemaCreateOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Post, null, $"{appBasePath}/{schema.Name}", operation => { + operation.OperationId = $"Create{schemaIdentifier}Content"; + operation.Summary = $"Create a {schemaName} content."; - operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); + var responseSchema = CreateContentSchema(schemaName, schemaIdentifier, dataSchema); - operation.AddResponse("201", $"{schemaName} created.", entityCreatedDtoSchema); + operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); + operation.AddResponse("201", $"{schemaName} created.", responseSchema); }); } - private SwaggerOperations GenerateSchemaUpdateOperation(Schema schema, string schemaName, JsonSchema4 dataSchema) + private SwaggerOperations GenerateSchemaUpdateOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}", operation => { + operation.OperationId = $"Update{schemaIdentifier}Content"; + operation.Summary = $"Update a {schemaName} content."; operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); - operation.AddResponse("204", $"{schemaName} element updated."); }); } - private SwaggerOperations GenerateSchemaPatchOperation(Schema schema, string schemaName, JsonSchema4 dataSchema) + private SwaggerOperations GenerateSchemaPatchOperation(Schema schema, string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { return AddOperation(SwaggerOperationMethod.Patch, schemaName, $"{appBasePath}/{schema.Name}/{{id}}", operation => { + operation.OperationId = $"Path{schemaIdentifier}Content"; + operation.Summary = $"Patchs a {schemaName} content."; operation.AddBodyParameter(dataSchema, "data", schemaBodyDescription); - operation.AddResponse("204", $"{schemaName} element updated."); }); } - private SwaggerOperations GenerateSchemaPublishOperation(Schema schema, string schemaName) + private SwaggerOperations GenerateSchemaPublishOperation(Schema schema, string schemaName, string schemaIdentifier) { return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/publish", operation => { + operation.OperationId = $"Publish{schemaIdentifier}Content"; + operation.Summary = $"Publish a {schemaName} content."; operation.AddResponse("204", $"{schemaName} element published."); }); } - private SwaggerOperations GenerateSchemaUnpublishOperation(Schema schema, string schemaName) + private SwaggerOperations GenerateSchemaUnpublishOperation(Schema schema, string schemaName, string schemaIdentifier) { return AddOperation(SwaggerOperationMethod.Put, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/unpublish", operation => { + operation.OperationId = $"Unpublish{schemaIdentifier}Content"; + operation.Summary = $"Unpublish a {schemaName} content."; operation.AddResponse("204", $"{schemaName} element unpublished."); }); } - private SwaggerOperations GenerateSchemaDeleteOperation(Schema schema, string schemaName) + private SwaggerOperations GenerateSchemaDeleteOperation(Schema schema, string schemaName, string schemaIdentifier) { return AddOperation(SwaggerOperationMethod.Delete, schemaName, $"{appBasePath}/{schema.Name}/{{id}}/", operation => { + operation.OperationId = $"Delete{schemaIdentifier}Content"; + operation.Summary = $"Delete a {schemaName} content."; operation.AddResponse("204", $"{schemaName} content deleted."); @@ -310,7 +343,7 @@ namespace Squidex.Controllers.ContentApi.Generator if (entityName != null) { - operation.AddPathParameter("id", JsonObjectType.String, $"The id of the {entityName} (GUID)."); + operation.AddPathParameter("id", JsonObjectType.String, $"The id of the {entityName} content (GUID)."); operation.AddResponse("404", $"App, schema or {entityName} content not found."); } @@ -341,29 +374,36 @@ namespace Squidex.Controllers.ContentApi.Generator return schema; } - private JsonSchema4 CreateContentSchema(string schemaName, string id, JsonSchema4 dataSchema) + private JsonSchema4 CreateContentSchema(string schemaName, string schemaIdentifier, JsonSchema4 dataSchema) { - var CreateProperty = - new Func((d, f) => - new JsonProperty { Description = d, Format = f, IsRequired = true, Type = JsonObjectType.String }); - var dataProperty = new JsonProperty { Description = schemaBodyDescription, Type = JsonObjectType.Object, IsRequired = true, SchemaReference = dataSchema }; var schema = new JsonSchema4 { Properties = { - ["id"] = CreateProperty($"The id of the {schemaName} content.", null), + ["id"] = CreateProperty($"The id of the {schemaName} content."), ["data"] = dataProperty, + ["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.", null), + ["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.", null) + ["lastModifiedBy"] = CreateProperty($"The user that has updated the {schemaName} content last.") }, Type = JsonObjectType.Object }; - return AppendSchema($"{id}ContentDto", schema); + return AppendSchema($"{schemaIdentifier}ContentDto", schema); + } + + 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 }; } private JsonSchema4 AppendSchema(string name, JsonSchema4 schema) diff --git a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs b/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs index f1e2693f6..a5fd1e83e 100644 --- a/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs +++ b/src/Squidex/Controllers/ContentApi/Models/ContentDto.cs @@ -9,7 +9,10 @@ using System; using System.ComponentModel.DataAnnotations; using NodaTime; +using Squidex.Core.Contents; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Write.Contents.Commands; namespace Squidex.Controllers.ContentApi.Models { @@ -52,5 +55,28 @@ namespace Squidex.Controllers.ContentApi.Models /// Indicates if the content element is publihed. /// public bool IsPublished { get; set; } + + /// + /// The version of the content. + /// + public long Version { get; set; } + + public static ContentDto Create(CreateContent command, EntityCreatedResult result) + { + var now = SystemClock.Instance.GetCurrentInstant(); + + var response = new ContentDto + { + Id = command.ContentId, + Data = result.IdOrValue, + Version = result.Version, + Created = now, + CreatedBy = command.Actor, + LastModified = now, + LastModifiedBy = command.Actor + }; + + return response; + } } } diff --git a/src/Squidex/Controllers/ContentApi/PingController.cs b/src/Squidex/Controllers/ContentApi/PingController.cs new file mode 100644 index 000000000..7f2db9b0e --- /dev/null +++ b/src/Squidex/Controllers/ContentApi/PingController.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// PingController.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Squidex.Core.Identity; +using Squidex.Pipeline; + +namespace Squidex.Controllers.ContentApi +{ + [Authorize(Roles = SquidexRoles.AppEditor)] + [ApiExceptionFilter] + [ServiceFilter(typeof(AppFilterAttribute))] + public class PingController : Controller + { + [HttpGet] + [Route("ping/{app}/")] + public IActionResult GetPing() + { + return Ok(); + } + } +} diff --git a/src/Squidex/Controllers/ControllerBase.cs b/src/Squidex/Controllers/ControllerBase.cs index ae8eb7f0b..93ba5ef00 100644 --- a/src/Squidex/Controllers/ControllerBase.cs +++ b/src/Squidex/Controllers/ControllerBase.cs @@ -23,11 +23,6 @@ namespace Squidex.Controllers CommandBus = commandBus; } - protected ControllerBase() - { - throw new NotImplementedException(); - } - protected IAppEntity App { get diff --git a/src/Squidex/Controllers/UI/Account/AccountController.cs b/src/Squidex/Controllers/UI/Account/AccountController.cs index 3e160cb73..b701907d1 100644 --- a/src/Squidex/Controllers/UI/Account/AccountController.cs +++ b/src/Squidex/Controllers/UI/Account/AccountController.cs @@ -22,6 +22,7 @@ using Microsoft.Extensions.Options; using Squidex.Config; using Squidex.Config.Identity; using Squidex.Core.Identity; +using Squidex.Infrastructure.Tasks; // ReSharper disable InvertIf // ReSharper disable RedundantIfElseBlock @@ -216,7 +217,7 @@ namespace Squidex.Controllers.UI.Account { if (isFirst || !identityOptions.Value.LockAutomatically) { - return Task.FromResult(true); + return TaskHelper.True; } return MakeIdentityOperation(() => userManager.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddYears(100))); @@ -226,7 +227,7 @@ namespace Squidex.Controllers.UI.Account { if (!isFirst) { - return Task.FromResult(true); + return TaskHelper.True; } return MakeIdentityOperation(() => userManager.AddToRoleAsync(user, SquidexRoles.Administrator)); diff --git a/src/Squidex/Docs/schemabody.md b/src/Squidex/Docs/schemabody.md index 13f64a25e..bf97dd0de 100644 --- a/src/Squidex/Docs/schemabody.md +++ b/src/Squidex/Docs/schemabody.md @@ -4,4 +4,4 @@ Please note that each field is an object with one entry per language. If the field is not localizable you must use iv (Invariant Language) as a key. When you change the field to be localizable the value will become the value for the master language, depending what the master language is at this point of time. -Read more about it at: https://docs.squidex.io/04-guides/api.html \ No newline at end of file +Read more about it at: https://docs.squidex.io/04-guides/01-api.html \ No newline at end of file diff --git a/src/Squidex/Docs/schemaquery.md b/src/Squidex/Docs/schemaquery.md index 84ce336d0..aa78298b9 100644 --- a/src/Squidex/Docs/schemaquery.md +++ b/src/Squidex/Docs/schemaquery.md @@ -8,4 +8,4 @@ We support the following query options. * **$filter**: The $filter query option allows clients to filter a collection of resources that are addressed by a request URL. * **$orderby**: The $orderby query option allows clients to request resources in a particular order. -Read more about it at: https://docs.squidex.io/04-guides/api.html +Read more about it at: https://docs.squidex.io/04-guides/01-api.html diff --git a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs index 5a9b7db4c..a57e20a54 100644 --- a/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs +++ b/src/Squidex/Pipeline/ApiExceptionFilterAttribute.cs @@ -18,13 +18,13 @@ using Squidex.Controllers.Api; namespace Squidex.Pipeline { - public class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter + public sealed class ApiExceptionFilterAttribute : ActionFilterAttribute, IExceptionFilter { - private static readonly List> handlers = new List>(); + private static readonly List> Handlers = new List>(); private static void AddHandler(Func handler) where T : Exception { - handlers.Add(ex => + Handlers.Add(ex => { var typed = ex as T; @@ -35,6 +35,7 @@ namespace Squidex.Pipeline static ApiExceptionFilterAttribute() { AddHandler(OnDomainObjectNotFoundException); + AddHandler(OnDomainObjectVersionException); AddHandler(OnDomainException); AddHandler(OnValidationException); } @@ -44,6 +45,11 @@ namespace Squidex.Pipeline return new NotFoundResult(); } + private static IActionResult OnDomainObjectVersionException(DomainObjectVersionException ex) + { + return new ObjectResult(new ErrorDto { Message = ex.Message }) { StatusCode = 412 }; + } + private static IActionResult OnDomainException(DomainException ex) { return new BadRequestObjectResult(new ErrorDto { Message = ex.Message }); @@ -68,7 +74,7 @@ namespace Squidex.Pipeline { IActionResult result = null; - foreach (var handler in handlers) + foreach (var handler in Handlers) { result = handler(context.Exception); diff --git a/src/Squidex/Pipeline/AppFilterAttribute.cs b/src/Squidex/Pipeline/AppFilterAttribute.cs index 4c426b520..38fd2a890 100644 --- a/src/Squidex/Pipeline/AppFilterAttribute.cs +++ b/src/Squidex/Pipeline/AppFilterAttribute.cs @@ -22,6 +22,7 @@ using Squidex.Read.Apps.Services; namespace Squidex.Pipeline { + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public sealed class AppFilterAttribute : Attribute, IAsyncAuthorizationFilter { private readonly IAppProvider appProvider; diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs index 9ceb6d18e..ae7cdfd1d 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithActorHandler.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Http; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.Tasks; using Squidex.Write; // ReSharper disable InvertIf @@ -29,23 +30,23 @@ namespace Squidex.Pipeline.CommandHandlers public Task HandleAsync(CommandContext context) { - var squidexCommand = context.Command as SquidexCommand; - - if (squidexCommand != null) + if (context.Command is SquidexCommand squidexCommand) { var actorToken = FindActorFromSubject() ?? FindActorFromClient(); +#pragma warning disable if (actorToken == null) { throw new SecurityException("No actor with subject or client id available"); } +#pragma warning enable squidexCommand.Actor = actorToken; } - return Task.FromResult(false); + return TaskHelper.False; } private RefToken FindActorFromSubject() diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs index 30482bd9f..5c69772d3 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithAppIdHandler.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Tasks; using Squidex.Write; // ReSharper disable InvertIf @@ -28,9 +29,7 @@ namespace Squidex.Pipeline.CommandHandlers public Task HandleAsync(CommandContext context) { - var appCommand = context.Command as AppCommand; - - if (appCommand != null) + if (context.Command is AppCommand appCommand) { var appFeature = httpContextAccessor.HttpContext.Features.Get(); @@ -42,7 +41,7 @@ namespace Squidex.Pipeline.CommandHandlers appCommand.AppId = new NamedId(appFeature.App.Id, appFeature.App.Name); } - return Task.FromResult(false); + return TaskHelper.False; } } } diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs new file mode 100644 index 000000000..937cbd0c4 --- /dev/null +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithExpectedVersionHandler.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// EnrichWithExpectedVersionHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Pipeline.CommandHandlers +{ + public sealed class EnrichWithExpectedVersionHandler : ICommandHandler + { + private readonly IHttpContextAccessor httpContextAccessor; + + public EnrichWithExpectedVersionHandler(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + public Task HandleAsync(CommandContext context) + { + var headers = httpContextAccessor.HttpContext.Request.Headers; + var headerMatch = headers["If-Match"].ToString(); + + if (!string.IsNullOrWhiteSpace(headerMatch) && long.TryParse(headerMatch, NumberStyles.Any, CultureInfo.InvariantCulture, out long expectedVersion)) + { + context.Command.ExpectedVersion = expectedVersion; + } + + return TaskHelper.False; + } + } +} diff --git a/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs b/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs index a600573f7..a4be18e38 100644 --- a/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs +++ b/src/Squidex/Pipeline/CommandHandlers/EnrichWithSchemaIdHandler.cs @@ -21,21 +21,19 @@ namespace Squidex.Pipeline.CommandHandlers { public sealed class EnrichWithSchemaIdHandler : ICommandHandler { - private readonly ISchemaProvider schemaProvider; + private readonly ISchemaProvider schemas; private readonly IActionContextAccessor actionContextAccessor; - public EnrichWithSchemaIdHandler(ISchemaProvider schemaProvider, IActionContextAccessor actionContextAccessor) + public EnrichWithSchemaIdHandler(ISchemaProvider schemas, IActionContextAccessor actionContextAccessor) { - this.schemaProvider = schemaProvider; + this.schemas = schemas; this.actionContextAccessor = actionContextAccessor; } public async Task HandleAsync(CommandContext context) { - var schemaCommand = context.Command as SchemaCommand; - - if (schemaCommand != null) + if (context.Command is SchemaCommand schemaCommand) { var routeValues = actionContextAccessor.ActionContext.RouteData.Values; @@ -43,7 +41,7 @@ namespace Squidex.Pipeline.CommandHandlers { var schemaName = routeValues["name"].ToString(); - var schema = await schemaProvider.FindSchemaByNameAsync(schemaCommand.AppId.Id, schemaName); + var schema = await schemas.FindSchemaByNameAsync(schemaCommand.AppId.Id, schemaName); if (schema == null) { diff --git a/src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs b/src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs new file mode 100644 index 000000000..74d01c118 --- /dev/null +++ b/src/Squidex/Pipeline/CommandHandlers/SetVersionAsETagHandler.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// SetVersionAsETagHandler.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Pipeline.CommandHandlers +{ + public class SetVersionAsETagHandler : ICommandHandler + { + private readonly IHttpContextAccessor httpContextAccessor; + + public SetVersionAsETagHandler(IHttpContextAccessor httpContextAccessor) + { + this.httpContextAccessor = httpContextAccessor; + } + + public Task HandleAsync(CommandContext context) + { + if (context.Result() is EntitySavedResult result) + { + httpContextAccessor.HttpContext.Response.Headers["ETag"] = new StringValues(result.Version.ToString()); + } + + return TaskHelper.False; + } + } +} diff --git a/src/Squidex/Pipeline/RandomErrorAttribute.cs b/src/Squidex/Pipeline/RandomErrorAttribute.cs deleted file mode 100644 index 93f24ae41..000000000 --- a/src/Squidex/Pipeline/RandomErrorAttribute.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// RandomErrorAttribute.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; - -namespace Squidex.Pipeline -{ - public class RandomErrorAttribute : ActionFilterAttribute - { - private static readonly Random random = new Random(); - - public override void OnActionExecuted(ActionExecutedContext context) - { - if (random.Next(10) < 5) - { - context.Result = new StatusCodeResult(500); - } - } - } -} diff --git a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs index 7494341d8..5ab298c48 100644 --- a/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs +++ b/src/Squidex/Pipeline/Swagger/SwaggerHelper.cs @@ -13,16 +13,17 @@ using NJsonSchema; using NSwag; using Squidex.Config; using System.Reflection; +using Squidex.Core.Identity; namespace Squidex.Pipeline.Swagger { public static class SwaggerHelper { - private static readonly ConcurrentDictionary docs = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary Docs = new ConcurrentDictionary(); public static string LoadDocs(string name) { - return docs.GetOrAdd(name, x => + return Docs.GetOrAdd(name, x => { var assembly = typeof(SwaggerHelper).GetTypeInfo().Assembly; @@ -50,7 +51,10 @@ namespace Squidex.Pipeline.Swagger Flow = SwaggerOAuth2Flow.Application, Scopes = new Dictionary { - { Constants.ApiScope, "Read and write access to the API" } + { Constants.ApiScope, "Read and write access to the API" }, + { SquidexRoles.AppOwner, "You get this scope / role when you are owner of the app you are accessing." }, + { SquidexRoles.AppEditor, "You get this scope / role when you are owner of the app you are accessing or when the subject is a client." }, + { SquidexRoles.AppDeveloper, "You get this scope / role when you are owner of the app you are accessing." } }, Description = securityDescription }; @@ -83,8 +87,6 @@ namespace Squidex.Pipeline.Swagger parameter.IsNullableRaw = false; operation.Parameters.Add(parameter); - - operation.Parameters.Add(parameter); } public static void AddBodyParameter(this SwaggerOperation operation, JsonSchema4 schema, string name, string description) diff --git a/src/Squidex/Pipeline/WebpackMiddleware.cs b/src/Squidex/Pipeline/WebpackMiddleware.cs index 7441352c3..a65f77cd9 100644 --- a/src/Squidex/Pipeline/WebpackMiddleware.cs +++ b/src/Squidex/Pipeline/WebpackMiddleware.cs @@ -20,8 +20,8 @@ namespace Squidex.Pipeline { private const string Host = "localhost"; private const string Port = "3000"; - private static readonly string[] Scripts = { "polyfills.js", "vendor.js", "app.js" }; - private static readonly string[] Styles = { "vendor.css" }; + private static readonly string[] Scripts = { "shims.js", "app.js" }; + private static readonly string[] Styles = new string[0]; private readonly RequestDelegate next; private readonly ILogger logger; diff --git a/src/Squidex/Pipeline/WebpackRunner.cs b/src/Squidex/Pipeline/WebpackRunner.cs deleted file mode 100644 index 381ec500a..000000000 --- a/src/Squidex/Pipeline/WebpackRunner.cs +++ /dev/null @@ -1,93 +0,0 @@ -// ========================================================================== -// WebpackRunner.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Diagnostics; -using System.IO; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; - -// ReSharper disable ConvertToConstant.Local - -namespace Squidex.Pipeline -{ - public sealed class WebpackRunner - { - private const string WebpackDevServer = "webpack-dev-server"; - - private readonly ILoggerFactory loggerFactory; - private readonly IApplicationLifetime lifetime; - private Process process; - - public WebpackRunner(ILoggerFactory loggerFactory, IApplicationLifetime lifetime) - { - this.loggerFactory = loggerFactory; - - this.lifetime = lifetime; - } - - public void Execute() - { - if (process != null) - { - return; - } - - var logger = loggerFactory.CreateLogger(WebpackDevServer); - - EnsuereNodeModluesInstalled(logger); - - logger.LogInformation($"{WebpackDevServer} Execution started"); - - var app = GetNodeExecutable(WebpackDevServer); - var args = "--inline --hot --port 3000"; - - process = Process.Start(new ProcessStartInfo - { - FileName = app, Arguments = args, UseShellExecute = false - }); - - lifetime.ApplicationStopping.Register(OnShutdown); - - logger.LogInformation($"{WebpackDevServer} started successfully"); - } - - private void OnShutdown() - { - process?.Kill(); - process = null; - } - - private static void EnsuereNodeModluesInstalled(ILogger logger) - { - logger.LogInformation("Verifying required tools are installed"); - - if (!File.Exists(GetNodeExecutable(WebpackDevServer))) - { - logger.LogError("webpack-dev-server is not installed. Please install it by executing npm i webpack-dev-server"); - } - - logger.LogInformation("All node modules are properly installed"); - } - - private static string GetNodeExecutable(string module) - { - var executablePath = Path.Combine(Directory.GetCurrentDirectory(), "node_modules", ".bin", module); - - var osEnVariable = Environment.GetEnvironmentVariable("OS"); - - if (!string.IsNullOrEmpty(osEnVariable) && - string.Equals(osEnVariable, "Windows_NT", StringComparison.OrdinalIgnoreCase)) - { - executablePath += ".cmd"; - } - - return executablePath; - } - } -} diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index e37dd2817..8f685d3ca 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -9,7 +9,7 @@ Exe Squidex $(PackageTargetFallback);dnxcore50 - 1.1.0 + 1.1.1 true @@ -32,41 +32,47 @@ - + - - + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - + - - + + - + + + + <_DocumentationFile Include="$(DocumentationFile)" /> + + + diff --git a/src/Squidex/Startup.cs b/src/Squidex/Startup.cs index 29207c837..2699676b9 100644 --- a/src/Squidex/Startup.cs +++ b/src/Squidex/Startup.cs @@ -37,9 +37,9 @@ namespace Squidex "/account" }; - public IConfigurationRoot Configuration { get; } + private IConfigurationRoot Configuration { get; } - public IHostingEnvironment Environment { get; } + private IHostingEnvironment Environment { get; } public Startup(IHostingEnvironment env) { @@ -60,14 +60,13 @@ namespace Squidex services.AddMyEventFormatter(); services.AddMyDataProtectection(Configuration); services.AddMyIdentity(); - services.AddMyIdentityServer(Environment); + services.AddMyIdentityServer(); services.AddMyMvc(); services.AddLogging(); services.AddMemoryCache(); services.AddOptions(); services.AddRouting(); - services.AddWebpackBuilder(); services.Configure( Configuration.GetSection("squidex:urls")); diff --git a/src/Squidex/Views/Account/ClientPopup.cshtml b/src/Squidex/Views/Account/ClientPopup.cshtml index 51163eba8..40d3af684 100644 --- a/src/Squidex/Views/Account/ClientPopup.cshtml +++ b/src/Squidex/Views/Account/ClientPopup.cshtml @@ -1,4 +1,8 @@ - +@{ + Layout = null; +} + + diff --git a/src/Squidex/Views/Account/ClientSilent.cshtml b/src/Squidex/Views/Account/ClientSilent.cshtml index 0c8766dbb..39f659cb3 100644 --- a/src/Squidex/Views/Account/ClientSilent.cshtml +++ b/src/Squidex/Views/Account/ClientSilent.cshtml @@ -1,4 +1,8 @@ - +@{ + Layout = null; +} + + diff --git a/src/Squidex/Views/Account/Error.cshtml b/src/Squidex/Views/Account/Error.cshtml index c63f6015e..a32b957da 100644 --- a/src/Squidex/Views/Account/Error.cshtml +++ b/src/Squidex/Views/Account/Error.cshtml @@ -1,20 +1,9 @@ - - - - +@{ + ViewBag.Title = "Login failed"; +} - - - - Squidex - Login failed - - - - -

Login failed

+

Login failed

-

- We are really sorry, something failed when you tried to login. -

- - \ No newline at end of file +

+ We are really sorry, something failed when you tried to login. +

\ No newline at end of file diff --git a/src/Squidex/Views/Account/LockedOut.cshtml b/src/Squidex/Views/Account/LockedOut.cshtml index 7c59a141f..d6c0c1f5f 100644 --- a/src/Squidex/Views/Account/LockedOut.cshtml +++ b/src/Squidex/Views/Account/LockedOut.cshtml @@ -1,24 +1,13 @@ - - - - +@{ + ViewBag.Title = "Account locked"; +} - - +

Account locked

- Squidex - Account locked - - - - -

Account locked

- -

- Your account is locked, please contact the administrator. -

+

+ Your account is locked, please contact the administrator. +

-

- Logout -

- - \ No newline at end of file +

+ Logout +

\ No newline at end of file diff --git a/src/Squidex/Views/Account/Login.cshtml b/src/Squidex/Views/Account/Login.cshtml index e641428f3..abf2c3d88 100644 --- a/src/Squidex/Views/Account/Login.cshtml +++ b/src/Squidex/Views/Account/Login.cshtml @@ -3,34 +3,21 @@ @using Microsoft.AspNetCore.Http.Authentication @model Squidex.Controllers.UI.Account.LoginVM - - - - - - - - - Squidex - Login - - - - -
-
-

- @foreach (var provider in Model.ExternalProviders) - { - - } -

-
-
- - - - +@{ + ViewBag.Title = "Login"; +} +
+
+

+ @foreach (var provider in Model.ExternalProviders) + { + + } +

+
+
+ \ No newline at end of file diff --git a/src/Squidex/Views/Account/LogoutCompleted.cshtml b/src/Squidex/Views/Account/LogoutCompleted.cshtml index 92f8d9178..a5d5b7e11 100644 --- a/src/Squidex/Views/Account/LogoutCompleted.cshtml +++ b/src/Squidex/Views/Account/LogoutCompleted.cshtml @@ -1,20 +1,9 @@ - - - - +@{ + ViewBag.Title = "Logout"; +} - - +

Logged out!

- Squidex - Logout - - - - -

Logged out!

- -

- Please close this popup. -

- - \ No newline at end of file +

+ Please close this popup. +

\ No newline at end of file diff --git a/src/Squidex/Views/Shared/Docs.cshtml b/src/Squidex/Views/Shared/Docs.cshtml index 82c5642bc..5fa318d48 100644 --- a/src/Squidex/Views/Shared/Docs.cshtml +++ b/src/Squidex/Views/Shared/Docs.cshtml @@ -1,4 +1,8 @@ - +@{ + Layout = null; +} + + API Docs @@ -6,22 +10,25 @@ - + \ No newline at end of file diff --git a/src/Squidex/Views/_Layout.cshtml b/src/Squidex/Views/_Layout.cshtml new file mode 100644 index 000000000..cf8a8f5fe --- /dev/null +++ b/src/Squidex/Views/_Layout.cshtml @@ -0,0 +1,28 @@ + + + + + + + + + Squidex - @ViewBag.Title + + + + + + @if (IsSectionDefined("header")) + { + @RenderSection("header") + } + + + @RenderBody() + + + + + + + \ No newline at end of file diff --git a/src/Squidex/Views/_ViewStart.cshtml b/src/Squidex/Views/_ViewStart.cshtml index be2a4f37a..935cd49e4 100644 --- a/src/Squidex/Views/_ViewStart.cshtml +++ b/src/Squidex/Views/_ViewStart.cshtml @@ -1,3 +1,3 @@ @{ - Layout = null; + Layout = "_Layout.cshtml"; } diff --git a/src/Squidex/app-config/clean-css-loader.js b/src/Squidex/app-config/clean-css-loader.js deleted file mode 100644 index c682f05c6..000000000 --- a/src/Squidex/app-config/clean-css-loader.js +++ /dev/null @@ -1,29 +0,0 @@ -var loaderUtils = require('loader-utils'), - CleanCSS = require('clean-css'); - -function cleanCss(css) { - this.cacheable(); - - var loader = this; - var callback = this.async(); - - new CleanCSS().minify(css, function (err, minified) { - if (err) { - if (Array.isArray(err) && (err[0] !== null)) { - return callback(err[0]); - } else { - return callback(err); - } - } - var warnings; - if (((warnings = minified.warnings) !== null ? warnings.length : void 0) !== 0) { - warnings.forEach(function (msg) { - loader.emitWarning(msg.toString()); - }); - } - - return callback(null, minified.styles, minified.sourceMap); - }); -}; - -module.exports = cleanCss; \ No newline at end of file diff --git a/src/Squidex/app-config/webpack.config.js b/src/Squidex/app-config/webpack.config.js index dc81dd19a..83b8b1f4a 100644 --- a/src/Squidex/app-config/webpack.config.js +++ b/src/Squidex/app-config/webpack.config.js @@ -73,13 +73,16 @@ module.exports = { * * See: https://github.com/webpack-contrib/extract-text-webpack-plugin */ - use: ExtractTextPlugin.extract({ fallbackLoader: 'style-loader', loader: 'css-loader?sourceMap' }) + use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader?sourceMap' }) }, { test: /\.scss$/, use: [{ loader: 'raw-loader' }, { - loader: 'sass-loader' + loader: 'sass-loader', + options: { + includePaths: [helpers.root('app', 'theme')] + } }], exclude: helpers.root('app', 'theme') } @@ -103,15 +106,12 @@ module.exports = { }, htmlLoader: { /** - * Define the root for images, so that we can use absolute urls + * Define the root for images, so that we can use absolute url's * * See: https://github.com/webpack/html-loader#Advanced_Options */ root: helpers.root('app', 'images') }, - sassLoader: { - includePaths: [helpers.root('app', 'theme')] - }, context: '/' } }), diff --git a/src/Squidex/app-config/webpack.run.base.js b/src/Squidex/app-config/webpack.run.base.js index d59ffd867..92315bf46 100644 --- a/src/Squidex/app-config/webpack.run.base.js +++ b/src/Squidex/app-config/webpack.run.base.js @@ -15,9 +15,8 @@ module.exports = webpackMerge(commonConfig, { * See: https://webpack.js.org/configuration/entry-context/ */ entry: { - 'polyfills': './app/polyfills.ts', - 'vendor': './app/vendor.ts', - 'app': './app/main.ts' + 'shims': './app/shims.ts', + 'app': './app/app.ts' }, plugins: [ @@ -27,7 +26,7 @@ module.exports = webpackMerge(commonConfig, { * See: https://webpack.js.org/plugins/commons-chunk-plugin/ */ new webpack.optimize.CommonsChunkPlugin({ - name: ['app', 'vendor', 'polyfills'] + name: ['app', 'shims'] }), /** @@ -38,7 +37,7 @@ module.exports = webpackMerge(commonConfig, { * See: https://github.com/ampedandwired/html-webpack-plugin */ new HtmlWebpackPlugin({ - template: 'wwwroot/index.html' + template: 'wwwroot/index.html', hash: true }) ] }); \ No newline at end of file diff --git a/src/Squidex/app-config/webpack.run.dev.js b/src/Squidex/app-config/webpack.run.dev.js index c75060200..130d5a672 100644 --- a/src/Squidex/app-config/webpack.run.dev.js +++ b/src/Squidex/app-config/webpack.run.dev.js @@ -43,6 +43,9 @@ module.exports = webpackMerge(runConfig, { loader: 'css-loader' }, { loader: 'sass-loader?sourceMap', + options: { + includePaths: [helpers.root('app', 'theme')] + } }], include: helpers.root('app', 'theme') } diff --git a/src/Squidex/app-config/webpack.run.prod.js b/src/Squidex/app-config/webpack.run.prod.js index ee31e3265..6c4d9227b 100644 --- a/src/Squidex/app-config/webpack.run.prod.js +++ b/src/Squidex/app-config/webpack.run.prod.js @@ -28,7 +28,7 @@ module.exports = webpackMerge(runConfig, { * * See: https://webpack.js.org/configuration/output/#output-filename */ - filename: '[name].[hash].js', + filename: '[name].js', /** * The filename of non-entry chunks as relative path @@ -58,7 +58,7 @@ module.exports = webpackMerge(runConfig, { * * See: https://github.com/webpack-contrib/extract-text-webpack-plugin */ - use: ExtractTextPlugin.extract({ fallbackLoader: 'style-loader', loader: 'css-loader!sass-loader?sourceMap' }), + use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader?minimize!sass-loader?sourceMap' }), /* * Do not include component styles */ @@ -68,9 +68,10 @@ module.exports = webpackMerge(runConfig, { use: [{ loader: 'raw-loader' }, { - loader: helpers.root('app-config', 'clean-css-loader') - }, { - loader: 'sass-loader' + loader: 'sass-loader', + options: { + includePaths: [helpers.root('app', 'theme')] + } }], exclude: helpers.root('app', 'theme'), }, { @@ -91,7 +92,7 @@ module.exports = webpackMerge(runConfig, { * * See: https://github.com/webpack/extract-text-webpack-plugin */ - new ExtractTextPlugin('[name].[hash].css'), + new ExtractTextPlugin('[name].css'), new webpack.optimize.UglifyJsPlugin({ beautify: false, diff --git a/src/Squidex/app/app.component.scss b/src/Squidex/app/app.component.scss index e69de29bb..fbb752506 100644 --- a/src/Squidex/app/app.component.scss +++ b/src/Squidex/app/app.component.scss @@ -0,0 +1,2 @@ +@import '_vars'; +@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/app.module.ts b/src/Squidex/app/app.module.ts index 419c2fdad..c9952071c 100644 --- a/src/Squidex/app/app.module.ts +++ b/src/Squidex/app/app.module.ts @@ -64,4 +64,12 @@ export function configUserReport() { ], bootstrap: [AppComponent] }) -export class AppModule { } \ No newline at end of file +export class AppModule { + /*public ngDoBootstrap(appRef: ApplicationRef) { + try { + appRef.bootstrap(AppComponent); + } catch (e) { + console.log('Application element not found'); + } + }*/ +} \ No newline at end of file diff --git a/src/Squidex/app/main.ts b/src/Squidex/app/app.ts similarity index 100% rename from src/Squidex/app/main.ts rename to src/Squidex/app/app.ts diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html index c321d2c11..a5ddd01f7 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html @@ -39,7 +39,7 @@ - + {{eventConsumer.name}} diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.html b/src/Squidex/app/features/administration/pages/users/users-page.component.html index 137223632..6f450329b 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.html @@ -72,14 +72,14 @@ -
+
diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.ts b/src/Squidex/app/features/administration/pages/users/users-page.component.ts index 7f5c44dc4..75b8f7530 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.ts +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.ts @@ -13,6 +13,7 @@ import { ComponentBase, ImmutableArray, NotificationService, + Pager, UserDto, UserManagementService, UsersProviderService @@ -27,20 +28,9 @@ export class UsersPageComponent extends ComponentBase implements OnInit { public currentUserId: string; public usersItems = ImmutableArray.empty(); - public usersTotal = 0; - - public pageSize = 10; - - public canGoNext = false; - public canGoPrev = false; - - public itemFirst = 0; - public itemLast = 0; - - public currentPage = 0; - public currentQuery = ''; - + public usersPager = new Pager(0); public usersFilter = new FormControl(); + public usersQuery = ''; constructor(notifications: NotificationService, users: UsersProviderService, private readonly userManagementService: UserManagementService, @@ -56,19 +46,17 @@ export class UsersPageComponent extends ComponentBase implements OnInit { } public search() { - this.currentPage = 0; - this.currentQuery = this.usersFilter.value; + this.usersPager = new Pager(0); + this.usersQuery = this.usersFilter.value; this.load(); } private load() { - this.userManagementService.getUsers(this.pageSize, this.currentPage * this.pageSize, this.currentQuery) + this.userManagementService.getUsers(this.usersPager.pageSize, this.usersPager.skip, this.usersQuery) .subscribe(dtos => { this.usersItems = ImmutableArray.of(dtos.items); - this.usersTotal = dtos.total; - - this.updatePaging(); + this.usersPager = this.usersPager.setCount(dtos.total); }, error => { this.notifyError(error); }); @@ -105,31 +93,15 @@ export class UsersPageComponent extends ComponentBase implements OnInit { } public goNext() { - if (this.canGoNext) { - this.currentPage++; + this.usersPager = this.usersPager.goNext(); - this.updatePaging(); - this.load(); - } + this.load(); } public goPrev() { - if (this.canGoPrev) { - this.currentPage--; - - this.updatePaging(); - this.load(); - } - } + this.usersPager = this.usersPager.goPrev(); - private updatePaging() { - const totalPages = Math.ceil(this.usersTotal / this.pageSize); - - this.itemFirst = this.currentPage * this.pageSize + 1; - this.itemLast = Math.min(this.usersTotal, (this.currentPage + 1) * this.pageSize); - - this.canGoNext = this.currentPage < totalPages - 1; - this.canGoPrev = this.currentPage > 0; + this.load(); } } diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.html b/src/Squidex/app/features/apps/pages/apps-page.component.html index 6d9bc12e2..078be25e7 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.html +++ b/src/Squidex/app/features/apps/pages/apps-page.component.html @@ -1,22 +1,20 @@  - +

You are not collaborating to any app yet

-
-
-
-

{{app.name}}

+
+
+

{{app.name}}

- Edit -
+ Edit
- +
+
+ +
@@ -87,4 +97,4 @@
{{field.properties.hints}}
-
\ No newline at end of file +
diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index 2d6377d02..5501c68c3 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -1,7 +1,7 @@
- +
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 bec6f4169..cd0d6e0cb 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 @@ -28,7 +28,8 @@ import { SchemaDetailsDto, StringFieldPropertiesDto, UsersProviderService, - ValidatorsEx + ValidatorsEx, + Version } from 'shared'; @Component({ @@ -38,6 +39,7 @@ import { }) export class ContentPageComponent extends AppComponentBase implements OnDestroy, OnInit { private messageSubscription: Subscription; + private version: Version = new Version(''); public schema: SchemaDetailsDto; @@ -65,23 +67,27 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, public ngOnInit() { this.messageSubscription = - this.messageBus.of(ContentDeleted).subscribe(message => { - if (message.id === this.contentId) { - this.router.navigate(['../'], { relativeTo: this.route }); - } - }); + this.messageBus.of(ContentDeleted) + .subscribe(message => { + if (message.id === this.contentId) { + this.router.navigate(['../'], { relativeTo: this.route }); + } + }); - this.route.parent.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => { - this.languages = languages; - }); + this.route.parent.data.map(p => p['appLanguages']) + .subscribe((languages: AppLanguageDto[]) => { + this.languages = languages; + }); - this.route.parent.data.map(p => p['schema']).subscribe((schema: SchemaDetailsDto) => { - this.setupForm(schema); - }); + this.route.parent.data.map(p => p['schema']) + .subscribe((schema: SchemaDetailsDto) => { + this.setupForm(schema); + }); - this.route.data.map(p => p['content']).subscribe((content: ContentDto) => { - this.populateForm(content); - }); + this.route.data.map(p => p['content']) + .subscribe((content: ContentDto) => { + this.populateForm(content); + }); } public saveContent() { @@ -94,9 +100,9 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, if (this.isNewMode) { this.appName() - .switchMap(app => this.contentsService.postContent(app, this.schema.name, data)) + .switchMap(app => this.contentsService.postContent(app, this.schema.name, data, this.version)) .subscribe(created => { - this.messageBus.publish(new ContentCreated(created.id, data)); + this.messageBus.publish(new ContentCreated(created.id, created.data, this.version.value)); this.router.navigate(['../'], { relativeTo: this.route }); }, error => { @@ -105,9 +111,9 @@ export class ContentPageComponent extends AppComponentBase implements OnDestroy, }); } else { this.appName() - .switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId, data)) + .switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId, data, this.version)) .subscribe(() => { - this.messageBus.publish(new ContentUpdated(this.contentId, data)); + this.messageBus.publish(new ContentUpdated(this.contentId, data, this.version.value)); this.router.navigate(['../'], { relativeTo: this.route }); }, error => { diff --git a/src/Squidex/app/features/content/pages/contents/content-item.component.html b/src/Squidex/app/features/content/pages/contents/content-item.component.html index 2fcccee7a..39c7edb48 100644 --- a/src/Squidex/app/features/content/pages/contents/content-item.component.html +++ b/src/Squidex/app/features/content/pages/contents/content-item.component.html @@ -22,7 +22,7 @@ Unpublish - + Delete
diff --git a/src/Squidex/app/features/content/pages/contents/content-item.component.ts b/src/Squidex/app/features/content/pages/contents/content-item.component.ts index b7abf920d..ebfd0bf4a 100644 --- a/src/Squidex/app/features/content/pages/contents/content-item.component.ts +++ b/src/Squidex/app/features/content/pages/contents/content-item.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { AppComponentBase, @@ -28,7 +28,7 @@ import { fadeAnimation ] }) -export class ContentItemComponent extends AppComponentBase implements OnInit { +export class ContentItemComponent extends AppComponentBase implements OnInit, OnChanges { public dropdown = new ModalView(false, true); @Output() @@ -58,7 +58,15 @@ export class ContentItemComponent extends AppComponentBase implements OnInit { super(notifications, users, apps); } + public ngOnChanges() { + this.updateValues(); + } + public ngOnInit() { + this.updateValues(); + } + + private updateValues() { this.values = []; for (let field of this.fields) { 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 4fbdc2187..e09438553 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 @@ -1,6 +1,6 @@ - +
@@ -67,14 +67,14 @@ -
+
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 6b305dea8..4e8c89e4d 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 @@ -28,8 +28,10 @@ import { ImmutableArray, MessageBus, NotificationService, + Pager, SchemaDetailsDto, - UsersProviderService + UsersProviderService, + Version } from 'shared'; @Component({ @@ -45,24 +47,13 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy public contentItems: ImmutableArray; public contentFields: FieldDto[]; - public contentTotal = 0; + public contentsFilter = new FormControl(); + public contentsQuery = ''; + public contentsPager = new Pager(0); public languages: AppLanguageDto[] = []; public languageSelected: AppLanguageDto; - public contentsFilter = new FormControl(); - - public pageSize = 10; - - public canGoNext = false; - public canGoPrev = false; - - public itemFirst = 0; - public itemLast = 0; - - public currentPage = 0; - public currentQuery = ''; - public get columnWidth() { return 100 / this.contentFields.length; } @@ -83,41 +74,52 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy public ngOnInit() { this.messageCreatedSubscription = - this.messageBus.of(ContentCreated).subscribe(message => { - this.itemLast++; - this.contentTotal++; - this.contentItems = this.contentItems.pushFront(this.createContent(message.id, message.data)); - }); + this.messageBus.of(ContentCreated) + .subscribe(message => { + this.contentItems = this.contentItems.pushFront(this.createContent(message.id, message.data, message.version)); + this.contentsPager = this.contentsPager.incrementCount(); + }); this.messageUpdatedSubscription = - this.messageBus.of(ContentUpdated).subscribe(message => { - this.updateContents(message.id, undefined, message.data); - }); + this.messageBus.of(ContentUpdated) + .subscribe(message => { + this.updateContents(message.id, undefined, message.data, message.version); + }); - this.route.data.map(p => p['appLanguages']).subscribe((languages: AppLanguageDto[]) => { - this.languages = languages; - }); + this.route.data.map(p => p['appLanguages']) + .subscribe((languages: AppLanguageDto[]) => { + this.languages = languages; + }); - this.route.data.map(p => p['schema']).subscribe(schema => { - this.schema = schema; + this.route.data.map(p => p['schema']) + .subscribe(schema => { + this.schema = schema; - this.reset(); - this.load(); - }); + this.reset(); + this.load(); + }); } public search() { - this.currentPage = 0; - this.currentQuery = this.contentsFilter.value; + this.contentsQuery = this.contentsFilter.value; + this.contentsPager = new Pager(0); this.load(); } + private reset() { + this.contentItems = ImmutableArray.empty(); + this.contentsFilter.setValue(''); + this.contentsPager = new Pager(0); + + this.loadFields(); + } + public publishContent(content: ContentDto) { this.appName() - .switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id)) + .switchMap(app => this.contentsService.publishContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { - this.updateContents(content.id, true, content.data); + this.updateContents(content.id, true, content.data, content.version.value); }, error => { this.notifyError(error); }); @@ -125,9 +127,9 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy public unpublishContent(content: ContentDto) { this.appName() - .switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id)) + .switchMap(app => this.contentsService.unpublishContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { - this.updateContents(content.id, false, content.data); + this.updateContents(content.id, false, content.data, content.version.value); }, error => { this.notifyError(error); }); @@ -135,9 +137,10 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy public deleteContent(content: ContentDto) { this.appName() - .switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id)) + .switchMap(app => this.contentsService.deleteContent(app, this.schema.name, content.id, content.version)) .subscribe(() => { this.contentItems = this.contentItems.removeAll(x => x.id === content.id); + this.contentsPager = this.contentsPager.decrementCount(); this.messageBus.publish(new ContentDeleted(content.id)); }, error => { @@ -149,12 +152,6 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy this.languageSelected = language; } - private reset() { - this.loadFields(); - - this.currentPage = 0; - } - private loadFields() { this.contentFields = this.schema.fields.filter(x => x.properties.isListField); @@ -165,50 +162,33 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy private load() { this.appName() - .switchMap(app => this.contentsService.getContents(app, this.schema.name, this.pageSize, this.currentPage * this.pageSize, this.currentQuery)) + .switchMap(app => this.contentsService.getContents(app, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery)) .subscribe(dtos => { this.contentItems = ImmutableArray.of(dtos.items); - this.contentTotal = dtos.total; - this.updatePaging(); + this.contentsPager = this.contentsPager.setCount(dtos.total); }, error => { this.notifyError(error); }); } public goNext() { - if (this.canGoNext) { - this.currentPage++; + this.contentsPager = this.contentsPager.goNext(); - this.updatePaging(); - this.load(); - } + this.load(); } public goPrev() { - if (this.canGoPrev) { - this.currentPage--; - - this.updatePaging(); - this.load(); - } - } + this.contentsPager = this.contentsPager.goPrev(); - private updatePaging() { - const totalPages = Math.ceil(this.contentTotal / this.pageSize); - - this.itemFirst = this.currentPage * this.pageSize + 1; - this.itemLast = Math.min(this.contentTotal, (this.currentPage + 1) * this.pageSize); - - this.canGoNext = this.currentPage < totalPages - 1; - this.canGoPrev = this.currentPage > 0; + this.load(); } - private updateContents(id: string, p: boolean | undefined, data: any) { - this.contentItems = this.contentItems.replaceAll(x => x.id === id, c => this.updateContent(c, p === undefined ? c.isPublished : p, data)); + private updateContents(id: string, p: boolean | undefined, data: any, version: string) { + this.contentItems = this.contentItems.replaceAll(x => x.id === id, c => this.updateContent(c, p === undefined ? c.isPublished : p, data, version)); } - private createContent(id: string, data: any): ContentDto { + private createContent(id: string, data: any, version: string): ContentDto { const me = `subject:${this.authService.user!.id}`; const newContent = @@ -217,12 +197,13 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy me, me, DateTime.now(), DateTime.now(), - data); + data, + new Version(version)); return newContent; } - private updateContent(content: ContentDto, isPublished: boolean, data: any): ContentDto { + private updateContent(content: ContentDto, isPublished: boolean, data: any, version: string): ContentDto { const me = `subject:${this.authService.user!.id}`; const newContent = @@ -230,7 +211,8 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy content.id, isPublished, content.createdBy, me, content.created, DateTime.now(), - data); + data, + new Version(version)); return newContent; } diff --git a/src/Squidex/app/features/content/pages/messages.ts b/src/Squidex/app/features/content/pages/messages.ts index 801c08dd3..93c69a917 100644 --- a/src/Squidex/app/features/content/pages/messages.ts +++ b/src/Squidex/app/features/content/pages/messages.ts @@ -8,7 +8,8 @@ export class ContentCreated { constructor( public readonly id: string, - public readonly data: any + public readonly data: any, + public readonly version: string ) { } } @@ -16,7 +17,8 @@ export class ContentCreated { export class ContentUpdated { constructor( public readonly id: string, - public readonly data: any + public readonly data: any, + public readonly version: string ) { } } diff --git a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts index bc8ea2c6f..bb696d0a3 100644 --- a/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts @@ -26,8 +26,8 @@ import { export class SchemasPageComponent extends AppComponentBase { public schemasFilter = new FormControl(); public schemasFiltered = - Observable.of(null) - .merge(this.schemasFilter.valueChanges) + this.schemasFilter.valueChanges + .startWith(null) .distinctUntilChanged() .debounceTime(300) .combineLatest(this.loadSchemas(), diff --git a/src/Squidex/app/features/schemas/declarations.ts b/src/Squidex/app/features/schemas/declarations.ts index bc742a5cd..3b194cfdd 100644 --- a/src/Squidex/app/features/schemas/declarations.ts +++ b/src/Squidex/app/features/schemas/declarations.ts @@ -9,6 +9,8 @@ export * from './pages/schema/types/boolean-ui.component'; export * from './pages/schema/types/boolean-validation.component'; export * from './pages/schema/types/date-time-ui.component'; export * from './pages/schema/types/date-time-validation.component'; +export * from './pages/schema/types/geolocation-ui.component'; +export * from './pages/schema/types/geolocation-validation.component'; export * from './pages/schema/types/json-ui.component'; export * from './pages/schema/types/json-validation.component'; export * from './pages/schema/types/number-ui.component'; diff --git a/src/Squidex/app/features/schemas/module.ts b/src/Squidex/app/features/schemas/module.ts index 2835b633c..5d71b6d99 100644 --- a/src/Squidex/app/features/schemas/module.ts +++ b/src/Squidex/app/features/schemas/module.ts @@ -22,6 +22,8 @@ import { BooleanValidationComponent, DateTimeUIComponent, DateTimeValidationComponent, + GeolocationUIComponent, + GeolocationValidationComponent, JsonUIComponent, JsonValidationComponent, NumberUIComponent, @@ -60,7 +62,7 @@ const routes: Routes = [ path: 'help', component: HelpComponent, data: { - helpPage: '04-guides/schemas' + helpPage: '05-integrated/schemas' } } ] @@ -80,6 +82,8 @@ const routes: Routes = [ BooleanValidationComponent, DateTimeUIComponent, DateTimeValidationComponent, + GeolocationUIComponent, + GeolocationValidationComponent, JsonUIComponent, JsonValidationComponent, NumberUIComponent, diff --git a/src/Squidex/app/features/schemas/pages/messages.ts b/src/Squidex/app/features/schemas/pages/messages.ts index 28f2287cb..a9ec973ea 100644 --- a/src/Squidex/app/features/schemas/pages/messages.ts +++ b/src/Squidex/app/features/schemas/pages/messages.ts @@ -9,7 +9,15 @@ export class SchemaUpdated { constructor( public readonly name: string, public readonly label: string, - public readonly isPublished: boolean + public readonly isPublished: boolean, + public readonly version: string + ) { + } +} + +export class SchemaDeleted { + constructor( + public readonly name: string ) { } } \ No newline at end of file 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 3a414d8be..1e5307adf 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field.component.html @@ -38,7 +38,7 @@ Show - + Delete
@@ -141,6 +141,9 @@
+
+ +
@@ -161,6 +164,9 @@
+
+ +
diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts index 4cee9eb3d..a80ca2bc0 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts @@ -11,7 +11,8 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { Notification, NotificationService, - SchemasService + SchemasService, + Version } from 'shared'; import { SchemaPropertiesDto } from './schema-properties'; @@ -31,6 +32,9 @@ export class SchemaEditFormComponent implements OnInit { @Input() public schema: SchemaPropertiesDto; + @Input() + public version: Version; + @Input() public appName: string; @@ -72,7 +76,7 @@ export class SchemaEditFormComponent implements OnInit { const requestDto = this.editForm.value; - this.schemas.putSchema(this.appName, this.schema.name, requestDto) + this.schemas.putSchema(this.appName, this.schema.name, requestDto, this.version) .subscribe(dto => { this.reset(); this.saved.emit(new SchemaPropertiesDto(this.schema.name, requestDto.label, requestDto.hints)); 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 0ded9fb1a..eaf21d029 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 @@ -12,6 +12,17 @@ Unpublished
+ +

@@ -50,7 +61,7 @@

- +
@@ -66,6 +77,30 @@
+ +
\ 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 29d71ff9b..04cb9a3eb 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 @@ -7,11 +7,11 @@ -
- +
+
- +
\ 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 14205bac8..58eff1ab4 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 @@ -30,8 +30,8 @@ export class BooleanValidationComponent implements OnInit { new FormControl(this.properties.defaultValue)); this.hideDefaultValue = - Observable.of(this.properties.isRequired) - .merge(this.editForm.get('isRequired').valueChanges) + this.editForm.get('isRequired').valueChanges + .startWith(this.properties.isRequired) .map(x => !!x); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.html b/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.html index 4d669c216..be0594485 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.html @@ -23,7 +23,7 @@ -
+
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 0509f0179..c3d65a206 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 @@ -42,8 +42,8 @@ export class DateTimeValidationComponent implements OnInit { ])); this.hideDefaultValue = - Observable.of(this.properties.isRequired) - .merge(this.editForm.get('isRequired').valueChanges) + this.editForm.get('isRequired').valueChanges + .startWith(this.properties.isRequired) .map(x => !!x); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html new file mode 100644 index 000000000..22a20a7ae --- /dev/null +++ b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.html @@ -0,0 +1,15 @@ +
+
+ + +
+ +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.scss b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.scss new file mode 100644 index 000000000..fbb752506 --- /dev/null +++ b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.scss @@ -0,0 +1,2 @@ +@import '_vars'; +@import '_mixins'; \ No newline at end of file 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 new file mode 100644 index 000000000..a0fc725a5 --- /dev/null +++ b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-ui.component.ts @@ -0,0 +1,31 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, Input, OnInit } from '@angular/core'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; + +import { GeolocationFieldPropertiesDto } from 'shared'; + +@Component({ + selector: 'sqx-geolocation-ui', + styleUrls: ['geolocation-ui.component.scss'], + templateUrl: 'geolocation-ui.component.html' +}) +export class GeolocationUIComponent implements OnInit { + @Input() + public editForm: FormGroup; + + @Input() + public properties: GeolocationFieldPropertiesDto; + + public ngOnInit() { + this.editForm.addControl('editor', + new FormControl(this.properties.editor, [ + Validators.required + ])); + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.html b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.html new file mode 100644 index 000000000..54887deda --- /dev/null +++ b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.html @@ -0,0 +1,9 @@ +
+
+ + +
+ +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.scss b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.scss new file mode 100644 index 000000000..f9405a205 --- /dev/null +++ b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.scss @@ -0,0 +1,10 @@ +@import '_vars'; +@import '_mixins'; + +.form-check-input { + margin: 0; +} + +.form-group { + margin-top: .5rem; +} \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.ts new file mode 100644 index 000000000..098fcacdc --- /dev/null +++ b/src/Squidex/app/features/schemas/pages/schema/types/geolocation-validation.component.ts @@ -0,0 +1,24 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { GeolocationFieldPropertiesDto } from 'shared'; + +@Component({ + selector: 'sqx-geolocation-validation', + styleUrls: ['geolocation-validation.component.scss'], + templateUrl: 'geolocation-validation.component.html' +}) +export class GeolocationValidationComponent { + @Input() + public editForm: FormGroup; + + @Input() + public properties: GeolocationFieldPropertiesDto; +} \ No newline at end of file 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 ff5fd97ce..193c44198 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 @@ -35,6 +35,13 @@ Radio +
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 c455d5892..7f8edc9c0 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 @@ -46,8 +46,8 @@ export class NumberUIComponent implements OnDestroy, OnInit { new FormControl(this.properties.allowedValues, [])); this.hideAllowedValues = - Observable.of(this.properties.editor) - .merge(this.editForm.get('editor').valueChanges) + this.editForm.get('editor').valueChanges + .startWith(this.properties.editor) .map(x => !x || x === 'Input' || x === 'Textarea'); this.editorSubscription = diff --git a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.html b/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.html index 664284932..b5b4ef42b 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.html @@ -20,7 +20,7 @@
-
+
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 dc494977e..51fc8b779 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 @@ -36,8 +36,8 @@ export class NumberValidationComponent implements OnInit { new FormControl(this.properties.defaultValue)); this.hideDefaultValue = - Observable.of(this.properties.isRequired) - .merge(this.editForm.get('isRequired').valueChanges) + this.editForm.get('isRequired').valueChanges + .startWith(this.properties.isRequired) .map(x => !!x); } } \ 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 2f75f145a..bb8a8f923 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 @@ -46,15 +46,16 @@ export class StringUIComponent implements OnDestroy, OnInit { new FormControl(this.properties.allowedValues)); this.hideAllowedValues = - Observable.of(this.properties.editor) - .merge(this.editForm.get('editor').valueChanges) + this.editForm.get('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.get('allowedValues').setValue(undefined); - } - }); + this.hideAllowedValues + .subscribe(isSelection => { + if (isSelection) { + this.editForm.get('allowedValues').setValue(undefined); + } + }); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html index 28ca6df86..ff0a4fc39 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html @@ -27,7 +27,7 @@
-
+
@@ -35,7 +35,7 @@
-
+
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 265506ccb..0387c7099 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 @@ -49,13 +49,13 @@ export class StringValidationComponent implements OnDestroy, OnInit { new FormControl(this.properties.defaultValue)); this.hideDefaultValue = - Observable.of(false) - .merge(this.editForm.get('isRequired').valueChanges) + this.editForm.get('isRequired').valueChanges + .startWith(this.properties.isRequired) .map(x => !!x); this.hidePatternMessage = - Observable.of(false) - .merge(this.editForm.get('pattern').valueChanges) + this.editForm.get('pattern').valueChanges + .startWith('') .map(x => !x || x.trim().length === 0); this.patternSubscription = diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html index 8ff470085..4e3fd75c2 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html @@ -13,7 +13,7 @@ - The schema name becomes part of the api url,
e.g https://{{appName}}.squidex.io/{{schemaName | async}}/. + The schema name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{appName}}/{{schemaName | async}}/.
It must contain lower case letters (a-z), numbers and dashes only, and cannot be longer than 40 characters. The name cannot be changed later. diff --git a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts index 8c24bcc55..baeb2dae0 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schema-form.component.ts @@ -7,16 +7,17 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { Observable } from 'rxjs'; import { + ApiUrlConfig, AuthService, CreateSchemaDto, DateTime, fadeAnimation, SchemaDto, SchemasService, - ValidatorsEx + ValidatorsEx, + Version } from 'shared'; const FALLBACK_NAME = 'my-schema'; @@ -52,10 +53,11 @@ export class SchemaFormComponent { }); public schemaName = - Observable.of(FALLBACK_NAME) - .merge(this.createForm.get('name').valueChanges.map(n => n || FALLBACK_NAME)); + this.createForm.get('name').valueChanges.map(n => n || FALLBACK_NAME) + .startWith(FALLBACK_NAME); constructor( + public readonly apiUrl: ApiUrlConfig, private readonly schemas: SchemasService, private readonly formBuilder: FormBuilder, private readonly authService: AuthService @@ -73,14 +75,15 @@ export class SchemaFormComponent { if (this.createForm.valid) { this.createForm.disable(); - const name = this.createForm.get('name').value; + const schemaVersion = new Version(); + const schemaName = this.createForm.get('name').value; - const requestDto = new CreateSchemaDto(name); + const requestDto = new CreateSchemaDto(schemaName); - this.schemas.postSchema(this.appName, requestDto) + this.schemas.postSchema(this.appName, requestDto, schemaVersion) .subscribe(dto => { this.reset(); - this.created.emit(this.createSchemaDto(dto.id, name)); + this.created.emit(this.createSchemaDto(dto.id, schemaName, schemaVersion)); }, error => { this.createForm.enable(); this.creationError = error.displayMessage; @@ -94,10 +97,10 @@ export class SchemaFormComponent { this.createFormSubmitted = false; } - private createSchemaDto(id: string, name: string) { + private createSchemaDto(id: string, name: string, version: Version) { const user = this.authService.user!.token; const now = DateTime.now(); - return new SchemaDto(id, name, undefined, false, user, user, now, now); + return new SchemaDto(id, name, undefined, false, user, user, now, now, version); } } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss index 8c4e6de75..36b77b2c7 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.scss @@ -27,6 +27,9 @@ $button-size: calc(2.5rem - 2px); &-modified { text-align: right; + width: auto; + white-space: nowrap; + padding-left: 0; } &-user { diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts index 1817ffaa5..119d96b9b 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts @@ -22,10 +22,11 @@ import { NotificationService, SchemaDto, SchemasService, - UsersProviderService + UsersProviderService, + Version } from 'shared'; -import { SchemaUpdated } from './../messages'; +import { SchemaDeleted, SchemaUpdated } from './../messages'; @Component({ selector: 'sqx-schemas-page', @@ -36,7 +37,8 @@ import { SchemaUpdated } from './../messages'; ] }) export class SchemasPageComponent extends AppComponentBase implements OnDestroy, OnInit { - private messageSubscription: Subscription; + private messageUpdatedSubscription: Subscription; + private messageDeletedSubscription: Subscription; public addSchemaDialog = new ModalView(); @@ -55,7 +57,8 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, } public ngOnDestroy() { - this.messageSubscription.unsubscribe(); + this.messageUpdatedSubscription.unsubscribe(); + this.messageDeletedSubscription.unsubscribe(); } public ngOnInit() { @@ -66,18 +69,25 @@ export class SchemasPageComponent extends AppComponentBase implements OnDestroy, this.updateSchemas(this.schemas, this.schemaQuery = q); }); - this.route.params.map(q => q['showDialog']).subscribe(showDialog => { - if (showDialog) { - this.addSchemaDialog.show(); - } - }); + this.route.params.map(q => q['showDialog']) + .subscribe(showDialog => { + if (showDialog) { + this.addSchemaDialog.show(); + } + }); - this.messageSubscription = + this.messageUpdatedSubscription = this.messageBus.of(SchemaUpdated) .subscribe(m => { this.updateSchemas(this.schemas.map(s => s.name === m.name ? updateSchema(s, this.authService, m) : s)); }); + this.messageDeletedSubscription = + this.messageBus.of(SchemaDeleted) + .subscribe(m => { + this.updateSchemas(this.schemas.filter(s => s.name !== m.name)); + }); + this.load(); } @@ -130,6 +140,7 @@ function updateSchema(schema: SchemaDto, authService: AuthService, message: Sche message.label, message.isPublished, schema.createdBy, me, - schema.created, DateTime.now()); + schema.created, DateTime.now(), + new Version(message.version)); } diff --git a/src/Squidex/app/features/settings/module.ts b/src/Squidex/app/features/settings/module.ts index 1fe9edda1..985d55dd2 100644 --- a/src/Squidex/app/features/settings/module.ts +++ b/src/Squidex/app/features/settings/module.ts @@ -46,7 +46,7 @@ const routes: Routes = [ path: 'help', component: HelpComponent, data: { - helpPage: '04-guides/clients' + helpPage: '05-integrated/clients' } } ] @@ -65,7 +65,7 @@ const routes: Routes = [ path: 'help', component: HelpComponent, data: { - helpPage: '04-guides/contributors' + helpPage: '05-integrated/contributors' } } ] @@ -84,7 +84,7 @@ const routes: Routes = [ path: 'help', component: HelpComponent, data: { - helpPage: '04-guides/languages' + helpPage: '05-integrated/languages' } } ] diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.html b/src/Squidex/app/features/settings/pages/clients/client.component.html index a6cb2a298..cb98e2425 100644 --- a/src/Squidex/app/features/settings/pages/clients/client.component.html +++ b/src/Squidex/app/features/settings/pages/clients/client.component.html @@ -13,7 +13,7 @@
-
+
@@ -22,7 +22,7 @@ - @@ -39,7 +39,7 @@
Access tokens expire after 30 days
- diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html b/src/Squidex/app/features/settings/pages/clients/clients-page.component.html index 16d74a829..af1a59b0f 100644 --- a/src/Squidex/app/features/settings/pages/clients/clients-page.component.html +++ b/src/Squidex/app/features/settings/pages/clients/clients-page.component.html @@ -31,7 +31,7 @@
- +
diff --git a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts b/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts index 49bbf37d3..c4aa99db6 100644 --- a/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts +++ b/src/Squidex/app/features/settings/pages/clients/clients-page.component.ts @@ -20,7 +20,8 @@ import { NotificationService, UpdateAppClientDto, UsersProviderService, - ValidatorsEx + ValidatorsEx, + Version } from 'shared'; @Component({ @@ -29,18 +30,23 @@ import { templateUrl: './clients-page.component.html' }) export class ClientsPageComponent extends AppComponentBase implements OnInit { + private version = new Version(); + public appClients: ImmutableArray; public addClientForm: FormGroup = this.formBuilder.group({ name: ['', [ - Validators.required, Validators.maxLength(40), ValidatorsEx.pattern('[a-z0-9]+(\-[a-z0-9]+)*', 'Name can contain lower case letters (a-z), numbers and dashes (not at the end).') ]] }); + public get hasName() { + return this.addClientForm.controls['name'].value && this.addClientForm.controls['name'].value.length > 0; + } + constructor(apps: AppsStoreService, notifications: NotificationService, users: UsersProviderService, private readonly appClientsService: AppClientsService, private readonly messageBus: MessageBus, @@ -55,7 +61,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit { public load() { this.appName() - .switchMap(app => this.appClientsService.getClients(app).retry(2)) + .switchMap(app => this.appClientsService.getClients(app, this.version).retry(2)) .subscribe(dtos => { this.updateClients(ImmutableArray.of(dtos)); }, error => { @@ -65,7 +71,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit { public revokeClient(client: AppClientDto) { this.appName() - .switchMap(app => this.appClientsService.deleteClient(app, client.id)) + .switchMap(app => this.appClientsService.deleteClient(app, client.id, this.version)) .subscribe(() => { this.updateClients(this.appClients.remove(client)); }, error => { @@ -77,7 +83,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit { const request = new UpdateAppClientDto(name); this.appName() - .switchMap(app => this.appClientsService.updateClient(app, client.id, request)) + .switchMap(app => this.appClientsService.updateClient(app, client.id, request, this.version)) .subscribe(() => { this.updateClients(this.appClients.replace(client, rename(client, name))); }, error => { @@ -103,7 +109,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit { }; this.appName() - .switchMap(app => this.appClientsService.postClient(app, requestDto)) + .switchMap(app => this.appClientsService.postClient(app, requestDto, this.version)) .subscribe(dto => { this.updateClients(this.appClients.push(dto)); reset(); diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts index 0338e65ac..06abbdfd8 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts @@ -22,7 +22,8 @@ import { MessageBus, NotificationService, UsersProviderService, - UsersService + UsersService, + Version } from 'shared'; export class UsersDataSource implements AutocompleteSource { @@ -58,6 +59,8 @@ export class UsersDataSource implements AutocompleteSource { templateUrl: './contributors-page.component.html' }) export class ContributorsPageComponent extends AppComponentBase implements OnInit { + private version = new Version(); + public appContributors = ImmutableArray.empty(); public currentUserId: string; @@ -96,7 +99,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni public load() { this.appName() - .switchMap(app => this.appContributorsService.getContributors(app).retry(2)) + .switchMap(app => this.appContributorsService.getContributors(app, this.version).retry(2)) .subscribe(dtos => { this.updateContributors(ImmutableArray.of(dtos)); }, error => { @@ -106,7 +109,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni public removeContributor(contributor: AppContributorDto) { this.appName() - .switchMap(app => this.appContributorsService.deleteContributor(app, contributor.contributorId)) + .switchMap(app => this.appContributorsService.deleteContributor(app, contributor.contributorId, this.version)) .subscribe(() => { this.updateContributors(this.appContributors.remove(contributor)); }, error => { @@ -118,7 +121,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni const newContributor = new AppContributorDto(this.addContributorForm.get('user').value.model.id, 'Editor'); this.appName() - .switchMap(app => this.appContributorsService.postContributor(app, newContributor)) + .switchMap(app => this.appContributorsService.postContributor(app, newContributor, this.version)) .subscribe(() => { this.updateContributors(this.appContributors.push(newContributor)); }, error => { @@ -132,7 +135,7 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni const newContributor = changePermission(contributor, permission); this.appName() - .switchMap(app => this.appContributorsService.postContributor(app, newContributor)) + .switchMap(app => this.appContributorsService.postContributor(app, newContributor, this.version)) .subscribe(() => { this.updateContributors(this.appContributors.replace(contributor, newContributor)); }, error => { diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts b/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts index 9bba7aa20..f1d8c6940 100644 --- a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts +++ b/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts @@ -21,7 +21,8 @@ import { LanguageService, NotificationService, UpdateAppLanguageDto, - UsersProviderService + UsersProviderService, + Version } from 'shared'; @Component({ @@ -30,6 +31,8 @@ import { templateUrl: './languages-page.component.html' }) export class LanguagesPageComponent extends AppComponentBase implements OnInit { + private version = new Version(); + public allLanguages: LanguageDto[] = []; public appLanguages = ImmutableArray.empty(); @@ -66,7 +69,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { public load() { this.appName() - .switchMap(app => this.appLanguagesService.getLanguages(app).retry(2)) + .switchMap(app => this.appLanguagesService.getLanguages(app, this.version).retry(2)) .subscribe(dtos => { this.updateLanguages(ImmutableArray.of(dtos)); }, error => { @@ -76,9 +79,9 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { public removeLanguage(language: AppLanguageDto) { this.appName() - .switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code)) + .switchMap(app => this.appLanguagesService.deleteLanguage(app, language.iso2Code, this.version)) .subscribe(dto => { - this.updateLanguages(this.appLanguages.remove(dto)); + this.updateLanguages(this.appLanguages.remove(language)); }, error => { this.notifyError(error); }); @@ -88,7 +91,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { const request = new AddAppLanguageDto(this.addLanguageForm.get('language').value.iso2Code); this.appName() - .switchMap(app => this.appLanguagesService.postLanguages(app, request)) + .switchMap(app => this.appLanguagesService.postLanguages(app, request, this.version)) .subscribe(dto => { this.updateLanguages(this.appLanguages.push(dto)); }, error => { @@ -100,7 +103,7 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { const request = new UpdateAppLanguageDto(true); this.appName() - .switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, request)) + .switchMap(app => this.appLanguagesService.updateLanguage(app, language.iso2Code, request, this.version)) .subscribe(() => { this.updateLanguages(this.appLanguages.map(l => { const isMasterLanguage = l === language; @@ -114,6 +117,8 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit { }, error => { this.notifyError(error); }); + + return false; } private updateLanguages(languages: ImmutableArray) { diff --git a/src/Squidex/app/framework/angular/autocomplete.component.ts b/src/Squidex/app/framework/angular/autocomplete.component.ts index 443711063..e93345090 100644 --- a/src/Squidex/app/framework/angular/autocomplete.component.ts +++ b/src/Squidex/app/framework/angular/autocomplete.component.ts @@ -26,8 +26,7 @@ export class AutocompleteItem { const KEY_ENTER = 13; const KEY_UP = 38; const KEY_DOWN = 40; -/* tslint:disable:no-empty */ -const NOOP = () => { }; +const NOOP = () => { /* NOOP */ }; export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AutocompleteComponent), multi: true diff --git a/src/Squidex/app/framework/angular/control-errors.component.ts b/src/Squidex/app/framework/angular/control-errors.component.ts index 108f35010..512941159 100644 --- a/src/Squidex/app/framework/angular/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/control-errors.component.ts @@ -47,12 +47,15 @@ export class ControlErrorsComponent implements OnChanges { @Input() public submitted: boolean; + @Input() + public submitOnly = false; + public get errorMessages(): string[] | null { if (!this.control) { return null; } - if (this.control.invalid && (this.control.touched || this.submitted)) { + if (this.control.invalid && ((this.control.touched && !this.submitOnly) || this.submitted)) { const errors: string[] = []; for (let key in this.control.errors) { diff --git a/src/Squidex/app/framework/angular/copy.directive.ts b/src/Squidex/app/framework/angular/copy.directive.ts index 6cef6031c..0c56035e0 100644 --- a/src/Squidex/app/framework/angular/copy.directive.ts +++ b/src/Squidex/app/framework/angular/copy.directive.ts @@ -7,6 +7,8 @@ import { Directive, HostListener, Input } from '@angular/core'; +import { Notification, NotificationService } from './../services/notification.service'; + @Directive({ selector: '[sqxCopy]' }) @@ -14,6 +16,11 @@ export class CopyDirective { @Input('sqxCopy') public inputElement: any; + constructor( + private readonly notifications: NotificationService + ) { + } + @HostListener('click') public onClick() { if (this.inputElement) { @@ -35,6 +42,8 @@ export class CopyDirective { try { document.execCommand('copy'); + + this.notifications.notify(Notification.info('Value has been added to your clipboard.')); } catch (e) { console.log('Copy failed'); } diff --git a/src/Squidex/app/framework/angular/date-time-editor.component.html b/src/Squidex/app/framework/angular/date-time-editor.component.html index 607c25cac..2bc487744 100644 --- a/src/Squidex/app/framework/angular/date-time-editor.component.html +++ b/src/Squidex/app/framework/angular/date-time-editor.component.html @@ -6,5 +6,8 @@
+
+ +
diff --git a/src/Squidex/app/framework/angular/date-time-editor.component.scss b/src/Squidex/app/framework/angular/date-time-editor.component.scss index 15ae9dda6..9c595c4d6 100644 --- a/src/Squidex/app/framework/angular/date-time-editor.component.scss +++ b/src/Squidex/app/framework/angular/date-time-editor.component.scss @@ -38,4 +38,8 @@ $form-color: #fff; .form-control { width: 7.5rem; } +} + +.clear { + margin-left: .5rem; } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/date-time-editor.component.ts b/src/Squidex/app/framework/angular/date-time-editor.component.ts index 8f863d1cc..755bd2f1e 100644 --- a/src/Squidex/app/framework/angular/date-time-editor.component.ts +++ b/src/Squidex/app/framework/angular/date-time-editor.component.ts @@ -11,8 +11,7 @@ import * as moment from 'moment'; let Pikaday = require('pikaday/pikaday'); -/* tslint:disable:no-empty */ -const NOOP = () => { }; +const NOOP = () => { /* NOOP */ }; export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DateTimeEditorComponent), multi: true @@ -46,9 +45,15 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnInit, Af @Input() public enforceTime: boolean; + public get hasValue() { + return this.dateValue !== null; + } + @ViewChild('dateInput') public dateInput: ElementRef; + public isDisabled = false; + public ngOnInit() { this.timeControl.valueChanges.subscribe(value => { if (!value || value.length === 0) { @@ -89,6 +94,8 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnInit, Af } public setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + if (isDisabled) { this.dateControl.disable({ emitEvent: false }); this.timeControl.disable({ emitEvent: false }); @@ -126,6 +133,18 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnInit, Af this.touchedCallback(); } + public reset() { + this.timeControl.setValue(null, { emitEvent: false }); + this.dateControl.setValue(null, { emitEvent: false }); + + this.dateValue = null; + + this.changeCallback(null); + this.touchedCallback(); + + return false; + } + private updateValue() { let result: string = null; diff --git a/src/Squidex/app/framework/angular/geolocation-editor.component.html b/src/Squidex/app/framework/angular/geolocation-editor.component.html new file mode 100644 index 000000000..fd3392b28 --- /dev/null +++ b/src/Squidex/app/framework/angular/geolocation-editor.component.html @@ -0,0 +1,19 @@ +
+
+ +
+
+
+ +
+
+ +
+
+ +
+ + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/geolocation-editor.component.scss b/src/Squidex/app/framework/angular/geolocation-editor.component.scss new file mode 100644 index 000000000..ffa89e6ba --- /dev/null +++ b/src/Squidex/app/framework/angular/geolocation-editor.component.scss @@ -0,0 +1,14 @@ +@import '_mixins'; +@import '_vars'; + +.editor { + height: 30rem; +} + +.form-inline { + margin-top: .5rem; +} + +.latitude-group { + margin-right: .25rem; +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/geolocation-editor.component.ts b/src/Squidex/app/framework/angular/geolocation-editor.component.ts new file mode 100644 index 000000000..a01e39ea9 --- /dev/null +++ b/src/Squidex/app/framework/angular/geolocation-editor.component.ts @@ -0,0 +1,205 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { AfterViewInit, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; +import { ControlValueAccessor, FormBuilder, NG_VALUE_ACCESSOR } from '@angular/forms'; + +import { ResourceLoaderService } from './../services/resource-loader.service'; +import { ValidatorsEx } from './validators'; + +const NOOP = () => { /* NOOP */ }; + +declare var L: any; + +export const SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => GeolocationEditorComponent), multi: true +}; + +@Component({ + selector: 'sqx-geolocation-editor', + styleUrls: ['./geolocation-editor.component.scss'], + templateUrl: './geolocation-editor.component.html', + providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR] +}) +export class GeolocationEditorComponent implements ControlValueAccessor, AfterViewInit { + private changeCallback: (value: any) => void = NOOP; + private touchedCallback: () => void = NOOP; + private marker: any; + private map: any; + private value: any; + + public get hasValue() { + return !!this.value; + } + + public geolocationForm = + this.formBuilder.group({ + latitude: ['', + [ + ValidatorsEx.between(-90, 90) + ]], + longitude: ['', + [ + ValidatorsEx.between(-180, 180) + ]] + }); + + @ViewChild('editor') + public editor: ElementRef; + + public isDisabled = false; + + constructor( + private readonly resourceLoader: ResourceLoaderService, + private readonly formBuilder: FormBuilder + ) { + } + + public writeValue(value: any) { + this.value = value; + + if (this.marker) { + this.updateMarker(true, false); + } + } + + public setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + + if (isDisabled) { + if (this.map) { + this.map.zoomControl.disable(); + this.map._handlers.forEach((handler: any) => { + handler.disable(); + }); + } + + if (this.marker) { + this.marker.dragging.disable(); + } + + this.geolocationForm.disable(); + } else { + if (this.map) { + this.map.zoomControl.enable(); + this.map._handlers.forEach((handler: any) => { + handler.enable(); + }); + } + + if (this.marker) { + this.marker.dragging.enable(); + } + + this.geolocationForm.enable(); + } + } + + public registerOnChange(fn: any) { + this.changeCallback = fn; + } + + public registerOnTouched(fn: any) { + this.touchedCallback = fn; + } + + public updateValueByInput() { + if (this.geolocationForm.controls['latitude'].value !== null && + this.geolocationForm.controls['longitude'].value !== null && + this.geolocationForm.valid) { + this.value = this.geolocationForm.value; + } else { + this.value = null; + } + + this.updateMarker(true, true); + } + + public ngAfterViewInit() { + this.resourceLoader.loadStyle('https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.css'); + this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.3/leaflet.js').then(() => { + this.map = L.map(this.editor.nativeElement).fitWorld(); + + L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' + }).addTo(this.map); + + this.map.on('click', (event: any) => { + if (!this.marker) { + const latlng = event.latlng.wrap(); + + this.value = { latitude: latlng.lat, longitude: latlng.lng }; + + this.updateMarker(false, true); + } + }); + + this.updateMarker(true, false); + + if (this.isDisabled) { + this.map.zoomControl.disable(); + this.map._handlers.forEach((handler: any) => { + handler.disable(); + }); + } + }); + } + + public reset() { + this.value = null; + + this.updateMarker(true, true); + } + + private updateMarker(zoom: boolean, fireEvent: boolean) { + if (this.value) { + if (!this.marker) { + this.marker = L.marker([0, 90], { draggable: true }).addTo(this.map); + + this.marker.on('drag', (event: any) => { + const latlng = event.latlng.wrap(); + + this.value = { latitude: latlng.lat, longitude: latlng.lng }; + }); + + this.marker.on('dragend', (event: any) => { + this.updateMarker(false, true); + }); + + if (this.isDisabled) { + this.marker.dragging.disable(); + } + } + + const latLng = L.latLng(this.value.latitude, this.value.longitude); + + if (zoom) { + this.map.setView(latLng, 8); + } else { + this.map.panTo(latLng); + } + + this.marker.setLatLng(latLng); + + this.geolocationForm.setValue(this.value, { emitEvent: false, onlySelf: false }); + } else { + if (this.marker) { + this.marker.removeFrom(this.map); + this.marker = null; + } + + this.map.fitWorld(); + + this.geolocationForm.reset(undefined, { emitEvent: false, onlySelf: false }); + } + + if (fireEvent) { + this.changeCallback(this.value); + this.touchedCallback(); + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/http-utils.ts b/src/Squidex/app/framework/angular/http-utils.ts index 301a35c12..2dec6e0de 100644 --- a/src/Squidex/app/framework/angular/http-utils.ts +++ b/src/Squidex/app/framework/angular/http-utils.ts @@ -53,10 +53,18 @@ export function catchError(message: string): Observable { return this.catch((error: any | Response) => { let result = new ErrorDto(500, message); - if (error instanceof Response && error.status !== 500) { - const body = error.json(); + if (error instanceof Response) { + try { + const body = error.json(); - result = new ErrorDto(error.status, body.message, body.details); + if (error.status === 412) { + result = new ErrorDto(error.status, 'Failed to make the update. Another user has made a change. Please reload.'); + } else if (error.status !== 500) { + result = new ErrorDto(error.status, body.message, body.details); + } + } catch (e) { + result = result; + } } return Observable.throw(result); diff --git a/src/Squidex/app/framework/angular/indeterminate-value.directive.ts b/src/Squidex/app/framework/angular/indeterminate-value.directive.ts new file mode 100644 index 000000000..8a0ccbc9f --- /dev/null +++ b/src/Squidex/app/framework/angular/indeterminate-value.directive.ts @@ -0,0 +1,61 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Directive, forwardRef, ElementRef, Renderer } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +const NOOP = () => { /* NOOP */ }; + +export const SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => IndeterminateValueDirective), multi: true +}; + +@Directive({ + selector: '[sqxIndeterminateValue]', + providers: [SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR], + host: { + '(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()' + } +}) +export class IndeterminateValueDirective implements ControlValueAccessor { + private changeCallback: (value: any) => void = NOOP; + private touchedCallback: () => void = NOOP; + + constructor( + private readonly renderer: Renderer, + private readonly elementRef: ElementRef + ) { + } + + public writeValue(value: any) { + if (value === undefined || value === null) { + this.renderer.setElementProperty(this.elementRef.nativeElement, 'indeterminate', true); + } else { + this.renderer.setElementProperty(this.elementRef.nativeElement, 'checked', value); + } + } + + public setDisabledState(isDisabled: boolean): void { + this.renderer.setElementProperty(this.elementRef.nativeElement, 'disabled', isDisabled); + } + + public registerOnChange(fn: any) { + this.changeCallback = fn; + } + + public registerOnTouched(fn: any) { + this.touchedCallback = fn; + } + + public onChange(value: any) { + this.changeCallback(value); + } + + public onTouched() { + this.touchedCallback(); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/json-editor.component.ts b/src/Squidex/app/framework/angular/json-editor.component.ts index 564b457e0..69f2de38f 100644 --- a/src/Squidex/app/framework/angular/json-editor.component.ts +++ b/src/Squidex/app/framework/angular/json-editor.component.ts @@ -13,8 +13,7 @@ import { ResourceLoaderService } from './../services/resource-loader.service'; declare var ace: any; -/* tslint:disable:no-empty */ -const NOOP = () => { }; +const NOOP = () => { /* NOOP */ }; export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => JsonEditorComponent), multi: true @@ -67,21 +66,22 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit } public ngAfterViewInit() { - this.valueChanged.debounceTime(1000).subscribe(() => { - const isValid = this.aceEditor.getSession().getAnnotations().length === 0; + this.valueChanged.debounceTime(1000) + .subscribe(() => { + const isValid = this.aceEditor.getSession().getAnnotations().length === 0; - if (!isValid) { - this.changeCallback(null); - } else { - try { - const value = JSON.parse(this.aceEditor.getValue()); - - this.changeCallback(value); - } catch (e) { + if (!isValid) { this.changeCallback(null); + } else { + try { + const value = JSON.parse(this.aceEditor.getValue()); + + this.changeCallback(value); + } catch (e) { + this.changeCallback(null); + } } - } - }); + }); this.resourceLoader.loadScript('https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.6/ace.js').then(() => { this.aceEditor = ace.edit(this.editor.nativeElement); diff --git a/src/Squidex/app/framework/angular/markdown-editor.component.ts b/src/Squidex/app/framework/angular/markdown-editor.component.ts index addf4be06..e194e015c 100644 --- a/src/Squidex/app/framework/angular/markdown-editor.component.ts +++ b/src/Squidex/app/framework/angular/markdown-editor.component.ts @@ -12,8 +12,7 @@ import { ResourceLoaderService } from './../services/resource-loader.service'; declare var SimpleMDE: any; -/* tslint:disable:no-empty */ -const NOOP = () => { }; +const NOOP = () => { /* NOOP */ }; export const SQX_MARKDOWN_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MarkdownEditorComponent), multi: true @@ -77,6 +76,7 @@ export class MarkdownEditorComponent implements ControlValueAccessor, AfterViewI this.resourceLoader.loadScript('https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js').then(() => { this.simplemde = new SimpleMDE({ element: this.editor.nativeElement }); this.simplemde.value(this.value || ''); + this.simplemde.codemirror.setOption('readOnly', this.isDisabled); this.simplemde.codemirror.on('change', () => { const value = this.simplemde.value(); diff --git a/src/Squidex/app/framework/angular/rich-editor.component.ts b/src/Squidex/app/framework/angular/rich-editor.component.ts index 62f21fe73..e6dcec5f1 100644 --- a/src/Squidex/app/framework/angular/rich-editor.component.ts +++ b/src/Squidex/app/framework/angular/rich-editor.component.ts @@ -12,8 +12,7 @@ import { ResourceLoaderService } from './../services/resource-loader.service'; declare var tinymce: any; -/* tslint:disable:no-empty */ -const NOOP = () => { }; +const NOOP = () => { /* NOOP */ }; export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => RichEditorComponent), multi: true @@ -44,7 +43,7 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, this.value = value; if (this.tinyEditor) { - this.tinyEditor.setContent(value); + this.tinyEditor.setContent(value || ''); } } @@ -71,6 +70,7 @@ export class RichEditorComponent implements ControlValueAccessor, AfterViewInit, tinymce.init({ setup: (editor: any) => { self.tinyEditor = editor; + self.tinyEditor.setMode(this.isDisabled ? 'readonly' : 'design'); self.tinyEditor.on('change', () => { const value = editor.getContent(); diff --git a/src/Squidex/app/framework/angular/slider.component.ts b/src/Squidex/app/framework/angular/slider.component.ts index d4c0bbbcb..a893e0e84 100644 --- a/src/Squidex/app/framework/angular/slider.component.ts +++ b/src/Squidex/app/framework/angular/slider.component.ts @@ -8,8 +8,7 @@ import { Component, ElementRef, forwardRef, Input, Renderer, ViewChild } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -/* tslint:disable:no-empty */ -const NOOP = () => { }; +const NOOP = () => { /* NOOP */ }; export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SliderComponent), multi: true diff --git a/src/Squidex/app/framework/angular/stars.component.html b/src/Squidex/app/framework/angular/stars.component.html new file mode 100644 index 000000000..3f6f87aec --- /dev/null +++ b/src/Squidex/app/framework/angular/stars.component.html @@ -0,0 +1,12 @@ +
+
+ Must not have more more than 15 stars +
+
+ + + + + +
+
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/stars.component.scss b/src/Squidex/app/framework/angular/stars.component.scss new file mode 100644 index 000000000..07aca0371 --- /dev/null +++ b/src/Squidex/app/framework/angular/stars.component.scss @@ -0,0 +1,47 @@ +@import '_mixins'; +@import '_vars'; + +$color-gold: #ffd700; + +.many-stars { + color: $color-empty; +} + +.stars { + & { + display: inline-block; + border: 0; + height: 2.4rem; + line-height: 2.4rem; + } + + &.disabled { + cursor: not-allowed; + } + + &-container { + height: 2.4rem; + } +} + +.star { + & { + background: transparent; + border: 0; + line-height: 1px; + } + + &::before { + display: inline-block; + color: $color-gold; + content: '☆'; + font-size: 1.8rem; + line-height: 1px; + } + + &.selected { + &::before { + content: '★'; + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/stars.component.ts b/src/Squidex/app/framework/angular/stars.component.ts new file mode 100644 index 000000000..a3ca5b000 --- /dev/null +++ b/src/Squidex/app/framework/angular/stars.component.ts @@ -0,0 +1,116 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, forwardRef, Input } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +const NOOP = () => { /* NOOP */ }; + +export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarsComponent), multi: true +}; + +@Component({ + selector: 'sqx-stars', + styleUrls: ['./stars.component.scss'], + templateUrl: './stars.component.html', + providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR] +}) +export class StarsComponent implements ControlValueAccessor { + private changeCallback: (value: any) => void = NOOP; + private touchedCallback: () => void = NOOP; + private maximumStarsValue = 5; + + public isDisabled = false; + + public stars: number; + public starsArray: number[] = [1, 2, 3, 4, 5]; + + public value: number | null = 1; + + public get maximumStars() { + return this.maximumStarsValue; + } + + @Input() + public set maximumStars(value: any) { + value = value || 5; + + if (!(typeof value === 'number')) { + value = 5; + } + + if (this.maximumStarsValue !== value) { + this.maximumStarsValue = value; + + this.starsArray = []; + + for (let i = 1; i <= value; i++) { + this.starsArray.push(i); + } + } + } + + public writeValue(value: any) { + this.value = this.stars = value; + } + + public setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + } + + public registerOnChange(fn: any) { + this.changeCallback = fn; + } + + public registerOnTouched(fn: any) { + this.touchedCallback = fn; + } + + public setPreview(value: number) { + if (this.isDisabled) { + return; + } + + this.stars = value; + } + + public stopPreview() { + if (this.isDisabled) { + return; + } + + this.stars = this.value; + } + + public reset() { + if (this.isDisabled) { + return; + } + + this.value = null; + this.stars = 0; + + this.changeCallback(this.value); + this.touchedCallback(); + + return false; + } + + public setValue(value: number) { + if (this.isDisabled) { + return; + } + + this.value = this.stars = value; + + this.changeCallback(this.value); + this.touchedCallback(); + + return false; + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/tag-editor.component.ts b/src/Squidex/app/framework/angular/tag-editor.component.ts index 7f4310cb8..65cb07aa2 100644 --- a/src/Squidex/app/framework/angular/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/tag-editor.component.ts @@ -9,8 +9,7 @@ import { Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; const KEY_ENTER = 13; -/* tslint:disable:no-empty */ -const NOOP = () => { }; +const NOOP = () => { /* NOOP */ }; export interface Converter { convert(input: string): any; diff --git a/src/Squidex/app/framework/angular/toggle.component.html b/src/Squidex/app/framework/angular/toggle.component.html new file mode 100644 index 000000000..1e98594bc --- /dev/null +++ b/src/Squidex/app/framework/angular/toggle.component.html @@ -0,0 +1,6 @@ +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/toggle.component.scss b/src/Squidex/app/framework/angular/toggle.component.scss new file mode 100644 index 000000000..ba591b7c5 --- /dev/null +++ b/src/Squidex/app/framework/angular/toggle.component.scss @@ -0,0 +1,58 @@ +@import '_mixins'; +@import '_vars'; + +$toggle-width: 3.2rem; +$toggle-height: 2rem; +$toggle-button-size: $toggle-height - .3rem; + +.toggle { + &-button { + @include circle($toggle-button-size); + @include box-shadow(0, 2px, 2px, .2); + @include absolute($toggle-height * .5, auto, auto, $toggle-width * .5); + @include transition(left .3s ease); + background: $color-accent-dark; + border: 0; + margin-left: -$toggle-button-size * .5; + margin-top: -$toggle-button-size * .5; + } + + &-container { + & { + @include border-radius($toggle-height * .5); + @include box-shadow-inner; + @include transition(background-color .3s ease); + position: relative; + background: lighten($color-border, 6%); + border: 0; + height: $toggle-height; + width: $toggle-width; + } + + &.checked { + & { + background: $color-theme-green-dark; + } + + .toggle-button { + left: $toggle-height * .5; + } + } + + &.unchecked { + & { + background: $color-theme-error; + } + + .toggle-button { + left: $toggle-width - $toggle-height * .5; + } + } + + &.disabled { + background: $color-disabled; + border: 0; + cursor: not-allowed; + } + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/toggle.component.ts b/src/Squidex/app/framework/angular/toggle.component.ts new file mode 100644 index 000000000..8073ecf9d --- /dev/null +++ b/src/Squidex/app/framework/angular/toggle.component.ts @@ -0,0 +1,55 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, forwardRef } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +const NOOP = () => { /* NOOP */ }; + +export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToggleComponent), multi: true +}; + +@Component({ + selector: 'sqx-toggle', + styleUrls: ['./toggle.component.scss'], + templateUrl: './toggle.component.html', + providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR] +}) +export class ToggleComponent implements ControlValueAccessor { + private changeCallback: (value: any) => void = NOOP; + private touchedCallback: () => void = NOOP; + + public isChecked: boolean | undefined = undefined; + public isDisabled = false; + + public writeValue(value: any) { + this.isChecked = value; + } + + public setDisabledState(isDisabled: boolean): void { + this.isDisabled = isDisabled; + } + + public registerOnChange(fn: any) { + this.changeCallback = fn; + } + + public registerOnTouched(fn: any) { + this.touchedCallback = fn; + } + + public changeState() { + if (this.isDisabled) { + return; + } + this.isChecked = !(this.isChecked === true); + + this.changeCallback(this.isChecked); + this.touchedCallback(); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 2f32c222e..00d6e3df4 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -15,7 +15,9 @@ export * from './angular/date-time-editor.component'; export * from './angular/date-time.pipes'; export * from './angular/focus-on-change.directive'; export * from './angular/focus-on-init.directive'; +export * from './angular/geolocation-editor.component'; export * from './angular/http-utils'; +export * from './angular/indeterminate-value.directive'; export * from './angular/json-editor.component'; export * from './angular/markdown-editor.component'; export * from './angular/modal-view.directive'; @@ -27,8 +29,10 @@ export * from './angular/rich-editor.component'; export * from './angular/scroll-active.directive'; export * from './angular/shortcut.component'; export * from './angular/slider.component'; +export * from './angular/stars.component'; export * from './angular/tag-editor.component'; export * from './angular/title.component'; +export * from './angular/toggle.component'; export * from './angular/user-report.component'; export * from './configurations'; @@ -49,4 +53,6 @@ export * from './utils/duration'; export * from './utils/immutable-array'; export * from './utils/math-helper'; export * from './utils/modal-view'; -export * from './utils/string-helper'; \ No newline at end of file +export * from './utils/pager'; +export * from './utils/string-helper'; +export * from './utils/version'; \ No newline at end of file diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 33a5101bc..50ef39dbd 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -25,6 +25,8 @@ import { FocusOnChangeDirective, FocusOnInitDirective, FromNowPipe, + GeolocationEditorComponent, + IndeterminateValueDirective, JsonEditorComponent, LocalStoreService, MarkdownEditorComponent, @@ -44,9 +46,11 @@ import { ShortDatePipe, ShortTimePipe, SliderComponent, + StarsComponent, TagEditorComponent, TitleService, TitleComponent, + ToggleComponent, UserReportComponent } from './declarations'; @@ -71,6 +75,8 @@ import { FocusOnChangeDirective, FocusOnInitDirective, FromNowPipe, + GeolocationEditorComponent, + IndeterminateValueDirective, JsonEditorComponent, MarkdownEditorComponent, ModalViewDirective, @@ -84,8 +90,10 @@ import { ShortDatePipe, ShortTimePipe, SliderComponent, + StarsComponent, TagEditorComponent, TitleComponent, + ToggleComponent, UserReportComponent ], exports: [ @@ -101,6 +109,8 @@ import { FocusOnChangeDirective, FocusOnInitDirective, FromNowPipe, + GeolocationEditorComponent, + IndeterminateValueDirective, JsonEditorComponent, MarkdownEditorComponent, ModalViewDirective, @@ -114,8 +124,10 @@ import { ShortDatePipe, ShortTimePipe, SliderComponent, + StarsComponent, TagEditorComponent, TitleComponent, + ToggleComponent, UserReportComponent, HttpModule, FormsModule, diff --git a/src/Squidex/app/framework/services/notification.service.spec.ts b/src/Squidex/app/framework/services/notification.service.spec.ts index 2dbd2b9a8..1d880647c 100644 --- a/src/Squidex/app/framework/services/notification.service.spec.ts +++ b/src/Squidex/app/framework/services/notification.service.spec.ts @@ -27,7 +27,7 @@ describe('NotificationService', () => { it('should create error', () => { const notification = Notification.error('MyError'); - expect(notification.displayTime).toBe(10000); + expect(notification.displayTime).toBe(5000); expect(notification.message).toBe('MyError'); expect(notification.messageType).toBe('error'); }); @@ -35,7 +35,7 @@ describe('NotificationService', () => { it('should create info', () => { const notification = Notification.info('MyInfo'); - expect(notification.displayTime).toBe(10000); + expect(notification.displayTime).toBe(5000); expect(notification.message).toBe('MyInfo'); expect(notification.messageType).toBe('info'); }); diff --git a/src/Squidex/app/framework/services/notification.service.ts b/src/Squidex/app/framework/services/notification.service.ts index c1a4a67bf..d659c73f4 100644 --- a/src/Squidex/app/framework/services/notification.service.ts +++ b/src/Squidex/app/framework/services/notification.service.ts @@ -16,7 +16,7 @@ export class Notification { constructor( public readonly message: string, public readonly messageType: string, - public readonly displayTime: number = 10000 + public readonly displayTime: number = 5000 ) { } diff --git a/src/Squidex/app/framework/utils/pager.spec.ts b/src/Squidex/app/framework/utils/pager.spec.ts new file mode 100644 index 000000000..da4838835 --- /dev/null +++ b/src/Squidex/app/framework/utils/pager.spec.ts @@ -0,0 +1,151 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Pager } from './../'; + +describe('Pager', () => { + it('Should init with page size and page', () => { + const pager_1 = new Pager(23, 2, 10); + + expect(Object.assign({}, pager_1)).toEqual({ + page: 2, + pageSize: 10, + itemFirst: 21, + itemLast: 23, + skip: 20, + numberOfItems: 23, + canGoNext: false, + canGoPrev: true + }); + }); + + it('Should reset page on reset', () => { + const pager_1 = new Pager(23, 2, 10); + const pager_2 = pager_1.reset(); + + expect(Object.assign({}, pager_2)).toEqual({ + page: 0, + pageSize: 10, + itemFirst: 0, + itemLast: 0, + skip: 0, + numberOfItems: 0, + canGoNext: false, + canGoPrev: false + }); + }); + + it('should return same instance when go next and being on last page', () => { + const pager_1 = new Pager(23, 2, 10); + const pager_2 = pager_1.goNext(); + + expect(pager_2).toBe(pager_1); + }); + + it('should update page when going next', () => { + const pager_1 = new Pager(23, 0, 10); + const pager_2 = pager_1.goNext(); + + expect(Object.assign({}, pager_2)).toEqual({ + page: 1, + pageSize: 10, + itemFirst: 11, + itemLast: 20, + skip: 10, + numberOfItems: 23, + canGoNext: true, + canGoPrev: true + }); + }); + + it('should return same instance when go prev and being on first page', () => { + const pager_1 = new Pager(23, 0, 10); + const pager_2 = pager_1.goPrev(); + + expect(pager_2).toBe(pager_1); + }); + + it('should update page when going prev', () => { + const pager_1 = new Pager(23, 2, 10); + const pager_2 = pager_1.goPrev(); + + expect(Object.assign({}, pager_2)).toEqual({ + page: 1, + pageSize: 10, + itemFirst: 11, + itemLast: 20, + skip: 10, + numberOfItems: 23, + canGoNext: true, + canGoPrev: true + }); + }); + + it('should update count when setting it', () => { + const pager_1 = new Pager(23, 2, 10); + const pager_2 = pager_1.setCount(30); + + expect(Object.assign({}, pager_2)).toEqual({ + page: 2, + pageSize: 10, + itemFirst: 21, + itemLast: 30, + skip: 20, + numberOfItems: 30, + canGoNext: false, + canGoPrev: true + }); + }); + + it('should update count when incrementing it', () => { + const pager_1 = new Pager(23, 2, 10); + const pager_2 = pager_1.incrementCount(); + + expect(Object.assign({}, pager_2)).toEqual({ + page: 2, + pageSize: 10, + itemFirst: 21, + itemLast: 24, + skip: 20, + numberOfItems: 24, + canGoNext: false, + canGoPrev: true + }); + }); + + it('should update count when decrementing it', () => { + const pager_1 = new Pager(23, 2, 10); + const pager_2 = pager_1.decrementCount(); + + expect(Object.assign({}, pager_2)).toEqual({ + page: 2, + pageSize: 10, + itemFirst: 21, + itemLast: 22, + skip: 20, + numberOfItems: 22, + canGoNext: false, + canGoPrev: true + }); + }); + + it('should also update page when new page is bigger than max page', () => { + const pager_1 = new Pager(21, 2, 10); + const pager_2 = pager_1.decrementCount(); + + expect(Object.assign({}, pager_2)).toEqual({ + page: 1, + pageSize: 10, + itemFirst: 11, + itemLast: 20, + skip: 10, + numberOfItems: 20, + canGoNext: false, + canGoPrev: true + }); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/framework/utils/pager.ts b/src/Squidex/app/framework/utils/pager.ts new file mode 100644 index 000000000..908680a24 --- /dev/null +++ b/src/Squidex/app/framework/utils/pager.ts @@ -0,0 +1,68 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export class Pager { + public canGoNext = false; + public canGoPrev = false; + + public itemFirst = 0; + public itemLast = 0; + + public skip = 0; + + constructor( + public readonly numberOfItems: number, + public readonly page = 0, + public readonly pageSize = 10 + ) { + const totalPages = Math.ceil(numberOfItems / this.pageSize); + + if (page >= totalPages && page > 0) { + page = this.page = totalPages - 1; + } + + this.itemFirst = numberOfItems === 0 ? 0 : page * this.pageSize + 1; + this.itemLast = Math.min(numberOfItems, (page + 1) * this.pageSize); + + this.canGoNext = page < totalPages - 1; + this.canGoPrev = page > 0; + + this.skip = page * pageSize; + } + + public goNext(): Pager { + if (!this.canGoNext) { + return this; + } + + return new Pager(this.numberOfItems, this.page + 1, this.pageSize); + } + + public goPrev(): Pager { + if (!this.canGoPrev) { + return this; + } + + return new Pager(this.numberOfItems, this.page - 1, this.pageSize); + } + + public reset(): Pager { + return new Pager(0, 0, this.pageSize); + } + + public setCount(numberOfItems: number): Pager { + return new Pager(numberOfItems, this.page, this.pageSize); + } + + public incrementCount(): Pager { + return new Pager(this.numberOfItems + 1, this.page, this.pageSize); + } + + public decrementCount(): Pager { + return new Pager(this.numberOfItems - 1, this.page, this.pageSize); + } +} \ No newline at end of file diff --git a/src/Squidex/app/framework/utils/version.spec.ts b/src/Squidex/app/framework/utils/version.spec.ts new file mode 100644 index 000000000..42a48c108 --- /dev/null +++ b/src/Squidex/app/framework/utils/version.spec.ts @@ -0,0 +1,30 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Version } from './../'; + +describe('Version', () => { + it('should initialize with default value', () => { + const version = new Version(); + + expect(version.value).toBe(''); + }); + + it('should initialize with init value', () => { + const version = new Version('1.0'); + + expect(version.value).toBe('1.0'); + }); + + it('should update to new value', () => { + const version = new Version(); + + version.update('1.0'); + + expect(version.value).toBe('1.0'); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/framework/utils/version.ts b/src/Squidex/app/framework/utils/version.ts new file mode 100644 index 000000000..e06958b00 --- /dev/null +++ b/src/Squidex/app/framework/utils/version.ts @@ -0,0 +1,21 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +export class Version { + public get value() { + return this.currentValue; + } + + constructor( + private currentValue: string = '' + ) { + } + + public update(newValue: string) { + this.currentValue = newValue; + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/components/app-form.component.html b/src/Squidex/app/shared/components/app-form.component.html index 1855817c8..77c9a57e3 100644 --- a/src/Squidex/app/shared/components/app-form.component.html +++ b/src/Squidex/app/shared/components/app-form.component.html @@ -13,7 +13,7 @@ - The app name becomes part of the api url,
e.g https://{{appName | async}}.squidex.io/. + The app name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{appName | async}}/.
diff --git a/src/Squidex/app/shared/components/app-form.component.scss b/src/Squidex/app/shared/components/app-form.component.scss index e69de29bb..fbb752506 100644 --- a/src/Squidex/app/shared/components/app-form.component.scss +++ b/src/Squidex/app/shared/components/app-form.component.scss @@ -0,0 +1,2 @@ +@import '_vars'; +@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/shared/components/app-form.component.ts b/src/Squidex/app/shared/components/app-form.component.ts index 5bf4f1bcc..4ca6c71d2 100644 --- a/src/Squidex/app/shared/components/app-form.component.ts +++ b/src/Squidex/app/shared/components/app-form.component.ts @@ -7,9 +7,8 @@ import { Component, EventEmitter, Output } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { Observable } from 'rxjs'; -import { ValidatorsEx } from 'framework'; +import { ApiUrlConfig, ValidatorsEx } from 'framework'; import { AppsStoreService } from './../services/apps-store.service'; import { AppDto, CreateAppDto } from './../services/apps.service'; @@ -41,10 +40,11 @@ export class AppFormComponent { }); public appName = - Observable.of(FALLBACK_NAME) - .merge(this.createForm.get('name').valueChanges.map(n => n || FALLBACK_NAME)); + this.createForm.get('name').valueChanges.map(n => n || FALLBACK_NAME) + .startWith(FALLBACK_NAME); constructor( + public readonly apiUrl: ApiUrlConfig, private readonly appsStore: AppsStoreService, private readonly formBuilder: FormBuilder ) { diff --git a/src/Squidex/app/shared/components/language-selector.component.ts b/src/Squidex/app/shared/components/language-selector.component.ts index af60a5a82..e6d2c3b1b 100644 --- a/src/Squidex/app/shared/components/language-selector.component.ts +++ b/src/Squidex/app/shared/components/language-selector.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Sebastian Stehle. All rights reserved */ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { fadeAnimation, ModalView } from 'framework'; @@ -19,7 +19,7 @@ export interface Language { iso2Code: string; englishName: string; isMasterLangu fadeAnimation ] }) -export class LanguageSelectorComponent implements OnInit { +export class LanguageSelectorComponent implements OnChanges, OnInit { public dropdown = new ModalView(false, true); @Input() @@ -42,8 +42,16 @@ export class LanguageSelectorComponent implements OnInit { return this.languages && this.languages.length > 3; } + public ngOnChanges() { + this.update(); + } + public ngOnInit() { - if (this.languages && this.languages.length > 0 && !this.selectedLanguage) { + this.update(); + } + + private update() { + if (this.languages && this.languages.length > 0 && (!this.selectedLanguage || this.languages.indexOf(this.selectedLanguage) < 0)) { const selectedLanguage = this.languages.find(l => l.isMasterLanguage) || this.languages[0]; diff --git a/src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts index 40c19615f..2ca36223a 100644 --- a/src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts @@ -33,7 +33,7 @@ describe('ResolveAppLanguagesGuard', () => { }); it('should navigate to 404 page if languages are not found', (done) => { - appLanguagesService.setup(x => x.getLanguages('my-app')) + appLanguagesService.setup(x => x.getLanguages('my-app', null)) .returns(() => Observable.of(null!)); const router = new RouterMockup(); @@ -49,7 +49,7 @@ describe('ResolveAppLanguagesGuard', () => { }); it('should navigate to 404 page if languages loading fails', (done) => { - appLanguagesService.setup(x => x.getLanguages('my-app')) + appLanguagesService.setup(x => x.getLanguages('my-app', null)) .returns(() => Observable.throw(null!)); const router = new RouterMockup(); @@ -67,7 +67,7 @@ describe('ResolveAppLanguagesGuard', () => { it('should return schema if loading succeeded', (done) => { const languages: AppLanguageDto[] = []; - appLanguagesService.setup(x => x.getLanguages('my-app')) + appLanguagesService.setup(x => x.getLanguages('my-app', null)) .returns(() => Observable.of(languages)); const router = new RouterMockup(); diff --git a/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts b/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts index 920a0094f..ced6111c2 100644 --- a/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-app-languages.guard.ts @@ -26,7 +26,7 @@ export class ResolveAppLanguagesGuard implements Resolve { } const result = - this.appLanguagesService.getLanguages(appName).toPromise() + this.appLanguagesService.getLanguages(appName, null).toPromise() .then(dto => { if (!dto) { this.router.navigate(['/404']); diff --git a/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts index f0ea8c9b8..52ef46ec9 100644 --- a/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-content.guard.spec.ts @@ -43,7 +43,7 @@ describe('ResolveContentGuard', () => { }); it('should navigate to 404 page if schema is not found', (done) => { - appsStore.setup(x => x.getContent('my-app', 'my-schema', '123')) + appsStore.setup(x => x.getContent('my-app', 'my-schema', '123', null)) .returns(() => Observable.of(null!)); const router = new RouterMockup(); @@ -59,7 +59,7 @@ describe('ResolveContentGuard', () => { }); it('should navigate to 404 page if schema loading fails', (done) => { - appsStore.setup(x => x.getContent('my-app', 'my-schema', '123')) + appsStore.setup(x => x.getContent('my-app', 'my-schema', '123', null)) .returns(() => Observable.throw(null!)); const router = new RouterMockup(); @@ -77,7 +77,7 @@ describe('ResolveContentGuard', () => { it('should return schema if loading succeeded', (done) => { const schema = {}; - appsStore.setup(x => x.getContent('my-app', 'my-schema', '123')) + appsStore.setup(x => x.getContent('my-app', 'my-schema', '123', null)) .returns(() => Observable.of(schema)); const router = new RouterMockup(); diff --git a/src/Squidex/app/shared/guards/resolve-content.guard.ts b/src/Squidex/app/shared/guards/resolve-content.guard.ts index 461028e88..c4cbbcb90 100644 --- a/src/Squidex/app/shared/guards/resolve-content.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-content.guard.ts @@ -28,7 +28,7 @@ export class ResolveContentGuard implements Resolve { } const result = - this.contentsService.getContent(appName, schemaName, contentId).toPromise() + this.contentsService.getContent(appName, schemaName, contentId, null).toPromise() .then(dto => { if (!dto) { this.router.navigate(['/404']); diff --git a/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts index 3b9dfee16..e76b358a2 100644 --- a/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts @@ -38,7 +38,7 @@ describe('ResolvePublishedSchemaGuard', () => { }); it('should navigate to 404 page if schema is not found', (done) => { - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.of(null!)); const router = new RouterMockup(); @@ -54,7 +54,7 @@ describe('ResolvePublishedSchemaGuard', () => { }); it('should navigate to 404 page if schema loading fails', (done) => { - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.throw(null)); const router = new RouterMockup(); @@ -72,7 +72,7 @@ describe('ResolvePublishedSchemaGuard', () => { it('should navigate to 404 page if schema not published', (done) => { const schema = { isPublished: false }; - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.of(schema)); const router = new RouterMockup(); @@ -90,7 +90,7 @@ describe('ResolvePublishedSchemaGuard', () => { it('should return schema if loading succeeded', (done) => { const schema = { isPublished: true }; - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.of(schema)); const router = new RouterMockup(); diff --git a/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts b/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts index 0cbdecc66..956fc1c4e 100644 --- a/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-published-schema.guard.ts @@ -27,7 +27,7 @@ export class ResolvePublishedSchemaGuard implements Resolve { } const result = - this.schemasService.getSchema(appName, schemaName).toPromise() + this.schemasService.getSchema(appName, schemaName, null).toPromise() .then(dto => { if (!dto || !dto.isPublished) { this.router.navigate(['/404']); diff --git a/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts b/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts index aaacd1ad7..7f7f9ffc6 100644 --- a/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts +++ b/src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts @@ -38,7 +38,7 @@ describe('ResolveSchemaGuard', () => { }); it('should navigate to 404 page if schema is not found', (done) => { - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.of(null!)); const router = new RouterMockup(); @@ -54,7 +54,7 @@ describe('ResolveSchemaGuard', () => { }); it('should navigate to 404 page if schema loading fails', (done) => { - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.throw(null!)); const router = new RouterMockup(); @@ -72,7 +72,7 @@ describe('ResolveSchemaGuard', () => { it('should return schema if loading succeeded', (done) => { const schema = {}; - schemasService.setup(x => x.getSchema('my-app', 'my-schema')) + schemasService.setup(x => x.getSchema('my-app', 'my-schema', null)) .returns(() => Observable.of(schema)); const router = new RouterMockup(); diff --git a/src/Squidex/app/shared/guards/resolve-schema.guard.ts b/src/Squidex/app/shared/guards/resolve-schema.guard.ts index ac9cc8752..293f1ca92 100644 --- a/src/Squidex/app/shared/guards/resolve-schema.guard.ts +++ b/src/Squidex/app/shared/guards/resolve-schema.guard.ts @@ -27,7 +27,7 @@ export class ResolveSchemaGuard implements Resolve { } const result = - this.schemasService.getSchema(appName, schemaName).toPromise() + this.schemasService.getSchema(appName, schemaName, null).toPromise() .then(dto => { if (!dto) { this.router.navigate(['/404']); diff --git a/src/Squidex/app/shared/services/app-clients.service.spec.ts b/src/Squidex/app/shared/services/app-clients.service.spec.ts index ed93d2ac3..d68bffd90 100644 --- a/src/Squidex/app/shared/services/app-clients.service.spec.ts +++ b/src/Squidex/app/shared/services/app-clients.service.spec.ts @@ -16,12 +16,14 @@ import { AppClientsService, AuthService, CreateAppClientDto, - UpdateAppClientDto + UpdateAppClientDto, + Version } from './../'; describe('AppClientsService', () => { let authService: IMock; let appClientsService: AppClientsService; + let version = new Version('1'); let http: IMock; beforeEach(() => { @@ -32,7 +34,7 @@ describe('AppClientsService', () => { }); it('should make get request to get app clients', () => { - authService.setup(x => x.authGet('http://service/p/api/apps/my-app/clients')) + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/clients', version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -52,7 +54,7 @@ describe('AppClientsService', () => { let clients: AppClientDto[] | null = null; - appClientsService.getClients('my-app').subscribe(result => { + appClientsService.getClients('my-app', version).subscribe(result => { clients = result; }).unsubscribe(); @@ -68,7 +70,7 @@ describe('AppClientsService', () => { it('should make post request to create client', () => { const dto = new CreateAppClientDto('client1'); - authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', dto)) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/clients', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -84,7 +86,7 @@ describe('AppClientsService', () => { let client: AppClientDto | null = null; - appClientsService.postClient('my-app', dto).subscribe(result => { + appClientsService.postClient('my-app', dto, version).subscribe(result => { client = result; }); @@ -97,7 +99,7 @@ describe('AppClientsService', () => { it('should make put request to rename client', () => { const dto = new UpdateAppClientDto('Client 1 New'); - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/clients/client1', dto)) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/clients/client1', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -105,13 +107,13 @@ describe('AppClientsService', () => { )) .verifiable(Times.once()); - appClientsService.updateClient('my-app', 'client1', dto); + appClientsService.updateClient('my-app', 'client1', dto, version); authService.verifyAll(); }); it('should make delete request to remove client', () => { - authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/clients/client1')) + authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/clients/client1', version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -119,7 +121,7 @@ describe('AppClientsService', () => { )) .verifiable(Times.once()); - appClientsService.deleteClient('my-app', 'client1'); + appClientsService.deleteClient('my-app', 'client1', version); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/app-clients.service.ts b/src/Squidex/app/shared/services/app-clients.service.ts index f15a35c00..cac70d755 100644 --- a/src/Squidex/app/shared/services/app-clients.service.ts +++ b/src/Squidex/app/shared/services/app-clients.service.ts @@ -11,7 +11,7 @@ import { Observable } from 'rxjs'; import 'framework/angular/http-extensions'; -import { ApiUrlConfig } from 'framework'; +import { ApiUrlConfig, Version } from 'framework'; import { AuthService } from './auth.service'; export class AppClientDto { @@ -54,10 +54,10 @@ export class AppClientsService { ) { } - public getClients(appName: string): Observable { + public getClients(appName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); - return this.authService.authGet(url) + return this.authService.authGet(url, version) .map(response => response.json()) .map(response => { const items: any[] = response; @@ -72,10 +72,10 @@ export class AppClientsService { .catchError('Failed to load clients. Please reload.'); } - public postClient(appName: string, dto: CreateAppClientDto): Observable { + public postClient(appName: string, dto: CreateAppClientDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients`); - return this.authService.authPost(url, dto) + return this.authService.authPost(url, dto, version) .map(response => response.json()) .map(response => { return new AppClientDto( @@ -86,17 +86,17 @@ export class AppClientsService { .catchError('Failed to add client. Please reload.'); } - public updateClient(appName: string, id: string, dto: UpdateAppClientDto): Observable { + public updateClient(appName: string, id: string, dto: UpdateAppClientDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`); - return this.authService.authPut(url, dto) + return this.authService.authPut(url, dto, version) .catchError('Failed to revoke client. Please reload.'); } - public deleteClient(appName: string, id: string): Observable { + public deleteClient(appName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/clients/${id}`); - return this.authService.authDelete(url) + return this.authService.authDelete(url, version) .catchError('Failed to revoke client. Please reload.'); } diff --git a/src/Squidex/app/shared/services/app-contributors.service.spec.ts b/src/Squidex/app/shared/services/app-contributors.service.spec.ts index e68d158ae..047da8cfa 100644 --- a/src/Squidex/app/shared/services/app-contributors.service.spec.ts +++ b/src/Squidex/app/shared/services/app-contributors.service.spec.ts @@ -13,12 +13,14 @@ import { ApiUrlConfig, AppContributorDto, AppContributorsService, - AuthService + AuthService, + Version } from './../'; describe('AppContributorsService', () => { let authService: IMock; let appContributorsService: AppContributorsService; + let version = new Version('1'); beforeEach(() => { authService = Mock.ofType(AuthService); @@ -26,7 +28,7 @@ describe('AppContributorsService', () => { }); it('should make get request to get app contributors', () => { - authService.setup(x => x.authGet('http://service/p/api/apps/my-app/contributors')) + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/contributors', version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -44,7 +46,7 @@ describe('AppContributorsService', () => { let contributors: AppContributorDto[] | null = null; - appContributorsService.getContributors('my-app').subscribe(result => { + appContributorsService.getContributors('my-app', version).subscribe(result => { contributors = result; }).unsubscribe(); @@ -60,7 +62,7 @@ describe('AppContributorsService', () => { it('should make post request to assign contributor', () => { const dto = new AppContributorDto('123', 'Owner'); - authService.setup(x => x.authPost('http://service/p/api/apps/my-app/contributors', dto)) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/contributors', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -68,13 +70,13 @@ describe('AppContributorsService', () => { )) .verifiable(Times.once()); - appContributorsService.postContributor('my-app', dto); + appContributorsService.postContributor('my-app', dto, version); authService.verifyAll(); }); it('should make delete request to remove contributor', () => { - authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/contributors/123')) + authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/contributors/123', version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -82,7 +84,7 @@ describe('AppContributorsService', () => { )) .verifiable(Times.once()); - appContributorsService.deleteContributor('my-app', '123'); + appContributorsService.deleteContributor('my-app', '123', version); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/app-contributors.service.ts b/src/Squidex/app/shared/services/app-contributors.service.ts index a5cc54be5..229db6446 100644 --- a/src/Squidex/app/shared/services/app-contributors.service.ts +++ b/src/Squidex/app/shared/services/app-contributors.service.ts @@ -10,7 +10,7 @@ import { Observable } from 'rxjs'; import 'framework/angular/http-extensions'; -import { ApiUrlConfig } from 'framework'; +import { ApiUrlConfig, Version } from 'framework'; import { AuthService } from './auth.service'; export class AppContributorDto { @@ -29,10 +29,10 @@ export class AppContributorsService { ) { } - public getContributors(appName: string): Observable { + public getContributors(appName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); - return this.authService.authGet(url) + return this.authService.authGet(url, version) .map(response => response.json()) .map(response => { const items: any[] = response; @@ -46,17 +46,17 @@ export class AppContributorsService { .catchError('Failed to load contributors. Please reload.'); } - public postContributor(appName: string, dto: AppContributorDto): Observable { + public postContributor(appName: string, dto: AppContributorDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors`); - return this.authService.authPost(url, dto) + return this.authService.authPost(url, dto, version) .catchError('Failed to add contributors. Please reload.'); } - public deleteContributor(appName: string, contributorId: string): Observable { + public deleteContributor(appName: string, contributorId: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/contributors/${contributorId}`); - return this.authService.authDelete(url) + return this.authService.authDelete(url, version) .catchError('Failed to delete contributors. Please reload.'); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/app-languages.service.spec.ts b/src/Squidex/app/shared/services/app-languages.service.spec.ts index 75e4b0ce0..9014f7beb 100644 --- a/src/Squidex/app/shared/services/app-languages.service.spec.ts +++ b/src/Squidex/app/shared/services/app-languages.service.spec.ts @@ -15,12 +15,14 @@ import { AppLanguageDto, AppLanguagesService, AuthService, - UpdateAppLanguageDto + UpdateAppLanguageDto, + Version } from './../'; describe('AppLanguagesService', () => { let authService: IMock; let appLanguagesService: AppLanguagesService; + let version = new Version('1'); beforeEach(() => { authService = Mock.ofType(AuthService); @@ -28,7 +30,7 @@ describe('AppLanguagesService', () => { }); it('should make get request to get app languages', () => { - authService.setup(x => x.authGet('http://service/p/api/apps/my-app/languages')) + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/languages', version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -47,7 +49,7 @@ describe('AppLanguagesService', () => { let languages: AppLanguageDto[] | null = null; - appLanguagesService.getLanguages('my-app').subscribe(result => { + appLanguagesService.getLanguages('my-app', version).subscribe(result => { languages = result; }).unsubscribe(); @@ -63,7 +65,7 @@ describe('AppLanguagesService', () => { it('should make post request to add language', () => { const dto = new AddAppLanguageDto('de'); - authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', dto)) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/languages', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -78,7 +80,7 @@ describe('AppLanguagesService', () => { let language: AppLanguageDto | null = null; - appLanguagesService.postLanguages('my-app', dto).subscribe(result => { + appLanguagesService.postLanguages('my-app', dto, version).subscribe(result => { language = result; }); @@ -91,7 +93,7 @@ describe('AppLanguagesService', () => { it('should make put request to make master language', () => { const dto = new UpdateAppLanguageDto(true); - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/languages/de', dto)) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/languages/de', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -99,13 +101,13 @@ describe('AppLanguagesService', () => { )) .verifiable(Times.once()); - appLanguagesService.updateLanguage('my-app', 'de', dto); + appLanguagesService.updateLanguage('my-app', 'de', dto, version); authService.verifyAll(); }); it('should make delete request to remove language', () => { - authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/languages/de')) + authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/languages/de', version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -113,7 +115,7 @@ describe('AppLanguagesService', () => { )) .verifiable(Times.once()); - appLanguagesService.deleteLanguage('my-app', 'de'); + appLanguagesService.deleteLanguage('my-app', 'de', version); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/app-languages.service.ts b/src/Squidex/app/shared/services/app-languages.service.ts index f527fe794..f0324db1d 100644 --- a/src/Squidex/app/shared/services/app-languages.service.ts +++ b/src/Squidex/app/shared/services/app-languages.service.ts @@ -10,7 +10,7 @@ import { Observable } from 'rxjs'; import 'framework/angular/http-extensions'; -import { ApiUrlConfig } from 'framework'; +import { ApiUrlConfig, Version } from 'framework'; import { AuthService } from './auth.service'; export class AppLanguageDto { @@ -44,10 +44,10 @@ export class AppLanguagesService { ) { } - public getLanguages(appName: string): Observable { + public getLanguages(appName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); - return this.authService.authGet(url) + return this.authService.authGet(url, version) .map(response => response.json()) .map(response => { const items: any[] = response; @@ -62,10 +62,10 @@ export class AppLanguagesService { .catchError('Failed to load languages. Please reload.'); } - public postLanguages(appName: string, dto: AddAppLanguageDto): Observable { + public postLanguages(appName: string, dto: AddAppLanguageDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages`); - return this.authService.authPost(url, dto) + return this.authService.authPost(url, dto, version) .map(response => response.json()) .map(response => { return new AppLanguageDto( @@ -76,17 +76,17 @@ export class AppLanguagesService { .catchError('Failed to add language. Please reload.'); } - public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto): Observable { + public updateLanguage(appName: string, languageCode: string, dto: UpdateAppLanguageDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`); - return this.authService.authPut(url, dto) + return this.authService.authPut(url, dto, version) .catchError('Failed to change language. Please reload.'); } - public deleteLanguage(appName: string, languageCode: string): Observable { + public deleteLanguage(appName: string, languageCode: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/languages/${languageCode}`); - return this.authService.authDelete(url) + return this.authService.authDelete(url, version) .catchError('Failed to add language. Please reload.'); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/apps-store.service.ts b/src/Squidex/app/shared/services/apps-store.service.ts index a45c49c42..7fca62d99 100644 --- a/src/Squidex/app/shared/services/apps-store.service.ts +++ b/src/Squidex/app/shared/services/apps-store.service.ts @@ -74,9 +74,10 @@ export class AppsStoreService { } private load() { - this.appsService.getApps().subscribe(apps => { - this.apps$.next(apps); - }); + this.appsService.getApps() + .subscribe(apps => { + this.apps$.next(apps); + }); } public selectApp(name: string | null): Promise { diff --git a/src/Squidex/app/shared/services/auth.service.ts b/src/Squidex/app/shared/services/auth.service.ts index 66814bad8..b695550c5 100644 --- a/src/Squidex/app/shared/services/auth.service.ts +++ b/src/Squidex/app/shared/services/auth.service.ts @@ -16,7 +16,7 @@ import { UserManager } from 'oidc-client'; -import { ApiUrlConfig } from 'framework'; +import { ApiUrlConfig, Version } from 'framework'; export class Profile { public get id(): string { @@ -169,46 +169,56 @@ export class AuthService { return resultPromise; } - public authGet(url: string, options?: RequestOptions): Observable { - options = this.setRequestOptions(options); + public authGet(url: string, version?: Version, options?: RequestOptions): Observable { + options = this.setRequestOptions(options, version); - return this.checkResponse(this.http.get(url, options)); + return this.checkResponse(this.http.get(url, options), version); } - public authPut(url: string, data: any, options?: RequestOptions): Observable { - options = this.setRequestOptions(options); + public authPut(url: string, data: any, version?: Version, options?: RequestOptions): Observable { + options = this.setRequestOptions(options, version); - return this.checkResponse(this.http.put(url, data, options)); + return this.checkResponse(this.http.put(url, data, options), version); } - public authDelete(url: string, options?: RequestOptions): Observable { - options = this.setRequestOptions(options); + public authDelete(url: string, version?: Version, options?: RequestOptions): Observable { + options = this.setRequestOptions(options, version); - return this.checkResponse(this.http.delete(url, options)); + return this.checkResponse(this.http.delete(url, options), version); } - public authPost(url: string, data: any, options?: RequestOptions): Observable { - options = this.setRequestOptions(options); + public authPost(url: string, data: any, version?: Version, options?: RequestOptions): Observable { + options = this.setRequestOptions(options, version); - return this.checkResponse(this.http.post(url, data, options)); + return this.checkResponse(this.http.post(url, data, options), version); } - private checkResponse(response: Observable) { - return response.catch((error: Response) => { - if (error.status === 401 || error.status === 404) { - this.logoutRedirect(); + private checkResponse(responseStream: Observable, version?: Version) { + return responseStream + .do((response: Response) => { + if (version && response.status.toString().indexOf('2') === 0) { + const etag = response.headers.get('etag'); - return Observable.empty(); - } else if (error.status === 403) { - this.router.navigate(['/404']); + if (etag) { + version.update(etag); + } + } + }) + .catch((error: Response) => { + if (error.status === 401 || error.status === 404) { + this.logoutRedirect(); - return Observable.empty(); - } - return Observable.throw(error); - }); + return Observable.empty(); + } else if (error.status === 403) { + this.router.navigate(['/404']); + + return Observable.empty(); + } + return Observable.throw(error); + }); } - private setRequestOptions(options?: RequestOptions) { + private setRequestOptions(options?: RequestOptions, version?: Version) { if (!options) { options = new RequestOptions(); } @@ -219,12 +229,17 @@ export class AuthService { } options.headers.append('Accept-Language', '*'); - options.headers.append('Pragma', 'no-cache'); + + if (version && version.value.length > 0) { + options.headers.append('If-Match', version.value); + } if (this.currentUser && this.currentUser.user) { options.headers.append('Authorization', `${this.currentUser.user.token_type} ${this.currentUser.user.access_token}`); } + options.headers.append('Pragma', 'no-cache'); + return options; } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/contents.service.spec.ts b/src/Squidex/app/shared/services/contents.service.spec.ts index 5521c3948..6aacdf637 100644 --- a/src/Squidex/app/shared/services/contents.service.spec.ts +++ b/src/Squidex/app/shared/services/contents.service.spec.ts @@ -12,16 +12,17 @@ import { IMock, It, Mock, Times } from 'typemoq'; import { ApiUrlConfig, AuthService, - EntityCreatedDto, ContentDto, ContentsDto, ContentsService, - DateTime + DateTime, + Version } from './../'; describe('ContentsService', () => { let authService: IMock; let contentsService: ContentsService; + let version = new Version('1'); beforeEach(() => { authService = Mock.ofType(AuthService); @@ -42,6 +43,7 @@ describe('ContentsService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + version: 11, data: {} }, { id: 'id2', @@ -50,6 +52,7 @@ describe('ContentsService', () => { createdBy: 'Created2', lastModified: '2017-10-12T10:10', lastModifiedBy: 'LastModifiedBy2', + version: 22, data: {} }] } @@ -66,8 +69,16 @@ describe('ContentsService', () => { expect(contents).toEqual( new ContentsDto(10, [ - new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), {}), - new ContentDto('id2', true, 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10'), {}) + new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + {}, + new Version('11')), + new ContentDto('id2', true, 'Created2', 'LastModifiedBy2', + DateTime.parseISO_UTC('2016-10-12T10:10'), + DateTime.parseISO_UTC('2017-10-12T10:10'), + {}, + new Version('22')) ])); authService.verifyAll(); @@ -118,7 +129,7 @@ describe('ContentsService', () => { }); it('should make get request to get content', () => { - authService.setup(x => x.authGet('http://service/p/api/content/my-app/my-schema/content1?hidden=true')) + authService.setup(x => x.authGet('http://service/p/api/content/my-app/my-schema/content1?hidden=true', version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -129,6 +140,7 @@ describe('ContentsService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + version: 11, data: {} } }) @@ -138,12 +150,16 @@ describe('ContentsService', () => { let content: ContentDto | null = null; - contentsService.getContent('my-app', 'my-schema', 'content1').subscribe(result => { + contentsService.getContent('my-app', 'my-schema', 'content1', version).subscribe(result => { content = result; }).unsubscribe(); expect(content).toEqual( - new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10'), {})); + new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + {}, + new Version('11'))); authService.verifyAll(); }); @@ -151,26 +167,37 @@ describe('ContentsService', () => { it('should make post request to create content', () => { const dto = {}; - authService.setup(x => x.authPost('http://service/p/api/content/my-app/my-schema', dto)) + authService.setup(x => x.authPost('http://service/p/api/content/my-app/my-schema', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions({ body: { - id: 'content1' + id: 'id1', + isPublished: true, + created: '2016-12-12T10:10', + createdBy: 'Created1', + lastModified: '2017-12-12T10:10', + lastModifiedBy: 'LastModifiedBy1', + version: 11, + data: {} } }) ) )) .verifiable(Times.once()); - let created: EntityCreatedDto | null = null; + let content: ContentDto | null = null; - contentsService.postContent('my-app', 'my-schema', dto).subscribe(result => { - created = result; + contentsService.postContent('my-app', 'my-schema', dto, version).subscribe(result => { + content = result; }); - expect(created).toEqual( - new EntityCreatedDto('content1')); + expect(content).toEqual( + new ContentDto('id1', true, 'Created1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + {}, + new Version('11'))); authService.verifyAll(); }); @@ -178,7 +205,7 @@ describe('ContentsService', () => { it('should make put request to update content', () => { const dto = {}; - authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1', dto)) + authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -186,13 +213,13 @@ describe('ContentsService', () => { )) .verifiable(Times.once()); - contentsService.putContent('my-app', 'my-schema', 'content1', dto); + contentsService.putContent('my-app', 'my-schema', 'content1', dto, version); authService.verifyAll(); }); it('should make put request to publish content', () => { - authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/publish', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/publish', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -200,13 +227,13 @@ describe('ContentsService', () => { )) .verifiable(Times.once()); - contentsService.publishContent('my-app', 'my-schema', 'content1'); + contentsService.publishContent('my-app', 'my-schema', 'content1', version); authService.verifyAll(); }); it('should make put request to unpublish content', () => { - authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/unpublish', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/content/my-app/my-schema/content1/unpublish', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -214,13 +241,13 @@ describe('ContentsService', () => { )) .verifiable(Times.once()); - contentsService.unpublishContent('my-app', 'my-schema', 'content1'); + contentsService.unpublishContent('my-app', 'my-schema', 'content1', version); authService.verifyAll(); }); it('should make delete request to delete content', () => { - authService.setup(x => x.authDelete('http://service/p/api/content/my-app/my-schema/content1')) + authService.setup(x => x.authDelete('http://service/p/api/content/my-app/my-schema/content1', version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -228,7 +255,7 @@ describe('ContentsService', () => { )) .verifiable(Times.once()); - contentsService.deleteContent('my-app', 'my-schema', 'content1'); + contentsService.deleteContent('my-app', 'my-schema', 'content1', version); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/contents.service.ts b/src/Squidex/app/shared/services/contents.service.ts index 50b5c46e5..a7cf32983 100644 --- a/src/Squidex/app/shared/services/contents.service.ts +++ b/src/Squidex/app/shared/services/contents.service.ts @@ -13,7 +13,7 @@ import 'framework/angular/http-extensions'; import { ApiUrlConfig, DateTime, - EntityCreatedDto + Version } from 'framework'; import { AuthService } from './auth.service'; @@ -34,7 +34,8 @@ export class ContentDto { public readonly lastModifiedBy: string, public readonly created: DateTime, public readonly lastModified: DateTime, - public readonly data: any + public readonly data: any, + public readonly version: Version ) { } } @@ -83,16 +84,17 @@ export class ContentsService { item.lastModifiedBy, DateTime.parseISO_UTC(item.created), DateTime.parseISO_UTC(item.lastModified), - item.data); + item.data, + new Version(item.version.toString())); })); }) .catchError('Failed to load contents. Please reload.'); } - public getContent(appName: string, schemaName: string, id: string): Observable { + public getContent(appName: string, schemaName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}?hidden=true`); - return this.authService.authGet(url) + return this.authService.authGet(url, version) .map(response => response.json()) .map(response => { return new ContentDto( @@ -102,47 +104,56 @@ export class ContentsService { response.lastModifiedBy, DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(response.lastModified), - response.data); + response.data, + new Version(response.version.toString())); }) .catchError('Failed to load content. Please reload.'); } - public postContent(appName: string, schemaName: string, dto: any): Observable { + public postContent(appName: string, schemaName: string, dto: any, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}`); - return this.authService.authPost(url, dto) + return this.authService.authPost(url, dto, version) .map(response => response.json()) .map(response => { - return new EntityCreatedDto(response.id); + return new ContentDto( + response.id, + response.isPublished, + response.createdBy, + response.lastModifiedBy, + DateTime.parseISO_UTC(response.created), + DateTime.parseISO_UTC(response.lastModified), + response.data, + new Version(response.version.toString())); }) .catchError('Failed to create content. Please reload.'); } - public putContent(appName: string, schemaName: string, id: string, dto: any): Observable { + public putContent(appName: string, schemaName: string, id: string, dto: any, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); - return this.authService.authPut(url, dto) + return this.authService.authPut(url, dto, version) .catchError('Failed to update content. Please reload.'); } - public publishContent(appName: string, schemaName: string, id: string): Observable { + public publishContent(appName: string, schemaName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/publish`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to publish content. Please reload.'); } - public unpublishContent(appName: string, schemaName: string, id: string): Observable { + public unpublishContent(appName: string, schemaName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}/unpublish`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to unpublish content. Please reload.'); } - public deleteContent(appName: string, schemaName: string, id: string): Observable { + public deleteContent(appName: string, schemaName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`/api/content/${appName}/${schemaName}/${id}`); - return this.authService.authDelete(url) + return this.authService.authDelete(url, version) .catchError('Failed to delete content. Please reload.'); } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts index 5b2fb5505..e7f9c0952 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -22,12 +22,14 @@ import { SchemaDto, SchemasService, UpdateFieldDto, - UpdateSchemaDto + UpdateSchemaDto, + Version } from './../'; describe('SchemasService', () => { let authService: IMock; let schemasService: SchemasService; + let version = new Version('1'); beforeEach(() => { authService = Mock.ofType(AuthService); @@ -52,6 +54,7 @@ describe('SchemasService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + version: 11, data: {} }, { id: 'id2', @@ -62,6 +65,7 @@ describe('SchemasService', () => { createdBy: 'Created2', lastModified: '2017-10-12T10:10', lastModifiedBy: 'LastModifiedBy2', + version: 22, data: {} }] }) @@ -76,8 +80,14 @@ describe('SchemasService', () => { }).unsubscribe(); expect(schemas).toEqual([ - new SchemaDto('id1', 'name1', 'label1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), DateTime.parseISO_UTC('2017-12-12T10:10')), - new SchemaDto('id2', 'name2', 'label2', true, 'Created2', 'LastModifiedBy2', DateTime.parseISO_UTC('2016-10-12T10:10'), DateTime.parseISO_UTC('2017-10-12T10:10')) + new SchemaDto('id1', 'name1', 'label1', true, 'Created1', 'LastModifiedBy1', + DateTime.parseISO_UTC('2016-12-12T10:10'), + DateTime.parseISO_UTC('2017-12-12T10:10'), + new Version('11')), + new SchemaDto('id2', 'name2', 'label2', true, 'Created2', 'LastModifiedBy2', + DateTime.parseISO_UTC('2016-10-12T10:10'), + DateTime.parseISO_UTC('2017-10-12T10:10'), + new Version('22')) ]); authService.verifyAll(); @@ -98,13 +108,14 @@ describe('SchemasService', () => { createdBy: 'Created1', lastModified: '2017-12-12T10:10', lastModifiedBy: 'LastModifiedBy1', + version: 11, fields: [{ fieldId: 1, name: 'field1', isHidden: true, isDisabled: true, properties: { - fieldType: 'number' + fieldType: 'Number' } }, { fieldId: 2, @@ -112,7 +123,7 @@ describe('SchemasService', () => { isHidden: true, isDisabled: true, properties: { - fieldType: 'string' + fieldType: 'String' } }, { fieldId: 3, @@ -120,7 +131,7 @@ describe('SchemasService', () => { isHidden: true, isDisabled: true, properties: { - fieldType: 'boolean' + fieldType: 'Boolean' } }, { fieldId: 4, @@ -128,7 +139,7 @@ describe('SchemasService', () => { isHidden: true, isDisabled: true, properties: { - fieldType: 'dateTime' + fieldType: 'DateTime' } }, { fieldId: 5, @@ -136,7 +147,15 @@ describe('SchemasService', () => { isHidden: true, isDisabled: true, properties: { - fieldType: 'json' + fieldType: 'Json' + } + }, { + fieldId: 6, + name: 'field6', + isHidden: true, + isDisabled: true, + properties: { + fieldType: 'Geolocation' } }] } @@ -147,19 +166,22 @@ describe('SchemasService', () => { let schema: SchemaDetailsDto | null = null; - schemasService.getSchema('my-app', 'my-schema').subscribe(result => { + schemasService.getSchema('my-app', 'my-schema', version).subscribe(result => { schema = result; }).unsubscribe(); expect(schema).toEqual( new SchemaDetailsDto('id1', 'name1', 'label1', 'hints1', true, 'Created1', 'LastModifiedBy1', DateTime.parseISO_UTC('2016-12-12T10:10'), - DateTime.parseISO_UTC('2017-12-12T10:10'), [ - new FieldDto(1, 'field1', true, true, createProperties('number')), - new FieldDto(2, 'field2', true, true, createProperties('string')), - new FieldDto(3, 'field3', true, true, createProperties('boolean')), - new FieldDto(4, 'field4', true, true, createProperties('dateTime')), - new FieldDto(5, 'field5', true, true, createProperties('json')) + DateTime.parseISO_UTC('2017-12-12T10:10'), + new Version('11'), + [ + new FieldDto(1, 'field1', true, true, createProperties('Number')), + new FieldDto(2, 'field2', true, true, createProperties('String')), + new FieldDto(3, 'field3', true, true, createProperties('Boolean')), + new FieldDto(4, 'field4', true, true, createProperties('DateTime')), + new FieldDto(5, 'field5', true, true, createProperties('Json')), + new FieldDto(6, 'field6', true, true, createProperties('Geolocation')) ])); authService.verifyAll(); @@ -168,7 +190,7 @@ describe('SchemasService', () => { it('should make post request to create schema', () => { const dto = new CreateSchemaDto('name'); - authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas', dto)) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -182,7 +204,7 @@ describe('SchemasService', () => { let created: EntityCreatedDto | null = null; - schemasService.postSchema('my-app', dto).subscribe(result => { + schemasService.postSchema('my-app', dto, version).subscribe(result => { created = result; }); @@ -193,9 +215,9 @@ describe('SchemasService', () => { }); it('should make post request to add field', () => { - const dto = new AddFieldDto('name', createProperties('number')); + const dto = new AddFieldDto('name', createProperties('Number')); - authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas/my-schema/fields', dto)) + authService.setup(x => x.authPost('http://service/p/api/apps/my-app/schemas/my-schema/fields', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -209,7 +231,7 @@ describe('SchemasService', () => { let created: EntityCreatedDto | null = null; - schemasService.postField('my-app', 'my-schema', dto).subscribe(result => { + schemasService.postField('my-app', 'my-schema', dto, version).subscribe(result => { created = result; }); @@ -222,7 +244,7 @@ describe('SchemasService', () => { it('should make put request to update schema', () => { const dto = new UpdateSchemaDto('label', 'hints'); - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema', dto)) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -230,15 +252,15 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.putSchema('my-app', 'my-schema', dto); + schemasService.putSchema('my-app', 'my-schema', dto, version); authService.verifyAll(); }); it('should make put request to update field', () => { - const dto = new UpdateFieldDto(createProperties('number')); + const dto = new UpdateFieldDto(createProperties('Number')); - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1', dto)) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1', dto, version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -246,13 +268,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.putField('my-app', 'my-schema', 1, dto); + schemasService.putField('my-app', 'my-schema', 1, dto, version); authService.verifyAll(); }); it('should make put request to publish schema', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/publish', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/publish', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -260,13 +282,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.publishSchema('my-app', 'my-schema'); + schemasService.publishSchema('my-app', 'my-schema', version); authService.verifyAll(); }); it('should make put request to unpublish schema', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/unpublish', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/unpublish', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -274,13 +296,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.unpublishSchema('my-app', 'my-schema'); + schemasService.unpublishSchema('my-app', 'my-schema', version); authService.verifyAll(); }); it('should make put request to enable field', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/enable', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/enable', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -288,13 +310,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.enableField('my-app', 'my-schema', 1); + schemasService.enableField('my-app', 'my-schema', 1, version); authService.verifyAll(); }); it('should make put request to disable field', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/disable', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/disable', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -302,13 +324,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.disableField('my-app', 'my-schema', 1); + schemasService.disableField('my-app', 'my-schema', 1, version); authService.verifyAll(); }); it('should make put request to show field', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/show', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/show', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -316,13 +338,13 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.showField('my-app', 'my-schema', 1); + schemasService.showField('my-app', 'my-schema', 1, version); authService.verifyAll(); }); it('should make put request to hide field', () => { - authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/hide', It.isAny())) + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/1/hide', It.isAny(), version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -330,13 +352,27 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.hideField('my-app', 'my-schema', 1); + schemasService.hideField('my-app', 'my-schema', 1, version); authService.verifyAll(); }); it('should make delete request to delete field', () => { - authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/schemas/my-schema/fields/1')) + authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/schemas/my-schema/fields/1', version)) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + schemasService.deleteField('my-app', 'my-schema', 1, version); + + authService.verifyAll(); + }); + + it('should make delete request to delete schema', () => { + authService.setup(x => x.authDelete('http://service/p/api/apps/my-app/schemas/my-schema', version)) .returns(() => Observable.of( new Response( new ResponseOptions() @@ -344,7 +380,7 @@ describe('SchemasService', () => { )) .verifiable(Times.once()); - schemasService.deleteField('my-app', 'my-schema', 1); + schemasService.deleteSchema('my-app', 'my-schema', version); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index 12858fb06..131e19757 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -13,7 +13,8 @@ import 'framework/angular/http-extensions'; import { ApiUrlConfig, DateTime, - EntityCreatedDto + EntityCreatedDto, + Version } from 'framework'; import { AuthService } from './auth.service'; @@ -49,6 +50,9 @@ export function createProperties(fieldType: string, values: Object | null = null case 'Json': properties = new JsonFieldPropertiesDto(undefined, undefined, undefined, false, false, false); break; + case 'Geolocation': + properties = new GeolocationFieldPropertiesDto(undefined, undefined, undefined, false, false, false, 'Map'); + break; default: throw 'Invalid properties type'; } @@ -69,7 +73,8 @@ export class SchemaDto { public readonly createdBy: string, public readonly lastModifiedBy: string, public readonly created: DateTime, - public readonly lastModified: DateTime + public readonly lastModified: DateTime, + public readonly version: Version ) { } } @@ -85,6 +90,7 @@ export class SchemaDetailsDto { public readonly lastModifiedBy: string, public readonly created: DateTime, public readonly lastModified: DateTime, + public readonly version: Version, public readonly fields: FieldDto[] ) { } @@ -179,6 +185,19 @@ export class BooleanFieldPropertiesDto extends FieldPropertiesDto { } } +export class GeolocationFieldPropertiesDto extends FieldPropertiesDto { + constructor(label: string | undefined, hints: string | undefined, placeholder: string | undefined, + isRequired: boolean, + isListField: boolean, + isLocalizable: boolean, + public readonly editor: string + ) { + super(label, hints, placeholder, isRequired, isListField, isLocalizable); + + this['fieldType'] = 'Geolocation'; + } +} + export class JsonFieldPropertiesDto extends FieldPropertiesDto { constructor(label: string | undefined, hints: string | undefined, placeholder: string | undefined, isRequired: boolean, @@ -246,13 +265,14 @@ export class SchemasService { item.createdBy, item.lastModifiedBy, DateTime.parseISO_UTC(item.created), - DateTime.parseISO_UTC(item.lastModified)); + DateTime.parseISO_UTC(item.lastModified), + new Version(item.version.toString())); }); }) .catchError('Failed to load schemas. Please reload.'); } - public getSchema(appName: string, id: string): Observable { + public getSchema(appName: string, id: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${id}`); return this.authService.authGet(url) @@ -282,15 +302,16 @@ export class SchemasService { response.lastModifiedBy, DateTime.parseISO_UTC(response.created), DateTime.parseISO_UTC(response.lastModified), + new Version(response.version.toString()), fields); }) .catchError('Failed to load schema. Please reload.'); } - public postSchema(appName: string, dto: CreateSchemaDto): Observable { + public postSchema(appName: string, dto: CreateSchemaDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas`); - return this.authService.authPost(url, dto) + return this.authService.authPost(url, dto, version) .map(response => response.json()) .map(response => { return new EntityCreatedDto(response.id); @@ -298,10 +319,10 @@ export class SchemasService { .catchError('Failed to create schema. Please reload.'); } - public postField(appName: string, schemaName: string, dto: AddFieldDto): Observable { + public postField(appName: string, schemaName: string, dto: AddFieldDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields`); - return this.authService.authPost(url, dto) + return this.authService.authPost(url, dto, version) .map(response => response.json()) .map(response => { return new EntityCreatedDto(response.id); @@ -309,66 +330,73 @@ export class SchemasService { .catchError('Failed to add field. Please reload.'); } - public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto): Observable { + public putSchema(appName: string, schemaName: string, dto: UpdateSchemaDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); - return this.authService.authPut(url, dto) + return this.authService.authPut(url, dto, version) .catchError('Failed to update schema. Please reload.'); } - public publishSchema(appName: string, schemaName: string): Observable { + public publishSchema(appName: string, schemaName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to publish schema. Please reload.'); } - public unpublishSchema(appName: string, schemaName: string): Observable { + public unpublishSchema(appName: string, schemaName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/unpublish`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to unpublish schema. Please reload.'); } - public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto): Observable { + public putField(appName: string, schemaName: string, fieldId: number, dto: UpdateFieldDto, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); - return this.authService.authPut(url, dto) + return this.authService.authPut(url, dto, version) .catchError('Failed to update field. Please reload.'); } - public enableField(appName: string, schemaName: string, fieldId: number): Observable { + public enableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/enable`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to enable field. Please reload.'); } - public disableField(appName: string, schemaName: string, fieldId: number): Observable { + public disableField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/disable`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to disable field. Please reload.'); } - public showField(appName: string, schemaName: string, fieldId: number): Observable { + public showField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/show`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to show field. Please reload.'); } - public hideField(appName: string, schemaName: string, fieldId: number): Observable { + public hideField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}/hide`); - return this.authService.authPut(url, {}) + return this.authService.authPut(url, {}, version) .catchError('Failed to hide field. Please reload.'); } - public deleteField(appName: string, schemaName: string, fieldId: number): Observable { + public deleteField(appName: string, schemaName: string, fieldId: number, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/${fieldId}`); - return this.authService.authDelete(url) + return this.authService.authDelete(url, version) .catchError('Failed to delete field. Please reload.'); } + + public deleteSchema(appName: string, schemaName: string, version: Version): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}`); + + return this.authService.authDelete(url, version) + .catchError('Failed to delete schema. Please reload.'); + } } \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/home/home-page.component.html b/src/Squidex/app/shell/pages/home/home-page.component.html index 0b41cdcdd..004956a58 100644 --- a/src/Squidex/app/shell/pages/home/home-page.component.html +++ b/src/Squidex/app/shell/pages/home/home-page.component.html @@ -1,13 +1,29 @@ -
- + \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/home/home-page.component.scss b/src/Squidex/app/shell/pages/home/home-page.component.scss index 864d75f09..e50190345 100644 --- a/src/Squidex/app/shell/pages/home/home-page.component.scss +++ b/src/Squidex/app/shell/pages/home/home-page.component.scss @@ -1,9 +1,80 @@ @import '_mixins'; @import '_vars'; +$color-footer: #f7f8fa; +$color-google: #d34736; +$color-google-dark: #af2c1a; + +.card-footer { + background: $color-footer; + padding-left: 5rem; + padding-top: 1.5rem; + padding-bottom: 1.5rem; + position: relative; + border-top: 0; + font-size: .8rem; +} + +.card-block { + padding: 4rem 1rem; +} + +.icon-info { + @include absolute(2rem, auto, auto, 2rem); + color: $color-text-decent; + font-weight: normal; + font-size: 1.4rem; +} + +.icon-google { + color: $color-google-dark; + font-size: 1.4rem; + font-weight: normal; + margin-right: .5rem; + vertical-align: middle; +} + +.logo { + height: 1.4rem; + margin-top: 1rem; + margin-bottom: 1rem; +} + .login { + &-area { + max-width: 40rem; + margin: 4rem auto 0; + padding: 0 1rem; + } + &-button { - margin: 2.5rem 0; + & { + @include border-radius(1.5rem); + color: $color-accent-dark; + cursor: pointer; + margin-top: 4rem; + height: 3rem; + padding-left: 1.4rem; + padding-right: 1.4rem; + background: $color-google; + text-transform: uppercase; + } + + &:hover { + background: darken($color-google, 5%); + } + + &:focus { + @include box-shadow-none; + } + } + + &-icon { + text-align: center; + } + + &-button-text { + vertical-align: middle; } &-hint { @@ -12,7 +83,8 @@ } .proudly-made { - margin-top: 16rem; + line-height: 3.4rem; font-size: .8rem; - font-style: italic; + font-style: normal; + color: $color-text-decent; } \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/internal/internal-area.component.scss b/src/Squidex/app/shell/pages/internal/internal-area.component.scss index ae07fbdd9..24c06aa80 100644 --- a/src/Squidex/app/shell/pages/internal/internal-area.component.scss +++ b/src/Squidex/app/shell/pages/internal/internal-area.component.scss @@ -40,4 +40,8 @@ &-error { background: $color-theme-error; } + + &-info { + background: $color-theme-blue; + } } \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/internal/profile-menu.component.ts b/src/Squidex/app/shell/pages/internal/profile-menu.component.ts index fa8cb9de2..28aba25c5 100644 --- a/src/Squidex/app/shell/pages/internal/profile-menu.component.ts +++ b/src/Squidex/app/shell/pages/internal/profile-menu.component.ts @@ -43,16 +43,17 @@ export class ProfileMenuComponent implements OnInit, OnDestroy { public ngOnInit() { this.authenticationSubscription = - this.auth.isAuthenticated.take(1).subscribe(() => { - const user = this.auth.user; + this.auth.isAuthenticated.take(1) + .subscribe(() => { + const user = this.auth.user; - if (user) { - this.profilePictureUrl = user.pictureUrl; - this.profileDisplayName = user.displayName; + if (user) { + this.profilePictureUrl = user.pictureUrl; + this.profileDisplayName = user.displayName; - this.isAdmin = user.isAdmin; - } - }); + this.isAdmin = user.isAdmin; + } + }); } public logout() { diff --git a/src/Squidex/app/shell/pages/login/login-page.component.ts b/src/Squidex/app/shell/pages/login/login-page.component.ts index 2aa92c2db..d4baf4e29 100644 --- a/src/Squidex/app/shell/pages/login/login-page.component.ts +++ b/src/Squidex/app/shell/pages/login/login-page.component.ts @@ -22,13 +22,14 @@ export class LoginPageComponent implements OnInit { } public ngOnInit() { - this.auth.loginRedirectComplete().subscribe( - () => { - this.router.navigate(['/app'], { replaceUrl: true }); - }, - () => { - this.router.navigate(['/'], { replaceUrl: true }); - } - ); + this.auth.loginRedirectComplete() + .subscribe( + () => { + this.router.navigate(['/app'], { replaceUrl: true }); + }, + () => { + this.router.navigate(['/'], { replaceUrl: true }); + } + ); } } \ No newline at end of file diff --git a/src/Squidex/app/shell/pages/logout/logout-page.component.ts b/src/Squidex/app/shell/pages/logout/logout-page.component.ts index d189f4e5c..e17c1d1e1 100644 --- a/src/Squidex/app/shell/pages/logout/logout-page.component.ts +++ b/src/Squidex/app/shell/pages/logout/logout-page.component.ts @@ -22,13 +22,14 @@ export class LogoutPageComponent implements OnInit { } public ngOnInit() { - this.auth.logoutRedirectComplete().subscribe( - () => { - this.router.navigate(['/'], { replaceUrl: true }); - }, - () => { - this.router.navigate(['/'], { replaceUrl: true }); - } - ); + this.auth.logoutRedirectComplete() + .subscribe( + () => { + this.router.navigate(['/'], { replaceUrl: true }); + }, + () => { + this.router.navigate(['/'], { replaceUrl: true }); + } + ); } } \ No newline at end of file diff --git a/src/Squidex/app/polyfills.ts b/src/Squidex/app/shims.ts similarity index 100% rename from src/Squidex/app/polyfills.ts rename to src/Squidex/app/shims.ts diff --git a/src/Squidex/app/theme/_bootstrap.scss b/src/Squidex/app/theme/_bootstrap.scss index beb1ee90a..6f97e0b5e 100644 --- a/src/Squidex/app/theme/_bootstrap.scss +++ b/src/Squidex/app/theme/_bootstrap.scss @@ -37,9 +37,34 @@ body { } .dropdown-menu { - @include box-shadow(0, 3px, 16px, .2px); - border: 0; - background: $panel-light-background; + & { + @include box-shadow(0, 3px, 16px, .2px); + border: 0; + background: $panel-light-background; + } + + a { + &.dropdown-item-delete { + & { + color: $color-theme-error; + } + + &:hover { + color: $color-theme-error-dark; + } + + &:active { + background: $color-theme-error-dark; + border: 0; + } + } + + &.dropdown-item { + &:active { + color: $color-accent-dark; + } + } + } } .nav-dark { @@ -151,8 +176,8 @@ body { } &-cancel { - padding: .2rem; - font-size: 1.5rem; + padding: .4rem; + font-size: 1.1rem; font-weight: normal; } diff --git a/src/Squidex/app/theme/_common.scss b/src/Squidex/app/theme/_common.scss index 3e7aa1301..5466b4c63 100644 --- a/src/Squidex/app/theme/_common.scss +++ b/src/Squidex/app/theme/_common.scss @@ -4,7 +4,7 @@ body { background: $color-background; padding-top: $size-navbar-height; - padding-left: $size-sidebar-width; + padding-left: 0; } h1 { diff --git a/src/Squidex/app/theme/_forms.scss b/src/Squidex/app/theme/_forms.scss index 0eb23effc..a946b35b7 100644 --- a/src/Squidex/app/theme/_forms.scss +++ b/src/Squidex/app/theme/_forms.scss @@ -39,6 +39,7 @@ font-weight: normal; line-height: 1.1rem; padding: .3rem .4rem; + padding-bottom: .5rem; background: $color-theme-error; } } @@ -78,6 +79,10 @@ select { &:last-child { margin-bottom: 0; } + + &~.hidden { + margin-bottom: 0; + } } .search-form { diff --git a/src/Squidex/app/theme/_lists.scss b/src/Squidex/app/theme/_lists.scss index ee9ec40a4..ef6b7d53e 100644 --- a/src/Squidex/app/theme/_lists.scss +++ b/src/Squidex/app/theme/_lists.scss @@ -28,6 +28,10 @@ font-weight: normal; border: 0; padding-top: 0; + + & > span { + @include truncate; + } } } diff --git a/src/Squidex/app/theme/_panels.scss b/src/Squidex/app/theme/_panels.scss index b1102440f..9f6e35d14 100644 --- a/src/Squidex/app/theme/_panels.scss +++ b/src/Squidex/app/theme/_panels.scss @@ -134,7 +134,7 @@ } &-close { - @include absolute($panel-padding - .3rem, $panel-padding - .8rem, auto, auto); + @include absolute($panel-padding - .2rem, $panel-padding - .8rem, auto, auto); @include panel-icon; font-size: 1rem; font-weight: 300; diff --git a/src/Squidex/app/theme/_static.scss b/src/Squidex/app/theme/_static.scss new file mode 100644 index 000000000..9bd560085 --- /dev/null +++ b/src/Squidex/app/theme/_static.scss @@ -0,0 +1,34 @@ +@import '_mixins'; +@import '_vars'; + +.splash { + &-h1, + &-text { + text-align: center; + } +} + +.loading { + & { + text-align: center; + margin-top: 200px; + margin-bottom: 20px; + } + + div { + font-size: 30px; + font-weight: lighter; + } +} + +noscript { + display: block; + color: $color-theme-error; + font-size: 30px; + font-weight: lighter; + margin-bottom: 20px; +} + +.redirect-button { + display: none; +} \ No newline at end of file diff --git a/src/Squidex/app/theme/_vars.scss b/src/Squidex/app/theme/_vars.scss index 0af78a760..20effb18d 100644 --- a/src/Squidex/app/theme/_vars.scss +++ b/src/Squidex/app/theme/_vars.scss @@ -5,6 +5,7 @@ $color-border-dark: darken($color-border, 20%); $color-title: #000; $color-text: #373a3c; +$color-text-decent: #a9b2bb; $color-subtext: #818181; $color-empty: #949494; $color-control: rgba(0, 0, 0, .15); diff --git a/src/Squidex/app/theme/icomoon/demo-files/demo.css b/src/Squidex/app/theme/icomoon/demo-files/demo.css index 889fb6c96..5b17a4076 100644 --- a/src/Squidex/app/theme/icomoon/demo-files/demo.css +++ b/src/Squidex/app/theme/icomoon/demo-files/demo.css @@ -147,10 +147,10 @@ p { font-size: 16px; } .fs1 { - font-size: 24px; + font-size: 28px; } .fs2 { - font-size: 32px; + font-size: 20px; } .fs3 { font-size: 32px; @@ -159,6 +159,9 @@ p { font-size: 32px; } .fs5 { + font-size: 24px; +} +.fs6 { font-size: 32px; } diff --git a/src/Squidex/app/theme/icomoon/demo.html b/src/Squidex/app/theme/icomoon/demo.html index c6614995d..74a8b71f5 100644 --- a/src/Squidex/app/theme/icomoon/demo.html +++ b/src/Squidex/app/theme/icomoon/demo.html @@ -9,30 +9,27 @@
-

Font Name: icomoon (Glyphs: 58)

+

Font Name: icomoon (Glyphs: 62)

-

Grid Size: 24

+

Grid Size: 14

- + - icon-control-RichText + icon-bug
- - + +
liga:
-
-
-

Grid Size: 14

-
+
@@ -48,7 +45,7 @@
-
+
@@ -64,7 +61,7 @@
-
+
@@ -80,7 +77,7 @@
-
+
@@ -96,7 +93,7 @@
-
+
@@ -112,7 +109,7 @@
-
+
@@ -128,7 +125,7 @@
-
+
@@ -144,7 +141,7 @@
-
+
@@ -160,7 +157,7 @@
-
+
@@ -176,7 +173,7 @@
-
+
@@ -192,7 +189,7 @@
-
+
@@ -208,7 +205,7 @@
-
+
@@ -226,17 +223,129 @@
-

Grid Size: 32

-
+

Grid Size: 20

+
- + - icon-browser + icon-info
- - + + +
+
+ liga: + +
+
+
+
+ + + + icon-unlocked +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-lock +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-reset +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-pause +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-play +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-settings2 +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-bin2 +
+
+ +
liga: @@ -246,7 +355,23 @@

Grid Size: 16

-
+
+
+ + + + icon-google +
+
+ + +
+
+ liga: + +
+
+
@@ -262,7 +387,7 @@
-
+
@@ -278,7 +403,7 @@
-
+
@@ -294,7 +419,7 @@
-
+
@@ -310,7 +435,7 @@
-
+
@@ -326,7 +451,7 @@
-
+
@@ -342,7 +467,7 @@
-
+
@@ -360,8 +485,78 @@
-

Grid Size: Unknown

+

Grid Size: 32

+
+
+ + + + icon-control-Stars +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-browser +
+
+ + +
+
+ liga: + +
+
+
+
+

Grid Size: 24

+
+ + + + icon-control-RichText +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-control-Date +
+
+ + +
+
+ liga: + +
+
+
+
+

Grid Size: Unknown

+
@@ -377,7 +572,7 @@
-
+
@@ -393,7 +588,7 @@
-
+
@@ -409,7 +604,7 @@
-
+
@@ -425,7 +620,7 @@
-
+
@@ -441,7 +636,7 @@
-
+
@@ -457,7 +652,7 @@
-
+
@@ -473,7 +668,7 @@
-
+
@@ -489,7 +684,7 @@
-
+
@@ -505,7 +700,7 @@
-
+
@@ -521,7 +716,7 @@
-
+
@@ -537,7 +732,7 @@
-
+
@@ -553,7 +748,7 @@
-
+
@@ -569,7 +764,7 @@
-
+
@@ -585,7 +780,7 @@
-
+
@@ -601,7 +796,7 @@
-
+
@@ -617,7 +812,7 @@
-
+
@@ -633,7 +828,7 @@
-
+
@@ -649,7 +844,7 @@
-
+
@@ -665,7 +860,7 @@
-
+
@@ -681,7 +876,7 @@
-
+
@@ -697,7 +892,7 @@
-
+
@@ -713,7 +908,7 @@
-
+
@@ -729,7 +924,7 @@
-
+
@@ -745,7 +940,7 @@
-
+
@@ -761,7 +956,7 @@
-
+
@@ -777,7 +972,7 @@
-
+
@@ -793,7 +988,7 @@
-
+
@@ -809,7 +1004,7 @@
-
+
@@ -825,7 +1020,39 @@
-
+
+
+ + + + icon-control-Map +
+
+ + +
+
+ liga: + +
+
+
+
+ + + + icon-type-Geolocation +
+
+ + +
+
+ liga: + +
+
+
-
+
@@ -857,7 +1084,7 @@
-
+
@@ -873,7 +1100,7 @@
-
+
@@ -889,7 +1116,7 @@
-
+
@@ -905,7 +1132,7 @@
-
+
@@ -921,7 +1148,7 @@
-
+
@@ -937,7 +1164,7 @@
-
+
@@ -953,7 +1180,7 @@
-
+
@@ -969,7 +1196,7 @@
-
+
@@ -985,7 +1212,7 @@
-
+
@@ -1001,7 +1228,7 @@
-
+
@@ -1017,7 +1244,7 @@
-
+
@@ -1033,7 +1260,7 @@
-
+
diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.eot b/src/Squidex/app/theme/icomoon/fonts/icomoon.eot index 59ae2834a..8b55c3e10 100644 Binary files a/src/Squidex/app/theme/icomoon/fonts/icomoon.eot and b/src/Squidex/app/theme/icomoon/fonts/icomoon.eot differ diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg b/src/Squidex/app/theme/icomoon/fonts/icomoon.svg index 7e4d5f268..bedf0c72c 100644 --- a/src/Squidex/app/theme/icomoon/fonts/icomoon.svg +++ b/src/Squidex/app/theme/icomoon/fonts/icomoon.svg @@ -34,7 +34,7 @@ - + @@ -65,4 +65,8 @@ + + + + \ No newline at end of file diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf b/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf index c6452f70f..b64359315 100644 Binary files a/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf and b/src/Squidex/app/theme/icomoon/fonts/icomoon.ttf differ diff --git a/src/Squidex/app/theme/icomoon/fonts/icomoon.woff b/src/Squidex/app/theme/icomoon/fonts/icomoon.woff index 877263ee5..0c72a003c 100644 Binary files a/src/Squidex/app/theme/icomoon/fonts/icomoon.woff and b/src/Squidex/app/theme/icomoon/fonts/icomoon.woff differ diff --git a/src/Squidex/app/theme/icomoon/selection.json b/src/Squidex/app/theme/icomoon/selection.json index e772b5346..37d5af448 100644 --- a/src/Squidex/app/theme/icomoon/selection.json +++ b/src/Squidex/app/theme/icomoon/selection.json @@ -4,30 +4,31 @@ { "icon": { "paths": [ - "M918 384v128h-128v298h-128v-298h-128v-128h384zM106 170h556v128h-214v512h-128v-512h-214v-128z" + "M932.571 548.571c0 20-16.571 36.571-36.571 36.571h-128c0 71.429-15.429 125.143-38.286 165.714l118.857 119.429c14.286 14.286 14.286 37.143 0 51.429-6.857 7.429-16.571 10.857-25.714 10.857s-18.857-3.429-25.714-10.857l-113.143-112.571s-74.857 68.571-172 68.571v-512h-73.143v512c-103.429 0-178.857-75.429-178.857-75.429l-104.571 118.286c-7.429 8-17.143 12-27.429 12-8.571 0-17.143-2.857-24.571-9.143-14.857-13.714-16-36.571-2.857-52l115.429-129.714c-20-39.429-33.143-90.286-33.143-156.571h-128c-20 0-36.571-16.571-36.571-36.571s16.571-36.571 36.571-36.571h128v-168l-98.857-98.857c-14.286-14.286-14.286-37.143 0-51.429s37.143-14.286 51.429 0l98.857 98.857h482.286l98.857-98.857c14.286-14.286 37.143-14.286 51.429 0s14.286 37.143 0 51.429l-98.857 98.857v168h128c20 0 36.571 16.571 36.571 36.571zM658.286 219.429h-365.714c0-101.143 81.714-182.857 182.857-182.857s182.857 81.714 182.857 182.857z" ], "attrs": [ {} ], + "width": 951, "isMulticolor": false, "isMulticolor2": false, "tags": [ - "text_fields" + "bug" ], - "grid": 24 + "grid": 14 }, "attrs": [ {} ], "properties": { - "order": 75, + "order": 1, "id": 0, - "prevSize": 24, - "code": 59705, - "name": "control-RichText" + "prevSize": 28, + "code": 59709, + "name": "bug" }, - "setIdx": 1, - "setId": 2, + "setIdx": 0, + "setId": 7, "iconIdx": 0 }, { @@ -52,10 +53,10 @@ "order": 72, "id": 43, "name": "control-Markdown", - "prevSize": 32, + "prevSize": 28, "code": 59704 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 0 }, @@ -81,10 +82,10 @@ "order": 71, "id": 42, "name": "control-Date", - "prevSize": 32, + "prevSize": 28, "code": 59702 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 9 }, @@ -110,10 +111,10 @@ "order": 70, "id": 41, "name": "control-DateTime", - "prevSize": 32, + "prevSize": 28, "code": 59703 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 10 }, @@ -139,11 +140,11 @@ "properties": { "order": 67, "id": 0, - "prevSize": 32, + "prevSize": 28, "code": 59697, "name": "angle-right" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 11 }, @@ -169,10 +170,10 @@ "order": 64, "id": 38, "name": "user-o", - "prevSize": 32, + "prevSize": 28, "code": 59698 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 12 }, @@ -198,11 +199,11 @@ "properties": { "order": 1, "id": 7, - "prevSize": 32, + "prevSize": 28, "code": 59689, "name": "caret-right" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 13 }, @@ -228,11 +229,11 @@ "properties": { "order": 2, "id": 6, - "prevSize": 32, + "prevSize": 28, "code": 59690, "name": "caret-left" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 14 }, @@ -258,11 +259,11 @@ "properties": { "order": 3, "id": 5, - "prevSize": 32, + "prevSize": 28, "code": 59691, "name": "caret-up" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 15 }, @@ -288,11 +289,11 @@ "properties": { "order": 4, "id": 4, - "prevSize": 32, + "prevSize": 28, "code": 59692, "name": "caret-down" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 16 }, @@ -318,11 +319,11 @@ "properties": { "order": 2, "id": 2, - "prevSize": 32, + "prevSize": 28, "code": 59651, "name": "angle-up" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 17 }, @@ -348,11 +349,11 @@ "properties": { "order": 1, "id": 1, - "prevSize": 32, + "prevSize": 28, "code": 59648, "name": "angle-down" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 18 }, @@ -378,32 +379,58 @@ "properties": { "order": 2, "id": 0, - "prevSize": 32, + "prevSize": 28, "code": 59649, "name": "angle-left" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 19 }, { "icon": { "paths": [ - "M1328 320c-8.832 0-16 7.168-16 16v640c0 8.832-7.168 16-16 16h-1248c-8.832 0-16-7.168-16-16v-640c0-8.832-7.168-16-16-16s-16 7.168-16 16v640c0 26.464 21.536 48 48 48h1248c26.464 0 48-21.536 48-48v-640c0-8.832-7.168-16-16-16zM1296 0h-1248c-26.464 0-48 21.536-48 48v192c0 8.832 7.168 16 16 16h1312c8.832 0 16-7.168 16-16v-192c0-26.464-21.536-48-48-48zM1312 224h-1280v-176c0-8.832 7.168-16 16-16h1248c8.832 0 16 7.168 16 16v176zM560 896c8.832 0 16-7.168 16-16v-512c0-8.832-7.168-16-16-16h-416c-8.832 0-16 7.168-16 16v512c0 8.832 7.168 16 16 16h416zM160 384h384v480h-384v-480zM720 480h480c8.832 0 16-7.168 16-16s-7.168-16-16-16h-480c-8.832 0-16 7.168-16 16s7.168 16 16 16zM720 640h480c8.832 0 16-7.168 16-16s-7.168-16-16-16h-480c-8.832 0-16 7.168-16 16s7.168 16 16 16zM720 800h480c8.832 0 16-7.168 16-16s-7.168-16-16-16h-480c-8.832 0-16 7.168-16 16s7.168 16 16 16zM96 128c0 17.673 14.327 32 32 32s32-14.327 32-32c0-17.673-14.327-32-32-32s-32 14.327-32 32zM224 128c0 17.673 14.327 32 32 32s32-14.327 32-32c0-17.673-14.327-32-32-32s-32 14.327-32 32zM352 128c0 17.673 14.327 32 32 32s32-14.327 32-32c0-17.673-14.327-32-32-32s-32 14.327-32 32z" + "M636.518 0c68.608 0 102.912 46.694 102.912 100.198 0 66.816-59.597 128.614-137.165 128.614-64.973 0-102.861-38.4-101.069-101.888 0-53.402 45.107-126.925 135.322-126.925zM425.421 1024c-54.17 0-93.85-33.382-55.962-180.429l62.157-260.71c10.803-41.677 12.595-58.419 0-58.419-16.23 0-86.477 28.774-128.102 57.19l-27.034-45.056c131.686-111.923 283.187-177.51 348.211-177.51 54.118 0 63.13 65.178 36.096 165.376l-71.219 274.022c-12.595 48.384-7.219 65.075 5.427 65.075 16.23 0 69.478-20.070 121.805-61.798l30.72 41.677c-128.102 130.406-268.032 180.582-322.099 180.582z" ], "attrs": [ {} ], - "width": 1344, "isMulticolor": false, "isMulticolor2": false, "tags": [ - "browser", - "window", - "software", - "program" + "info" ], - "grid": 32 + "grid": 20 + }, + "attrs": [ + {} + ], + "properties": { + "order": 1, + "id": 0, + "prevSize": 20, + "code": 59708, + "name": "info" + }, + "setIdx": 1, + "setId": 6, + "iconIdx": 0 + }, + { + "icon": { + "paths": [ + "M522.2 438.8v175.6h290.4c-11.8 75.4-87.8 220.8-290.4 220.8-174.8 0-317.4-144.8-317.4-323.2s142.6-323.2 317.4-323.2c99.4 0 166 42.4 204 79l139-133.8c-89.2-83.6-204.8-134-343-134-283 0-512 229-512 512s229 512 512 512c295.4 0 491.6-207.8 491.6-500.2 0-33.6-3.6-59.2-8-84.8l-483.6-0.2z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "google", + "brand" + ], + "grid": 16 }, "attrs": [ {} @@ -412,12 +439,12 @@ "order": 1, "id": 0, "prevSize": 32, - "code": 59701, - "name": "browser" + "code": 59707, + "name": "google" }, "setIdx": 2, - "setId": 1, - "iconIdx": 1 + "setId": 5, + "iconIdx": 0 }, { "icon": { @@ -445,7 +472,7 @@ "code": 59699, "name": "unlocked" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 2 }, @@ -477,7 +504,7 @@ "code": 59700, "name": "lock" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 3 }, @@ -516,7 +543,7 @@ "code": 59694, "name": "reset" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 4 }, @@ -546,7 +573,7 @@ "code": 59695, "name": "pause" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 5 }, @@ -576,7 +603,7 @@ "code": 59696, "name": "play" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 6 }, @@ -611,7 +638,7 @@ "code": 59693, "name": "settings2" }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 7 }, @@ -648,10 +675,102 @@ "prevSize": 32, "code": 59650 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 8 }, + { + "icon": { + "paths": [ + "M1020.192 401.824c-8.864-25.568-31.616-44.288-59.008-48.352l-266.432-39.616-115.808-240.448c-12.192-25.248-38.272-41.408-66.944-41.408s-54.752 16.16-66.944 41.408l-115.808 240.448-266.464 39.616c-27.36 4.064-50.112 22.784-58.944 48.352-8.8 25.632-2.144 53.856 17.184 73.12l195.264 194.944-45.28 270.432c-4.608 27.232 7.2 54.56 30.336 70.496 12.704 8.736 27.648 13.184 42.592 13.184 12.288 0 24.608-3.008 35.776-8.992l232.288-125.056 232.32 125.056c11.168 5.984 23.488 8.992 35.744 8.992 14.944 0 29.888-4.448 42.624-13.184 23.136-15.936 34.88-43.264 30.304-70.496l-45.312-270.432 195.328-194.944c19.296-19.296 25.92-47.52 17.184-73.12zM754.816 619.616c-16.384 16.32-23.808 39.328-20.064 61.888l45.312 270.432-232.32-124.992c-11.136-6.016-23.424-8.992-35.776-8.992-12.288 0-24.608 3.008-35.744 8.992l-232.32 124.992 45.312-270.432c3.776-22.56-3.648-45.568-20.032-61.888l-195.264-194.944 266.432-39.68c24.352-3.616 45.312-18.848 55.776-40.576l115.872-240.384 115.84 240.416c10.496 21.728 31.424 36.928 55.744 40.576l266.496 39.68-195.264 194.912z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "star", + "favorite" + ], + "grid": 32 + }, + "attrs": [ + {} + ], + "properties": { + "order": 1, + "id": 0, + "prevSize": 32, + "code": 59706, + "name": "control-Stars" + }, + "setIdx": 3, + "setId": 4, + "iconIdx": 0 + }, + { + "icon": { + "paths": [ + "M1328 320c-8.832 0-16 7.168-16 16v640c0 8.832-7.168 16-16 16h-1248c-8.832 0-16-7.168-16-16v-640c0-8.832-7.168-16-16-16s-16 7.168-16 16v640c0 26.464 21.536 48 48 48h1248c26.464 0 48-21.536 48-48v-640c0-8.832-7.168-16-16-16zM1296 0h-1248c-26.464 0-48 21.536-48 48v192c0 8.832 7.168 16 16 16h1312c8.832 0 16-7.168 16-16v-192c0-26.464-21.536-48-48-48zM1312 224h-1280v-176c0-8.832 7.168-16 16-16h1248c8.832 0 16 7.168 16 16v176zM560 896c8.832 0 16-7.168 16-16v-512c0-8.832-7.168-16-16-16h-416c-8.832 0-16 7.168-16 16v512c0 8.832 7.168 16 16 16h416zM160 384h384v480h-384v-480zM720 480h480c8.832 0 16-7.168 16-16s-7.168-16-16-16h-480c-8.832 0-16 7.168-16 16s7.168 16 16 16zM720 640h480c8.832 0 16-7.168 16-16s-7.168-16-16-16h-480c-8.832 0-16 7.168-16 16s7.168 16 16 16zM720 800h480c8.832 0 16-7.168 16-16s-7.168-16-16-16h-480c-8.832 0-16 7.168-16 16s7.168 16 16 16zM96 128c0 17.673 14.327 32 32 32s32-14.327 32-32c0-17.673-14.327-32-32-32s-32 14.327-32 32zM224 128c0 17.673 14.327 32 32 32s32-14.327 32-32c0-17.673-14.327-32-32-32s-32 14.327-32 32zM352 128c0 17.673 14.327 32 32 32s32-14.327 32-32c0-17.673-14.327-32-32-32s-32 14.327-32 32z" + ], + "attrs": [ + {} + ], + "width": 1344, + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "browser", + "window", + "software", + "program" + ], + "grid": 32 + }, + "attrs": [ + {} + ], + "properties": { + "order": 1, + "id": 0, + "prevSize": 32, + "code": 59701, + "name": "browser" + }, + "setIdx": 6, + "setId": 1, + "iconIdx": 1 + }, + { + "icon": { + "paths": [ + "M918 384v128h-128v298h-128v-298h-128v-128h384zM106 170h556v128h-214v512h-128v-512h-214v-128z" + ], + "attrs": [ + {} + ], + "isMulticolor": false, + "isMulticolor2": false, + "tags": [ + "text_fields" + ], + "grid": 24 + }, + "attrs": [ + {} + ], + "properties": { + "order": 75, + "id": 0, + "prevSize": 24, + "code": 59705, + "name": "control-RichText" + }, + "setIdx": 5, + "setId": 2, + "iconIdx": 0 + }, { "icon": { "paths": [ @@ -677,7 +796,7 @@ "prevSize": 32, "code": 59652 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 20 }, @@ -706,7 +825,7 @@ "prevSize": 32, "code": 59653 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 21 }, @@ -735,7 +854,7 @@ "prevSize": 32, "code": 59654 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 22 }, @@ -764,7 +883,7 @@ "prevSize": 32, "code": 59655 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 23 }, @@ -793,7 +912,7 @@ "prevSize": 32, "code": 59656 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 24 }, @@ -822,7 +941,7 @@ "prevSize": 32, "code": 59657 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 25 }, @@ -851,7 +970,7 @@ "prevSize": 32, "code": 59658 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 26 }, @@ -880,7 +999,7 @@ "prevSize": 32, "code": 59659 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 27 }, @@ -909,7 +1028,7 @@ "prevSize": 32, "code": 59660 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 28 }, @@ -938,7 +1057,7 @@ "prevSize": 32, "code": 59661 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 29 }, @@ -967,7 +1086,7 @@ "prevSize": 32, "code": 59662 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 30 }, @@ -996,7 +1115,7 @@ "prevSize": 32, "code": 59663 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 31 }, @@ -1025,7 +1144,7 @@ "prevSize": 32, "code": 59664 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 32 }, @@ -1054,7 +1173,7 @@ "prevSize": 32, "code": 59665 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 33 }, @@ -1083,7 +1202,7 @@ "prevSize": 32, "code": 59666 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 34 }, @@ -1112,7 +1231,7 @@ "prevSize": 32, "code": 59667 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 35 }, @@ -1141,7 +1260,7 @@ "prevSize": 32, "code": 59668 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 36 }, @@ -1170,7 +1289,7 @@ "prevSize": 32, "code": 59669 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 37 }, @@ -1199,7 +1318,7 @@ "prevSize": 32, "code": 59670 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 38 }, @@ -1228,7 +1347,7 @@ "prevSize": 32, "code": 59671 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 39 }, @@ -1257,7 +1376,7 @@ "prevSize": 32, "code": 59672 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 40 }, @@ -1286,7 +1405,7 @@ "prevSize": 32, "code": 59673 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 41 }, @@ -1315,7 +1434,7 @@ "prevSize": 32, "code": 59674 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 42 }, @@ -1340,11 +1459,11 @@ "properties": { "order": 25, "id": 13, - "name": "location", + "name": "location, control-Map, type-Geolocation", "prevSize": 32, "code": 59675 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 43 }, @@ -1373,7 +1492,7 @@ "prevSize": 32, "code": 59676 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 44 }, @@ -1402,7 +1521,7 @@ "prevSize": 32, "code": 59677 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 45 }, @@ -1431,7 +1550,7 @@ "prevSize": 32, "code": 59678 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 46 }, @@ -1460,7 +1579,7 @@ "prevSize": 32, "code": 59679 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 47 }, @@ -1489,7 +1608,7 @@ "prevSize": 32, "code": 59680 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 48 }, @@ -1518,7 +1637,7 @@ "prevSize": 32, "code": 59681 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 49 }, @@ -1547,7 +1666,7 @@ "prevSize": 32, "code": 59682 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 50 }, @@ -1576,7 +1695,7 @@ "prevSize": 32, "code": 59683 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 51 }, @@ -1605,7 +1724,7 @@ "prevSize": 32, "code": 59684 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 52 }, @@ -1634,7 +1753,7 @@ "prevSize": 32, "code": 59685 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 53 }, @@ -1663,7 +1782,7 @@ "prevSize": 32, "code": 59686 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 54 }, @@ -1692,7 +1811,7 @@ "prevSize": 32, "code": 59687 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 55 }, @@ -1721,7 +1840,7 @@ "prevSize": 32, "code": 59688 }, - "setIdx": 2, + "setIdx": 6, "setId": 1, "iconIdx": 56 } diff --git a/src/Squidex/app/theme/icomoon/style.css b/src/Squidex/app/theme/icomoon/style.css index f1314a0b4..55ad04871 100644 --- a/src/Squidex/app/theme/icomoon/style.css +++ b/src/Squidex/app/theme/icomoon/style.css @@ -1,10 +1,10 @@ @font-face { font-family: 'icomoon'; - src: url('fonts/icomoon.eot?y1amtc'); - src: url('fonts/icomoon.eot?y1amtc#iefix') format('embedded-opentype'), - url('fonts/icomoon.ttf?y1amtc') format('truetype'), - url('fonts/icomoon.woff?y1amtc') format('woff'), - url('fonts/icomoon.svg?y1amtc#icomoon') format('svg'); + src: url('fonts/icomoon.eot?jcn9ba'); + src: url('fonts/icomoon.eot?jcn9ba#iefix') format('embedded-opentype'), + url('fonts/icomoon.ttf?jcn9ba') format('truetype'), + url('fonts/icomoon.woff?jcn9ba') format('woff'), + url('fonts/icomoon.svg?jcn9ba#icomoon') format('svg'); font-weight: normal; font-style: normal; } @@ -24,8 +24,8 @@ -moz-osx-font-smoothing: grayscale; } -.icon-control-RichText:before { - content: "\e939"; +.icon-bug:before { + content: "\e93d"; } .icon-control-Markdown:before { content: "\e938"; @@ -63,8 +63,32 @@ .icon-angle-left:before { content: "\e901"; } -.icon-browser:before { - content: "\e935"; +.icon-info:before { + content: "\e93c"; +} +.icon-unlocked:before { + content: "\e933"; +} +.icon-lock:before { + content: "\e934"; +} +.icon-reset:before { + content: "\e92e"; +} +.icon-pause:before { + content: "\e92f"; +} +.icon-play:before { + content: "\e930"; +} +.icon-settings2:before { + content: "\e92d"; +} +.icon-bin2:before { + content: "\e902"; +} +.icon-google:before { + content: "\e93b"; } .icon-unlocked:before { content: "\e933"; @@ -87,6 +111,18 @@ .icon-bin2:before { content: "\e902"; } +.icon-control-Stars:before { + content: "\e93a"; +} +.icon-browser:before { + content: "\e935"; +} +.icon-control-RichText:before { + content: "\e939"; +} +.icon-control-Date:before { + content: "\e936"; +} .icon-activity:before { content: "\e904"; } @@ -174,6 +210,12 @@ .icon-location:before { content: "\e91b"; } +.icon-control-Map:before { + content: "\e91b"; +} +.icon-type-Geolocation:before { + content: "\e91b"; +} .icon-logo:before { content: "\e91c"; } diff --git a/src/Squidex/app/theme/theme.scss b/src/Squidex/app/theme/theme.scss index bff0fd203..1eca554d7 100644 --- a/src/Squidex/app/theme/theme.scss +++ b/src/Squidex/app/theme/theme.scss @@ -1,4 +1,19 @@ +@import '_bootstrap-vars.scss'; + +// Bootstrap +@import './../../node_modules/bootstrap/scss/bootstrap.scss'; + +// Pikaday +@import './../../node_modules/pikaday/css/pikaday.css'; + +// Bootstrap Overrides +@import '_bootstrap.scss'; + +// icomoon +@import './icomoon/style.css'; + @import '_common'; @import '_panels'; @import '_forms'; -@import '_lists'; \ No newline at end of file +@import '_lists'; +@import '_static'; \ No newline at end of file diff --git a/src/Squidex/app/theme/vendor.scss b/src/Squidex/app/theme/vendor.scss deleted file mode 100644 index 8037bdf3d..000000000 --- a/src/Squidex/app/theme/vendor.scss +++ /dev/null @@ -1,13 +0,0 @@ -@import '_bootstrap-vars.scss'; - -// Bootstrap -@import './../../node_modules/bootstrap/scss/bootstrap.scss'; - -// Pikaday -@import './../../node_modules/pikaday/css/pikaday.css'; - -// Bootstrap Overrides -@import '_bootstrap.scss'; - -// icomoon -@import './icomoon/style.css'; \ No newline at end of file diff --git a/src/Squidex/app/vendor.ts b/src/Squidex/app/vendor.ts deleted file mode 100644 index 48f5af125..000000000 --- a/src/Squidex/app/vendor.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Sebastian Stehle. All rights reserved - */ - -// Bootstrap -import 'theme/vendor.scss'; \ No newline at end of file diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index c97e21e19..c72c9e935 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -4,7 +4,7 @@ "baseUrl": "http://localhost:5000" }, "clusterer": { - "type": "redis", + "type": "none", "redis": { "connectionString": "localhost:6379,resolveDns=1" } @@ -20,7 +20,8 @@ "type": "mongoDb", "mongoDb": { "connectionString": "mongodb://localhost", - "databaseName": "Squidex" + "databaseName": "Squidex", + "databaseNameContent": "SquidexContent" } }, "identity": { diff --git a/src/Squidex/dockerfile b/src/Squidex/dockerfile index 18e2cfb63..83ddcad0a 100644 --- a/src/Squidex/dockerfile +++ b/src/Squidex/dockerfile @@ -1,4 +1,4 @@ -FROM microsoft/aspnetcore:1.1.0 +FROM microsoft/aspnetcore:1.1.1 WORKDIR /app COPY . . EXPOSE 80 diff --git a/src/Squidex/package.json b/src/Squidex/package.json index 315606576..d2611084e 100644 --- a/src/Squidex/package.json +++ b/src/Squidex/package.json @@ -8,75 +8,76 @@ "test": "karma start", "test:coverage": "karma start karma.coverage.conf.js", "test:clean": "rimraf _test-output", - "dev": "cpx node_modules/oidc-client/dist/oidc-client.min.js wwwroot/scripts/ & webpack-dev-server --config app-config/webpack.run.dev.js --inline --hot --port 3000", - "build": "webpack --config app-config/webpack.run.prod.js --display-error-details", - "build:copy": "cpx node_modules/oidc-client/dist/oidc-client.min.js wwwroot/scripts/", + "dev": "cpx node_modules/oidc-client/dist/oidc-client.min.js wwwroot/scripts/ & cpx node_modules/redoc/dist/redoc.min.js wwwroot/scripts/ && webpack-dev-server --config app-config/webpack.run.dev.js --inline --port 3000", + "build": "webpack --config app-config/webpack.run.prod.js --display-error-details --bail", + "build:copy": "cpx node_modules/oidc-client/dist/oidc-client.min.js wwwroot/scripts/ & cpx node_modules/redoc/dist/redoc.min.js wwwroot/scripts/", "build:clean": "rimraf wwwroot/build" }, "dependencies": { - "@angular/common": "2.4.5", - "@angular/compiler": "2.4.5", - "@angular/core": "2.4.5", - "@angular/forms": "2.4.5", - "@angular/http": "2.4.5", - "@angular/platform-browser": "2.4.5", - "@angular/platform-browser-dynamic": "2.4.5", - "@angular/router": "3.4.5", - "babel-polyfill": "^6.22.0", - "bootstrap": "^4.0.0-alpha.2", - "core-js": "^2.4.1", - "moment": "^2.17.1", - "mousetrap": "^1.6.0", - "oidc-client": "^1.2.2", - "pikaday": "^1.5.1", - "rxjs": "5.0.3", - "zone.js": "^0.7.6" + "@angular/common": "2.4.9", + "@angular/compiler": "2.4.9", + "@angular/core": "2.4.9", + "@angular/forms": "2.4.9", + "@angular/http": "2.4.9", + "@angular/platform-browser": "2.4.9", + "@angular/platform-browser-dynamic": "2.4.9", + "@angular/router": "3.4.9", + "babel-polyfill": "6.23.0", + "bootstrap": "4.0.0-alpha.6", + "core-js": "2.4.1", + "moment": "2.17.1", + "mousetrap": "1.6.0", + "oidc-client": "1.3.0-beta.3", + "pikaday": "1.5.1", + "redoc": "1.10.0", + "rxjs": "5.2.0", + "zone.js": "0.7.7" }, "devDependencies": { - "@angular/compiler-cli": "2.4.5", - "@angular/tsc-wrapped": "0.5.1", - "@ngtools/webpack": "^1.2.4", - "@types/core-js": "^0.9.35", - "@types/jasmine": "^2.5.41", - "@types/mousetrap": "^1.5.33", - "@types/node": "^7.0.4", - "angular2-router-loader": "^0.3.4", - "angular2-template-loader": "^0.6.0", - "awesome-typescript-loader": "^3.0.0-beta.18", - "cpx": "^1.5.0", - "css-loader": "^0.26.1", - "exports-loader": "^0.6.3", - "extract-text-webpack-plugin": "^2.0.0-beta.4", - "file-loader": "^0.10.0", - "html-loader": "^0.4.4", - "html-webpack-plugin": "^2.26.0", - "istanbul-instrumenter-loader": "^0.2.0", - "jasmine-core": "^2.5.2", - "karma": "^1.4.0", - "karma-chrome-launcher": "^2.0.0", - "karma-cli": "^1.0.1", - "karma-coverage": "^1.1.1", - "karma-htmlfile-reporter": "^0.3.5", - "karma-jasmine": "^1.1.0", - "karma-mocha-reporter": "^2.2.2", - "karma-phantomjs-launcher": "^1.0.2", - "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^2.0.2", - "node-sass": "^4.4.0", - "null-loader": "^0.1.1", - "phantomjs-prebuilt": "^2.1.14", - "raw-loader": "^0.5.1", - "rimraf": "^2.5.4", - "sass-lint": "^1.10.2", - "sass-loader": "^4.1.1", - "style-loader": "^0.13.1", - "tslint": "^4.4.2", - "tslint-loader": "^3.3.0", - "typemoq": "^1.1.0", - "typescript": "2.1.5", - "underscore": "^1.8.3", - "webpack": "2.2.0", - "webpack-dev-server": "2.2.0", - "webpack-merge": "^2.6.0" + "@angular/compiler-cli": "2.4.9", + "@angular/tsc-wrapped": "0.5.2", + "@ngtools/webpack": "1.2.12", + "@types/core-js": "0.9.35", + "@types/jasmine": "2.5.43", + "@types/mousetrap": "1.5.33", + "@types/node": "7.0.5", + "angular2-router-loader": "0.3.5", + "angular2-template-loader": "0.6.2", + "awesome-typescript-loader": "3.1.2", + "cpx": "1.5.0", + "css-loader": "0.27.2", + "exports-loader": "0.6.4", + "extract-text-webpack-plugin": "2.1.0", + "file-loader": "0.10.1", + "html-loader": "0.4.5", + "html-webpack-plugin": "2.28.0", + "istanbul-instrumenter-loader": "0.2.0", + "jasmine-core": "2.5.2", + "karma": "1.5.0", + "karma-chrome-launcher": "2.0.0", + "karma-cli": "1.0.1", + "karma-coverage": "1.1.1", + "karma-htmlfile-reporter": "0.3.5", + "karma-jasmine": "1.1.0", + "karma-mocha-reporter": "2.2.2", + "karma-phantomjs-launcher": "1.0.4", + "karma-sourcemap-loader": "0.3.7", + "karma-webpack": "2.0.2", + "node-sass": "4.5.0", + "null-loader": "0.1.1", + "phantomjs-prebuilt": "2.1.14", + "raw-loader": "0.5.1", + "rimraf": "2.6.1", + "sass-lint": "1.10.2", + "sass-loader": "6.0.3", + "style-loader": "0.13.2", + "tslint": "4.5.1", + "tslint-loader": "3.4.3", + "typemoq": "1.3.1", + "typescript": "2.2.1", + "underscore": "1.8.3", + "webpack": "2.2.1", + "webpack-dev-server": "2.4.1", + "webpack-merge": "4.0.0" } } diff --git a/src/Squidex/tsconfig.json b/src/Squidex/tsconfig.json index b554536d3..c0b2161b3 100644 --- a/src/Squidex/tsconfig.json +++ b/src/Squidex/tsconfig.json @@ -1,17 +1,17 @@ { "compilerOptions": { - "target": "es5", - "moduleResolution": "node", - "sourceMap": true, + "baseUrl": ".", "emitDecoratorMetadata": true, "experimentalDecorators": true, - "removeComments": false, + "moduleResolution": "node", "noImplicitAny": true, "noUnusedLocals": true, "noUnusedParameters": false, + "removeComments": false, + "sourceMap": true, "strictNullChecks": false, "suppressImplicitAnyIndexErrors": true, - "baseUrl": ".", + "target": "es5", "paths": { "framework": ["app/framework"], "features": ["app/features"], diff --git a/src/Squidex/typings.json b/src/Squidex/typings.json deleted file mode 100644 index f2df2ea6d..000000000 --- a/src/Squidex/typings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "globalDependencies": { - "core-js": "registry:dt/core-js#0.0.0+20160317120654", - "jasmine": "registry:dt/jasmine#2.2.0+20160505161446", - "node": "registry:dt/node#4.0.0+20160509154515" - } -} \ No newline at end of file diff --git a/src/Squidex/web.config b/src/Squidex/web.config index 142605f6f..3622c3223 100644 --- a/src/Squidex/web.config +++ b/src/Squidex/web.config @@ -4,6 +4,6 @@ - + \ No newline at end of file diff --git a/src/Squidex/wwwroot/images/client.png b/src/Squidex/wwwroot/images/client.png index e50f3c93b..d3ed9984e 100644 Binary files a/src/Squidex/wwwroot/images/client.png and b/src/Squidex/wwwroot/images/client.png differ diff --git a/src/Squidex/wwwroot/images/dashboard-api.png b/src/Squidex/wwwroot/images/dashboard-api.png index b24932b8d..65ea7c2be 100644 Binary files a/src/Squidex/wwwroot/images/dashboard-api.png and b/src/Squidex/wwwroot/images/dashboard-api.png differ diff --git a/src/Squidex/wwwroot/images/dashboard-feedback.png b/src/Squidex/wwwroot/images/dashboard-feedback.png index b1964ac9e..bcdafb08b 100644 Binary files a/src/Squidex/wwwroot/images/dashboard-feedback.png and b/src/Squidex/wwwroot/images/dashboard-feedback.png differ diff --git a/src/Squidex/wwwroot/images/dashboard-github.png b/src/Squidex/wwwroot/images/dashboard-github.png index 97a285976..d9bf35462 100644 Binary files a/src/Squidex/wwwroot/images/dashboard-github.png and b/src/Squidex/wwwroot/images/dashboard-github.png differ diff --git a/src/Squidex/wwwroot/images/dashboard-schema.png b/src/Squidex/wwwroot/images/dashboard-schema.png index 17b7cebc7..e43b8848f 100644 Binary files a/src/Squidex/wwwroot/images/dashboard-schema.png and b/src/Squidex/wwwroot/images/dashboard-schema.png differ diff --git a/src/Squidex/wwwroot/images/login-icon.png b/src/Squidex/wwwroot/images/login-icon.png new file mode 100644 index 000000000..33d29ab0f Binary files /dev/null and b/src/Squidex/wwwroot/images/login-icon.png differ diff --git a/src/Squidex/wwwroot/images/logo-small.png b/src/Squidex/wwwroot/images/logo-small.png new file mode 100644 index 000000000..7b04fa7a0 Binary files /dev/null and b/src/Squidex/wwwroot/images/logo-small.png differ diff --git a/src/Squidex/wwwroot/images/logo-squared-120.png b/src/Squidex/wwwroot/images/logo-squared-120.png index 5f676457f..b2f4f6763 100644 Binary files a/src/Squidex/wwwroot/images/logo-squared-120.png and b/src/Squidex/wwwroot/images/logo-squared-120.png differ diff --git a/src/Squidex/wwwroot/images/logo-white.png b/src/Squidex/wwwroot/images/logo-white.png new file mode 100644 index 000000000..f9a2e0f60 Binary files /dev/null and b/src/Squidex/wwwroot/images/logo-white.png differ diff --git a/src/Squidex/wwwroot/images/logo.png b/src/Squidex/wwwroot/images/logo.png index 605a97115..bb6c27a6b 100644 Binary files a/src/Squidex/wwwroot/images/logo.png and b/src/Squidex/wwwroot/images/logo.png differ diff --git a/src/Squidex/wwwroot/index.html b/src/Squidex/wwwroot/index.html index 92c78a5a1..97654ba85 100644 --- a/src/Squidex/wwwroot/index.html +++ b/src/Squidex/wwwroot/index.html @@ -7,8 +7,6 @@ Squidex Headless CMS - - + + + + + @@ -26,5 +46,17 @@
Loading Squidex
+ +
+
Your browser is out-of-date!
+ +

+ Update your browser to view this website correctly. Update my browser now +

+ +

+ × +

+
\ No newline at end of file diff --git a/src/Squidex/wwwroot/styles/static.css b/src/Squidex/wwwroot/styles/static.css deleted file mode 100644 index cfef8b617..000000000 --- a/src/Squidex/wwwroot/styles/static.css +++ /dev/null @@ -1,51 +0,0 @@ -body { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - background: #F4F8F9; - margin: 0; - padding-top: 3.25rem; - padding-left: 7rem; - padding-right: 7rem; - line-height: 1.5; -} - -img { - vertical-align: middle; -} - -.splash-h1, -.splash-text { - text-align: center; -} - -.splash-text a, -.splash-text a:visited { - color: #146feb; -} - -.redirect-button { - display: none; -} - -.loading { - text-align: center; -} - -.loading img { - height: 60px; -} - -.loading, noscript { - margin-top: 200px; -} - -.loading div { - font-size: 30px; - font-weight: lighter; -} - -noscript { - display: block; - font-size: 30px; - font-weight: lighter; - color: red; -} \ No newline at end of file diff --git a/tests/RunCoverage.ps1 b/tests/RunCoverage.ps1 index c95c806c1..d2e1b6072 100644 --- a/tests/RunCoverage.ps1 +++ b/tests/RunCoverage.ps1 @@ -50,6 +50,6 @@ New-Item -ItemType directory -Path $reportsFolder -output:"$workingFolder\$reportsFolder\Read.xml" ` -oldStyle -&"$userProfile\.nuget\packages\ReportGenerator\2.5.2\tools\ReportGenerator.exe" ` +&"$userProfile\.nuget\packages\ReportGenerator\2.5.6\tools\ReportGenerator.exe" ` -reports:"$workingFolder\$reportsFolder\*.xml" ` -targetdir:"$workingFolder\$reportsFolder\Output" \ No newline at end of file diff --git a/tests/Squidex.Core.Tests/ContentEnrichmentTests.cs b/tests/Squidex.Core.Tests/ContentEnrichmentTests.cs new file mode 100644 index 000000000..5b172a510 --- /dev/null +++ b/tests/Squidex.Core.Tests/ContentEnrichmentTests.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// ContentEnrichmentTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using NodaTime; +using NodaTime.Text; +using Squidex.Core.Contents; +using Squidex.Core.Schemas; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Core +{ + public class ContentEnrichmentTests + { + private readonly HashSet languages = new HashSet(new[] { Language.DE, Language.EN }); + + [Fact] + private void Should_enrich_with_default_values() + { + var now = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); + + var schema = + Schema.Create("my-schema", new SchemaProperties()) + .AddOrUpdateField(new JsonField(1, "my-json", + new JsonFieldProperties())) + .AddOrUpdateField(new StringField(2, "my-string", + new StringFieldProperties { DefaultValue = "EN-String", IsLocalizable = true })) + .AddOrUpdateField(new NumberField(3, "my-number", + new NumberFieldProperties { DefaultValue = 123 })) + .AddOrUpdateField(new BooleanField(4, "my-boolean", + new BooleanFieldProperties { DefaultValue = true })) + .AddOrUpdateField(new DateTimeField(5, "my-datetime", + new DateTimeFieldProperties { DefaultValue = now })) + .AddOrUpdateField(new GeolocationField(6, "my-geolocation", + new GeolocationFieldProperties())); + + var data = + new ContentData() + .AddField("my-string", + new ContentFieldData() + .AddValue("de", "DE-String")) + .AddField("my-number", + new ContentFieldData() + .AddValue("iv", 456)); + + data.Enrich(schema, languages); + + Assert.Equal(456, (int)data["my-number"]["iv"]); + + Assert.Equal("DE-String", (string)data["my-string"]["de"]); + Assert.Equal("EN-String", (string)data["my-string"]["en"]); + + Assert.Equal(now, InstantPattern.General.Parse((string)data["my-datetime"]["iv"]).Value); + + Assert.Equal(true, (bool)data["my-boolean"]["iv"]); + } + } +} diff --git a/tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs b/tests/Squidex.Core.Tests/ContentValidationTests.cs similarity index 63% rename from tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs rename to tests/Squidex.Core.Tests/ContentValidationTests.cs index 8999e19a2..7a5b22c77 100644 --- a/tests/Squidex.Core.Tests/Schemas/SchemaValidationTests.cs +++ b/tests/Squidex.Core.Tests/ContentValidationTests.cs @@ -1,5 +1,5 @@ // ========================================================================== -// SchemaValidationTests.cs +// ContentValidationTests.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -9,19 +9,18 @@ using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; -using NodaTime; -using NodaTime.Text; using Squidex.Core.Contents; +using Squidex.Core.Schemas; using Squidex.Infrastructure; using Xunit; -namespace Squidex.Core.Schemas +namespace Squidex.Core { - public class SchemaValidationTests + public class ContentValidationTests { private readonly HashSet languages = new HashSet(new[] { Language.DE, Language.EN }); private readonly List errors = new List(); - private Schema sut = Schema.Create("my-name", new SchemaProperties()); + private Schema schema = Schema.Create("my-name", new SchemaProperties()); [Fact] public async Task Should_add_error_if_validating_data_with_unknown_field() @@ -31,7 +30,7 @@ namespace Squidex.Core.Schemas .AddField("unknown", new ContentFieldData()); - await sut.ValidateAsync(data, errors, languages); + await data.ValidateAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -43,7 +42,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_error_if_validating_data_with_invalid_field() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { MaxValue = 100 })); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { MaxValue = 100 })); var data = new ContentData() @@ -51,7 +50,7 @@ namespace Squidex.Core.Schemas new ContentFieldData() .SetValue(1000)); - await sut.ValidateAsync(data, errors, languages); + await data.ValidateAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -63,7 +62,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_error_non_localizable_data_field_contains_language() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties())); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties())); var data = new ContentData() @@ -72,7 +71,7 @@ namespace Squidex.Core.Schemas .AddValue("es", 1) .AddValue("it", 1)); - await sut.ValidateAsync(data, errors, languages); + await data.ValidateAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -84,12 +83,12 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_error_if_validating_data_with_invalid_localizable_field() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true, IsLocalizable = true })); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true, IsLocalizable = true })); var data = new ContentData(); - await sut.ValidateAsync(data, errors, languages); + await data.ValidateAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -102,12 +101,12 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_error_if_required_data_field_is_not_in_bag() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true })); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true })); var data = new ContentData(); - await sut.ValidateAsync(data, errors, languages); + await data.ValidateAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -119,7 +118,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_error_if_data_contains_invalid_language() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsLocalizable = true })); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsLocalizable = true })); var data = new ContentData() @@ -128,7 +127,7 @@ namespace Squidex.Core.Schemas .AddValue("de", 1) .AddValue("xx", 1)); - await sut.ValidateAsync(data, errors, languages); + await data.ValidateAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -140,7 +139,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_error_if_data_contains_unsupported_language() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsLocalizable = true })); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsLocalizable = true })); var data = new ContentData() @@ -149,7 +148,7 @@ namespace Squidex.Core.Schemas .AddValue("es", 1) .AddValue("it", 1)); - await sut.ValidateAsync(data, errors, languages); + await data.ValidateAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -167,7 +166,7 @@ namespace Squidex.Core.Schemas .AddField("unknown", new ContentFieldData()); - await sut.ValidatePartialAsync(data, errors, languages); + await data.ValidatePartialAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -179,7 +178,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_error_if_validating_partial_data_with_invalid_field() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { MaxValue = 100 })); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { MaxValue = 100 })); var data = new ContentData() @@ -187,7 +186,7 @@ namespace Squidex.Core.Schemas new ContentFieldData() .SetValue(1000)); - await sut.ValidatePartialAsync(data, errors, languages); + await data.ValidatePartialAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -199,7 +198,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_error_non_localizable_partial_data_field_contains_language() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties())); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties())); var data = new ContentData() @@ -208,7 +207,7 @@ namespace Squidex.Core.Schemas .AddValue("es", 1) .AddValue("it", 1)); - await sut.ValidatePartialAsync(data, errors, languages); + await data.ValidatePartialAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -220,12 +219,12 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_not_add_error_if_validating_partial_data_with_invalid_localizable_field() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true, IsLocalizable = true })); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true, IsLocalizable = true })); var data = new ContentData(); - await sut.ValidatePartialAsync(data, errors, languages); + await data.ValidatePartialAsync(schema, languages, errors); Assert.Equal(0, errors.Count); } @@ -233,12 +232,12 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_not_add_error_if_required_partial_data_field_is_not_in_bag() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true })); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsRequired = true })); var data = new ContentData(); - await sut.ValidatePartialAsync(data, errors, languages); + await data.ValidatePartialAsync(schema, languages, errors); Assert.Equal(0, errors.Count); } @@ -246,7 +245,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_error_if_partial_data_contains_invalid_language() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsLocalizable = true })); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsLocalizable = true })); var data = new ContentData() @@ -255,7 +254,7 @@ namespace Squidex.Core.Schemas .AddValue("de", 1) .AddValue("xx", 1)); - await sut.ValidatePartialAsync(data, errors, languages); + await data.ValidatePartialAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -267,7 +266,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_error_if_partial_data_contains_unsupported_language() { - sut = sut.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsLocalizable = true })); + schema = schema.AddOrUpdateField(new NumberField(1, "my-field", new NumberFieldProperties { IsLocalizable = true })); var data = new ContentData() @@ -276,7 +275,7 @@ namespace Squidex.Core.Schemas .AddValue("es", 1) .AddValue("it", 1)); - await sut.ValidatePartialAsync(data, errors, languages); + await data.ValidatePartialAsync(schema, languages, errors); errors.ShouldBeEquivalentTo( new List @@ -285,44 +284,5 @@ namespace Squidex.Core.Schemas new ValidationError("my-field has an unsupported language 'it'", "my-field") }); } - - [Fact] - private void Should_enrich_with_default_values() - { - var now = Instant.FromUnixTimeSeconds(SystemClock.Instance.GetCurrentInstant().ToUnixTimeSeconds()); - - var schema = - Schema.Create("my-schema", new SchemaProperties()) - .AddOrUpdateField(new JsonField(0, "my-json", - new JsonFieldProperties())) - .AddOrUpdateField(new StringField(1, "my-string", - new StringFieldProperties { DefaultValue = "EN-String", IsLocalizable = true })) - .AddOrUpdateField(new NumberField(2, "my-number", - new NumberFieldProperties { DefaultValue = 123 })) - .AddOrUpdateField(new BooleanField(3, "my-boolean", - new BooleanFieldProperties { DefaultValue = true })) - .AddOrUpdateField(new DateTimeField(4, "my-datetime", - new DateTimeFieldProperties { DefaultValue = now })); - - var data = - new ContentData() - .AddField("my-string", - new ContentFieldData() - .AddValue("de", "DE-String")) - .AddField("my-number", - new ContentFieldData() - .AddValue("iv", 456)); - - schema.Enrich(data, languages); - - Assert.Equal(456, (int)data["my-number"]["iv"]); - - Assert.Equal("DE-String", (string)data["my-string"]["de"]); - Assert.Equal("EN-String", (string)data["my-string"]["en"]); - - Assert.Equal(now, InstantPattern.General.Parse((string)data["my-datetime"]["iv"]).Value); - - Assert.Equal(true, (bool)data["my-boolean"]["iv"]); - } } } diff --git a/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs b/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs index 68f2824e1..ca0446480 100644 --- a/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs +++ b/tests/Squidex.Core.Tests/Contents/ContentDataTests.cs @@ -156,7 +156,49 @@ namespace Squidex.Core.Contents Assert.Equal(expected, actual); } - + + [Fact] + public void Should_provide_master_language_from_invariant() + { + var expected = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("en", 3)); + + var input = + new ContentData() + .AddField("field1", + new ContentFieldData() + .AddValue("iv", 3)); + + var actual = input.ToApiModel(schema, languages, masterLanguage); + + Assert.Equal(expected, actual); + } + + [Fact] + public void Should_remove_null_values_when_cleaning() + { + var expected = + new ContentData() + .AddField("field2", + new ContentFieldData() + .AddValue("en", 2)); + + var input = + new ContentData() + .AddField("field1", null) + .AddField("field2", + new ContentFieldData() + .AddValue("en", 2) + .AddValue("it", null)); + + var actual = input.ToCleaned(); + + Assert.Equal(expected, actual); + } + [Fact] public void Should_provide_invariant_from_first_language() { diff --git a/tests/Squidex.Core.Tests/Schemas/BooleanFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/BooleanFieldTests.cs index e1cfda931..546c9262a 100644 --- a/tests/Squidex.Core.Tests/Schemas/BooleanFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/BooleanFieldTests.cs @@ -37,7 +37,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_not_add_error_if_null_boolean_is_valid() { - var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties { Label = "My-Boolean" }); + var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties()); await sut.ValidateAsync(CreateValue(null), errors); @@ -47,7 +47,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_not_add_error_if_boolean_is_valid() { - var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties { Label = "My-Boolean" }); + var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties()); await sut.ValidateAsync(CreateValue(true), errors); @@ -57,23 +57,23 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_errors_if_boolean_is_required() { - var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties { Label = "My-Boolean", IsRequired = true }); + var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties { IsRequired = true }); await sut.ValidateAsync(CreateValue(null), errors); errors.ShouldBeEquivalentTo( - new[] { "My-Boolean is required" }); + new[] { " is required" }); } [Fact] public async Task Should_add_errors_if_value_is_not_valid() { - var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties { Label = "My-Boolean" }); + var sut = new BooleanField(1, "my-bolean", new BooleanFieldProperties()); await sut.ValidateAsync(CreateValue("Invalid"), errors); errors.ShouldBeEquivalentTo( - new[] { "My-Boolean is not a valid value" }); + new[] { " is not a valid value" }); } private static JValue CreateValue(object v) diff --git a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs index 067b08a35..365b65bb5 100644 --- a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs @@ -136,7 +136,7 @@ namespace Squidex.Core.Schemas private static Instant FutureDays(int days) { - return SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(days)); + return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); } } } \ No newline at end of file diff --git a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs index 9a251e461..a7602eb5e 100644 --- a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using System.Threading.Tasks; using FluentAssertions; @@ -38,7 +39,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_not_add_error_if_datetime_is_valid() { - var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { Label = "My-DateTime" }); + var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties()); await sut.ValidateAsync(CreateValue(null), errors); @@ -48,50 +49,61 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_errors_if_datetime_is_required() { - var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { Label = "My-DateTime", IsRequired = true }); + var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { IsRequired = true }); await sut.ValidateAsync(CreateValue(null), errors); errors.ShouldBeEquivalentTo( - new[] { "My-DateTime is required" }); + new[] { " is required" }); } [Fact] public async Task Should_add_errors_if_datetime_is_less_than_min() { - var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { Label = "My-DateTime", MinValue = FutureDays(10) }); + var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { MinValue = FutureDays(10) }); await sut.ValidateAsync(CreateValue(FutureDays(0)), errors); errors.ShouldBeEquivalentTo( - new[] { $"My-DateTime must be greater than '{FutureDays(10)}'" }); + new[] { $" must be greater than '{FutureDays(10)}'" }); } [Fact] public async Task Should_add_errors_if_datetime_is_greater_than_max() { - var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { Label = "My-DateTime", MaxValue = FutureDays(10) }); + var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { MaxValue = FutureDays(10) }); await sut.ValidateAsync(CreateValue(FutureDays(20)), errors); errors.ShouldBeEquivalentTo( - new[] { $"My-DateTime must be less than '{FutureDays(10)}'" }); + new[] { $" must be less than '{FutureDays(10)}'" }); } [Fact] public async Task Should_add_errors_if_value_is_not_valid() { - var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties { Label = "My-DateTime" }); + var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties()); await sut.ValidateAsync(CreateValue("Invalid"), errors); errors.ShouldBeEquivalentTo( - new[] { "My-DateTime is not a valid value" }); + new[] { " is not a valid value" }); + } + + [Fact] + public async Task Should_add_errors_if_value_is_another_type() + { + var sut = new DateTimeField(1, "my-datetime", new DateTimeFieldProperties()); + + await sut.ValidateAsync(CreateValue(123), errors); + + errors.ShouldBeEquivalentTo( + new[] { " is not a valid value" }); } private static Instant FutureDays(int days) { - return SystemClock.Instance.GetCurrentInstant().Plus(Duration.FromDays(days)); + return Instant.FromDateTimeUtc(DateTime.UtcNow.Date.AddDays(days)); } private static JValue CreateValue(object v) diff --git a/tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs new file mode 100644 index 000000000..cd799d5b8 --- /dev/null +++ b/tests/Squidex.Core.Tests/Schemas/GeolocationFieldTests.cs @@ -0,0 +1,108 @@ +// ========================================================================== +// GeolocationFieldTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Squidex.Core.Schemas +{ + public class GeolocationFieldTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_instantiate_field() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties()); + + Assert.Equal("my-geolocation", sut.Name); + } + + [Fact] + public void Should_clone_object() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties()); + + Assert.NotEqual(sut, sut.Enable()); + } + + [Fact] + public async Task Should_not_add_error_if_geolocation_is_valid_null() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties()); + + await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_geolocation_is_valid() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties()); + + var geolocation = new JObject( + new JProperty("latitude", 0), + new JProperty("longitude", 0)); + + await sut.ValidateAsync(CreateValue(geolocation), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_errors_if_geolocation_has_invalid_properties() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties { IsRequired = true }); + + var geolocation = new JObject( + new JProperty("latitude", 200), + new JProperty("longitude", 0)); + + await sut.ValidateAsync(CreateValue(geolocation), errors); + + errors.ShouldBeEquivalentTo( + new[] { " is not a valid value" }); + } + + [Fact] + public async Task Should_add_errors_if_geolocation_has_too_many_properties() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties { IsRequired = true }); + + var geolocation = new JObject( + new JProperty("invalid", 0), + new JProperty("latitude", 0), + new JProperty("longitude", 0)); + + await sut.ValidateAsync(CreateValue(geolocation), errors); + + errors.ShouldBeEquivalentTo( + new[] { " is not a valid value" }); + } + + [Fact] + public async Task Should_add_errors_if_geolocation_is_required() + { + var sut = new GeolocationField(1, "my-geolocation", new GeolocationFieldProperties { IsRequired = true }); + + await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); + + errors.ShouldBeEquivalentTo( + new[] { " is required" }); + } + + private static JToken CreateValue(JToken v) + { + return v; + } + } +} diff --git a/tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs b/tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs new file mode 100644 index 000000000..d858f704e --- /dev/null +++ b/tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// GeolocationFieldTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using FluentAssertions; +using Squidex.Infrastructure; +using Xunit; + +namespace Squidex.Core.Schemas +{ + public class GeolocationFieldPropertiesTests + { + private readonly List errors = new List(); + + [Fact] + public void Should_add_error_if_editor_is_not_valid() + { + var sut = new GeolocationFieldProperties { Editor = (GeolocationFieldEditor)123 }; + + sut.Validate(errors); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Editor ist not a valid value", "Editor") + }); + } + + [Fact] + public void Should_set_or_freeze_sut() + { + var sut = new GeolocationFieldProperties(); + + foreach (var property in sut.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) + { + var value = + property.PropertyType.GetTypeInfo().IsValueType ? + Activator.CreateInstance(property.PropertyType) : + null; + + property.SetValue(sut, value); + + var result = property.GetValue(sut); + + Assert.Equal(value, result); + } + + sut.Freeze(); + + foreach (var property in sut.GetType().GetRuntimeProperties().Where(x => x.Name != "IsFrozen")) + { + var value = + property.PropertyType.GetTypeInfo().IsValueType ? + Activator.CreateInstance(property.PropertyType) : + null; + + Assert.Throws(() => + { + try + { + property.SetValue(sut, value); + } + catch (Exception ex) + { + throw ex.InnerException; + } + }); + } + } + } +} diff --git a/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs b/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs index 41069bcac..c4ea8cac8 100644 --- a/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Json/JsonSerializerTests.cs @@ -34,13 +34,17 @@ namespace Squidex.Core.Schemas.Json var schema = Schema.Create("my-schema", new SchemaProperties()) .AddOrUpdateField(new StringField(1, "my-string", - new StringFieldProperties { Label = "My-String", Pattern = "[0-9]{3}" })).DisableField(1) + new StringFieldProperties { Label = "My-String", Pattern = "[0-9]{3}" })) .AddOrUpdateField(new NumberField(2, "my-number", new NumberFieldProperties { Hints = "My-Number" })) .AddOrUpdateField(new BooleanField(3, "my-boolean", - new BooleanFieldProperties())).HideField(2) - .AddOrUpdateField(new DateTimeField(4, "my-datetime", - new DateTimeFieldProperties())).HideField(2) + new BooleanFieldProperties())).HideField(3) + .AddOrUpdateField(new JsonField(4, "my-json", + new JsonFieldProperties())).DisableField(4) + .AddOrUpdateField(new DateTimeField(5, "my-datetime", + new DateTimeFieldProperties())) + .AddOrUpdateField(new GeolocationField(6, "my-geolocation", + new GeolocationFieldProperties())) .Publish(); var deserialized = sut.Deserialize(sut.Serialize(schema)); diff --git a/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs index 0076e4a8a..e77e17404 100644 --- a/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/JsonFieldTests.cs @@ -37,22 +37,22 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_not_add_error_if_json_is_valid() { - var sut = new JsonField(1, "my-json", new JsonFieldProperties { Label = "My-Json" }); + var sut = new JsonField(1, "my-json", new JsonFieldProperties()); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(new JValue(1)), errors); Assert.Empty(errors); } [Fact] - public async Task Should_add_errors_if_datetime_is_required() + public async Task Should_add_errors_if_json_is_required() { - var sut = new JsonField(1, "my-json", new JsonFieldProperties { Label = "My-Json", IsRequired = true }); + var sut = new JsonField(1, "my-json", new JsonFieldProperties { IsRequired = true }); - await sut.ValidateAsync(CreateValue(null), errors); + await sut.ValidateAsync(CreateValue(JValue.CreateNull()), errors); errors.ShouldBeEquivalentTo( - new[] { "My-Json is required" }); + new[] { " is required" }); } private static JValue CreateValue(JValue v) diff --git a/tests/Squidex.Core.Tests/Schemas/NumberFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/NumberFieldTests.cs index ab2250c88..c2bd13063 100644 --- a/tests/Squidex.Core.Tests/Schemas/NumberFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/NumberFieldTests.cs @@ -38,7 +38,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_not_add_error_if_number_is_valid() { - var sut = new NumberField(1, "my-number", new NumberFieldProperties { Label = "My-Number" }); + var sut = new NumberField(1, "my-number", new NumberFieldProperties()); await sut.ValidateAsync(CreateValue(null), errors); @@ -48,56 +48,56 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_errors_if_number_is_required() { - var sut = new NumberField(1, "my-number", new NumberFieldProperties { Label = "My-Number", IsRequired = true }); + var sut = new NumberField(1, "my-number", new NumberFieldProperties { IsRequired = true }); await sut.ValidateAsync(CreateValue(null), errors); errors.ShouldBeEquivalentTo( - new [] { "My-Number is required" }); + new [] { " is required" }); } [Fact] public async Task Should_add_errors_if_number_is_less_than_min() { - var sut = new NumberField(1, "my-number", new NumberFieldProperties { Label = "My-Number", MinValue = 10 }); + var sut = new NumberField(1, "my-number", new NumberFieldProperties { MinValue = 10 }); await sut.ValidateAsync(CreateValue(5), errors); errors.ShouldBeEquivalentTo( - new[] { "My-Number must be greater than '10'" }); + new[] { " must be greater than '10'" }); } [Fact] public async Task Should_add_errors_if_number_is_greater_than_max() { - var sut = new NumberField(1, "my-number", new NumberFieldProperties { Label = "My-Number", MaxValue = 10 }); + var sut = new NumberField(1, "my-number", new NumberFieldProperties { MaxValue = 10 }); await sut.ValidateAsync(CreateValue(20), errors); errors.ShouldBeEquivalentTo( - new[] { "My-Number must be less than '10'" }); + new[] { " must be less than '10'" }); } [Fact] public async Task Should_add_errors_if_number_is_not_allowed() { - var sut = new NumberField(1, "my-number", new NumberFieldProperties { Label = "My-Number", AllowedValues = ImmutableList.Create(10d) }); + var sut = new NumberField(1, "my-number", new NumberFieldProperties { AllowedValues = ImmutableList.Create(10d) }); await sut.ValidateAsync(CreateValue(20), errors); errors.ShouldBeEquivalentTo( - new[] { "My-Number is not an allowed value" }); + new[] { " is not an allowed value" }); } [Fact] public async Task Should_add_errors_if_value_is_not_valid() { - var sut = new NumberField(1, "my-number", new NumberFieldProperties { Label = "My-Number" }); + var sut = new NumberField(1, "my-number", new NumberFieldProperties()); await sut.ValidateAsync(CreateValue("Invalid"), errors); errors.ShouldBeEquivalentTo( - new[] { "My-Number is not a valid value" }); + new[] { " is not a valid value" }); } private static JValue CreateValue(object v) diff --git a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs index c12608625..c2dfc7edb 100644 --- a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using Newtonsoft.Json.Linq; +using NJsonSchema; using Squidex.Infrastructure; using Xunit; @@ -255,9 +256,9 @@ namespace Squidex.Core.Schemas { var languages = new HashSet(new[] { Language.DE, Language.EN }); - var json = BuildMixedSchema().BuildSchema(languages, (n, s) => s).ToJson(); + var jsonSchema = BuildMixedSchema().BuildJsonSchema(languages, (n, s) => new JsonSchema4 { SchemaReference = s }); - Assert.NotNull(json); + Assert.NotNull(jsonSchema); } [Fact] @@ -276,20 +277,22 @@ namespace Squidex.Core.Schemas var schema = Schema.Create("user", new SchemaProperties { Hints = "The User" }) - .AddOrUpdateField(new JsonField(0, "my-json", + .AddOrUpdateField(new JsonField(1, "my-json", new JsonFieldProperties())) - .AddOrUpdateField(new StringField(1, "my-string1", + .AddOrUpdateField(new StringField(2, "my-string1", new StringFieldProperties { Label = "My String1", IsLocalizable = true, IsRequired = true, AllowedValues = allowedValues })) - .AddOrUpdateField(new StringField(2, "my-string2", + .AddOrUpdateField(new StringField(3, "my-string2", new StringFieldProperties { Hints = "My String1" })) - .AddOrUpdateField(new NumberField(3, "my-number", + .AddOrUpdateField(new NumberField(4, "my-number", new NumberFieldProperties { MinValue = 1, MaxValue = 10 })) - .AddOrUpdateField(new BooleanField(4, "my-boolean", + .AddOrUpdateField(new BooleanField(5, "my-boolean", new BooleanFieldProperties())) - .AddOrUpdateField(new DateTimeField(5, "my-datetime", + .AddOrUpdateField(new DateTimeField(6, "my-datetime", new DateTimeFieldProperties { Editor = DateTimeFieldEditor.DateTime })) - .AddOrUpdateField(new DateTimeField(6, "my-date", - new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })); + .AddOrUpdateField(new DateTimeField(7, "my-date", + new DateTimeFieldProperties { Editor = DateTimeFieldEditor.Date })) + .AddOrUpdateField(new GeolocationField(8, "my-geolocation", + new GeolocationFieldProperties())); return schema; } diff --git a/tests/Squidex.Core.Tests/Schemas/StringFieldTests.cs b/tests/Squidex.Core.Tests/Schemas/StringFieldTests.cs index c487d6e39..6ad3b59b3 100644 --- a/tests/Squidex.Core.Tests/Schemas/StringFieldTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/StringFieldTests.cs @@ -38,7 +38,7 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_not_add_error_if_string_is_valid() { - var sut = new StringField(1, "my-string", new StringFieldProperties { Label = "My-String" }); + var sut = new StringField(1, "my-string", new StringFieldProperties { Label = "" }); await sut.ValidateAsync(CreateValue(null), errors); @@ -48,62 +48,62 @@ namespace Squidex.Core.Schemas [Fact] public async Task Should_add_errors_if_string_is_required() { - var sut = new StringField(1, "my-string", new StringFieldProperties { Label = "My-String", IsRequired = true }); + var sut = new StringField(1, "my-string", new StringFieldProperties { IsRequired = true }); await sut.ValidateAsync(CreateValue(null), errors); errors.ShouldBeEquivalentTo( - new[] { "My-String is required" }); + new[] { " is required" }); } [Fact] public async Task Should_add_errors_if_string_is_shorter_than_min_length() { - var sut = new StringField(1, "my-string", new StringFieldProperties { Label = "My-String", MinLength = 10 }); + var sut = new StringField(1, "my-string", new StringFieldProperties { MinLength = 10 }); await sut.ValidateAsync(CreateValue("123"), errors); errors.ShouldBeEquivalentTo( - new[] { "My-String must have more than '10' characters" }); + new[] { " must have more than '10' characters" }); } [Fact] public async Task Should_add_errors_if_string_is_longer_than_max_length() { - var sut = new StringField(1, "my-string", new StringFieldProperties { Label = "My-String", MaxLength = 5 }); + var sut = new StringField(1, "my-string", new StringFieldProperties { MaxLength = 5 }); await sut.ValidateAsync(CreateValue("12345678"), errors); errors.ShouldBeEquivalentTo( - new[] { "My-String must have less than '5' characters" }); + new[] { " must have less than '5' characters" }); } [Fact] public async Task Should_add_errors_if_string_not_allowed() { - var sut = new StringField(1, "my-string", new StringFieldProperties { Label = "My-String", AllowedValues = ImmutableList.Create("Foo") }); + var sut = new StringField(1, "my-string", new StringFieldProperties { AllowedValues = ImmutableList.Create("Foo") }); await sut.ValidateAsync(CreateValue("Bar"), errors); errors.ShouldBeEquivalentTo( - new[] { "My-String is not an allowed value" }); + new[] { " is not an allowed value" }); } [Fact] public async Task Should_add_errors_if_number_is_not_valid_pattern() { - var sut = new StringField(1, "my-string", new StringFieldProperties { Label = "My-String", Pattern = "[0-9]{3}" }); + var sut = new StringField(1, "my-string", new StringFieldProperties { Pattern = "[0-9]{3}" }); await sut.ValidateAsync(CreateValue("abc"), errors); errors.ShouldBeEquivalentTo( - new[] { "My-String is not valid" }); + new[] { " is not valid" }); } [Fact] public async Task Should_add_errors_if_number_is_not_valid_pattern_with_message() { - var sut = new StringField(1, "my-string", new StringFieldProperties { Label = "My-String", Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message" }); + var sut = new StringField(1, "my-string", new StringFieldProperties { Pattern = "[0-9]{3}", PatternMessage = "Custom Error Message" }); await sut.ValidateAsync(CreateValue("abc"), errors); diff --git a/tests/Squidex.Core.Tests/Schemas/ValidationTestExtensions.cs b/tests/Squidex.Core.Tests/Schemas/ValidationTestExtensions.cs new file mode 100644 index 000000000..55b454c8f --- /dev/null +++ b/tests/Squidex.Core.Tests/Schemas/ValidationTestExtensions.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// ValidationTestExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; + +namespace Squidex.Core.Schemas +{ + public static class ValidationTestExtensions + { + public static Task ValidateAsync(this Field field, JToken value, IList errors) + { + return field.ValidateAsync(value, errors.Add); + } + } +} diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs index 6cb0bc0e2..e7e400a5d 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/AllowedValuesValidatorTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new AllowedValuesValidator(100, 200); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors.Add); Assert.Equal(0, errors.Count); } @@ -32,7 +32,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new AllowedValuesValidator(100, 200); - await sut.ValidateAsync(100, errors); + await sut.ValidateAsync(100, errors.Add); Assert.Equal(0, errors.Count); } @@ -42,7 +42,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new AllowedValuesValidator(100, 200); - await sut.ValidateAsync(50, errors); + await sut.ValidateAsync(50, errors.Add); errors.ShouldBeEquivalentTo( new[] { " is not an allowed value" }); diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/PatternValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/PatternValidatorTests.cs index 73176ab8f..a7787da55 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/PatternValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/PatternValidatorTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); - await sut.ValidateAsync("abc:12", errors); + await sut.ValidateAsync("abc:12", errors.Add); Assert.Equal(0, errors.Count); } @@ -32,7 +32,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors.Add); Assert.Equal(0, errors.Count); } @@ -42,7 +42,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new PatternValidator("[a-z]{3}:[0-9]{2}"); - await sut.ValidateAsync("foo", errors); + await sut.ValidateAsync("foo", errors.Add); errors.ShouldBeEquivalentTo( new[] { " is not valid" }); @@ -53,7 +53,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new PatternValidator("[a-z]{3}:[0-9]{2}", "Custom Error Message"); - await sut.ValidateAsync("foo", errors); + await sut.ValidateAsync("foo", errors.Add); errors.ShouldBeEquivalentTo( new[] { "Custom Error Message" }); diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/RangeValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/RangeValidatorTests.cs index 4b23e6196..638d9ad20 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/RangeValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/RangeValidatorTests.cs @@ -23,7 +23,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RangeValidator(100, 200); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors.Add); Assert.Equal(0, errors.Count); } @@ -37,7 +37,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RangeValidator(min, max); - await sut.ValidateAsync(1500, errors); + await sut.ValidateAsync(1500, errors.Add); Assert.Equal(0, errors.Count); } @@ -55,7 +55,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RangeValidator(2000, null); - await sut.ValidateAsync(1500, errors); + await sut.ValidateAsync(1500, errors.Add); errors.ShouldBeEquivalentTo( new[] { " must be greater than '2000'" }); @@ -66,7 +66,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RangeValidator(null, 1000); - await sut.ValidateAsync(1500, errors); + await sut.ValidateAsync(1500, errors.Add); errors.ShouldBeEquivalentTo( new[] { " must be less than '1000'" }); diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs index 29232a097..24df1015f 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/RequiredStringValidatorTests.cs @@ -26,7 +26,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredStringValidator(); - await sut.ValidateAsync(value, errors); + await sut.ValidateAsync(value, errors.Add); Assert.Equal(0, errors.Count); } @@ -36,7 +36,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredStringValidator(); - await sut.ValidateAsync(true, errors); + await sut.ValidateAsync(true, errors.Add); Assert.Equal(0, errors.Count); } @@ -46,7 +46,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredStringValidator(true); - await sut.ValidateAsync(string.Empty, errors); + await sut.ValidateAsync(string.Empty, errors.Add); errors.ShouldBeEquivalentTo( new[] { " is required" }); @@ -57,7 +57,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredStringValidator(); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors.Add); errors.ShouldBeEquivalentTo( new[] { " is required" }); diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs index acdf2e72f..bcd3c6e1d 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/RequiredValidatorTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredValidator(); - await sut.ValidateAsync(true, errors); + await sut.ValidateAsync(true, errors.Add); Assert.Equal(0, errors.Count); } @@ -32,7 +32,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredValidator(); - await sut.ValidateAsync(string.Empty, errors); + await sut.ValidateAsync(string.Empty, errors.Add); Assert.Equal(0, errors.Count); } @@ -42,7 +42,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new RequiredValidator(); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors.Add); errors.ShouldBeEquivalentTo( new[] { " is required" }); diff --git a/tests/Squidex.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs b/tests/Squidex.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs index ba4b89dc0..f1c728c1a 100644 --- a/tests/Squidex.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/Validators/StringLengthValidatorTests.cs @@ -24,7 +24,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new StringLengthValidator(100, 200); - await sut.ValidateAsync(null, errors); + await sut.ValidateAsync(null, errors.Add); Assert.Equal(0, errors.Count); } @@ -38,7 +38,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new StringLengthValidator(min, max); - await sut.ValidateAsync(CreateString(1500), errors); + await sut.ValidateAsync(CreateString(1500), errors.Add); Assert.Equal(0, errors.Count); } @@ -56,7 +56,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new StringLengthValidator(2000, null); - await sut.ValidateAsync(CreateString(1500), errors); + await sut.ValidateAsync(CreateString(1500), errors.Add); errors.ShouldBeEquivalentTo( new[] { " must have more than '2000' characters" }); @@ -67,7 +67,7 @@ namespace Squidex.Core.Schemas.Validators { var sut = new StringLengthValidator(null, 1000); - await sut.ValidateAsync(CreateString(1500), errors); + await sut.ValidateAsync(CreateString(1500), errors.Add); errors.ShouldBeEquivalentTo( new[] { " must have less than '1000' characters" }); diff --git a/tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj b/tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj index c8adff1a5..b017976ac 100644 --- a/tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj +++ b/tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj @@ -10,11 +10,11 @@ - - - - - + + + + + diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs index c51c8490c..ecc495b05 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/AggregateHandlerTests.cs @@ -25,11 +25,13 @@ namespace Squidex.Infrastructure.CQRS.Commands private sealed class MyCommand : IAggregateCommand { public Guid AggregateId { get; set; } + + public long? ExpectedVersion { get; set; } } - private sealed class MyDomainObject : DomainObject + private sealed class MyDomainObject : DomainObjectBase { - public MyDomainObject(Guid id, int version) + public MyDomainObject(Guid id, int version) : base(id, version) { } @@ -48,19 +50,16 @@ namespace Squidex.Infrastructure.CQRS.Commands private readonly Mock factory = new Mock(); private readonly Mock repository = new Mock(); - private readonly Mock processor1 = new Mock(); - private readonly Mock processor2 = new Mock(); private readonly Envelope event1 = new Envelope(new MyEvent()); private readonly Envelope event2 = new Envelope(new MyEvent()); + private readonly CommandContext context; private readonly MyCommand command; private readonly AggregateHandler sut; private readonly MyDomainObject domainObject; public AggregateHandlerTests() { - var processors = new[] { processor1.Object, processor2.Object }; - - sut = new AggregateHandler(factory.Object, repository.Object, processors); + sut = new AggregateHandler(factory.Object, repository.Object); domainObject = new MyDomainObject(Guid.NewGuid(), 1) @@ -68,6 +67,7 @@ namespace Squidex.Infrastructure.CQRS.Commands .RaiseNewEvent(event2); command = new MyCommand { AggregateId = domainObject.Id }; + context = new CommandContext(command); } [Fact] @@ -82,6 +82,12 @@ namespace Squidex.Infrastructure.CQRS.Commands Assert.Equal(repository.Object, sut.Repository); } + [Fact] + public Task Create_async_should_throw_if_not_aggregate_command() + { + return Assert.ThrowsAnyAsync(() => sut.CreateAsync(new CommandContext(new Mock().Object), x => TaskHelper.False)); + } + [Fact] public async Task Create_async_should_create_domain_object_and_save() { @@ -89,21 +95,23 @@ namespace Squidex.Infrastructure.CQRS.Commands .Returns(domainObject) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) + .Returns(TaskHelper.Done) + .Verifiable(); - await sut.CreateAsync(command, x => - { - passedDomainObject = x; + MyDomainObject passedDomainObject = null; - return TaskHelper.Done; - }); + await sut.CreateAsync(context, async x => + { + await Task.Delay(1); - Assert.Equal(domainObject, passedDomainObject); + passedDomainObject = x; }); - factory.VerifyAll(); + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result>()); + + repository.VerifyAll(); } [Fact] @@ -113,95 +121,75 @@ namespace Squidex.Infrastructure.CQRS.Commands .Returns(domainObject) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) + .Returns(TaskHelper.Done) + .Verifiable(); - await sut.CreateAsync(command, x => - { - passedDomainObject = x; - }); + MyDomainObject passedDomainObject = null; - Assert.Equal(domainObject, passedDomainObject); + await sut.CreateAsync(context, x => + { + passedDomainObject = x; }); - factory.VerifyAll(); + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result>()); + + repository.VerifyAll(); + } + + [Fact] + public Task Update_async_should_throw_if_not_aggregate_command() + { + return Assert.ThrowsAnyAsync(() => sut.UpdateAsync(new CommandContext(new Mock().Object), x => TaskHelper.False)); } [Fact] public async Task Update_async_should_create_domain_object_and_save() { - repository.Setup(x => x.GetByIdAsync(command.AggregateId, int.MaxValue)) + repository.Setup(x => x.GetByIdAsync(command.AggregateId, null)) .Returns(Task.FromResult(domainObject)) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) + .Returns(TaskHelper.Done) + .Verifiable(); - await sut.UpdateAsync(command, x => - { - passedDomainObject = x; + MyDomainObject passedDomainObject = null; - return TaskHelper.Done; - }); + await sut.UpdateAsync(context, async x => + { + await Task.Delay(1); - Assert.Equal(domainObject, passedDomainObject); + passedDomainObject = x; }); + + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result()); + + repository.VerifyAll(); } [Fact] public async Task Update_sync_should_create_domain_object_and_save() { - repository.Setup(x => x.GetByIdAsync(command.AggregateId, int.MaxValue)) + repository.Setup(x => x.GetByIdAsync(command.AggregateId, null)) .Returns(Task.FromResult(domainObject)) .Verifiable(); - await TestFlowAsync(async () => - { - MyDomainObject passedDomainObject = null; - - await sut.UpdateAsync(command, x => - { - passedDomainObject = x; - }); - - Assert.Equal(domainObject, passedDomainObject); - }); - } - - private async Task TestFlowAsync(Func action) - { - repository.Setup(x => x.SaveAsync(domainObject, - It.IsAny>>(), - It.IsAny())) - .Returns(TaskHelper.Done) - .Verifiable(); - - processor1.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event1.Payload), domainObject, command)) - .Returns(TaskHelper.Done) - .Verifiable(); - - processor2.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event1.Payload), domainObject, command)) - .Returns(TaskHelper.Done) - .Verifiable(); - - processor1.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event2.Payload), domainObject, command)) + repository.Setup(x => x.SaveAsync(domainObject, It.IsAny>>(), It.IsAny())) .Returns(TaskHelper.Done) .Verifiable(); - processor2.Setup(x => x.ProcessEventAsync( - It.Is>(y => y.Payload == event2.Payload), domainObject, command)) - .Returns(TaskHelper.Done) - .Verifiable(); + MyDomainObject passedDomainObject = null; - await action(); + await sut.UpdateAsync(context, x => + { + passedDomainObject = x; + }); - processor1.VerifyAll(); - processor2.VerifyAll(); + Assert.Equal(domainObject, passedDomainObject); + Assert.NotNull(context.Result()); repository.VerifyAll(); } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs index 189894e93..f98ca3d70 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using Moq; using System; using Xunit; @@ -13,11 +14,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { public class CommandContextTests { - private readonly MyCommand command = new MyCommand(); - - private sealed class MyCommand : ICommand - { - } + private readonly ICommand command = new Mock().Object; [Fact] public void Should_instantiate_and_provide_command() diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectFactoryTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectFactoryTests.cs index e1e579bdb..531a3edef 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectFactoryTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectFactoryTests.cs @@ -17,9 +17,10 @@ namespace Squidex.Infrastructure.CQRS.Commands { public class DefaultDomainObjectFactoryTests { - private sealed class DO : DomainObject + private sealed class DO : DomainObjectBase { - public DO(Guid id, int version) : base(id, version) + public DO(Guid id, int version) + : base(id, version) { } @@ -35,7 +36,7 @@ namespace Squidex.Infrastructure.CQRS.Commands var factoryFunction = new DomainObjectFactoryFunction(passedId => { - return new DO(passedId, 0); + return new DO(passedId, -1); }); serviceProvider.Setup(x => x.GetService(typeof(DomainObjectFactoryFunction))).Returns(factoryFunction); @@ -47,7 +48,24 @@ namespace Squidex.Infrastructure.CQRS.Commands var domainObject = sut.CreateNew(typeof(DO), id); Assert.Equal(id, domainObject.Id); - Assert.Equal(0, domainObject.Version); + Assert.Equal(-1, domainObject.Version); + } + + [Fact] + public void Should_throw_if_new_entity_has_invalid_version() + { + var serviceProvider = new Mock(); + + var factoryFunction = new DomainObjectFactoryFunction(passedId => + { + return new DO(passedId, 0); + }); + + serviceProvider.Setup(x => x.GetService(typeof(DomainObjectFactoryFunction))).Returns(factoryFunction); + + var sut = new DefaultDomainObjectFactory(serviceProvider.Object); + + Assert.Throws(() => sut.CreateNew(typeof(DO), Guid.NewGuid())); } } } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs index 997a3050c..c35e40e2c 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs @@ -14,6 +14,7 @@ using Squidex.Infrastructure.CQRS.Events; using Xunit; using System.Collections.Generic; using System.Linq; +using Squidex.Infrastructure.Tasks; // ReSharper disable ImplicitlyCapturedClosure // ReSharper disable PrivateFieldCanBeConvertedToLocalVariable @@ -46,7 +47,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { } - public sealed class MyDomainObject : DomainObject + public sealed class MyDomainObject : DomainObjectBase { private readonly List appliedEvents = new List(); @@ -89,8 +90,8 @@ namespace Squidex.Infrastructure.CQRS.Commands var events = new[] { - new StoredEvent(0, eventData1), - new StoredEvent(1, eventData2) + new StoredEvent(0, 0, eventData1), + new StoredEvent(1, 1, eventData2) }; eventStore.Setup(x => x.GetEventsAsync(streamName)).Returns(events.ToObservable()); @@ -114,8 +115,8 @@ namespace Squidex.Infrastructure.CQRS.Commands var events = new[] { - new StoredEvent(0, eventData1), - new StoredEvent(1, eventData2) + new StoredEvent(0, 0, eventData1), + new StoredEvent(1, 1, eventData2) }; eventStore.Setup(x => x.GetEventsAsync(streamName)).Returns(events.ToObservable()); @@ -140,8 +141,9 @@ namespace Squidex.Infrastructure.CQRS.Commands eventDataFormatter.Setup(x => x.ToEventData(It.Is>(e => e.Payload == event1), commitId)).Returns(eventData1); eventDataFormatter.Setup(x => x.ToEventData(It.Is>(e => e.Payload == event2), commitId)).Returns(eventData2); - eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, It.Is>(e => e.Count() == 2))) - .Returns(Task.FromResult(true)).Verifiable(); + eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 123, It.Is>(e => e.Count() == 2))) + .Returns(TaskHelper.Done) + .Verifiable(); domainObject.AddEvent(event1); domainObject.AddEvent(event2); @@ -165,8 +167,9 @@ namespace Squidex.Infrastructure.CQRS.Commands eventDataFormatter.Setup(x => x.ToEventData(It.Is>(e => e.Payload == event1), commitId)).Returns(eventData1); eventDataFormatter.Setup(x => x.ToEventData(It.Is>(e => e.Payload == event2), commitId)).Returns(eventData2); - eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 122, new List { eventData1, eventData2 })) - .Throws(new WrongEventVersionException(1, 2)).Verifiable(); + eventStore.Setup(x => x.AppendEventsAsync(commitId, streamName, 123, new List { eventData1, eventData2 })) + .Throws(new WrongEventVersionException(1, 2)) + .Verifiable(); domainObject.AddEvent(event1); domainObject.AddEvent(event2); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs index 4140b90f8..c24c679d4 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/EnrichWithTimestampHandlerTests.cs @@ -15,13 +15,11 @@ namespace Squidex.Infrastructure.CQRS.Commands { public sealed class EnrichWithTimestampHandlerTests { - private sealed class MyNormalCommand : ICommand - { - } - private sealed class MyTimestampCommand : ITimestampCommand { public Instant Timestamp { get; set; } + + public long? ExpectedVersion { get; set; } } private readonly Mock clock = new Mock(); @@ -47,7 +45,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { var sut = new EnrichWithTimestampHandler(clock.Object); - var result = await sut.HandleAsync(new CommandContext(new MyNormalCommand())); + var result = await sut.HandleAsync(new CommandContext(new Mock().Object)); Assert.False(result); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs index 408a4b423..e56468030 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/InMemoryCommandBusTests.cs @@ -8,18 +8,16 @@ using System; using System.Threading.Tasks; +using Moq; +using Squidex.Infrastructure.Tasks; using Xunit; namespace Squidex.Infrastructure.CQRS.Commands { public class InMemoryCommandBusTests { - private readonly MyCommand command = new MyCommand(); - - private sealed class MyCommand : ICommand - { - } - + private readonly ICommand command = new Mock().Object; + private sealed class HandledHandler : ICommandHandler { public ICommand LastCommand; @@ -28,7 +26,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { LastCommand = context.Command; - return Task.FromResult(true); + return TaskHelper.True; } } @@ -40,7 +38,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { LastCommand = context.Command; - return Task.FromResult(false); + return TaskHelper.False; } } @@ -64,7 +62,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { LastException = context.Exception; - return Task.FromResult(false); + return TaskHelper.False; } } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs index 60f3da8c0..e2290acb2 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using Xunit; using System.Collections.Generic; using System.Linq; +using Moq; namespace Squidex.Infrastructure.CQRS.Commands { @@ -19,6 +20,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { private readonly MyLogger logger = new MyLogger(); private readonly LogExceptionHandler sut; + private readonly ICommand command = new Mock().Object; private sealed class MyLogger : ILogger { @@ -40,10 +42,6 @@ namespace Squidex.Infrastructure.CQRS.Commands } } - private sealed class MyCommand : ICommand - { - } - public LogExceptionHandlerTests() { sut = new LogExceptionHandler(logger); @@ -52,7 +50,7 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_do_nothing_if_command_is_succeeded() { - var context = new CommandContext(new MyCommand()); + var context = new CommandContext(command); context.Succeed(); @@ -65,7 +63,7 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_log_if_command_failed() { - var context = new CommandContext(new MyCommand()); + var context = new CommandContext(command); context.Fail(new InvalidOperationException()); @@ -78,8 +76,8 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_log_if_command_is_not_handled() { - var context = new CommandContext(new MyCommand()); - + var context = new CommandContext(command); + var isHandled = await sut.HandleAsync(context); Assert.False(isHandled); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs index c09e6a353..cbf04c40b 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs @@ -9,6 +9,7 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Moq; using Xunit; namespace Squidex.Infrastructure.CQRS.Commands @@ -17,6 +18,7 @@ namespace Squidex.Infrastructure.CQRS.Commands { private readonly MyLogger logger = new MyLogger(); private readonly LogExecutingHandler sut; + private readonly ICommand command = new Mock().Object; private sealed class MyLogger : ILogger { @@ -37,11 +39,7 @@ namespace Squidex.Infrastructure.CQRS.Commands return null; } } - - private sealed class MyCommand : ICommand - { - } - + public LogExecutingHandlerTests() { sut = new LogExecutingHandler(logger); @@ -50,7 +48,7 @@ namespace Squidex.Infrastructure.CQRS.Commands [Fact] public async Task Should_log_once() { - var context = new CommandContext(new MyCommand()); + var context = new CommandContext(command); var isHandled = await sut.HandleAsync(context); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectTest.cs b/tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectBaseTests.cs similarity index 96% rename from tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectTest.cs rename to tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectBaseTests.cs index c2e87e6ce..32c770f19 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectTest.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/DomainObjectBaseTests.cs @@ -1,5 +1,5 @@ // ========================================================================== -// DomainObjectTest.cs +// DomainObjectBaseTests.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -15,13 +15,13 @@ using System.Linq; namespace Squidex.Infrastructure.CQRS { - public class DomainObjectTest + public class DomainObjectBaseTests { private sealed class MyEvent : IEvent { } - private sealed class DO : DomainObject + private sealed class DO : DomainObjectBase { public DO(Guid id, int version) : base(id, version) diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs index 575039533..bc35d66a6 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs @@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.CQRS.Events { private readonly DefaultNameResolver sut = new DefaultNameResolver(); - private sealed class MyUser : DomainObject + private sealed class MyUser : DomainObjectBase { public MyUser(Guid id, int version) : base(id, version) @@ -27,7 +27,7 @@ namespace Squidex.Infrastructure.CQRS.Events } } - private sealed class MyUserDomainObject : DomainObject + private sealed class MyUserDomainObject : DomainObjectBase { public MyUserDomainObject(Guid id, int version) : base(id, version) diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs deleted file mode 100644 index f75c712a3..000000000 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnrichWithAggregateIdProcessorTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// EnrichWithAggregateIdProcessorTests.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.CQRS.Commands; -using Xunit; - -namespace Squidex.Infrastructure.CQRS.Events -{ - public class EnrichWithAggregateIdProcessorTests - { - public sealed class MyAggregateIdCommand : IAggregateCommand - { - public Guid AggregateId { get; set; } - } - - public sealed class MyNormalCommand : ICommand - { - } - - public sealed class MyEvent : IEvent - { - } - - private readonly EnrichWithAggregateIdProcessor sut = new EnrichWithAggregateIdProcessor(); - - [Fact] - public async Task Should_not_do_anything_if_not_aggregate_command() - { - var envelope = new Envelope(new MyEvent()); - - await sut.ProcessEventAsync(envelope, null, new MyNormalCommand()); - - Assert.False(envelope.Headers.Contains("AggregateId")); - } - - [Fact] - public async Task Should_attach_aggregate_to_event_envelope() - { - var aggregateId = Guid.NewGuid(); - var aggregateCommand = new MyAggregateIdCommand { AggregateId = aggregateId }; - - var envelope = new Envelope(new MyEvent()); - - await sut.ProcessEventAsync(envelope, null, aggregateCommand); - - Assert.Equal(aggregateId, envelope.Headers.AggregateId()); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeExtensionsTests.cs similarity index 84% rename from tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs rename to tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeExtensionsTests.cs index 1e20276fa..c66c25fe3 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeExtensionsTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeExtensionsTests.cs @@ -11,7 +11,7 @@ using System.Globalization; using NodaTime; using Xunit; -namespace Squidex.Infrastructure.CQRS +namespace Squidex.Infrastructure.CQRS.Events { public class EnvelopeExtensionsTests { @@ -72,5 +72,16 @@ namespace Squidex.Infrastructure.CQRS Assert.Equal(eventNumber, sut.Headers.EventNumber()); Assert.Equal(eventNumber, sut.Headers["EventNumber"].ToInt32(culture)); } + + [Fact] + public void Should_set_and_get_event_stream_number() + { + const int eventStreamNumber = 123; + + sut.SetEventStreamNumber(eventStreamNumber); + + Assert.Equal(eventStreamNumber, sut.Headers.EventStreamNumber()); + Assert.Equal(eventStreamNumber, sut.Headers["EventStreamNumber"].ToInt32(culture)); + } } } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeHeaderTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeHeaderTests.cs similarity index 97% rename from tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeHeaderTests.cs rename to tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeHeaderTests.cs index c77ad7af8..af879bba2 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/EnvelopeHeaderTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EnvelopeHeaderTests.cs @@ -9,7 +9,7 @@ using System.Linq; using Xunit; -namespace Squidex.Infrastructure.CQRS +namespace Squidex.Infrastructure.CQRS.Events { public class EnvelopeHeaderTests { diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs index a96515f52..e76e34877 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs @@ -42,6 +42,7 @@ namespace Squidex.Infrastructure.CQRS.Events inputEvent.SetCommitId(commitId); inputEvent.SetEventId(Guid.NewGuid()); inputEvent.SetEventNumber(1); + inputEvent.SetEventStreamNumber(1); inputEvent.SetTimestamp(SystemClock.Instance.GetCurrentInstant()); var sut = new EventDataFormatter(typeNameRegistry, serializerSettings); diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs index 4848058b0..08d8aa308 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs @@ -12,6 +12,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Moq; +using Squidex.Infrastructure.Tasks; using Xunit; // ReSharper disable UnusedAutoPropertyAccessor.Local @@ -80,9 +81,9 @@ namespace Squidex.Infrastructure.CQRS.Events { events = new[] { - new StoredEvent(3, eventData1), - new StoredEvent(4, eventData2), - new StoredEvent(4, eventData3) + new StoredEvent(3, 3, eventData1), + new StoredEvent(4, 4, eventData2), + new StoredEvent(4, 4, eventData3) }; consumerName = eventConsumer.Object.GetType().Name; @@ -134,7 +135,7 @@ namespace Squidex.Infrastructure.CQRS.Events { consumerInfo.LastHandledEventNumber = 2L; - eventConsumer.Setup(x => x.On(envelope1)).Returns(Task.FromResult(true)); + eventConsumer.Setup(x => x.On(envelope1)).Returns(TaskHelper.True); eventConsumer.Setup(x => x.On(envelope2)).Throws(new InvalidOperationException()); sut.Subscribe(eventConsumer.Object); diff --git a/tests/Squidex.Infrastructure.Tests/DisposableObjectTests.cs b/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs similarity index 94% rename from tests/Squidex.Infrastructure.Tests/DisposableObjectTests.cs rename to tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs index 2801b87fd..365dc7edb 100644 --- a/tests/Squidex.Infrastructure.Tests/DisposableObjectTests.cs +++ b/tests/Squidex.Infrastructure.Tests/DisposableObjectBaseTests.cs @@ -1,5 +1,5 @@ // ========================================================================== -// DisposableObjectTest.cs +// DisposableObjectBaseTests.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -11,9 +11,9 @@ using Xunit; namespace Squidex.Infrastructure { - public class DisposableObjectTests + public class DisposableObjectBaseTests { - public sealed class MyDisposableObject : DisposableObject + public sealed class MyDisposableObject : DisposableObjectBase { public int DisposeCallCount { get; set; } diff --git a/tests/Squidex.Infrastructure.Tests/Json/ConverterContractResolverTests.cs b/tests/Squidex.Infrastructure.Tests/Json/ConverterContractResolverTests.cs new file mode 100644 index 000000000..ba92a6551 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Json/ConverterContractResolverTests.cs @@ -0,0 +1,84 @@ +// ========================================================================== +// ConverterContractResolverTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; +using NodaTime; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Json +{ + public class ConverterContractResolverTests + { + public class MyClass + { + [JsonConverter(typeof(TodayConverter))] + public Instant MyProperty { get; set; } + } + + public sealed class TodayConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue("TODAY"); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Instant); + } + } + + [Fact] + public void Should_respect_property_converter() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + var serializerSettings = new JsonSerializerSettings + { + ContractResolver = new ConverterContractResolver(new InstantConverter()) + }; + + var json = JsonConvert.SerializeObject(new MyClass { MyProperty = value }, serializerSettings); + + Assert.Equal(@"{""myProperty"":""TODAY""}", json); + } + + [Fact] + public void Should_ignore_other_converters() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + var serializerSettings = new JsonSerializerSettings + { + ContractResolver = new ConverterContractResolver(new InstantConverter()) + }; + + serializerSettings.Converters.Add(new TodayConverter()); + + var result = JsonConvert.SerializeObject(Tuple.Create(value), serializerSettings); + var output = JsonConvert.DeserializeObject>(result, serializerSettings); + + Assert.Equal(value, output.Item1); + } + + [Fact] + public void Should_serialize_and_deserialize_instant() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + value.SerializeAndDeserialize(new ConverterContractResolver(new InstantConverter())); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs b/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs new file mode 100644 index 000000000..59d9492be --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Json/InstantConverterTests.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// InstantConverterTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using NodaTime; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Infrastructure.Json +{ + public sealed class InstantConverterTests + { + [Fact] + public void Should_serialize_and_deserialize() + { + var value = Instant.FromDateTimeUtc(DateTime.UtcNow.Date); + + value.SerializeAndDeserialize(new InstantConverter()); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs index be10d78c4..9160ed344 100644 --- a/tests/Squidex.Infrastructure.Tests/LanguageTests.cs +++ b/tests/Squidex.Infrastructure.Tests/LanguageTests.cs @@ -74,7 +74,7 @@ namespace Squidex.Infrastructure [InlineData("en ", "en")] public void Should_parse_valid_languages(string input, string languageCode) { - var language = Language.TryParse(input); + var language = Language.ParseOrNull(input); Assert.Equal(language, Language.GetLanguage(languageCode)); } @@ -87,7 +87,7 @@ namespace Squidex.Infrastructure [InlineData(null)] public void Should_parse_invalid_languages(string input) { - var language = Language.TryParse(input); + var language = Language.ParseOrNull(input); Assert.Null(language); } diff --git a/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs b/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs index 4f5aed92e..b03254a30 100644 --- a/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs +++ b/tests/Squidex.Infrastructure.Tests/RefTokenTests.cs @@ -89,7 +89,7 @@ namespace Squidex.Infrastructure object token1a = RefToken.Parse("client:client1"); object token1b = RefToken.Parse("client:client1"); object token2a = RefToken.Parse("client:client2"); - + Assert.True(token1a.Equals(token1b)); Assert.False(token1a.Equals(token2a)); diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 5bc0fe25e..0f133eb0f 100644 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -8,12 +8,12 @@ - - - - - - + + + + + + diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs index a1f0799be..d1585a0dc 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/JsonHelper.cs @@ -8,13 +8,28 @@ using System; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; using Xunit; namespace Squidex.Infrastructure.TestHelpers { public static class JsonHelper { - public static void SerializeAndDeserialize(this T value, JsonConverter converter) where T : class + public static void SerializeAndDeserialize(this T value, IContractResolver contractResolver) + { + var serializerSettings = new JsonSerializerSettings + { + ContractResolver = contractResolver, + NullValueHandling = NullValueHandling.Include + }; + + var result = JsonConvert.SerializeObject(Tuple.Create(value), serializerSettings); + var output = JsonConvert.DeserializeObject>(result, serializerSettings); + + Assert.Equal(value, output.Item1); + } + + public static void SerializeAndDeserialize(this T value, JsonConverter converter) { var serializerSettings = new JsonSerializerSettings(); @@ -27,14 +42,14 @@ namespace Squidex.Infrastructure.TestHelpers Assert.Equal(value, output.Item1); } - public static void DoesNotDeserialize(string value, JsonConverter converter) where T : class + public static void DoesNotDeserialize(string value, JsonConverter converter) { var serializerSettings = new JsonSerializerSettings(); serializerSettings.Converters.Add(converter); serializerSettings.NullValueHandling = NullValueHandling.Include; - Assert.Throws(() => JsonConvert.DeserializeObject>($"{{ \"Item1\": \"{value}\" }}")); + Assert.ThrowsAny(() => JsonConvert.DeserializeObject>($"{{ \"Item1\": \"{value}\" }}", serializerSettings)); } } } diff --git a/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs b/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs index 5f4429110..a3f80b572 100644 --- a/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs +++ b/tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs @@ -14,7 +14,6 @@ using Moq; using Squidex.Infrastructure; using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Services.Implementations; -using Squidex.Read.MongoDb.Apps; using Xunit; // ReSharper disable ConvertToConstant.Local @@ -27,14 +26,23 @@ namespace Squidex.Read.Apps private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly Mock repository = new Mock(); private readonly CachingAppProvider sut; - private readonly MongoAppEntity appV1; - private readonly MongoAppEntity appV2; + private readonly IAppEntity appV1; + private readonly IAppEntity appV2; private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); public CachingAppProviderTests() { - appV1 = new MongoAppEntity { Name = appId.Name, Id = appId.Id }; - appV2 = new MongoAppEntity { Name = appId.Name, Id = appId.Id }; + var appV1Mock = new Mock(); + var appV2Mock = new Mock(); + + appV1Mock.Setup(x => x.Id).Returns(appId.Id); + appV1Mock.Setup(x => x.Name).Returns(appId.Name); + + appV2Mock.Setup(x => x.Id).Returns(appId.Id); + appV2Mock.Setup(x => x.Name).Returns(appId.Name); + + appV1 = appV1Mock.Object; + appV2 = appV2Mock.Object; sut = new CachingAppProvider(cache, repository.Object); } @@ -42,7 +50,7 @@ namespace Squidex.Read.Apps [Fact] public async Task Should_also_retrieve_app_by_name_if_retrieved_by_id_before() { - repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(Task.FromResult(appV1)); + repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(Task.FromResult(appV1)); await ProvideAppById(appV1); await ProvideAppByName(appV1); @@ -54,7 +62,7 @@ namespace Squidex.Read.Apps [Fact] public async Task Should_also_retrieve_app_by_id_if_retrieved_by_name_before() { - repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(Task.FromResult(appV1)); + repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(Task.FromResult(appV1)); await ProvideAppByName(appV1); await ProvideAppById(appV1); @@ -68,7 +76,7 @@ namespace Squidex.Read.Apps { var apps = ProviderResults(appV1, appV2); - repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(() => Task.FromResult(apps())); + repository.Setup(x => x.FindAppAsync(appId.Id)).Returns(() => Task.FromResult(apps())); await ProvideAppById(appV1); @@ -84,7 +92,7 @@ namespace Squidex.Read.Apps { var apps = ProviderResults(appV1, appV2); - repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(() => Task.FromResult(apps())); + repository.Setup(x => x.FindAppAsync(appId.Name)).Returns(() => Task.FromResult(apps())); await ProvideAppByName(appV1); diff --git a/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs b/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs index 882ea67b9..dca834f67 100644 --- a/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs +++ b/tests/Squidex.Read.Tests/MongoDb/Contents/ODataQueryTests.cs @@ -9,13 +9,18 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.OData.Edm; using MongoDB.Bson.Serialization; using MongoDB.Driver; using Moq; using Squidex.Core.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.MongoDb; +using Squidex.Read.Contents.Builders; using Squidex.Read.MongoDb.Contents.Visitors; +using Squidex.Read.Schemas; using Xunit; // ReSharper disable SpecifyACultureInStringConversionExplicitly @@ -39,6 +44,7 @@ namespace Squidex.Read.MongoDb.Contents private readonly IBsonSerializerRegistry registry = BsonSerializer.SerializerRegistry; private readonly IBsonSerializer serializer = BsonSerializer.SerializerRegistry.GetSerializer(); + private readonly IEdmModel edmModel; private readonly HashSet languages = new HashSet { Language.EN, @@ -50,10 +56,22 @@ namespace Squidex.Read.MongoDb.Contents InstantSerializer.Register(); } + public ODataQueryTests() + { + var builder = new EdmModelBuilder(new MemoryCache(Options.Create(new MemoryCacheOptions()))); + + var schemaEntity = new Mock(); + schemaEntity.Setup(x => x.Id).Returns(Guid.NewGuid()); + schemaEntity.Setup(x => x.Version).Returns(3); + schemaEntity.Setup(x => x.Schema).Returns(schema); + + edmModel = builder.BuildEdmModel(schemaEntity.Object, languages); + } + [Fact] public void Should_parse_query() { - var parser = schema.ParseQuery(languages, "$filter=data/firstName/de eq 'Sebastian'"); + var parser = edmModel.ParseQuery("$filter=data/firstName/de eq 'Sebastian'"); Assert.NotNull(parser); } @@ -223,7 +241,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_top() { - var parser = schema.ParseQuery(languages, "$top=3"); + var parser = edmModel.ParseQuery("$top=3"); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -234,7 +252,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_max_top_if_larger() { - var parser = schema.ParseQuery(languages, "$top=300"); + var parser = edmModel.ParseQuery("$top=300"); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -245,7 +263,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_default_top() { - var parser = schema.ParseQuery(languages, ""); + var parser = edmModel.ParseQuery(""); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -256,7 +274,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_set_skip() { - var parser = schema.ParseQuery(languages, "$skip=3"); + var parser = edmModel.ParseQuery("$skip=3"); var cursor = new Mock>(); cursor.Object.Skip(parser); @@ -267,7 +285,7 @@ namespace Squidex.Read.MongoDb.Contents [Fact] public void Should_not_set_skip() { - var parser = schema.ParseQuery(languages, ""); + var parser = edmModel.ParseQuery(""); var cursor = new Mock>(); cursor.Object.Take(parser); @@ -282,7 +300,7 @@ namespace Squidex.Read.MongoDb.Contents private string S(string value) { - var parser = schema.ParseQuery(languages, value); + var parser = edmModel.ParseQuery(value); var cursor = new Mock>(); var i = string.Empty; @@ -299,7 +317,7 @@ namespace Squidex.Read.MongoDb.Contents private string F(string value) { - var parser = schema.ParseQuery(languages, value); + var parser = edmModel.ParseQuery(value); var query = FilterBuilder.Build(parser, schema).Render(serializer, registry).ToString(); diff --git a/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs b/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs index 8c1ddb489..a86cb4db7 100644 --- a/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs +++ b/tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs @@ -14,7 +14,6 @@ using Moq; using Squidex.Infrastructure; using Squidex.Read.Schemas.Repositories; using Squidex.Read.Schemas.Services.Implementations; -using Squidex.Read.MongoDb.Schemas; using Xunit; // ReSharper disable ConvertToConstant.Local @@ -27,15 +26,26 @@ namespace Squidex.Read.Schemas private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly Mock repository = new Mock(); private readonly CachingSchemaProvider sut; - private readonly MongoSchemaEntity schemaV1; - private readonly MongoSchemaEntity schemaV2; + private readonly ISchemaEntityWithSchema schemaV1; + private readonly ISchemaEntityWithSchema schemaV2; private readonly NamedId schemaId = new NamedId(Guid.NewGuid(), "my-schema"); private readonly NamedId appId = new NamedId(Guid.NewGuid(), "my-app"); public CachingSchemaProviderTests() { - schemaV1 = new MongoSchemaEntity { Name = schemaId.Name, Id = schemaId.Id, AppId = appId.Id }; - schemaV2 = new MongoSchemaEntity { Name = schemaId.Name, Id = schemaId.Id, AppId = appId.Id }; + var schemaV1Mock = new Mock(); + var schemaV2Mock = new Mock(); + + schemaV1Mock.Setup(x => x.Id).Returns(schemaId.Id); + schemaV1Mock.Setup(x => x.Name).Returns(schemaId.Name); + schemaV1Mock.Setup(x => x.AppId).Returns(appId.Id); + + schemaV2Mock.Setup(x => x.Id).Returns(schemaId.Id); + schemaV2Mock.Setup(x => x.Name).Returns(schemaId.Name); + schemaV2Mock.Setup(x => x.AppId).Returns(appId.Id); + + schemaV1 = schemaV1Mock.Object; + schemaV2 = schemaV2Mock.Object; sut = new CachingSchemaProvider(cache, repository.Object); } @@ -43,7 +53,7 @@ namespace Squidex.Read.Schemas [Fact] public async Task Should_also_retrieve_schema_by_name_if_retrieved_by_id_before() { - repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(Task.FromResult(schemaV1)); + repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(Task.FromResult(schemaV1)); await ProvideSchemaById(schemaV1); await ProvideSchemaByName(schemaV1); @@ -55,7 +65,7 @@ namespace Squidex.Read.Schemas [Fact] public async Task Should_also_retrieve_schema_by_id_if_retrieved_by_name_before() { - repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(Task.FromResult(schemaV1)); + repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(Task.FromResult(schemaV1)); await ProvideSchemaByName(schemaV1); await ProvideSchemaById(schemaV1); @@ -69,7 +79,7 @@ namespace Squidex.Read.Schemas { var schemas = ProviderResults(schemaV1, schemaV2); - repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(() => Task.FromResult(schemas())); + repository.Setup(x => x.FindSchemaAsync(schemaId.Id)).Returns(() => Task.FromResult(schemas())); await ProvideSchemaById(schemaV1); @@ -85,7 +95,7 @@ namespace Squidex.Read.Schemas { var schemas = ProviderResults(schemaV1, schemaV2); - repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(() => Task.FromResult(schemas())); + repository.Setup(x => x.FindSchemaAsync(appId.Id, schemaId.Name)).Returns(() => Task.FromResult(schemas())); await ProvideSchemaByName(schemaV1); diff --git a/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj b/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj index cf8496346..b31514114 100644 --- a/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj +++ b/tests/Squidex.Read.Tests/Squidex.Read.Tests.csproj @@ -12,12 +12,12 @@ - - - - - - + + + + + + diff --git a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs index aed05cd5b..c99271606 100644 --- a/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using FluentAssertions; using Moq; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Read.Apps; using Squidex.Read.Apps.Repositories; using Squidex.Read.Users; @@ -48,7 +49,9 @@ namespace Squidex.Write.Apps { var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId }); - appRepository.Setup(x => x.FindAppAsync(AppName)).Returns(Task.FromResult(new Mock().Object)).Verifiable(); + appRepository.Setup(x => x.FindAppAsync(AppName)) + .Returns(Task.FromResult(new Mock().Object)) + .Verifiable(); await TestCreate(app, async _ => { @@ -63,14 +66,16 @@ namespace Squidex.Write.Apps { var context = CreateContextForCommand(new CreateApp { Name = AppName, AggregateId = AppId }); - appRepository.Setup(x => x.FindAppAsync(AppName)).Returns(Task.FromResult(null)).Verifiable(); + appRepository.Setup(x => x.FindAppAsync(AppName)) + .Returns(Task.FromResult(null)) + .Verifiable(); await TestCreate(app, async _ => { await sut.HandleAsync(context); }); - Assert.Equal(AppId, context.Result()); + Assert.Equal(AppId, context.Result>().IdOrValue); } [Fact] @@ -135,7 +140,9 @@ namespace Squidex.Write.Apps [Fact] public async Task AttachClient_should_update_domain_object() { - keyGenerator.Setup(x => x.GenerateKey()).Returns(clientSecret).Verifiable(); + keyGenerator.Setup(x => x.GenerateKey()) + .Returns(clientSecret) + .Verifiable(); CreateApp(); @@ -148,7 +155,7 @@ namespace Squidex.Write.Apps keyGenerator.VerifyAll(); - context.Result().ShouldBeEquivalentTo(new AppClient(clientName, clientSecret)); + context.Result>().IdOrValue.ShouldBeEquivalentTo(new AppClient(clientName, clientSecret)); } [Fact] diff --git a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs index 9f329362f..2c953a11a 100644 --- a/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Contents/ContentCommandHandlerTests.cs @@ -12,6 +12,7 @@ using Moq; using Squidex.Core.Contents; using Squidex.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Read.Apps; using Squidex.Read.Apps.Services; using Squidex.Read.Schemas; @@ -50,7 +51,7 @@ namespace Squidex.Write.Contents appProvider.Setup(x => x.FindAppByIdAsync(AppId)).Returns(Task.FromResult(appEntity.Object)); schemaEntity.Setup(x => x.Schema).Returns(schema); - schemaProvider.Setup(x => x.FindSchemaByIdAsync(SchemaId)).Returns(Task.FromResult(schemaEntity.Object)); + schemaProvider.Setup(x => x.FindSchemaByIdAsync(SchemaId, true)).Returns(Task.FromResult(schemaEntity.Object)); } [Fact] @@ -74,7 +75,7 @@ namespace Squidex.Write.Contents await sut.HandleAsync(context); }); - Assert.Equal(contentId, context.Result()); + Assert.Equal(data, context.Result>().IdOrValue); } [Fact] diff --git a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs index b1d977fa2..80cd0b6a5 100644 --- a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs @@ -6,10 +6,12 @@ // All rights reserved. // ========================================================================== +using System; using System.Threading.Tasks; using Moq; using Squidex.Core.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Read.Schemas; using Squidex.Read.Schemas.Services; using Squidex.Write.Schemas.Commands; @@ -40,7 +42,9 @@ namespace Squidex.Write.Schemas { var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); - schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)).Returns(Task.FromResult(new Mock().Object)).Verifiable(); + schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)) + .Returns(Task.FromResult(new Mock().Object)) + .Verifiable(); await TestCreate(schema, async _ => { @@ -55,14 +59,16 @@ namespace Squidex.Write.Schemas { var context = CreateContextForCommand(new CreateSchema { Name = SchemaName, SchemaId = SchemaId }); - schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)).Returns(Task.FromResult(null)).Verifiable(); + schemaProvider.Setup(x => x.FindSchemaByNameAsync(AppId, SchemaName)) + .Returns(Task.FromResult(null)) + .Verifiable(); await TestCreate(schema, async _ => { await sut.HandleAsync(context); }); - Assert.Equal(SchemaName, context.Result()); + Assert.Equal(SchemaId, context.Result>().IdOrValue); } [Fact] @@ -130,7 +136,7 @@ namespace Squidex.Write.Schemas await sut.HandleAsync(context); }); - Assert.Equal(1, context.Result()); + Assert.Equal(1, context.Result>().IdOrValue); } [Fact] diff --git a/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj b/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj index b9ef1380c..dd886c00a 100644 --- a/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj +++ b/tests/Squidex.Write.Tests/Squidex.Write.Tests.csproj @@ -5,22 +5,18 @@ $(PackageTargetFallback);dnxcore50 Squidex.Write - - full - True - - - - - - - + + + + + + diff --git a/tests/Squidex.Write.Tests/TestHelpers/AssertHelper.cs b/tests/Squidex.Write.Tests/TestHelpers/AssertHelper.cs index 27dacb7e1..115ff11ea 100644 --- a/tests/Squidex.Write.Tests/TestHelpers/AssertHelper.cs +++ b/tests/Squidex.Write.Tests/TestHelpers/AssertHelper.cs @@ -9,7 +9,6 @@ using System.Collections.Generic; using System.Linq; using FluentAssertions; -using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Write.TestHelpers diff --git a/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs index c594f85e2..bc958663a 100644 --- a/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Write.Tests/TestHelpers/HandlerTestBase.cs @@ -13,9 +13,11 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Commands; +#pragma warning disable IDE0019 + namespace Squidex.Write.TestHelpers { - public abstract class HandlerTestBase where T : DomainObject + public abstract class HandlerTestBase where T : DomainObjectBase { private sealed class MockupHandler : IAggregateHandler { @@ -32,14 +34,14 @@ namespace Squidex.Write.TestHelpers IsUpdated = false; } - public Task CreateAsync(IAggregateCommand command, Func creator) where V : class, IAggregate + public Task CreateAsync(CommandContext context, Func creator) where V : class, IAggregate { IsCreated = true; return creator(domainObject as V); } - public Task UpdateAsync(IAggregateCommand command, Func updater) where V : class, IAggregate + public Task UpdateAsync(CommandContext context, Func updater) where V : class, IAggregate { IsUpdated = true; @@ -146,3 +148,6 @@ namespace Squidex.Write.TestHelpers } } } + + +#pragma warning restore IDE0019 \ No newline at end of file diff --git a/tools/GenerateLanguages/Program.cs b/tools/GenerateLanguages/Program.cs index ef474517c..4d25e559b 100644 --- a/tools/GenerateLanguages/Program.cs +++ b/tools/GenerateLanguages/Program.cs @@ -30,9 +30,13 @@ namespace GenerateLanguages writer.WriteLine("// Copyright (c) Squidex Group"); writer.WriteLine("// All rights reserved."); writer.WriteLine("// =========================================================================="); + writer.WriteLine("// "); + writer.WriteLine(); + writer.WriteLine("using System.CodeDom.Compiler;"); writer.WriteLine(); writer.WriteLine("namespace Squidex.Infrastructure"); writer.WriteLine("{"); + writer.WriteLine(" [GeneratedCode(\"LanguagesGenerator\", \"1.0\")]"); writer.WriteLine(" partial class Language"); writer.WriteLine(" {");