Browse Source

Merge branch 'master' into feature-asset-management

# Conflicts:
#	src/Squidex/Squidex.csproj
pull/65/head
Sebastian Stehle 9 years ago
parent
commit
29c46387e6
  1. 4
      src/Squidex.Core/ContentExtensions.cs
  2. 2
      src/Squidex.Core/Contents/ContentData.cs
  3. 2
      src/Squidex.Core/Schemas/BooleanFieldProperties.cs
  4. 16
      src/Squidex.Core/Schemas/DateTimeCalculatedDefaultValue.cs
  5. 45
      src/Squidex.Core/Schemas/DateTimeFieldProperties.cs
  6. 2
      src/Squidex.Core/Schemas/GeolocationFieldProperties.cs
  7. 2
      src/Squidex.Core/Schemas/Json/JsonFieldModel.cs
  8. 2
      src/Squidex.Core/Schemas/Json/JsonSchemaModel.cs
  9. 31
      src/Squidex.Core/Schemas/Json/SchemaJsonSerializer.cs
  10. 2
      src/Squidex.Core/Schemas/NumberFieldProperties.cs
  11. 61
      src/Squidex.Core/Schemas/Schema.cs
  12. 2
      src/Squidex.Core/Schemas/StringFieldProperties.cs
  13. 19
      src/Squidex.Events/Schemas/SchemaFieldsReordered.cs
  14. 2
      src/Squidex.Events/Schemas/SchemaPublished.cs
  15. 2
      src/Squidex.Events/Schemas/SchemaUnpublished.cs
  16. 5
      src/Squidex.Events/Schemas/Utils/SchemaEventDispatcher.cs
  17. 5
      src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs
  18. 15
      src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs
  19. 5
      src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs
  20. 2
      src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj
  21. 3
      src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs
  22. 26
      src/Squidex.Write/Schemas/Commands/ReorderFields.cs
  23. 7
      src/Squidex.Write/Schemas/SchemaCommandHandler.cs
  24. 20
      src/Squidex.Write/Schemas/SchemaDomainObject.cs
  25. 2
      src/Squidex/Config/Domain/StoreMongoDbModule.cs
  26. 5
      src/Squidex/Config/Web/WebUsages.cs
  27. 4
      src/Squidex/Controllers/Api/Apps/AppClientsController.cs
  28. 1
      src/Squidex/Controllers/Api/Apps/AppContributorsController.cs
  29. 10
      src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs
  30. 6
      src/Squidex/Controllers/Api/Schemas/Models/DateTimeFieldPropertiesDto.cs
  31. 22
      src/Squidex/Controllers/Api/Schemas/Models/ReorderFieldsDto.cs
  32. 50
      src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs
  33. 16
      src/Squidex/Controllers/Api/Schemas/SchemasController.cs
  34. 5
      src/Squidex/Squidex.csproj
  35. 4
      src/Squidex/Startup.cs
  36. 2
      src/Squidex/app/app.module.ts
  37. 4
      src/Squidex/app/features/content/pages/content/content-field.component.html
  38. 5
      src/Squidex/app/features/schemas/module.ts
  39. 6
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  40. 4
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.scss
  41. 12
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  42. 21
      src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.html
  43. 17
      src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts
  44. 2
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html
  45. 39
      src/Squidex/app/features/schemas/utils/sorted.directive.ts
  46. 2
      src/Squidex/app/features/settings/pages/clients/clients-page.component.html
  47. 2
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  48. 62
      src/Squidex/app/framework/angular/lowercase-input.directive.ts
  49. 1
      src/Squidex/app/framework/declarations.ts
  50. 3
      src/Squidex/app/framework/module.ts
  51. 6
      src/Squidex/app/framework/utils/immutable-array.spec.ts
  52. 4
      src/Squidex/app/framework/utils/immutable-array.ts
  53. 2
      src/Squidex/app/shared/components/app-form.component.html
  54. 16
      src/Squidex/app/shared/services/schemas.service.spec.ts
  55. 10
      src/Squidex/app/shared/services/schemas.service.ts
  56. 6
      src/Squidex/app/shell/pages/internal/apps-menu.component.scss
  57. 3
      src/Squidex/app/theme/theme.scss
  58. 1
      src/Squidex/package.json
  59. 2
      tests/Squidex.Core.Tests/Schemas/BooleanFieldPropertiesTests.cs
  60. 54
      tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs
  61. 2
      tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs
  62. 2
      tests/Squidex.Core.Tests/Schemas/NumberFieldPropertiesTests.cs
  63. 56
      tests/Squidex.Core.Tests/Schemas/SchemaTests.cs
  64. 2
      tests/Squidex.Core.Tests/Schemas/StringFieldPropertiesTests.cs
  65. 14
      tests/Squidex.Write.Tests/Schemas/SchemaCommandHandlerTests.cs
  66. 72
      tests/Squidex.Write.Tests/Schemas/SchemaDomainObjectTests.cs

4
src/Squidex.Core/ContentExtensions.cs

@ -18,9 +18,9 @@ namespace Squidex.Core
{
public static ContentData Enrich(this ContentData data, Schema schema, HashSet<Language> languages)
{
var validator = new ContentEnricher(languages, schema);
var enricher = new ContentEnricher(languages, schema);
validator.Enrich(data);
enricher.Enrich(data);
return data;
}

2
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;
}

2
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));
}
}
}

16
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
}
}

45
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<ValidationError> 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));
}
}
}
}
}

2
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));
}
}
}

2
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; }

2
src/Squidex.Core/Schemas/Json/JsonSchemaModel.cs

@ -18,6 +18,6 @@ namespace Squidex.Core.Schemas.Json
public SchemaProperties Properties { get; set; }
public Dictionary<long, JsonFieldModel> Fields { get; set; }
public List<JsonFieldModel> Fields { get; set; }
}
}

31
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<long, JsonFieldModel>(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<JsonSchemaModel>(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(

2
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))

61
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<Field> fields;
private readonly ImmutableDictionary<long, Field> fieldsById;
private readonly ImmutableDictionary<string, Field> fieldsByName;
private readonly bool isPublished;
@ -37,7 +38,12 @@ namespace Squidex.Core.Schemas
get { return isPublished; }
}
public ImmutableDictionary<long, Field> Fields
public ImmutableList<Field> Fields
{
get { return fields; }
}
public ImmutableDictionary<long, Field> FieldsById
{
get { return fieldsById; }
}
@ -52,17 +58,19 @@ namespace Squidex.Core.Schemas
get { return properties; }
}
public Schema(string name, bool isPublished, SchemaProperties properties, ImmutableDictionary<long, Field> fields)
public Schema(string name, bool isPublished, SchemaProperties properties, ImmutableList<Field> 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<long, Field>.Empty);
return new Schema(name, false, newProperties, ImmutableList<Field>.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<long> 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<Field, Field> 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<Field> 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<Language> languages, Func<EdmComplexType, EdmComplexType> typeResolver)
{
Guard.NotEmpty(languages, nameof(languages));

2
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))

19
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<long> FieldIds { get; set; }
}
}

2
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
{
}

2
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
{
}

5
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);

5
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();
}
}
}

15
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);
}
}
}

5
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));

2
src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj

@ -15,7 +15,7 @@
<ProjectReference Include="..\Squidex.Read\Squidex.Read.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="IdentityServer4" Version="1.4.2" />
<PackageReference Include="IdentityServer4" Version="1.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.MongoDB" Version="1.0.2" />
<PackageReference Include="MongoDB.Driver" Version="2.4.3" />

3
src/Squidex.Read/Schemas/SchemaHistoryEventsCreator.cs

@ -37,6 +37,9 @@ namespace Squidex.Read.Schemas
AddEventMessage<SchemaUnpublished>(
"unpublished schema {[Name]}");
AddEventMessage<SchemaFieldsReordered>(
"reordered fields of schema {[Name]}");
AddEventMessage<FieldAdded>(
"added field {[Field]} to schema {[Name]}");

26
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<long> FieldIds { get; set; }
public void Validate(IList<ValidationError> errors)
{
if (FieldIds == null)
{
errors.Add(new ValidationError("Field ids must be specified", nameof(FieldIds)));
}
}
}
}

7
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<SchemaDomainObject>(context, s => s.ShowField(command));
}
protected Task On(ReorderFields command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Reorder(command));
}
protected Task On(UpdateSchema command, CommandContext context)
{
return handler.UpdateAsync<SchemaDomainObject>(context, s => s.Update(command));

20
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<long>(++totalFields, command.Name) }));
RaiseEvent(SimpleMapper.Map(command, new FieldAdded { FieldId = new NamedId<long>(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<long>(field.Id, field.Name);
}

2
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<string>("store:mongoDb:databaseNameContent");
var contentDatabase = Configuration.GetValue<string>("store:mongoDb:contentDatabase");
if (string.IsNullOrWhiteSpace(contentDatabase))
{

5
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

4
src/Squidex/Controllers/Api/Apps/AppClientsController.cs

@ -103,7 +103,7 @@ namespace Squidex.Controllers.Api.Apps
/// <param name="clientId">The id of the client that must be updated.</param>
/// <param name="request">Client object that needs to be updated.</param>
/// <returns>
/// 201 => Client key generated.
/// 204 => Client updated.
/// 404 => App not found or client not found.
/// </returns>
[HttpPut]
@ -121,8 +121,8 @@ namespace Squidex.Controllers.Api.Apps
/// <param name="app">The name of the app.</param>
/// <param name="clientId">The id of the client that must be deleted.</param>
/// <returns>
/// 404 => App not found or client not found.
/// 204 => Client revoked.
/// 404 => App not found or client not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/clients/{clientId}/")]

1
src/Squidex/Controllers/Api/Apps/AppContributorsController.cs

@ -94,6 +94,7 @@ namespace Squidex.Controllers.Api.Apps
/// <returns>
/// 204 => User removed from app.
/// 400 => User is not assigned to the app.
/// 404 => App not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/contributors/{id}/")]

10
src/Squidex/Controllers/Api/Schemas/Models/Converters/SchemaConverter.cs

@ -55,14 +55,12 @@ namespace Squidex.Controllers.Api.Schemas.Models.Converters
dto.Fields = new List<FieldDto>();
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;

6
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; }
/// <summary>
/// The calculated default value for the field value.
/// </summary>
[JsonConverter(typeof(StringEnumConverter))]
public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; set; }
public override FieldProperties ToProperties()
{
var result = SimpleMapper.Map(this, new DateTimeFieldProperties());

22
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
{
/// <summary>
/// The field ids in the target order.
/// </summary>
[Required]
public List<long> FieldIds { get; set; }
}
}

50
src/Squidex/Controllers/Api/Schemas/SchemaFieldsController.cs

@ -40,9 +40,9 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="request">The field object that needs to be added to the schema.</param>
/// <returns>
/// 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.
/// </returns>
[HttpPost]
[Route("apps/{app}/schemas/{name}/fields/")]
@ -61,6 +61,29 @@ namespace Squidex.Controllers.Api.Schemas
return StatusCode(201, response);
}
/// <summary>
/// Reorders the fields.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="request">The request that contains the field ids.</param>
/// <returns>
/// 204 => Schema fields reorderd.
/// 400 => Schema field ids do not cover the fields of the schema.
/// 404 => Schema or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/ordering")]
[ProducesResponseType(typeof(ErrorDto), 400)]
public async Task<IActionResult> PutFieldOrdering(string app, string name, [FromBody] ReorderFields request)
{
var command = new ReorderFields { FieldIds = request.FieldIds };
await CommandBus.PublishAsync(command);
return NoContent();
}
/// <summary>
/// Update a schema field.
/// </summary>
@ -69,10 +92,9 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="id">The id of the field to update.</param>
/// <param name="request">The field object that needs to be added to the schema.</param>
/// <returns>
/// 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.
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/")]
@ -94,9 +116,9 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the field to hide.</param>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// 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
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the field to shows.</param>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// 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
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the field to enable.</param>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// A disabled field cannot not be edited in the squidex portal anymore,
@ -173,9 +195,9 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the field to disable.</param>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// A disabled field cannot not be edited in the squidex portal anymore,
@ -201,7 +223,7 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="id">The id of the field to disable.</param>
/// <returns>
/// 204 => Schema field deleted.
/// 404 => App, schema or field not found.
/// 404 => Schema, field or app not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/schemas/{name}/fields/{id:long}/")]

16
src/Squidex/Controllers/Api/Schemas/SchemasController.cs

@ -46,6 +46,7 @@ namespace Squidex.Controllers.Api.Schemas
/// </summary>
/// <returns>
/// 200 => Schemas returned.
/// 404 => App not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/schemas/")]
@ -65,7 +66,7 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="name">The name of the schema to retrieve.</param>
/// <returns>
/// 200 => Schema found.
/// 404 => Schema not found.
/// 404 => Schema or app not found.
/// </returns>
[HttpGet]
[Route("apps/{app}/schemas/{name}/")]
@ -93,7 +94,7 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="app">The name of the app to create the schema to.</param>
/// <returns>
/// 201 => Schema created.
/// 400 => Schema name is not valid.
/// 400 => Schema name or properties are not valid.
/// 409 => Schema name already in use.
/// </returns>
[HttpPost]
@ -117,7 +118,9 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="name">The name of the schema.</param>
/// <param name="request">The schema object that needs to updated.</param>
/// <returns>
/// 204 => Schema updated.
/// 204 => Schema has been updated.
/// 400 => Schema properties are not valid.
/// 404 => Schema or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/")]
@ -136,8 +139,9 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="app">The app where the schema is a part of.</param>
/// <param name="name">The name of the schema to publish.</param>
/// <returns>
/// 204 => Schema has been published.
/// 400 => Schema is already published.
/// 204 => Schema has been deleted.
/// 404 => Schema or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/publish")]
@ -155,8 +159,9 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="app">The app where the schema is a part of.</param>
/// <param name="name">The name of the schema to unpublish.</param>
/// <returns>
/// 204 => Schema has been unpublished.
/// 400 => Schema is not published.
/// 204 => Schema has been deleted.
/// 404 => Schema or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/schemas/{name}/unpublish")]
@ -175,6 +180,7 @@ namespace Squidex.Controllers.Api.Schemas
/// <param name="name">The name of the schema to delete.</param>
/// <returns>
/// 204 => Schema has been deleted.
/// 404 => Schema or app not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/schemas/{name}/")]

5
src/Squidex/Squidex.csproj

@ -35,7 +35,7 @@
<ItemGroup>
<PackageReference Include="Autofac" Version="4.5.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.0.0" />
<PackageReference Include="IdentityServer4" Version="1.4.2" />
<PackageReference Include="IdentityServer4" Version="1.5.0" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.1.0" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="1.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="1.1.1" />
@ -59,12 +59,11 @@
<PackageReference Include="MongoDB.Driver" Version="2.4.3" />
<PackageReference Include="NJsonSchema" Version="8.30.6304.31883" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />
<PackageReference Include="NSwag.AspNetCore" Version="10.0.0" />
<PackageReference Include="NSwag.AspNetCore" Version="10.1.0" />
<PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="ReportGenerator" Version="2.5.6" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.1" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.ValueTuple" Version="4.3.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.1' ">

4
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);

2
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,

4
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -26,7 +26,7 @@
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="language">
<option></option>
<option *ngFor="let value of field.properties.allowedValues">{{value}}</option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">
@ -48,7 +48,7 @@
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="language">
<option *ngFor="let value of field.properties.allowedValues">{{value}}</option>
<option *ngFor="let value of field.properties.allowedValues" [ngValue]="value">{{value}}</option>
</select>
</div>
<div *ngSwitchCase="'Radio'">

5
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
]

6
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -36,8 +36,8 @@
</div>
<div class="panel-main">
<div class="panel-content panel-content-scroll">
<div *ngFor="let field of schemaFields">
<div class="panel-content panel-content-scroll" dnd-sortable-container [sortableData]="schemaFields.mutableValues">
<div *ngFor="let field of schemaFields; let i = index" dnd-sortable [sortableIndex]="i" (sorted)="sortFields($event)">
<sqx-field [field]="field"
(disabling)="disableField(field)"
(deleting)="deleteField(field)"
@ -51,7 +51,7 @@
<form class="form-inline" [formGroup]="addFieldForm" (ngSubmit)="addField()">
<div class="form-group mr-2">
<select class="form-control" formControlName="type">
<option *ngFor="let type of fieldTypes">{{type}}</option>
<option *ngFor="let type of fieldTypes" [ngValue]="type">{{type}}</option>
</select>
</div>

4
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;
}

12
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);

21
src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.html

@ -23,11 +23,24 @@
</div>
</div>
<div class="form-group row" *ngIf="!(hideDefaultValue | async)">
<label class="col col-3 col-form-label" for="field-default-value">Default Value</label>
<div *ngIf="!(hideDefaultValues | async)">
<div class="form-group row">
<label class="col col-3 col-form-label">Default Mode:</label>
<div class="col col-9">
<sqx-date-time-editor enforceTime="true" formControlName="defaultValue"></sqx-date-time-editor>
<div class="col col-3">
<select class="form-control" formControlName="calculatedDefaultValue">
<option></option>
<option *ngFor="let value of calculatedDefaultValues" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
<div class="form-group row" *ngIf="!(hideDefaultValue | async)">
<label class="col col-3 col-form-label" for="field-default-value">Default Value</label>
<div class="col col-9">
<sqx-date-time-editor enforceTime="true" mode="DateTime" formControlName="defaultValue"></sqx-date-time-editor>
</div>
</div>
</div>
</div>

17
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<boolean>;
public hideDefaultValue: Observable<boolean>;
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);
}
}

2
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html

@ -10,7 +10,7 @@
<sqx-control-errors for="name" [submitted]="createFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="schema-name" formControlName="name" sqxFocusOnInit />
<input type="text" class="form-control" id="schema-name" formControlName="name" autocomplete="off" sqxFocusOnInit sqxLowerCaseInput />
<span class="form-hint">
The schema name becomes part of the api url,<br /> e.g {{apiUrl.buildUrl("api/content/")}}{{appName}}/<b>{{schemaName | async}}</b>/.

39
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<Array<any>>();
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);
}
};
}
}

2
src/Squidex/app/features/settings/pages/clients/clients-page.component.html

@ -28,7 +28,7 @@
<div class="form-group mr-2">
<sqx-control-errors for="name" [submitted]="addClientFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter client name" />
<input type="text" class="form-control" formControlName="name" maxlength="40" placeholder="Enter client name" autocomplete="off" sqxLowerCaseInput />
</div>
<button type="submit" class="btn btn-success" [disabled]="!hasName">Add Client</button>

2
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -53,7 +53,7 @@
</td>
<td>
<select class="form-control" [ngModel]="contributor.permission" (ngModelChange)="changePermission(contributor, $event)" [disabled]="currentUserId === contributor.contributorId">
<option *ngFor="let permission of usersPermissions">{{permission}}</option>
<option *ngFor="let permission of usersPermissions" [ngValue]="permission">{{permission}}</option>
</select>
</td>
<td>

62
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();
}
}

1
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';

3
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,

6
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]);

4
src/Squidex/app/framework/utils/immutable-array.ts

@ -23,6 +23,10 @@ export class ImmutableArray<T> implements Iterable<T> {
return [...this.items];
}
public get mutableValues(): T[] {
return this.items;
}
private constructor(items: T[]) {
this.items = items;
}

2
src/Squidex/app/shared/components/app-form.component.html

@ -10,7 +10,7 @@
<sqx-control-errors for="name" [submitted]="createFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="app-name" formControlName="name" />
<input type="text" class="form-control" id="app-name" formControlName="name" autocomplete="off" sqxLowerCaseInput />
<span class="form-hint">
The app name becomes part of the api url,<br /> e.g {{apiUrl.buildUrl("api/content/")}}<b>{{appName | async}}</b>/.

16
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(

10
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<any> {
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<any> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/schemas/${schemaName}/publish`);

6
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;
}

3
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';

1
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",

2
tests/Squidex.Core.Tests/Schemas/BooleanFieldPropertiesTests.cs

@ -30,7 +30,7 @@ namespace Squidex.Core.Schemas
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Editor ist not a valid value", "Editor")
new ValidationError("Editor is not a valid value", "Editor")
});
}

54
tests/Squidex.Core.Tests/Schemas/DateTimeFieldPropertiesTests.cs

@ -88,10 +88,62 @@ namespace Squidex.Core.Schemas
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
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<ValidationError>
{
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<ValidationError>
{
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()
{

2
tests/Squidex.Core.Tests/Schemas/GeolocationPropertiesTests.cs

@ -30,7 +30,7 @@ namespace Squidex.Core.Schemas
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Editor ist not a valid value", "Editor")
new ValidationError("Editor is not a valid value", "Editor")
});
}

2
tests/Squidex.Core.Tests/Schemas/NumberFieldPropertiesTests.cs

@ -130,7 +130,7 @@ namespace Squidex.Core.Schemas
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Editor ist not a valid value", "Editor")
new ValidationError("Editor is not a valid value", "Editor")
});
}

56
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<DomainException>(() => 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<long> { 3, 2, 1 });
Assert.Equal(new List<Field> { 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<ArgumentException>(() => sut.ReorderFields(new List<long> { 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<ArgumentException>(() => sut.ReorderFields(new List<long> { 1, 4 }));
}
[Fact]
public void Should_build_schema()
{

2
tests/Squidex.Core.Tests/Schemas/StringFieldPropertiesTests.cs

@ -87,7 +87,7 @@ namespace Squidex.Core.Schemas
errors.ShouldBeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Editor ist not a valid value", "Editor")
new ValidationError("Editor is not a valid value", "Editor")
});
}

14
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<long>() });
await TestUpdate(schema, async _ =>
{
await sut.HandleAsync(context);
});
}
[Fact]
public async Task PublishSchema_should_update_domain_object()
{

72
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<ValidationException>(() =>
Assert.Throws<DomainException>(() =>
{
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<DomainException>(() =>
{
sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List<long>() }));
});
}
[Fact]
public void Reorder_should_throw_if_schema_is_deleted()
{
CreateSchema();
DeleteSchema();
Assert.Throws<DomainException>(() =>
{
sut.Reorder(CreateCommand(new ReorderFields { FieldIds = new List<long>() }));
});
}
[Fact]
public void Reorder_should_throw_if_command_is_not_valid()
{
CreateSchema();
Assert.Throws<ValidationException>(() =>
{
sut.Reorder(CreateCommand(new ReorderFields()));
});
}
[Fact]
public void Reorder_should_refresh_properties_and_create_events()
{
var fieldIds = new List<long> { 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(

Loading…
Cancel
Save