diff --git a/src/Squidex.Core/ContentExtensions.cs b/src/Squidex.Core/ContentExtensions.cs index 3decba2bf..7ad7ac1b7 100644 --- a/src/Squidex.Core/ContentExtensions.cs +++ b/src/Squidex.Core/ContentExtensions.cs @@ -18,9 +18,9 @@ namespace Squidex.Core { public static ContentData Enrich(this ContentData data, Schema schema, HashSet languages) { - var validator = new ContentEnricher(languages, schema); + var enricher = new ContentEnricher(languages, schema); - validator.Enrich(data); + enricher.Enrich(data); return data; } diff --git a/src/Squidex.Core/Contents/ContentData.cs b/src/Squidex.Core/Contents/ContentData.cs index 283dd52a4..a3715ebdd 100644 --- a/src/Squidex.Core/Contents/ContentData.cs +++ b/src/Squidex.Core/Contents/ContentData.cs @@ -111,7 +111,7 @@ namespace Squidex.Core.Contents foreach (var fieldValue in this) { - if (!long.TryParse(fieldValue.Key, out long fieldId) || !schema.Fields.TryGetValue(fieldId, out Field field)) + if (!long.TryParse(fieldValue.Key, out long fieldId) || !schema.FieldsById.TryGetValue(fieldId, out Field field)) { continue; } diff --git a/src/Squidex.Core/Schemas/BooleanFieldProperties.cs b/src/Squidex.Core/Schemas/BooleanFieldProperties.cs index b125e0602..f7ce8c23c 100644 --- a/src/Squidex.Core/Schemas/BooleanFieldProperties.cs +++ b/src/Squidex.Core/Schemas/BooleanFieldProperties.cs @@ -49,7 +49,7 @@ namespace Squidex.Core.Schemas { if (!Editor.IsEnumValue()) { - yield return new ValidationError("Editor ist not a valid value", nameof(Editor)); + yield return new ValidationError("Editor is not a valid value", nameof(Editor)); } } } diff --git a/src/Squidex.Core/Schemas/DateTimeCalculatedDefaultValue.cs b/src/Squidex.Core/Schemas/DateTimeCalculatedDefaultValue.cs new file mode 100644 index 000000000..090985be7 --- /dev/null +++ b/src/Squidex.Core/Schemas/DateTimeCalculatedDefaultValue.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// DateTimeCalculatedDefaultValue.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Core.Schemas +{ + public enum DateTimeCalculatedDefaultValue + { + Now, + Today + } +} diff --git a/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs b/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs index 189911847..3bd74b467 100644 --- a/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs +++ b/src/Squidex.Core/Schemas/DateTimeFieldProperties.cs @@ -6,17 +6,23 @@ // All rights reserved. // ========================================================================== +using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; using NodaTime; using Squidex.Infrastructure; +// ReSharper disable ConvertIfStatementToSwitchStatement +// ReSharper disable RedundantIfElseBlock +// ReSharper disable InvertIf + namespace Squidex.Core.Schemas { [TypeName("DateTime")] public sealed class DateTimeFieldProperties : FieldProperties { private DateTimeFieldEditor editor; + private DateTimeCalculatedDefaultValue? calculatedDefaultValue; private Instant? maxValue; private Instant? minValue; private Instant? defaultValue; @@ -54,6 +60,17 @@ namespace Squidex.Core.Schemas } } + public DateTimeCalculatedDefaultValue? CalculatedDefaultValue + { + get { return calculatedDefaultValue; } + set + { + ThrowIfFrozen(); + + calculatedDefaultValue = value; + } + } + public DateTimeFieldEditor Editor { get { return editor; } @@ -67,14 +84,25 @@ namespace Squidex.Core.Schemas public override JToken GetDefaultValue() { - return DefaultValue?.ToString(); + if (CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Now) + { + return DateTime.UtcNow.ToString("o"); + } + else if (CalculatedDefaultValue == DateTimeCalculatedDefaultValue.Today) + { + return DateTime.UtcNow.Date.ToString("o"); + } + else + { + return DefaultValue?.ToString(); + } } protected override IEnumerable ValidateCore() { if (!Editor.IsEnumValue()) { - yield return new ValidationError("Editor ist not a valid value", nameof(Editor)); + yield return new ValidationError("Editor is not a valid value", nameof(Editor)); } if (MaxValue.HasValue && MinValue.HasValue && MinValue.Value >= MaxValue.Value) @@ -91,6 +119,19 @@ namespace Squidex.Core.Schemas { yield return new ValidationError("Default value must be less than max value", nameof(DefaultValue)); } + + if (CalculatedDefaultValue.HasValue) + { + if (!CalculatedDefaultValue.Value.IsEnumValue()) + { + yield return new ValidationError("Calculated default value is not valid", nameof(CalculatedDefaultValue)); + } + + if (DefaultValue.HasValue) + { + yield return new ValidationError("Calculated default value and default value cannot be used together", nameof(CalculatedDefaultValue), nameof(DefaultValue)); + } + } } } } diff --git a/src/Squidex.Core/Schemas/GeolocationFieldProperties.cs b/src/Squidex.Core/Schemas/GeolocationFieldProperties.cs index ffb4cbca4..324de2aa8 100644 --- a/src/Squidex.Core/Schemas/GeolocationFieldProperties.cs +++ b/src/Squidex.Core/Schemas/GeolocationFieldProperties.cs @@ -37,7 +37,7 @@ namespace Squidex.Core.Schemas { if (!Editor.IsEnumValue()) { - yield return new ValidationError("Editor ist not a valid value", nameof(Editor)); + yield return new ValidationError("Editor is not a valid value", nameof(Editor)); } } } diff --git a/src/Squidex.Core/Schemas/Json/JsonFieldModel.cs b/src/Squidex.Core/Schemas/Json/JsonFieldModel.cs index 8697a94ea..6c7767802 100644 --- a/src/Squidex.Core/Schemas/Json/JsonFieldModel.cs +++ b/src/Squidex.Core/Schemas/Json/JsonFieldModel.cs @@ -12,6 +12,8 @@ namespace Squidex.Core.Schemas.Json { public string Name { get; set; } + public long Id { get; set; } + public bool IsHidden { get; set; } public bool IsDisabled { get; set; } diff --git a/src/Squidex.Core/Schemas/Json/JsonSchemaModel.cs b/src/Squidex.Core/Schemas/Json/JsonSchemaModel.cs index 3df33cfad..55c964c25 100644 --- a/src/Squidex.Core/Schemas/Json/JsonSchemaModel.cs +++ b/src/Squidex.Core/Schemas/Json/JsonSchemaModel.cs @@ -18,6 +18,6 @@ namespace Squidex.Core.Schemas.Json public SchemaProperties Properties { get; set; } - public Dictionary Fields { get; set; } + public List Fields { get; set; } } } \ No newline at end of file diff --git a/src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs b/src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs index 31dd33e94..6af61337a 100644 --- a/src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs +++ b/src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs @@ -6,7 +6,6 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Newtonsoft.Json; @@ -36,18 +35,16 @@ namespace Squidex.Core.Schemas.Json { var model = new JsonSchemaModel { Name = schema.Name, IsPublished = schema.IsPublished, Properties = schema.Properties }; - model.Fields = - schema.Fields - .Select(x => - new KeyValuePair(x.Key, - new JsonFieldModel - { - Name = x.Value.Name, - IsHidden = x.Value.IsHidden, - IsDisabled = x.Value.IsDisabled, - Properties = x.Value.RawProperties - })) - .ToDictionary(x => x.Key, x => x.Value); + model.Fields = + schema.Fields.Select(x => + new JsonFieldModel + { + Id = x.Id, + Name = x.Name, + IsHidden = x.IsHidden, + IsDisabled = x.IsDisabled, + Properties = x.RawProperties + }).ToList(); return JToken.FromObject(model, serializer); } @@ -57,11 +54,9 @@ namespace Squidex.Core.Schemas.Json var model = token.ToObject(serializer); var fields = - model.Fields.Select(kvp => + model.Fields.Select(fieldModel => { - var fieldModel = kvp.Value; - - var field = fieldRegistry.CreateField(kvp.Key, fieldModel.Name, fieldModel.Properties); + var field = fieldRegistry.CreateField(fieldModel.Id, fieldModel.Name, fieldModel.Properties); if (fieldModel.IsDisabled) { @@ -74,7 +69,7 @@ namespace Squidex.Core.Schemas.Json } return field; - }).ToImmutableDictionary(x => x.Id, x => x); + }).ToImmutableList(); var schema = new Schema( diff --git a/src/Squidex.Core/Schemas/NumberFieldProperties.cs b/src/Squidex.Core/Schemas/NumberFieldProperties.cs index 9f2625c34..ea80ef7e3 100644 --- a/src/Squidex.Core/Schemas/NumberFieldProperties.cs +++ b/src/Squidex.Core/Schemas/NumberFieldProperties.cs @@ -86,7 +86,7 @@ namespace Squidex.Core.Schemas { if (!Editor.IsEnumValue()) { - yield return new ValidationError("Editor ist not a valid value", nameof(Editor)); + yield return new ValidationError("Editor is not a valid value", nameof(Editor)); } if ((Editor == NumberFieldEditor.Radio || Editor == NumberFieldEditor.Dropdown) && (AllowedValues == null || AllowedValues.Count == 0)) diff --git a/src/Squidex.Core/Schemas/Schema.cs b/src/Squidex.Core/Schemas/Schema.cs index 9a3ac2b26..08c438d4b 100644 --- a/src/Squidex.Core/Schemas/Schema.cs +++ b/src/Squidex.Core/Schemas/Schema.cs @@ -23,6 +23,7 @@ namespace Squidex.Core.Schemas { private readonly string name; private readonly SchemaProperties properties; + private readonly ImmutableList fields; private readonly ImmutableDictionary fieldsById; private readonly ImmutableDictionary fieldsByName; private readonly bool isPublished; @@ -37,7 +38,12 @@ namespace Squidex.Core.Schemas get { return isPublished; } } - public ImmutableDictionary Fields + public ImmutableList Fields + { + get { return fields; } + } + + public ImmutableDictionary FieldsById { get { return fieldsById; } } @@ -52,17 +58,19 @@ namespace Squidex.Core.Schemas get { return properties; } } - public Schema(string name, bool isPublished, SchemaProperties properties, ImmutableDictionary fields) + public Schema(string name, bool isPublished, SchemaProperties properties, ImmutableList fields) { Guard.NotNull(fields, nameof(fields)); Guard.NotNull(properties, nameof(properties)); Guard.ValidSlug(name, nameof(name)); - fieldsById = fields; - fieldsByName = fields.Values.ToImmutableDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); + fieldsById = fields.ToImmutableDictionary(x => x.Id); + fieldsByName = fields.ToImmutableDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); this.name = name; + this.fields = fields; + this.properties = properties; this.properties.Freeze(); @@ -78,14 +86,14 @@ namespace Squidex.Core.Schemas throw new ValidationException("Cannot create a new schema", error); } - return new Schema(name, false, newProperties, ImmutableDictionary.Empty); + return new Schema(name, false, newProperties, ImmutableList.Empty); } public Schema Update(SchemaProperties newProperties) { Guard.NotNull(newProperties, nameof(newProperties)); - return new Schema(name, isPublished, newProperties, fieldsById); + return new Schema(name, isPublished, newProperties, fields); } public Schema UpdateField(long fieldId, FieldProperties newProperties) @@ -120,7 +128,7 @@ namespace Squidex.Core.Schemas public Schema DeleteField(long fieldId) { - return new Schema(name, isPublished, properties, fieldsById.Remove(fieldId)); + return new Schema(name, isPublished, properties, fields.Where(x => x.Id != fieldId).ToImmutableList()); } public Schema Publish() @@ -130,7 +138,7 @@ namespace Squidex.Core.Schemas throw new DomainException("Schema is already published"); } - return new Schema(name, true, properties, fieldsById); + return new Schema(name, true, properties, fields); } public Schema Unpublish() @@ -140,19 +148,21 @@ namespace Squidex.Core.Schemas throw new DomainException("Schema is not published"); } - return new Schema(name, false, properties, fieldsById); + return new Schema(name, false, properties, fields); } - public Schema AddOrUpdateField(Field field) + public Schema ReorderFields(List ids) { - Guard.NotNull(field, nameof(field)); + Guard.NotNull(ids, nameof(ids)); - if (fieldsById.Values.Any(f => f.Name == field.Name && f.Id != field.Id)) + if (ids.Count != fields.Count || ids.Any(x => !fieldsById.ContainsKey(x))) { - throw new ValidationException($"A field with name '{field.Name}' already exists."); + throw new ArgumentException("Ids must cover all fields.", nameof(ids)); } - return new Schema(name, isPublished, properties, fieldsById.SetItem(field.Id, field)); + var newFields = fields.OrderBy(f => ids.IndexOf(f.Id)).ToImmutableList(); + + return new Schema(name, isPublished, properties, newFields); } public Schema UpdateField(long fieldId, Func updater) @@ -169,6 +179,29 @@ namespace Squidex.Core.Schemas return AddOrUpdateField(newField); } + public Schema AddOrUpdateField(Field field) + { + Guard.NotNull(field, nameof(field)); + + if (fieldsById.Values.Any(f => f.Name == field.Name && f.Id != field.Id)) + { + throw new ValidationException($"A field with name '{field.Name}' already exists."); + } + + ImmutableList newFields; + + if (fieldsById.ContainsKey(field.Id)) + { + newFields = fields.Select(f => f.Id == field.Id ? field : f).ToImmutableList(); + } + else + { + newFields = fields.Add(field); + } + + return new Schema(name, isPublished, properties, newFields); + } + public EdmComplexType BuildEdmType(HashSet languages, Func typeResolver) { Guard.NotEmpty(languages, nameof(languages)); diff --git a/src/Squidex.Core/Schemas/StringFieldProperties.cs b/src/Squidex.Core/Schemas/StringFieldProperties.cs index 41f80867a..2345b3529 100644 --- a/src/Squidex.Core/Schemas/StringFieldProperties.cs +++ b/src/Squidex.Core/Schemas/StringFieldProperties.cs @@ -112,7 +112,7 @@ namespace Squidex.Core.Schemas { if (!Editor.IsEnumValue()) { - yield return new ValidationError("Editor ist not a valid value", nameof(Editor)); + yield return new ValidationError("Editor is not a valid value", nameof(Editor)); } if ((Editor == StringFieldEditor.Radio || Editor == StringFieldEditor.Dropdown) && (AllowedValues == null || AllowedValues.Count == 0)) diff --git a/src/Squidex.Events/Schemas/SchemaFieldsReordered.cs b/src/Squidex.Events/Schemas/SchemaFieldsReordered.cs new file mode 100644 index 000000000..0a89708e8 --- /dev/null +++ b/src/Squidex.Events/Schemas/SchemaFieldsReordered.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// SchemaFieldsReordered.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Events.Schemas +{ + [TypeName("SchemaFieldsReorderedEvent")] + public class SchemaFieldsReordered : SchemaEvent + { + public List FieldIds { get; set; } + } +} diff --git a/src/Squidex.Events/Schemas/SchemaPublished.cs b/src/Squidex.Events/Schemas/SchemaPublished.cs index 77b49a043..052031a58 100644 --- a/src/Squidex.Events/Schemas/SchemaPublished.cs +++ b/src/Squidex.Events/Schemas/SchemaPublished.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Events.Schemas { - [TypeName("SchemaPublished")] + [TypeName("SchemaPublishedEvent")] public class SchemaPublished : SchemaEvent { } diff --git a/src/Squidex.Events/Schemas/SchemaUnpublished.cs b/src/Squidex.Events/Schemas/SchemaUnpublished.cs index d3dc1ac3d..f9682c4da 100644 --- a/src/Squidex.Events/Schemas/SchemaUnpublished.cs +++ b/src/Squidex.Events/Schemas/SchemaUnpublished.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure; namespace Squidex.Events.Schemas { - [TypeName("SchemaUnpublished")] + [TypeName("SchemaUnpublishedEvent")] public class SchemaUnpublished : SchemaEvent { } diff --git a/src/Squidex.Events/Schemas/Utils/SchemaEventDispatcher.cs b/src/Squidex.Events/Schemas/Utils/SchemaEventDispatcher.cs index c66e8caac..a08f724b8 100644 --- a/src/Squidex.Events/Schemas/Utils/SchemaEventDispatcher.cs +++ b/src/Squidex.Events/Schemas/Utils/SchemaEventDispatcher.cs @@ -54,6 +54,11 @@ namespace Squidex.Events.Schemas.Utils return schema.Update(@event.Properties); } + public static Schema Dispatch(SchemaFieldsReordered @event, Schema schema) + { + return schema.ReorderFields(@event.FieldIds); + } + public static Schema Dispatch(FieldDeleted @event, Schema schema) { return schema.DeleteField(@event.FieldId.Id); diff --git a/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs b/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs index 41666b8cf..f5df96cea 100644 --- a/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs +++ b/src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs @@ -25,15 +25,14 @@ namespace Squidex.Infrastructure.Log.Internal outputBuilder.Append(message); outputBuilder.Append("\x1B[39m\x1B[22m"); outputBuilder.AppendLine(); + outputBuilder.Clear(); Console.Error.Write(outputBuilder.ToString()); } else { - Console.Out.Write(outputBuilder.ToString()); + Console.WriteLine(message); } - - outputBuilder.Clear(); } } } diff --git a/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs b/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs index b471cc5a5..41118b1a2 100644 --- a/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs +++ b/src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs @@ -16,13 +16,20 @@ namespace Squidex.Infrastructure.Log.Internal { if (level >= SemanticLogLevel.Error) { - Console.ForegroundColor = ConsoleColor.Red; - Console.Error.WriteLine(message); - Console.ResetColor(); + try + { + Console.ForegroundColor = ConsoleColor.Red; + + Console.Error.WriteLine(message); + } + finally + { + Console.ResetColor(); + } } else { - Console.Out.WriteLine(message); + Console.WriteLine(message); } } } diff --git a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs index c6652b884..809745d82 100644 --- a/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs +++ b/src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs @@ -73,6 +73,11 @@ namespace Squidex.Read.MongoDb.Schemas return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); } + protected Task On(SchemaFieldsReordered @event, EnvelopeHeaders headers) + { + return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); + } + protected Task On(SchemaUpdated @event, EnvelopeHeaders headers) { return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s)); diff --git a/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj b/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj index 6248495aa..836dbff66 100644 --- a/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj +++ b/src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs b/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs index 9c395f9a3..7ec680cd5 100644 --- a/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs +++ b/src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs @@ -37,6 +37,9 @@ namespace Squidex.Read.Schemas AddEventMessage( "unpublished schema {[Name]}"); + AddEventMessage( + "reordered fields of schema {[Name]}"); + AddEventMessage( "added field {[Field]} to schema {[Name]}"); diff --git a/src/Squidex.Write/Schemas/Commands/ReorderFields.cs b/src/Squidex.Write/Schemas/Commands/ReorderFields.cs new file mode 100644 index 000000000..1128aa414 --- /dev/null +++ b/src/Squidex.Write/Schemas/Commands/ReorderFields.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// ReorderFields.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using Squidex.Infrastructure; + +namespace Squidex.Write.Schemas.Commands +{ + public class ReorderFields : SchemaAggregateCommand, IValidatable + { + public List FieldIds { get; set; } + + public void Validate(IList errors) + { + if (FieldIds == null) + { + errors.Add(new ValidationError("Field ids must be specified", nameof(FieldIds))); + } + } + } +} diff --git a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs index d4af27a0f..b9757c423 100644 --- a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs +++ b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs @@ -56,7 +56,7 @@ namespace Squidex.Write.Schemas { s.AddField(command); - context.Succeed(EntityCreatedResult.Create(s.Schema.Fields.Values.First(x => x.Name == command.Name).Id, s.Version)); + context.Succeed(EntityCreatedResult.Create(s.Schema.FieldsById.Values.First(x => x.Name == command.Name).Id, s.Version)); }); } @@ -90,6 +90,11 @@ namespace Squidex.Write.Schemas return handler.UpdateAsync(context, s => s.ShowField(command)); } + protected Task On(ReorderFields command, CommandContext context) + { + return handler.UpdateAsync(context, s => s.Reorder(command)); + } + protected Task On(UpdateSchema command, CommandContext context) { return handler.UpdateAsync(context, s => s.Update(command)); diff --git a/src/Squidex.Write/Schemas/SchemaDomainObject.cs b/src/Squidex.Write/Schemas/SchemaDomainObject.cs index bda7fdab9..c55bb6141 100644 --- a/src/Squidex.Write/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Write/Schemas/SchemaDomainObject.cs @@ -91,6 +91,11 @@ namespace Squidex.Write.Schemas schema = SchemaEventDispatcher.Dispatch(@event, schema); } + protected void On(SchemaFieldsReordered @event) + { + schema = SchemaEventDispatcher.Dispatch(@event, schema); + } + protected void On(SchemaPublished @event) { schema = SchemaEventDispatcher.Dispatch(@event, schema); @@ -112,7 +117,7 @@ namespace Squidex.Write.Schemas VerifyCreatedAndNotDeleted(); - RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId(++totalFields, command.Name) })); + RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId(totalFields + 1, command.Name) })); return this; } @@ -139,6 +144,17 @@ namespace Squidex.Write.Schemas return this; } + public SchemaDomainObject Reorder(ReorderFields command) + { + Guard.Valid(command, nameof(command), () => $"Cannot reorder fields for schema '{Id}'"); + + VerifyCreatedAndNotDeleted(); + + RaiseEvent(SimpleMapper.Map(command, new SchemaFieldsReordered())); + + return this; + } + public SchemaDomainObject Update(UpdateSchema command) { Guard.Valid(command, nameof(command), () => $"Cannot update schema '{Id}'"); @@ -240,7 +256,7 @@ namespace Squidex.Write.Schemas { SimpleMapper.Map(fieldCommand, @event); - if (schema.Fields.TryGetValue(fieldCommand.FieldId, out Field field)) + if (schema.FieldsById.TryGetValue(fieldCommand.FieldId, out Field field)) { @event.FieldId = new NamedId(field.Id, field.Name); } diff --git a/src/Squidex/Config/Domain/StoreMongoDbModule.cs b/src/Squidex/Config/Domain/StoreMongoDbModule.cs index fac1918fb..bfa40ee54 100644 --- a/src/Squidex/Config/Domain/StoreMongoDbModule.cs +++ b/src/Squidex/Config/Domain/StoreMongoDbModule.cs @@ -63,7 +63,7 @@ namespace Squidex.Config.Domain throw new ConfigurationException("Configure the Store MongoDb database with 'store:mongoDb:database'."); } - var contentDatabase = Configuration.GetValue("store:mongoDb:databaseNameContent"); + var contentDatabase = Configuration.GetValue("store:mongoDb:contentDatabase"); if (string.IsNullOrWhiteSpace(contentDatabase)) { diff --git a/src/Squidex/Config/Web/WebUsages.cs b/src/Squidex/Config/Web/WebUsages.cs index b6874df63..5df387b3b 100644 --- a/src/Squidex/Config/Web/WebUsages.cs +++ b/src/Squidex/Config/Web/WebUsages.cs @@ -19,6 +19,11 @@ namespace Squidex.Config.Web { public static class WebUsages { + public static void UseMyCors(this IApplicationBuilder app) + { + app.UseCors(builder => builder.AllowAnyOrigin()); + + } public static void UseMyForwardingRules(this IApplicationBuilder app) { app.UseForwardedHeaders(new ForwardedHeadersOptions diff --git a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs index 90807b329..d403464b1 100644 --- a/src/Squidex/Controllers/Api/Apps/AppClientsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppClientsController.cs @@ -103,7 +103,7 @@ namespace Squidex.Controllers.Api.Apps /// The id of the client that must be updated. /// Client object that needs to be updated. /// - /// 201 => Client key generated. + /// 204 => Client updated. /// 404 => App not found or client not found. /// [HttpPut] @@ -121,8 +121,8 @@ namespace Squidex.Controllers.Api.Apps /// The name of the app. /// The id of the client that must be deleted. /// - /// 404 => App not found or client not found. /// 204 => Client revoked. + /// 404 => App not found or client not found. /// [HttpDelete] [Route("apps/{app}/clients/{clientId}/")] diff --git a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs index bd9133b3e..b39866ced 100644 --- a/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppContributorsController.cs @@ -94,6 +94,7 @@ namespace Squidex.Controllers.Api.Apps /// /// 204 => User removed from app. /// 400 => User is not assigned to the app. + /// 404 => App not found. /// [HttpDelete] [Route("apps/{app}/contributors/{id}/")] diff --git a/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs b/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs index d0c4e0a2c..d399f1447 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs @@ -55,14 +55,12 @@ namespace Squidex.Controllers.Api.Schemas.Models.Converters dto.Fields = new List(); - foreach (var kvp in entity.Schema.Fields) + foreach (var field in entity.Schema.Fields) { - var fieldPropertiesDto = - Factories[kvp.Value.RawProperties.GetType()](kvp.Value.RawProperties); + var fieldPropertiesDto = Factories[field.RawProperties.GetType()](field.RawProperties); + var fieldInstanceDto = SimpleMapper.Map(field, new FieldDto { FieldId = field.Id, Properties = fieldPropertiesDto }); - var fieldDto = SimpleMapper.Map(kvp.Value, new FieldDto { FieldId = kvp.Key, Properties = fieldPropertiesDto }); - - dto.Fields.Add(fieldDto); + dto.Fields.Add(fieldInstanceDto); } return dto; diff --git a/src/Squidex/Controllers/Api/Schemas/Models/DateTimeFieldPropertiesDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/DateTimeFieldPropertiesDto.cs index b327fa5e8..6a53e428d 100644 --- a/src/Squidex/Controllers/Api/Schemas/Models/DateTimeFieldPropertiesDto.cs +++ b/src/Squidex/Controllers/Api/Schemas/Models/DateTimeFieldPropertiesDto.cs @@ -39,6 +39,12 @@ namespace Squidex.Controllers.Api.Schemas.Models [JsonConverter(typeof(StringEnumConverter))] public DateTimeFieldEditor Editor { get; set; } + /// + /// The calculated default value for the field value. + /// + [JsonConverter(typeof(StringEnumConverter))] + public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; } + public override FieldProperties ToProperties() { var result = SimpleMapper.Map(this, new DateTimeFieldProperties()); diff --git a/src/Squidex/Controllers/Api/Schemas/Models/ReorderFieldsDto.cs b/src/Squidex/Controllers/Api/Schemas/Models/ReorderFieldsDto.cs new file mode 100644 index 000000000..b4e2ab89e --- /dev/null +++ b/src/Squidex/Controllers/Api/Schemas/Models/ReorderFieldsDto.cs @@ -0,0 +1,22 @@ +// ========================================================================== +// ReorderFieldsDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Squidex.Controllers.Api.Schemas.Models +{ + public class ReorderFieldsDto + { + /// + /// The field ids in the target order. + /// + [Required] + public List FieldIds { get; set; } + } +} diff --git a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs b/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs index c44c6ba69..432420d00 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs @@ -40,9 +40,9 @@ namespace Squidex.Controllers.Api.Schemas /// The field object that needs to be added to the schema. /// /// 201 => Schema field created. - /// 409 => Schema field name already in use. - /// 404 => App or schema not found. /// 400 => Schema field properties not valid. + /// 404 => Schema or app not found. + /// 409 => Schema field name already in use. /// [HttpPost] [Route("apps/{app}/schemas/{name}/fields/")] @@ -61,6 +61,29 @@ namespace Squidex.Controllers.Api.Schemas return StatusCode(201, response); } + /// + /// Reorders the fields. + /// + /// The name of the app. + /// The name of the schema. + /// The request that contains the field ids. + /// + /// 204 => Schema fields reorderd. + /// 400 => Schema field ids do not cover the fields of the schema. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/fields/ordering")] + [ProducesResponseType(typeof(ErrorDto), 400)] + public async Task PutFieldOrdering(string app, string name, [FromBody] ReorderFields request) + { + var command = new ReorderFields { FieldIds = request.FieldIds }; + + await CommandBus.PublishAsync(command); + + return NoContent(); + } + /// /// Update a schema field. /// @@ -69,10 +92,9 @@ namespace Squidex.Controllers.Api.Schemas /// The id of the field to update. /// The field object that needs to be added to the schema. /// - /// 204 => Schema field created. - /// 409 => Schema field name already in use. - /// 404 => App, schema or field not found. + /// 204 => Schema field updated. /// 400 => Schema field properties not valid. + /// 404 => Schema, field or app not found. /// [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/")] @@ -94,9 +116,9 @@ namespace Squidex.Controllers.Api.Schemas /// The name of the schema. /// The id of the field to hide. /// - /// 400 => Schema field already hidden. /// 204 => Schema field hidden. - /// 404 => App, schema or field not found. + /// 400 => Schema field already hidden. + /// 404 => Schema, field or app not found. /// /// /// A hidden field is not part of the API response, but can still be edited in the portal. @@ -120,9 +142,9 @@ namespace Squidex.Controllers.Api.Schemas /// The name of the schema. /// The id of the field to shows. /// - /// 400 => Schema field already visible. /// 204 => Schema field shown. - /// 404 => App, schema or field not found. + /// 400 => Schema field already visible. + /// 404 => Schema, field or app not found. /// /// /// A hidden field is not part of the API response, but can still be edited in the portal. @@ -146,9 +168,9 @@ namespace Squidex.Controllers.Api.Schemas /// The name of the schema. /// The id of the field to enable. /// - /// 400 => Schema field already enabled. /// 204 => Schema field enabled. - /// 404 => App, schema or field not found. + /// 400 => Schema field already enabled. + /// 404 => Schema, field or app not found. /// /// /// A disabled field cannot not be edited in the squidex portal anymore, @@ -173,9 +195,9 @@ namespace Squidex.Controllers.Api.Schemas /// The name of the schema. /// The id of the field to disable. /// - /// 400 => Schema field already disabled. /// 204 => Schema field disabled. - /// 404 => App, schema or field not found. + /// 400 => Schema field already disabled. + /// 404 => Schema, field or app not found. /// /// /// A disabled field cannot not be edited in the squidex portal anymore, @@ -201,7 +223,7 @@ namespace Squidex.Controllers.Api.Schemas /// The id of the field to disable. /// /// 204 => Schema field deleted. - /// 404 => App, schema or field not found. + /// 404 => Schema, field or app not found. /// [HttpDelete] [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 a1e933331..cf2266fb8 100644 --- a/src/Squidex/Controllers/Api/Schemas/SchemasController.cs +++ b/src/Squidex/Controllers/Api/Schemas/SchemasController.cs @@ -46,6 +46,7 @@ namespace Squidex.Controllers.Api.Schemas /// /// /// 200 => Schemas returned. + /// 404 => App not found. /// [HttpGet] [Route("apps/{app}/schemas/")] @@ -65,7 +66,7 @@ namespace Squidex.Controllers.Api.Schemas /// The name of the schema to retrieve. /// /// 200 => Schema found. - /// 404 => Schema not found. + /// 404 => Schema or app not found. /// [HttpGet] [Route("apps/{app}/schemas/{name}/")] @@ -93,7 +94,7 @@ namespace Squidex.Controllers.Api.Schemas /// The name of the app to create the schema to. /// /// 201 => Schema created. - /// 400 => Schema name is not valid. + /// 400 => Schema name or properties are not valid. /// 409 => Schema name already in use. /// [HttpPost] @@ -117,7 +118,9 @@ namespace Squidex.Controllers.Api.Schemas /// The name of the schema. /// The schema object that needs to updated. /// - /// 204 => Schema updated. + /// 204 => Schema has been updated. + /// 400 => Schema properties are not valid. + /// 404 => Schema or app not found. /// [HttpPut] [Route("apps/{app}/schemas/{name}/")] @@ -136,8 +139,9 @@ namespace Squidex.Controllers.Api.Schemas /// The app where the schema is a part of. /// The name of the schema to publish. /// + /// 204 => Schema has been published. /// 400 => Schema is already published. - /// 204 => Schema has been deleted. + /// 404 => Schema or app not found. /// [HttpPut] [Route("apps/{app}/schemas/{name}/publish")] @@ -155,8 +159,9 @@ namespace Squidex.Controllers.Api.Schemas /// The app where the schema is a part of. /// The name of the schema to unpublish. /// + /// 204 => Schema has been unpublished. /// 400 => Schema is not published. - /// 204 => Schema has been deleted. + /// 404 => Schema or app not found. /// [HttpPut] [Route("apps/{app}/schemas/{name}/unpublish")] @@ -175,6 +180,7 @@ namespace Squidex.Controllers.Api.Schemas /// The name of the schema to delete. /// /// 204 => Schema has been deleted. + /// 404 => Schema or app not found. /// [HttpDelete] [Route("apps/{app}/schemas/{name}/")] diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 4a36a74d5..336c1e2c4 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -35,7 +35,7 @@ - + @@ -59,12 +59,11 @@ - + - diff --git a/src/Squidex/Startup.cs b/src/Squidex/Startup.cs index dd70c9d5f..d38e3fa57 100644 --- a/src/Squidex/Startup.cs +++ b/src/Squidex/Startup.cs @@ -51,7 +51,7 @@ namespace Squidex .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", true, true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true) - .AddEnvironmentVariables("SQUIDEX__"); + .AddEnvironmentVariables(); Configuration = builder.Build(); } @@ -65,6 +65,7 @@ namespace Squidex services.AddMyIdentityServer(); services.AddMyMvc(); + services.AddCors(); services.AddLogging(); services.AddMemoryCache(); services.AddOptions(); @@ -103,6 +104,7 @@ namespace Squidex app.TestExternalSystems(); + app.UseMyCors(); app.UseMyForwardingRules(); MapAndUseIdentity(app); diff --git a/src/Squidex/app/app.module.ts b/src/Squidex/app/app.module.ts index 8f00fce4f..1055e5c57 100644 --- a/src/Squidex/app/app.module.ts +++ b/src/Squidex/app/app.module.ts @@ -8,6 +8,7 @@ import { ApplicationRef, NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { DndModule } from 'ng2-dnd'; import { AppComponent } from './app.component'; @@ -49,6 +50,7 @@ export function configUserReport() { imports: [ BrowserModule, BrowserAnimationsModule, + DndModule.forRoot(), SqxFrameworkModule.forRoot(), SqxSharedModule.forRoot(), SqxShellModule, diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 85f8f9781..815d5f232 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -26,7 +26,7 @@
@@ -48,7 +48,7 @@
diff --git a/src/Squidex/app/features/schemas/module.ts b/src/Squidex/app/features/schemas/module.ts index 5d71b6d99..78557351b 100644 --- a/src/Squidex/app/features/schemas/module.ts +++ b/src/Squidex/app/features/schemas/module.ts @@ -7,6 +7,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; +import { DndModule } from 'ng2-dnd'; import { HelpComponent, @@ -16,6 +17,8 @@ import { SqxSharedModule } from 'shared'; +import { SortedDirective } from './utils/sorted.directive'; + import { FieldComponent, BooleanUIComponent, @@ -74,6 +77,7 @@ const routes: Routes = [ imports: [ SqxFrameworkModule, SqxSharedModule, + DndModule, RouterModule.forChild(routes) ], declarations: [ @@ -92,6 +96,7 @@ const routes: Routes = [ SchemaFormComponent, SchemaPageComponent, SchemasPageComponent, + SortedDirective, StringUIComponent, StringValidationComponent ] 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 eaf21d029..ccc722830 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 @@ -36,8 +36,8 @@
-
-
+
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss index ea9ebaf7b..bd8bc2213 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss @@ -28,4 +28,8 @@ background: transparent; vertical-align: baseline; } +} + +.dnd-sortable-drag { + border: 0; } \ No newline at end of file diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts index 391f96d11..80db5e02a 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts @@ -176,6 +176,18 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit { }); } + public sortFields(fields: FieldDto[]) { + this.updateFields(ImmutableArray.of(fields)); + + this.appName() + .switchMap(app => this.schemasService.putFieldOrdering(app, this.schemaName, fields.map(t => t.fieldId), this.version)).retry(2) + .subscribe(() => { + this.updateFields(ImmutableArray.of(fields)); + }, error => { + this.notifyError(error); + }); + } + public saveField(field: FieldDto, newField: FieldDto) { const request = new UpdateFieldDto(newField.properties); 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 be0594485..e40bdf9ed 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,11 +23,24 @@
-
- +
+
+ -
- +
+ +
+
+ +
+ + +
+ +
\ No newline at end of file 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 3c0803105..1527b47ee 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 @@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core import { FormControl, FormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; -import { NumberFieldPropertiesDto, ValidatorsEx } from 'shared'; +import { DateTimeFieldPropertiesDto, ValidatorsEx } from 'shared'; @Component({ selector: 'sqx-date-time-validation', @@ -22,11 +22,17 @@ export class DateTimeValidationComponent implements OnInit { public editForm: FormGroup; @Input() - public properties: NumberFieldPropertiesDto; + public properties: DateTimeFieldPropertiesDto; + public hideDefaultValues: Observable; public hideDefaultValue: Observable; + public calculatedDefaultValues = ['Now', 'Today']; + public ngOnInit() { + this.editForm.addControl('calculatedDefaultValue', + new FormControl(this.properties.calculatedDefaultValue)); + this.editForm.addControl('maxValue', new FormControl(this.properties.maxValue, [ ValidatorsEx.validDateTime() @@ -42,9 +48,14 @@ export class DateTimeValidationComponent implements OnInit { ValidatorsEx.validDateTime() ])); - this.hideDefaultValue = + this.hideDefaultValues = this.editForm.get('isRequired').valueChanges .startWith(this.properties.isRequired) .map(x => !!x); + + this.hideDefaultValue = + this.editForm.get('calculatedDefaultValue').valueChanges + .startWith(this.properties.calculatedDefaultValue) + .map(x => !!x); } } \ No newline at end of file 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 4e3fd75c2..61d52405c 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 @@ -10,7 +10,7 @@ - + The schema name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{appName}}/{{schemaName | async}}/. diff --git a/src/Squidex/app/features/schemas/utils/sorted.directive.ts b/src/Squidex/app/features/schemas/utils/sorted.directive.ts new file mode 100644 index 000000000..3ccf1c3e5 --- /dev/null +++ b/src/Squidex/app/features/schemas/utils/sorted.directive.ts @@ -0,0 +1,39 @@ + +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Directive, EventEmitter, Output } from '@angular/core'; + +import { + SortableComponent, + SortableContainer, + DragDropSortableService +} from 'ng2-dnd'; + +@Directive({ + selector: '[sorted]' +}) +export class SortedDirective { + @Output() + public sorted = new EventEmitter>(); + + constructor( + sortableComponent: SortableComponent, + sortableContainer: SortableContainer, + sortableDragDropService: DragDropSortableService + ) { + const oldCallback = sortableComponent._onDropCallback.bind(sortableComponent); + + sortableComponent._onDropCallback = (event: Event) => { + oldCallback(event); + + if (sortableDragDropService.isDragged) { + this.sorted.emit(sortableContainer.sortableData); + } + }; + } +} 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 af1a59b0f..1e85110c7 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 @@ -28,7 +28,7 @@
- +
diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html index de510b727..6ad89bc72 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html @@ -53,7 +53,7 @@ diff --git a/src/Squidex/app/framework/angular/lowercase-input.directive.ts b/src/Squidex/app/framework/angular/lowercase-input.directive.ts new file mode 100644 index 000000000..e510ba12f --- /dev/null +++ b/src/Squidex/app/framework/angular/lowercase-input.directive.ts @@ -0,0 +1,62 @@ +/* + * 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_LOWERCASE_INPUT_VALUE_ACCESSOR: any = { + provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => LowerCaseInputDirective), multi: true +}; + +@Directive({ + selector: '[sqxLowerCaseInput]', + providers: [SQX_LOWERCASE_INPUT_VALUE_ACCESSOR], + host: { + '(input)': 'onChange($event.target.value)', '(blur)': 'onTouched()' + } +}) +export class LowerCaseInputDirective 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) { + const normalizedValue = (value == null ? '' : value.toString()).toLowerCase(); + + this.renderer.setElementProperty(this.elementRef.nativeElement, 'value', normalizedValue); + } + + 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) { + const normalizedValue = (value == null ? '' : value.toString()).toLowerCase(); + + this.renderer.setElementProperty(this.elementRef.nativeElement, 'value', normalizedValue); + this.changeCallback(normalizedValue); + } + + public onTouched() { + 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 67afb631d..411e7a496 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -21,6 +21,7 @@ export * from './angular/hide-invalid-image.directive'; export * from './angular/http-utils'; export * from './angular/indeterminate-value.directive'; export * from './angular/json-editor.component'; +export * from './angular/lowercase-input.directive'; export * from './angular/markdown-editor.component'; export * from './angular/modal-view.directive'; export * from './angular/money.pipe'; diff --git a/src/Squidex/app/framework/module.ts b/src/Squidex/app/framework/module.ts index 6d841759c..ce8621350 100644 --- a/src/Squidex/app/framework/module.ts +++ b/src/Squidex/app/framework/module.ts @@ -31,6 +31,7 @@ import { IndeterminateValueDirective, JsonEditorComponent, LocalStoreService, + LowerCaseInputDirective, MarkdownEditorComponent, MessageBus, ModalViewDirective, @@ -82,6 +83,7 @@ import { HideInvalidImage, IndeterminateValueDirective, JsonEditorComponent, + LowerCaseInputDirective, MarkdownEditorComponent, ModalViewDirective, MoneyPipe, @@ -118,6 +120,7 @@ import { HideInvalidImage, IndeterminateValueDirective, JsonEditorComponent, + LowerCaseInputDirective, MarkdownEditorComponent, ModalViewDirective, MoneyPipe, diff --git a/src/Squidex/app/framework/utils/immutable-array.spec.ts b/src/Squidex/app/framework/utils/immutable-array.spec.ts index b9e57db0c..d447e9d8b 100644 --- a/src/Squidex/app/framework/utils/immutable-array.spec.ts +++ b/src/Squidex/app/framework/utils/immutable-array.spec.ts @@ -154,6 +154,12 @@ describe('ImmutableArray', () => { expect(array_2.values).toEqual([1, 2, 3, 4]); }); + it('should provide mutable values', () => { + const array_1 = ImmutableArray.of([3, 1, 4, 2]); + + expect(array_1.mutableValues).toBe(array_1.mutableValues); + }); + it('should iterate over array items', () => { const array_1 = ImmutableArray.of([3, 1, 4, 2]); diff --git a/src/Squidex/app/framework/utils/immutable-array.ts b/src/Squidex/app/framework/utils/immutable-array.ts index 769553803..1a94449ab 100644 --- a/src/Squidex/app/framework/utils/immutable-array.ts +++ b/src/Squidex/app/framework/utils/immutable-array.ts @@ -23,6 +23,10 @@ export class ImmutableArray implements Iterable { return [...this.items]; } + public get mutableValues(): T[] { + return this.items; + } + private constructor(items: T[]) { this.items = items; } diff --git a/src/Squidex/app/shared/components/app-form.component.html b/src/Squidex/app/shared/components/app-form.component.html index 77c9a57e3..fcdd9b023 100644 --- a/src/Squidex/app/shared/components/app-form.component.html +++ b/src/Squidex/app/shared/components/app-form.component.html @@ -10,7 +10,7 @@ - + The app name becomes part of the api url,
e.g {{apiUrl.buildUrl("api/content/")}}{{appName | async}}/. diff --git a/src/Squidex/app/shared/services/schemas.service.spec.ts b/src/Squidex/app/shared/services/schemas.service.spec.ts index e7f9c0952..a1fc75160 100644 --- a/src/Squidex/app/shared/services/schemas.service.spec.ts +++ b/src/Squidex/app/shared/services/schemas.service.spec.ts @@ -273,6 +273,22 @@ describe('SchemasService', () => { authService.verifyAll(); }); + it('should make put request to update field ordering', () => { + const dto = [1, 2, 3] + + authService.setup(x => x.authPut('http://service/p/api/apps/my-app/schemas/my-schema/fields/ordering', It.isAny(), version)) + .returns(() => Observable.of( + new Response( + new ResponseOptions() + ) + )) + .verifiable(Times.once()); + + schemasService.putFieldOrdering('my-app', 'my-schema', 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(), version)) .returns(() => Observable.of( diff --git a/src/Squidex/app/shared/services/schemas.service.ts b/src/Squidex/app/shared/services/schemas.service.ts index d51561ee8..79a9a4f7e 100644 --- a/src/Squidex/app/shared/services/schemas.service.ts +++ b/src/Squidex/app/shared/services/schemas.service.ts @@ -160,7 +160,8 @@ export class DateTimeFieldPropertiesDto extends FieldPropertiesDto { public readonly editor: string, public readonly defaultValue?: string, public readonly maxValue?: string, - public readonly minValue?: string + public readonly minValue?: string, + public readonly calculatedDefaultValue?: string ) { super('DateTime', label, hints, placeholder, isRequired, isListField, isLocalizable); } @@ -326,6 +327,13 @@ export class SchemasService { .catchError('Failed to update schema. Please reload.'); } + public putFieldOrdering(appName: string, schemaName: string, dto: number[], version: Version): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/fields/ordering`); + + return this.authService.authPut(url, { fieldIds: dto }, version) + .catchError('Failed to reorder fields. Please reload.'); + } + public publishSchema(appName: string, schemaName: string, version: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`); diff --git a/src/Squidex/app/shell/pages/internal/apps-menu.component.scss b/src/Squidex/app/shell/pages/internal/apps-menu.component.scss index ebbca3da0..ec59b86ca 100644 --- a/src/Squidex/app/shell/pages/internal/apps-menu.component.scss +++ b/src/Squidex/app/shell/pages/internal/apps-menu.component.scss @@ -52,11 +52,11 @@ &-pill { @include absolute(.8rem, .625rem, auto, auto); @include border-radius(8px); - padding: 0 .6rem; + padding: 0 .4rem; background: $color-theme-blue-lightest; border: 0; - line-height: 1.5rem; - font-size: .9rem; + line-height: 1.2rem; + font-size: .8rem; font-weight: normal; color: $color-theme-blue; } diff --git a/src/Squidex/app/theme/theme.scss b/src/Squidex/app/theme/theme.scss index 1eca554d7..55e4f8af4 100644 --- a/src/Squidex/app/theme/theme.scss +++ b/src/Squidex/app/theme/theme.scss @@ -6,6 +6,9 @@ // Pikaday @import './../../node_modules/pikaday/css/pikaday.css'; +// Drag and Drop +@import './../../node_modules/ng2-dnd/bundles/style.css'; + // Bootstrap Overrides @import '_bootstrap.scss'; diff --git a/src/Squidex/package.json b/src/Squidex/package.json index a93710ba9..98c663cac 100644 --- a/src/Squidex/package.json +++ b/src/Squidex/package.json @@ -29,6 +29,7 @@ "core-js": "2.4.1", "moment": "2.18.1", "mousetrap": "1.6.1", + "ng2-dnd": "^4.0.2", "oidc-client": "1.3.0", "pikaday": "1.5.1", "redoc": "1.12.1", diff --git a/tests/Squidex.Core.Tests/Schemas/BooleanFieldPropertiesTests.cs b/tests/Squidex.Core.Tests/Schemas/BooleanFieldPropertiesTests.cs index 25371a717..80506d4ea 100644 --- a/tests/Squidex.Core.Tests/Schemas/BooleanFieldPropertiesTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/BooleanFieldPropertiesTests.cs @@ -30,7 +30,7 @@ namespace Squidex.Core.Schemas errors.ShouldBeEquivalentTo( new List { - new ValidationError("Editor ist not a valid value", "Editor") + new ValidationError("Editor is not a valid value", "Editor") }); } diff --git a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs index 365b65bb5..ab8c15b1a 100644 --- a/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs @@ -88,10 +88,62 @@ namespace Squidex.Core.Schemas errors.ShouldBeEquivalentTo( new List { - new ValidationError("Editor ist not a valid value", "Editor") + new ValidationError("Editor is not a valid value", "Editor") }); } + [Fact] + public void Should_add_error_if_calculated_default_value_is_not_valid() + { + var sut = new DateTimeFieldProperties { CalculatedDefaultValue = (DateTimeCalculatedDefaultValue)123 }; + + sut.Validate(errors); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Calculated default value is not valid", "CalculatedDefaultValue") + }); + } + + [Fact] + public void Should_add_error_if_calculated_default_value_default_value_is_defined() + { + var sut = new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now, DefaultValue = FutureDays(10) }; + + sut.Validate(errors); + + errors.ShouldBeEquivalentTo( + new List + { + new ValidationError("Calculated default value and default value cannot be used together", "CalculatedDefaultValue", "DefaultValue") + }); + } + + [Fact] + public void Should_provide_today_default_value() + { + var sut = new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Today }; + + Assert.Equal(DateTime.UtcNow.Date.ToString("o"), sut.GetDefaultValue().ToString()); + } + + [Fact] + public void Should_provide_now_default_value() + { + var sut = new DateTimeFieldProperties { CalculatedDefaultValue = DateTimeCalculatedDefaultValue.Now }; + + Assert.Equal(DateTime.UtcNow.ToString("o").Substring(0, 16), sut.GetDefaultValue().ToString().Substring(0, 16)); + } + + [Fact] + public void Should_provide_specific_default_value() + { + var sut = new DateTimeFieldProperties { DefaultValue = FutureDays(15) }; + + Assert.Equal(FutureDays(15).ToString(), sut.GetDefaultValue()); + } + [Fact] public void Should_set_or_freeze_sut() { diff --git a/tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs b/tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs index d858f704e..a8c96ae83 100644 --- a/tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs @@ -30,7 +30,7 @@ namespace Squidex.Core.Schemas errors.ShouldBeEquivalentTo( new List { - new ValidationError("Editor ist not a valid value", "Editor") + new ValidationError("Editor is not a valid value", "Editor") }); } diff --git a/tests/Squidex.Core.Tests/Schemas/NumberFieldPropertiesTests.cs b/tests/Squidex.Core.Tests/Schemas/NumberFieldPropertiesTests.cs index e9199d4a3..bc56e3514 100644 --- a/tests/Squidex.Core.Tests/Schemas/NumberFieldPropertiesTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/NumberFieldPropertiesTests.cs @@ -130,7 +130,7 @@ namespace Squidex.Core.Schemas errors.ShouldBeEquivalentTo( new List { - new ValidationError("Editor ist not a valid value", "Editor") + new ValidationError("Editor is not a valid value", "Editor") }); } diff --git a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs index c2dfc7edb..35b8c8242 100644 --- a/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/SchemaTests.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using Newtonsoft.Json.Linq; using NJsonSchema; using Squidex.Infrastructure; @@ -65,7 +66,7 @@ namespace Squidex.Core.Schemas { var field = AddField(); - Assert.Equal(field, sut.Fields[1]); + Assert.Equal(field, sut.FieldsById[1]); } [Fact] @@ -84,7 +85,7 @@ namespace Squidex.Core.Schemas sut = sut.HideField(1); sut = sut.HideField(1); - Assert.True(sut.Fields[1].IsHidden); + Assert.True(sut.FieldsById[1].IsHidden); } [Fact] @@ -102,7 +103,7 @@ namespace Squidex.Core.Schemas sut = sut.ShowField(1); sut = sut.ShowField(1); - Assert.False(sut.Fields[1].IsHidden); + Assert.False(sut.FieldsById[1].IsHidden); } [Fact] @@ -119,7 +120,7 @@ namespace Squidex.Core.Schemas sut = sut.DisableField(1); sut = sut.DisableField(1); - Assert.True(sut.Fields[1].IsDisabled); + Assert.True(sut.FieldsById[1].IsDisabled); } [Fact] @@ -137,7 +138,7 @@ namespace Squidex.Core.Schemas sut = sut.EnableField(1); sut = sut.EnableField(1); - Assert.False(sut.Fields[1].IsDisabled); + Assert.False(sut.FieldsById[1].IsDisabled); } [Fact] @@ -153,7 +154,7 @@ namespace Squidex.Core.Schemas sut = sut.RenameField(1, "new-name"); - Assert.Equal("new-name", sut.Fields[1].Name); + Assert.Equal("new-name", sut.FieldsById[1].Name); } [Fact] @@ -187,7 +188,7 @@ namespace Squidex.Core.Schemas sut = sut.DeleteField(1); - Assert.Equal(0, sut.Fields.Count); + Assert.Equal(0, sut.FieldsById.Count); } [Fact] @@ -203,7 +204,7 @@ namespace Squidex.Core.Schemas sut = sut.UpdateField(1, new NumberFieldProperties { Hints = "my-hints" }); - Assert.Equal("my-hints", sut.Fields[1].RawProperties.Hints); + Assert.Equal("my-hints", sut.FieldsById[1].RawProperties.Hints); } [Fact] @@ -251,6 +252,45 @@ namespace Squidex.Core.Schemas Assert.Throws(() => sut.Unpublish()); } + [Fact] + public void Should_reorder_fields() + { + var field1 = new StringField(1, "1", new StringFieldProperties()); + var field2 = new StringField(2, "2", new StringFieldProperties()); + var field3 = new StringField(3, "3", new StringFieldProperties()); + + sut = sut.AddOrUpdateField(field1); + sut = sut.AddOrUpdateField(field2); + sut = sut.AddOrUpdateField(field3); + sut = sut.ReorderFields(new List { 3, 2, 1 }); + + Assert.Equal(new List { field3, field2, field1 }, sut.Fields.ToList()); + } + + [Fact] + public void Should_throw_if_not_all_fields_are_covered_for_reordering() + { + var field1 = new StringField(1, "1", new StringFieldProperties()); + var field2 = new StringField(2, "2", new StringFieldProperties()); + + sut = sut.AddOrUpdateField(field1); + sut = sut.AddOrUpdateField(field2); + + Assert.Throws(() => sut.ReorderFields(new List { 1 })); + } + + [Fact] + public void Should_throw_if_field_to_reorder_does_not_exist() + { + var field1 = new StringField(1, "1", new StringFieldProperties()); + var field2 = new StringField(2, "2", new StringFieldProperties()); + + sut = sut.AddOrUpdateField(field1); + sut = sut.AddOrUpdateField(field2); + + Assert.Throws(() => sut.ReorderFields(new List { 1, 4 })); + } + [Fact] public void Should_build_schema() { diff --git a/tests/Squidex.Core.Tests/Schemas/StringFieldPropertiesTests.cs b/tests/Squidex.Core.Tests/Schemas/StringFieldPropertiesTests.cs index 4af87e959..fc1600ade 100644 --- a/tests/Squidex.Core.Tests/Schemas/StringFieldPropertiesTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/StringFieldPropertiesTests.cs @@ -87,7 +87,7 @@ namespace Squidex.Core.Schemas errors.ShouldBeEquivalentTo( new List { - new ValidationError("Editor ist not a valid value", "Editor") + new ValidationError("Editor is not a valid value", "Editor") }); } diff --git a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs index c6e4bfe10..6f69fa45c 100644 --- a/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs +++ b/tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs @@ -7,6 +7,7 @@ // ========================================================================== using System; +using System.Collections.Generic; using System.Threading.Tasks; using Moq; using Squidex.Core.Schemas; @@ -84,6 +85,19 @@ namespace Squidex.Write.Schemas }); } + [Fact] + public async Task ReorderSchema_should_update_domain_object() + { + CreateSchema(); + + var context = CreateContextForCommand(new ReorderFields { FieldIds = new List() }); + + await TestUpdate(schema, async _ => + { + await sut.HandleAsync(context); + }); + } + [Fact] public async Task PublishSchema_should_update_domain_object() { diff --git a/tests/Squidex.Write.Tests/Schemas/SchemaDomainObjectTests.cs b/tests/Squidex.Write.Tests/Schemas/SchemaDomainObjectTests.cs index 022ade096..69c8e3f9f 100644 --- a/tests/Squidex.Write.Tests/Schemas/SchemaDomainObjectTests.cs +++ b/tests/Squidex.Write.Tests/Schemas/SchemaDomainObjectTests.cs @@ -6,6 +6,7 @@ // All rights reserved. // ========================================================================== +using System.Collections.Generic; using System.Linq; using Squidex.Core.Schemas; using Squidex.Events.Schemas; @@ -68,6 +69,7 @@ namespace Squidex.Write.Schemas CreateEvent(new SchemaCreated { Name = SchemaName, Properties = properties }) ); } + [Fact] public void Update_should_throw_if_not_created() { @@ -83,9 +85,9 @@ namespace Squidex.Write.Schemas CreateSchema(); DeleteSchema(); - Assert.Throws(() => + Assert.Throws(() => { - sut.Update(CreateCommand(new UpdateSchema())); + sut.Update(CreateCommand(new UpdateSchema { Properties = new SchemaProperties() })); }); } @@ -117,6 +119,58 @@ namespace Squidex.Write.Schemas ); } + [Fact] + public void Reorder_should_throw_if_not_created() + { + Assert.Throws(() => + { + sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List() })); + }); + } + + [Fact] + public void Reorder_should_throw_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => + { + sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List() })); + }); + } + + [Fact] + public void Reorder_should_throw_if_command_is_not_valid() + { + CreateSchema(); + + Assert.Throws(() => + { + sut.Reorder(CreateCommand(new ReorderFields())); + }); + } + + [Fact] + public void Reorder_should_refresh_properties_and_create_events() + { + var fieldIds = new List { 1, 2 }; + + CreateSchema(); + + sut.AddField(new AddField { Name = "field1", Properties = new StringFieldProperties() }); + sut.AddField(new AddField { Name = "field2", Properties = new StringFieldProperties() }); + + ((IAggregate)sut).ClearUncommittedEvents(); + + sut.Reorder(CreateCommand(new ReorderFields { FieldIds = fieldIds })); + + sut.GetUncomittedEvents() + .ShouldHaveSameEvents( + CreateEvent(new SchemaFieldsReordered { FieldIds = fieldIds }) + ); + } + [Fact] public void Publish_should_throw_if_not_created() { @@ -265,7 +319,7 @@ namespace Squidex.Write.Schemas sut.AddField(CreateCommand(new AddField { Name = fieldName, Properties = properties })); - Assert.Equal(properties, sut.Schema.Fields[1].RawProperties); + Assert.Equal(properties, sut.Schema.FieldsById[1].RawProperties); sut.GetUncomittedEvents() .ShouldHaveSameEvents( @@ -324,7 +378,7 @@ namespace Squidex.Write.Schemas sut.UpdateField(CreateCommand(new UpdateField { FieldId = 1, Properties = properties })); - Assert.Equal(properties, sut.Schema.Fields[1].RawProperties); + Assert.Equal(properties, sut.Schema.FieldsById[1].RawProperties); sut.GetUncomittedEvents() .ShouldHaveSameEvents( @@ -372,7 +426,7 @@ namespace Squidex.Write.Schemas sut.HideField(CreateCommand(new HideField { FieldId = 1 })); - Assert.True(sut.Schema.Fields[1].IsHidden); + Assert.True(sut.Schema.FieldsById[1].IsHidden); sut.GetUncomittedEvents() .ShouldHaveSameEvents( @@ -421,7 +475,7 @@ namespace Squidex.Write.Schemas sut.HideField(CreateCommand(new HideField { FieldId = 1 })); sut.ShowField(CreateCommand(new ShowField { FieldId = 1 })); - Assert.False(sut.Schema.Fields[1].IsHidden); + Assert.False(sut.Schema.FieldsById[1].IsHidden); sut.GetUncomittedEvents().Skip(1) .ShouldHaveSameEvents( @@ -469,7 +523,7 @@ namespace Squidex.Write.Schemas sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); - Assert.True(sut.Schema.Fields[1].IsDisabled); + Assert.True(sut.Schema.FieldsById[1].IsDisabled); sut.GetUncomittedEvents() .ShouldHaveSameEvents( @@ -518,7 +572,7 @@ namespace Squidex.Write.Schemas sut.DisableField(CreateCommand(new DisableField { FieldId = 1 })); sut.EnableField(CreateCommand(new EnableField { FieldId = 1 })); - Assert.False(sut.Schema.Fields[1].IsDisabled); + Assert.False(sut.Schema.FieldsById[1].IsDisabled); sut.GetUncomittedEvents().Skip(1) .ShouldHaveSameEvents( @@ -555,7 +609,7 @@ namespace Squidex.Write.Schemas sut.DeleteField(CreateCommand(new DeleteField { FieldId = 1 })); - Assert.False(sut.Schema.Fields.ContainsKey(1)); + Assert.False(sut.Schema.FieldsById.ContainsKey(1)); sut.GetUncomittedEvents() .ShouldHaveSameEvents(