Browse Source

Unique array fields. (#768)

* Unique array fields.

* Fix clear indicator.
pull/771/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
7bc262fff3
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/i18n/frontend_en.json
  2. 2
      backend/i18n/frontend_it.json
  3. 2
      backend/i18n/frontend_nl.json
  4. 2
      backend/i18n/frontend_zh.json
  5. 1
      backend/i18n/source/backend_en.json
  6. 2
      backend/i18n/source/frontend_en.json
  7. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
  8. 3
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs
  10. 9
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs
  11. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs
  12. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  13. 59
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs
  14. 3
      backend/src/Squidex.Shared/Texts.it.resx
  15. 3
      backend/src/Squidex.Shared/Texts.nl.resx
  16. 3
      backend/src/Squidex.Shared/Texts.resx
  17. 3
      backend/src/Squidex.Shared/Texts.zh.resx
  18. 6
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs
  19. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs
  20. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs
  21. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs
  22. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs
  23. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs
  24. 30
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs
  25. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs
  26. 12
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs
  27. 49
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs
  28. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs
  29. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs
  30. 16
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs
  31. 33
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs
  32. 108
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs
  33. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs
  34. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs
  35. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs
  36. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs
  37. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs
  38. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs
  39. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs
  40. 26
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs
  41. 118
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs
  42. 60
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs
  43. 4
      frontend/app/features/content/shared/forms/field-editor.component.ts
  44. 3
      frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html
  45. 1
      frontend/app/features/schemas/pages/schema/fields/field.component.html
  46. 7
      frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.ts
  47. 7
      frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.ts
  48. 4
      frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html
  49. 7
      frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts
  50. 7
      frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.html
  51. 7
      frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.ts
  52. 8
      frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html
  53. 8
      frontend/app/features/schemas/pages/schema/fields/types/components-validation.component.html
  54. 9
      frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.ts
  55. 9
      frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts
  56. 18
      frontend/app/features/schemas/pages/schema/schema-page.component.html
  57. 564
      frontend/app/framework/angular/forms/validators.spec.ts
  58. 41
      frontend/app/framework/angular/forms/validators.ts
  59. 2
      frontend/app/shared/services/schemas.types.ts
  60. 8
      frontend/app/shared/state/contents.forms.visitors.ts
  61. 2
      frontend/app/shared/state/schemas.forms.ts

2
backend/i18n/frontend_en.json

@ -798,6 +798,7 @@
"schemas.fieldTypes.array.countMax": "Max Items",
"schemas.fieldTypes.array.countMin": "Min Items",
"schemas.fieldTypes.array.description": "List of embedded objects.",
"schemas.fieldTypes.array.uniqueFields": "Unique Fields",
"schemas.fieldTypes.assets.allowDuplicates": "Allow duplicate values",
"schemas.fieldTypes.assets.count": "Count",
"schemas.fieldTypes.assets.countMax": "Max Assets",
@ -1006,6 +1007,7 @@
"validation.patternmessage": "{message}",
"validation.required": "{field|upper} is required.",
"validation.requiredTrue": "{field|upper} is required.",
"validation.uniqueobjectvalues": "{field|upper} has items with duplicate '{fields}' fields.",
"validation.uniquestrings": "{field|upper} must not contain duplicate values.",
"validation.validarrayvalues": "{field|upper} contains an invalid value: {invalidvalue}.",
"validation.validdatetime": "{field|upper} is not a valid date time.",

2
backend/i18n/frontend_it.json

@ -798,6 +798,7 @@
"schemas.fieldTypes.array.countMax": "Max num. Elementi",
"schemas.fieldTypes.array.countMin": "Min num. Elementi",
"schemas.fieldTypes.array.description": "Lista di oggetti incorporati.",
"schemas.fieldTypes.array.uniqueFields": "Unique Fields",
"schemas.fieldTypes.assets.allowDuplicates": "Consente valori duplicati",
"schemas.fieldTypes.assets.count": "Conteggio",
"schemas.fieldTypes.assets.countMax": "Max num di Risorse",
@ -1006,6 +1007,7 @@
"validation.patternmessage": "{message}",
"validation.required": "{field|upper} è obbligatorio.",
"validation.requiredTrue": "{field|upper} è obbligatorio.",
"validation.uniqueobjectvalues": "{field|upper} has items with duplicate '{fields}' fields.",
"validation.uniquestrings": "{field|upper} non deve contenere valori duplicati.",
"validation.validarrayvalues": "{field|upper} contiene valori non validicontains an invalid value: {invalidvalue}.",
"validation.validdatetime": "{field|upper} non è una data e ora valida.",

2
backend/i18n/frontend_nl.json

@ -798,6 +798,7 @@
"schemas.fieldTypes.array.countMax": "Max. aantal items",
"schemas.fieldTypes.array.countMin": "Min. items",
"schemas.fieldTypes.array.description": "Lijst met ingesloten objecten.",
"schemas.fieldTypes.array.uniqueFields": "Unique Fields",
"schemas.fieldTypes.assets.allowDuplicates": "Dubbele waarden toestaan",
"schemas.fieldTypes.assets.count": "Tellen",
"schemas.fieldTypes.assets.countMax": "Max. bestanden",
@ -1006,6 +1007,7 @@
"validation.patternmessage": "{message}",
"validation.required": "{field|upper} is vereist.",
"validation.requiredTrue": "{field|upper} is vereist.",
"validation.uniqueobjectvalues": "{field|upper} has items with duplicate '{fields}' fields.",
"validation.uniquestrings": "{field|upper} mag geen dubbele waarden bevatten.",
"validation.validarrayvalues": "{field|upper} bevat een ongeldige waarde: {invalidvalue}.",
"validation.validdatetime": "{field|upper} is geen geldige datum en tijd.",

2
backend/i18n/frontend_zh.json

@ -798,6 +798,7 @@
"schemas.fieldTypes.array.countMax": "最大项目数",
"schemas.fieldTypes.array.countMin": "最小项目",
"schemas.fieldTypes.array.description": "嵌入对象列表。",
"schemas.fieldTypes.array.uniqueFields": "Unique Fields",
"schemas.fieldTypes.assets.allowDuplicates": "允许重复值",
"schemas.fieldTypes.assets.count": "计数",
"schemas.fieldTypes.assets.countMax": "最大资源",
@ -1006,6 +1007,7 @@
"validation.patternmessage": "{message}",
"validation.required": "{field|upper} 是必需的。",
"validation.requiredTrue": "{field|upper} 是必需的。",
"validation.uniqueobjectvalues": "{field|upper} has items with duplicate '{fields}' fields.",
"validation.uniquestrings": "{field|upper} 不得包含重复值。",
"validation.validarrayvalues": "{field|upper} 包含无效值:{invalidvalue}。",
"validation.validdatetime": "{field|upper} 不是有效的日期时间。",

1
backend/i18n/source/backend_en.json

@ -183,6 +183,7 @@
"contents.validation.regexTooSlow": "Regex is too slow.",
"contents.validation.required": "Field is required.",
"contents.validation.unique": "Another content with the same value exists.",
"contents.validation.uniqueObjectValues": "Must not contain items with duplicate '{field}' fields.",
"contents.validation.unknownField": "Not a known {fieldType}.",
"contents.validation.wordCount": "Must have exactly {count} word(s).",
"contents.validation.wordsBetween": "Must have between {min} and {max} word(s).",

2
backend/i18n/source/frontend_en.json

@ -798,6 +798,7 @@
"schemas.fieldTypes.array.countMax": "Max Items",
"schemas.fieldTypes.array.countMin": "Min Items",
"schemas.fieldTypes.array.description": "List of embedded objects.",
"schemas.fieldTypes.array.uniqueFields": "Unique Fields",
"schemas.fieldTypes.assets.allowDuplicates": "Allow duplicate values",
"schemas.fieldTypes.assets.count": "Count",
"schemas.fieldTypes.assets.countMax": "Max Assets",
@ -1006,6 +1007,7 @@
"validation.patternmessage": "{message}",
"validation.required": "{field|upper} is required.",
"validation.requiredTrue": "{field|upper} is required.",
"validation.uniqueobjectvalues": "{field|upper} has items with duplicate '{fields}' fields.",
"validation.uniquestrings": "{field|upper} must not contain duplicate values.",
"validation.validarrayvalues": "{field|upper} contains an invalid value: {invalidvalue}.",
"validation.validdatetime": "{field|upper} is not a valid date time.",

7
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs

@ -30,13 +30,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public FieldCollection<NestedField> FieldCollection { get; private set; } = FieldCollection<NestedField>.Empty;
public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties? properties = null, IFieldSettings? settings = null)
: base(id, name, partitioning, properties, settings)
{
}
public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties? properties = null, IFieldSettings? settings = null)
: this(id, name, partitioning, properties, settings)
: base(id, name, partitioning, properties, settings)
{
FieldCollection = new FieldCollection<NestedField>(fields);
}

3
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayFieldProperties.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas
{
@ -15,6 +16,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public int? MaxItems { get; init; }
public ImmutableList<string>? UniqueFields { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{
return visitor.Visit(this, args);

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/ComponentsFieldProperties.cs

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public int? MaxItems { get; init; }
public ImmutableList<string>? UniqueFields { get; init; }
public DomainId SchemaId
{
init

9
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs

@ -11,15 +11,10 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public static class Fields
{
public static ArrayField Array(long id, string name, Partitioning partitioning, params NestedField[] fields)
{
return new ArrayField(id, name, partitioning, fields);
}
public static ArrayField Array(long id, string name, Partitioning partitioning,
ArrayFieldProperties? properties = null, IFieldSettings? settings = null)
ArrayFieldProperties? properties = null, IFieldSettings? settings = null, params NestedField[] fields)
{
return new ArrayField(id, name, partitioning, properties, settings);
return new ArrayField(id, name, partitioning, fields, properties, settings);
}
public static RootField<AssetsFieldProperties> Assets(long id, string name, Partitioning partitioning,

10
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs

@ -45,6 +45,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems);
}
if (properties.UniqueFields?.Count > 0)
{
yield return new UniqueObjectValuesValidator(properties.UniqueFields);
}
var nestedValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(field.Fields.Count);
foreach (var nestedField in field.Fields)
@ -97,6 +102,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems);
}
if (properties.UniqueFields?.Count > 0)
{
yield return new UniqueObjectValuesValidator(properties.UniqueFields);
}
yield return new CollectionItemValidator(ComponentValidator(args.Factory));
}

4
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs

@ -222,7 +222,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
{
if (value is JsonArray array)
{
var result = new List<object>(array.Count);
var result = new List<Component>(array.Count);
for (var i = 0; i < array.Count; i++)
{
@ -245,7 +245,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
return (null, new JsonError(T.Get("contents.invalidArrayOfObjects")));
}
private static (object? Result, JsonError? Error) ConvertToComponent(IJsonValue value,
private static (Component? Result, JsonError? Error) ConvertToComponent(IJsonValue value,
ResolvedComponents components, ImmutableList<DomainId>? allowedIds)
{
if (value is not JsonObject obj)

59
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs

@ -0,0 +1,59 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Translations;
namespace Squidex.Domain.Apps.Core.ValidateContent.Validators
{
public sealed class UniqueObjectValuesValidator : IValidator
{
private readonly IEnumerable<string> fields;
public UniqueObjectValuesValidator(IEnumerable<string> fields)
{
this.fields = fields;
}
public Task ValidateAsync(object? value, ValidationContext context, AddError addError)
{
if (value is IEnumerable<JsonObject> objects && objects.Count() > 1)
{
Validate(context, addError, objects);
}
else if (value is IEnumerable<Component> components && components.Count() > 1)
{
Validate(context, addError, components.Select(x => x.Data));
}
return Task.CompletedTask;
}
private void Validate(ValidationContext context, AddError addError, IEnumerable<JsonObject> items)
{
var duplicates = new HashSet<IJsonValue>(10);
foreach (var field in fields)
{
duplicates.Clear();
foreach (var item in items)
{
if (item.TryGetValue(field, out var fieldValue) && !duplicates.Add(fieldValue))
{
addError(context.Path, T.Get("contents.validation.uniqueObjectValues", new { field }));
break;
}
}
}
}
}
}

3
backend/src/Squidex.Shared/Texts.it.resx

@ -634,6 +634,9 @@
<data name="contents.validation.unique" xml:space="preserve">
<value>Esiste un altro contenuto con lo stesso valore.</value>
</data>
<data name="contents.validation.uniqueObjectValues" xml:space="preserve">
<value>Must not contain items with duplicate '{field}' fields.</value>
</data>
<data name="contents.validation.unknownField" xml:space="preserve">
<value>Non è noto {fieldType}.</value>
</data>

3
backend/src/Squidex.Shared/Texts.nl.resx

@ -634,6 +634,9 @@
<data name="contents.validation.unique" xml:space="preserve">
<value>Er bestaat een andere inhoud met dezelfde waarde.</value>
</data>
<data name="contents.validation.uniqueObjectValues" xml:space="preserve">
<value>Must not contain items with duplicate '{field}' fields.</value>
</data>
<data name="contents.validation.unknownField" xml:space="preserve">
<value>Onbekend {fieldType}.</value>
</data>

3
backend/src/Squidex.Shared/Texts.resx

@ -634,6 +634,9 @@
<data name="contents.validation.unique" xml:space="preserve">
<value>Another content with the same value exists.</value>
</data>
<data name="contents.validation.uniqueObjectValues" xml:space="preserve">
<value>Must not contain items with duplicate '{field}' fields.</value>
</data>
<data name="contents.validation.unknownField" xml:space="preserve">
<value>Not a known {fieldType}.</value>
</data>

3
backend/src/Squidex.Shared/Texts.zh.resx

@ -634,6 +634,9 @@
<data name="contents.validation.unique" xml:space="preserve">
<value>存在另一个具有相同值的内容。</value>
</data>
<data name="contents.validation.uniqueObjectValues" xml:space="preserve">
<value>Must not contain items with duplicate '{field}' fields.</value>
</data>
<data name="contents.validation.unknownField" xml:space="preserve">
<value>不是已知的 {fieldType}。</value>
</data>

6
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ArrayFieldPropertiesDto.cs

@ -6,6 +6,7 @@
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
@ -22,6 +23,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public int? MaxItems { get; set; }
/// <summary>
/// The fields that must be unique.
/// </summary>
public ImmutableList<string>? UniqueFields { get; set; }
public override FieldProperties ToProperties()
{
var result = SimpleMapper.Map(this, new ArrayFieldProperties());

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/ComponentsFieldPropertiesDto.cs

@ -29,6 +29,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary>
public ImmutableList<DomainId>? SchemaIds { get; set; }
/// <summary>
/// The fields that must be unique.
/// </summary>
public ImmutableList<string>? UniqueFields { get; set; }
public override FieldProperties ToProperties()
{
var result = SimpleMapper.Map(this, new ComponentsFieldProperties());

6
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/ArrayFieldTests.cs

@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
{
var parent_1 = parent_0.AddField(CreateField(1));
Assert.Throws<ArgumentException>(() => parent_1.AddNumber(2, "my-field-1"));
Assert.Throws<ArgumentException>(() => parent_1.AddNumber(2, "myField1"));
}
[Fact]
@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
{
var parent_1 = parent_0.AddField(CreateField(1));
Assert.Throws<ArgumentException>(() => parent_1.AddNumber(1, "my-field-2"));
Assert.Throws<ArgumentException>(() => parent_1.AddNumber(1, "myField2"));
}
[Fact]
@ -238,7 +238,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
private static NestedField<NumberFieldProperties> CreateField(int id)
{
return Fields.Number(id, $"my-field-{id}");
return Fields.Number(id, $"myField{id}");
}
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaFieldTests.cs

@ -24,12 +24,12 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
.Select(x => new[] { x })
.ToList()!;
private readonly RootField<NumberFieldProperties> field_0 = Fields.Number(1, "my-field", Partitioning.Invariant);
private readonly RootField<NumberFieldProperties> field_0 = Fields.Number(1, "myField", Partitioning.Invariant);
[Fact]
public void Should_instantiate_field()
{
Assert.Equal("my-field", field_0.Name);
Assert.Equal("myField", field_0.Name);
}
[Fact]

6
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs

@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
{
var schema_1 = schema_0.AddField(CreateField(1));
Assert.Throws<ArgumentException>(() => schema_1.AddNumber(2, "my-field-1", Partitioning.Invariant));
Assert.Throws<ArgumentException>(() => schema_1.AddNumber(2, "myField1", Partitioning.Invariant));
}
[Fact]
@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
{
var schema_1 = schema_0.AddField(CreateField(1));
Assert.Throws<ArgumentException>(() => schema_1.AddNumber(1, "my-field-2", Partitioning.Invariant));
Assert.Throws<ArgumentException>(() => schema_1.AddNumber(1, "myField2", Partitioning.Invariant));
}
[Fact]
@ -498,7 +498,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
private static RootField<NumberFieldProperties> CreateField(int id)
{
return Fields.Number(id, $"my-field-{id}", Partitioning.Invariant);
return Fields.Number(id, $"myField{id}", Partitioning.Invariant);
}
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs

@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
[InlineData("*")]
public void Should_convert_nested_asset_ids_to_urls(string path)
{
var field = Fields.Array(1, "parent", Partitioning.Invariant, Fields.Assets(11, "assets"));
var field = Fields.Array(1, "parent", Partitioning.Invariant, null, null, Fields.Assets(11, "assets"));
var source = JsonValue.Array(id1, id2);
@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
[InlineData("other.assets")]
public void Should_not_convert_nested_asset_ids_if_field_name_does_not_match(string path)
{
var field = Fields.Array(1, "parent", Partitioning.Invariant, Fields.Assets(11, "assets"));
var field = Fields.Array(1, "parent", Partitioning.Invariant, null, null, Fields.Assets(11, "assets"));
var source = JsonValue.Array(id1, id2);

30
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/DefaultValues/DefaultValuesTests.cs

@ -29,13 +29,13 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues
{
schema =
new Schema("my-schema")
.AddString(1, "my-string", Partitioning.Language,
.AddString(1, "myString", Partitioning.Language,
new StringFieldProperties { DefaultValue = "en-string" })
.AddNumber(2, "my-number", Partitioning.Invariant,
.AddNumber(2, "myNumber", Partitioning.Invariant,
new NumberFieldProperties())
.AddDateTime(3, "my-datetime", Partitioning.Invariant,
.AddDateTime(3, "myDatetime", Partitioning.Invariant,
new DateTimeFieldProperties { DefaultValue = now })
.AddBoolean(4, "my-boolean", Partitioning.Invariant,
.AddBoolean(4, "myBoolean", Partitioning.Invariant,
new BooleanFieldProperties { DefaultValue = true });
}
@ -44,23 +44,23 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues
{
var data =
new ContentData()
.AddField("my-string",
.AddField("myString",
new ContentFieldData()
.AddLocalized("de", "de-string"))
.AddField("my-number",
.AddField("myNumber",
new ContentFieldData()
.AddInvariant(456));
data.GenerateDefaultValues(schema, languagesConfig.ToResolver());
Assert.Equal(456, ((JsonNumber)data["my-number"]!["iv"]).Value);
Assert.Equal(456, ((JsonNumber)data["myNumber"]!["iv"]).Value);
Assert.Equal("de-string", data["my-string"]!["de"].ToString());
Assert.Equal("en-string", data["my-string"]!["en"].ToString());
Assert.Equal("de-string", data["myString"]!["de"].ToString());
Assert.Equal("en-string", data["myString"]!["en"].ToString());
Assert.Equal(now.ToString(), data["my-datetime"]!["iv"].ToString());
Assert.Equal(now.ToString(), data["myDatetime"]!["iv"].ToString());
Assert.True(((JsonBoolean)data["my-boolean"]!["iv"]).Value);
Assert.True(((JsonBoolean)data["myBoolean"]!["iv"]).Value);
}
[Fact]
@ -68,17 +68,17 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues
{
var data =
new ContentData()
.AddField("my-string",
.AddField("myString",
new ContentFieldData()
.AddLocalized("de", string.Empty))
.AddField("my-number",
.AddField("myNumber",
new ContentFieldData()
.AddInvariant(456));
data.GenerateDefaultValues(schema, languagesConfig.ToResolver());
Assert.Equal(string.Empty, data["my-string"]!["de"].ToString());
Assert.Equal("en-string", data["my-string"]!["en"].ToString());
Assert.Equal(string.Empty, data["myString"]!["de"].ToString());
Assert.Equal("en-string", data["myString"]!["en"].ToString());
}
[Fact]

6
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs

@ -19,9 +19,9 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization
public class SchemaSynchronizerTests
{
private readonly Func<long> idGenerator;
private readonly NamedId<long> stringId = NamedId.Of(13L, "my-value");
private readonly NamedId<long> nestedId = NamedId.Of(141L, "my-value");
private readonly NamedId<long> arrayId = NamedId.Of(14L, "11-array");
private readonly NamedId<long> stringId = NamedId.Of(13L, "myValue");
private readonly NamedId<long> nestedId = NamedId.Of(141L, "myValue");
private readonly NamedId<long> arrayId = NamedId.Of(14L, "11Array");
private int fields = 50;
public SchemaSynchronizerTests()

12
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/ReferenceExtractionTests.cs

@ -204,7 +204,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
[Fact]
public void Should_return_empty_list_from_non_references_field()
{
var sut = Fields.String(1, "my-string", Partitioning.Invariant);
var sut = Fields.String(1, "myString", Partitioning.Invariant);
var result = sut.GetReferencedIds(JsonValue.Create("invalid"), components).ToArray();
@ -218,7 +218,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
var id1 = DomainId.NewGuid();
var id2 = DomainId.NewGuid();
var arrayField = Fields.Array(1, "my-array", Partitioning.Invariant, field);
var arrayField = Fields.Array(1, "myArray", Partitioning.Invariant, null, null, field);
var value =
JsonValue.Array(
@ -305,14 +305,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
public static IEnumerable<object[]> ReferencingNestedFields()
{
yield return new object[] { Fields.References(1, "my-refs") };
yield return new object[] { Fields.Assets(1, "my-assets") };
yield return new object[] { Fields.References(1, "myRefs") };
yield return new object[] { Fields.Assets(1, "myAssets") };
}
public static IEnumerable<object[]> ReferencingFields()
{
yield return new object[] { Fields.References(1, "my-refs", Partitioning.Invariant) };
yield return new object[] { Fields.Assets(1, "my-assets", Partitioning.Invariant) };
yield return new object[] { Fields.References(1, "myRefs", Partitioning.Invariant) };
yield return new object[] { Fields.Assets(1, "myAssets", Partitioning.Invariant) };
}
private static HashSet<DomainId> RandomIds()

49
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ArrayFieldTests.cs

@ -6,11 +6,11 @@
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ArrayFieldProperties());
Assert.Equal("my-array", sut.Name);
Assert.Equal("myArray", sut.Name);
}
[Fact]
@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ArrayFieldProperties());
await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors);
await sut.ValidateAsync(CreateValue(Object()), errors);
Assert.Empty(errors);
}
@ -53,7 +53,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ArrayFieldProperties { MinItems = 2, MaxItems = 2 });
await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors);
await sut.ValidateAsync(CreateValue(Object(), Object()), errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_value_has_not_duplicates()
{
var sut = Field(new ArrayFieldProperties { UniqueFields = ImmutableList.Create("myString") });
await sut.ValidateAsync(CreateValue(Object("myString", "1"), Object("myString", "2")), errors);
Assert.Empty(errors);
}
@ -96,7 +106,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ArrayFieldProperties { MinItems = 3 });
await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors);
await sut.ValidateAsync(CreateValue(Object(), Object()), errors);
errors.Should().BeEquivalentTo(
new[] { "Must have at least 3 item(s)." });
@ -107,20 +117,41 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ArrayFieldProperties { MaxItems = 1 });
await sut.ValidateAsync(CreateValue(JsonValue.Object(), JsonValue.Object()), errors);
await sut.ValidateAsync(CreateValue(Object(), Object()), errors);
errors.Should().BeEquivalentTo(
new[] { "Must not have more than 1 item(s)." });
}
private static IJsonValue CreateValue(params JsonObject[]? ids)
[Fact]
public async Task Should_add_error_if_value_has_duplicates()
{
var sut = Field(new ArrayFieldProperties { UniqueFields = ImmutableList.Create("myString") });
await sut.ValidateAsync(CreateValue(Object("myString", "1"), Object("myString", "1")), errors);
errors.Should().BeEquivalentTo(
new[] { "Must not contain items with duplicate 'myString' fields." });
}
private static IJsonValue CreateValue(params JsonObject[]? objects)
{
return objects == null ? JsonValue.Null : JsonValue.Array(objects);
}
private static JsonObject Object()
{
return JsonValue.Object();
}
private static JsonObject Object(string key, object value)
{
return ids == null ? JsonValue.Null : JsonValue.Array(ids.OfType<object>().ToArray());
return JsonValue.Object().Add(key, value);
}
private static RootField<ArrayFieldProperties> Field(ArrayFieldProperties properties)
{
return Fields.Array(1, "my-array", Partitioning.Invariant, properties);
return Fields.Array(1, "myArray", Partitioning.Invariant, properties, null, Fields.String(2, "myString"));
}
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/AssetsFieldTests.cs

@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new AssetsFieldProperties());
Assert.Equal("my-assets", sut.Name);
Assert.Equal("myAssets", sut.Name);
}
[Fact]
@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<AssetsFieldProperties> Field(AssetsFieldProperties properties)
{
return Fields.Assets(1, "my-assets", Partitioning.Invariant, properties);
return Fields.Assets(1, "myAssets", Partitioning.Invariant, properties);
}
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/BooleanFieldTests.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new BooleanFieldProperties());
Assert.Equal("my-boolean", sut.Name);
Assert.Equal("myBoolean", sut.Name);
}
[Fact]
@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<BooleanFieldProperties> Field(BooleanFieldProperties properties)
{
return Fields.Boolean(1, "my-boolean", Partitioning.Invariant, properties);
return Fields.Boolean(1, "myBoolean", Partitioning.Invariant, properties);
}
}
}

16
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentFieldTests.cs

@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (_, sut, _) = Field(new ComponentFieldProperties());
Assert.Equal("my-component", sut.Name);
Assert.Equal("myComponent", sut.Name);
}
[Fact]
@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (id, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 });
await sut.ValidateAsync(CreateValue(id.ToString(), "component-field", 1), errors, components: components);
await sut.ValidateAsync(CreateValue(id.ToString(), "componentField", 1), errors, components: components);
Assert.Empty(errors);
}
@ -68,10 +68,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (id, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1, IsRequired = true }, true);
await sut.ValidateAsync(CreateValue(id.ToString(), "component-field", null), errors, components: components);
await sut.ValidateAsync(CreateValue(id.ToString(), "componentField", null), errors, components: components);
errors.Should().BeEquivalentTo(
new[] { "component-field: Field is required." });
new[] { "componentField: Field is required." });
}
[Fact]
@ -123,7 +123,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 });
var value = CreateValue("my-component", "component-field", 1, "schemaName");
var value = CreateValue("my-component", "componentField", 1, "schemaName");
await sut.ValidateAsync(value, errors, components: components);
@ -136,7 +136,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 });
var value = CreateValue(null, "component-field", 1);
var value = CreateValue(null, "componentField", 1);
await sut.ValidateAsync(value, errors, components: components);
@ -164,10 +164,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var schema =
new Schema("my-component")
.AddNumber(1, "component-field", Partitioning.Invariant,
.AddNumber(1, "componentField", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = isRequired });
var field = Fields.Component(1, "my-component", Partitioning.Invariant, properties);
var field = Fields.Component(1, "myComponent", Partitioning.Invariant, properties);
var components = new ResolvedComponents(new Dictionary<DomainId, Schema>
{

33
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ComponentsFieldTests.cs

@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (_, sut, _) = Field(new ComponentsFieldProperties());
Assert.Equal("my-components", sut.Name);
Assert.Equal("myComponents", sut.Name);
}
[Fact]
@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 });
await sut.ValidateAsync(CreateValue(1, id.ToString(), "component-field", 1), errors, components: components);
await sut.ValidateAsync(CreateValue(1, id.ToString(), "componentField", 1), errors, components: components);
Assert.Empty(errors);
}
@ -57,7 +57,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MinItems = 2, MaxItems = 2 });
await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors, components: components);
await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components);
Assert.Empty(errors);
}
@ -78,10 +78,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, IsRequired = true }, true);
await sut.ValidateAsync(CreateValue(1, id.ToString(), "component-field", null), errors, components: components);
await sut.ValidateAsync(CreateValue(1, id.ToString(), "componentField", null), errors, components: components);
errors.Should().BeEquivalentTo(
new[] { "[1].component-field: Field is required." });
new[] { "[1].componentField: Field is required." });
}
[Fact]
@ -144,7 +144,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MinItems = 3 });
await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors, components: components);
await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components);
errors.Should().BeEquivalentTo(
new[] { "Must have at least 3 item(s)." });
@ -155,18 +155,29 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MaxItems = 1 });
await sut.ValidateAsync(CreateValue(2, id.ToString(), "component-field", 1), errors, components: components);
await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components);
errors.Should().BeEquivalentTo(
new[] { "Must not have more than 1 item(s)." });
}
[Fact]
public async Task Should_add_error_if_value_has_duplicates()
{
var (id, sut, components) = Field(new ComponentsFieldProperties { UniqueFields = ImmutableList.Create("componentField") });
await sut.ValidateAsync(CreateValue(2, id.ToString(), "componentField", 1), errors, components: components);
errors.Should().BeEquivalentTo(
new[] { "Must not contain items with duplicate 'componentField' fields." });
}
[Fact]
public async Task Should_resolve_schema_id_from_name()
{
var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 });
var value = CreateValue(1, "my-component", "component-field", 1, "schemaName");
var value = CreateValue(1, "my-component", "componentField", 1, "schemaName");
await sut.ValidateAsync(value, errors, components: components);
@ -179,7 +190,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 });
var value = CreateValue(1, null, "component-field", 1);
var value = CreateValue(1, null, "componentField", 1);
await sut.ValidateAsync(value, errors, components: components);
@ -214,10 +225,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var schema =
new Schema("my-component")
.AddNumber(1, "component-field", Partitioning.Invariant,
.AddNumber(1, "componentField", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = isRequired });
var field = Fields.Components(1, "my-components", Partitioning.Invariant, properties);
var field = Fields.Components(1, "myComponents", Partitioning.Invariant, properties);
var components = new ResolvedComponents(new Dictionary<DomainId, Schema>
{

108
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ContentValidationTests.cs

@ -42,12 +42,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
A.CallTo(() => validatorFactory.CreateValueValidators(A<ValidationContext>._, A<IField>._, A<ValidatorFactory>._))
.Returns(Enumerable.Repeat(validator, 1));
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant,
schema = schema.AddNumber(1, "myField", Partitioning.Invariant,
new NumberFieldProperties());
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddInvariant(1000));
@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Validation failed with internal error.", "my-field.iv")
new ValidationError("Validation failed with internal error.", "myField.iv")
});
}
@ -73,12 +73,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
A.CallTo(() => validatorFactory.CreateFieldValidators(A<ValidationContext>._, A<IField>._, A<ValidatorFactory>._))
.Returns(Enumerable.Repeat(validator, 1));
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant,
schema = schema.AddNumber(1, "myField", Partitioning.Invariant,
new NumberFieldProperties());
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddInvariant(1000));
@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Validation failed with internal error.", "my-field")
new ValidationError("Validation failed with internal error.", "myField")
});
}
@ -111,12 +111,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
[Fact]
public async Task Should_add_error_if_validating_data_with_invalid_field()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant,
schema = schema.AddNumber(1, "myField", Partitioning.Invariant,
new NumberFieldProperties { MaxValue = 100 });
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddInvariant(1000));
@ -125,18 +125,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Must be less or equal to 100.", "my-field.iv")
new ValidationError("Must be less or equal to 100.", "myField.iv")
});
}
[Fact]
public async Task Should_add_error_if_non_localizable_data_field_contains_language()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant);
schema = schema.AddNumber(1, "myField", Partitioning.Invariant);
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddLocalized("es", 1)
.AddLocalized("it", 1));
@ -146,15 +146,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Not a known invariant value.", "my-field.es"),
new ValidationError("Not a known invariant value.", "my-field.it")
new ValidationError("Not a known invariant value.", "myField.es"),
new ValidationError("Not a known invariant value.", "myField.it")
});
}
[Fact]
public async Task Should_add_error_if_validating_data_with_invalid_localizable_field()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Language,
schema = schema.AddNumber(1, "myField", Partitioning.Language,
new NumberFieldProperties { IsRequired = true });
var data =
@ -165,15 +165,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Field is required.", "my-field.de"),
new ValidationError("Field is required.", "my-field.en")
new ValidationError("Field is required.", "myField.de"),
new ValidationError("Field is required.", "myField.en")
});
}
[Fact]
public async Task Should_add_error_if_required_data_field_is_not_in_bag()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant,
schema = schema.AddNumber(1, "myField", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = true });
var data =
@ -184,14 +184,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Field is required.", "my-field.iv")
new ValidationError("Field is required.", "myField.iv")
});
}
[Fact]
public async Task Should_add_error_if_required_data_string_field_is_not_in_bag()
{
schema = schema.AddString(1, "my-field", Partitioning.Invariant,
schema = schema.AddString(1, "myField", Partitioning.Invariant,
new StringFieldProperties { IsRequired = true });
var data =
@ -202,18 +202,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Field is required.", "my-field.iv")
new ValidationError("Field is required.", "myField.iv")
});
}
[Fact]
public async Task Should_add_error_if_data_contains_invalid_language()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Language);
schema = schema.AddNumber(1, "myField", Partitioning.Language);
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddLocalized("de", 1)
.AddLocalized("ru", 1));
@ -223,7 +223,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Not a known language.", "my-field.ru")
new ValidationError("Not a known language.", "myField.ru")
});
}
@ -236,12 +236,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
.Set(Language.IT, true)
.Remove(Language.EN);
schema = schema.AddString(1, "my-field", Partitioning.Language,
schema = schema.AddString(1, "myField", Partitioning.Language,
new StringFieldProperties { IsRequired = true });
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddLocalized("es", "value"));
@ -253,11 +253,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
[Fact]
public async Task Should_add_error_if_data_contains_unsupported_language()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Language);
schema = schema.AddNumber(1, "myField", Partitioning.Language);
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddLocalized("es", 1)
.AddLocalized("it", 1));
@ -267,8 +267,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Not a known language.", "my-field.es"),
new ValidationError("Not a known language.", "my-field.it")
new ValidationError("Not a known language.", "myField.es"),
new ValidationError("Not a known language.", "myField.it")
});
}
@ -292,12 +292,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
[Fact]
public async Task Should_add_error_if_validating_partial_data_with_invalid_field()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant,
schema = schema.AddNumber(1, "myField", Partitioning.Invariant,
new NumberFieldProperties { MaxValue = 100 });
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddInvariant(1000));
@ -306,18 +306,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Must be less or equal to 100.", "my-field.iv")
new ValidationError("Must be less or equal to 100.", "myField.iv")
});
}
[Fact]
public async Task Should_add_error_if_non_localizable_partial_data_field_contains_language()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant);
schema = schema.AddNumber(1, "myField", Partitioning.Invariant);
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddLocalized("es", 1)
.AddLocalized("it", 1));
@ -327,15 +327,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Not a known invariant value.", "my-field.es"),
new ValidationError("Not a known invariant value.", "my-field.it")
new ValidationError("Not a known invariant value.", "myField.es"),
new ValidationError("Not a known invariant value.", "myField.it")
});
}
[Fact]
public async Task Should_not_add_error_if_validating_partial_data_with_invalid_localizable_field()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Language,
schema = schema.AddNumber(1, "myField", Partitioning.Language,
new NumberFieldProperties { IsRequired = true });
var data =
@ -349,7 +349,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
[Fact]
public async Task Should_not_add_error_if_required_partial_data_field_is_not_in_bag()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant,
schema = schema.AddNumber(1, "myField", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = true });
var data =
@ -363,11 +363,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
[Fact]
public async Task Should_add_error_if_partial_data_contains_invalid_language()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Language);
schema = schema.AddNumber(1, "myField", Partitioning.Language);
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddLocalized("de", 1)
.AddLocalized("ru", 1));
@ -377,18 +377,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Not a known language.", "my-field.ru")
new ValidationError("Not a known language.", "myField.ru")
});
}
[Fact]
public async Task Should_add_error_if_partial_data_contains_unsupported_language()
{
schema = schema.AddNumber(1, "my-field", Partitioning.Language);
schema = schema.AddNumber(1, "myField", Partitioning.Language);
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddLocalized("es", 1)
.AddLocalized("it", 1));
@ -398,25 +398,25 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Not a known language.", "my-field.es"),
new ValidationError("Not a known language.", "my-field.it")
new ValidationError("Not a known language.", "myField.es"),
new ValidationError("Not a known language.", "myField.it")
});
}
[Fact]
public async Task Should_add_error_if_array_field_has_required_nested_field()
{
schema = schema.AddArray(1, "my-field", Partitioning.Invariant, f => f.
AddNumber(2, "my-nested", new NumberFieldProperties { IsRequired = true }));
schema = schema.AddArray(1, "myField", Partitioning.Invariant, f => f.
AddNumber(2, "myNested", new NumberFieldProperties { IsRequired = true }));
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddInvariant(
JsonValue.Array(
JsonValue.Object(),
JsonValue.Object().Add("my-nested", 1),
JsonValue.Object().Add("myNested", 1),
JsonValue.Object())));
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
@ -424,8 +424,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Field is required.", "my-field.iv[1].my-nested"),
new ValidationError("Field is required.", "my-field.iv[3].my-nested")
new ValidationError("Field is required.", "myField.iv[1].myNested"),
new ValidationError("Field is required.", "myField.iv[3].myNested")
});
}
@ -445,12 +445,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
[Fact]
public async Task Should_not_add_error_if_nested_separator_not_defined()
{
schema = schema.AddArray(1, "my-field", Partitioning.Invariant, f => f.
AddUI(2, "my-nested"));
schema = schema.AddArray(1, "myField", Partitioning.Invariant, f => f.
AddUI(2, "myNested"));
var data =
new ContentData()
.AddField("my-field",
.AddField("myField",
new ContentFieldData()
.AddInvariant(
JsonValue.Array(

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/DateTimeFieldTests.cs

@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new DateTimeFieldProperties());
Assert.Equal("my-datetime", sut.Name);
Assert.Equal("myDatetime", sut.Name);
}
[Fact]
@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<DateTimeFieldProperties> Field(DateTimeFieldProperties properties)
{
return Fields.DateTime(1, "my-datetime", Partitioning.Invariant, properties);
return Fields.DateTime(1, "myDatetime", Partitioning.Invariant, properties);
}
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/GeolocationFieldTests.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new GeolocationFieldProperties());
Assert.Equal("my-geolocation", sut.Name);
Assert.Equal("myGeolocation", sut.Name);
}
[Fact]
@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<GeolocationFieldProperties> Field(GeolocationFieldProperties properties)
{
return Fields.Geolocation(1, "my-geolocation", Partitioning.Invariant, properties);
return Fields.Geolocation(1, "myGeolocation", Partitioning.Invariant, properties);
}
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/JsonFieldTests.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new JsonFieldProperties());
Assert.Equal("my-json", sut.Name);
Assert.Equal("myJson", sut.Name);
}
[Fact]
@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<JsonFieldProperties> Field(JsonFieldProperties properties)
{
return Fields.Json(1, "my-json", Partitioning.Invariant, properties);
return Fields.Json(1, "myJson", Partitioning.Invariant, properties);
}
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/NumberFieldTests.cs

@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new NumberFieldProperties());
Assert.Equal("my-number", sut.Name);
Assert.Equal("myNumber", sut.Name);
}
[Fact]
@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<NumberFieldProperties> Field(NumberFieldProperties properties)
{
return Fields.Number(1, "my-number", Partitioning.Invariant, properties);
return Fields.Number(1, "myNumber", Partitioning.Invariant, properties);
}
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/ReferencesFieldTests.cs

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new ReferencesFieldProperties());
Assert.Equal("my-refs", sut.Name);
Assert.Equal("myRefs", sut.Name);
}
[Fact]
@ -173,7 +173,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<ReferencesFieldProperties> Field(ReferencesFieldProperties properties)
{
return Fields.References(1, "my-refs", Partitioning.Invariant, properties);
return Fields.References(1, "myRefs", Partitioning.Invariant, properties);
}
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs

@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new StringFieldProperties());
Assert.Equal("my-string", sut.Name);
Assert.Equal("myString", sut.Name);
}
[Fact]
@ -144,7 +144,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<StringFieldProperties> Field(StringFieldProperties properties)
{
return Fields.String(1, "my-string", Partitioning.Invariant, properties);
return Fields.String(1, "myString", Partitioning.Invariant, properties);
}
}
}

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/TagsFieldTests.cs

@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new TagsFieldProperties());
Assert.Equal("my-tags", sut.Name);
Assert.Equal("myTags", sut.Name);
}
[Fact]
@ -154,7 +154,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<TagsFieldProperties> Field(TagsFieldProperties properties)
{
return Fields.Tags(1, "my-tags", Partitioning.Invariant, properties);
return Fields.Tags(1, "myTags", Partitioning.Invariant, properties);
}
}
}

26
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/UIFieldTests.cs

@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var sut = Field(new UIFieldProperties());
Assert.Equal("my-ui", sut.Name);
Assert.Equal("myUI", sut.Name);
}
[Fact]
@ -67,13 +67,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var schema =
new Schema("my-schema")
.AddUI(1, "my-ui1", Partitioning.Invariant)
.AddUI(2, "my-ui2", Partitioning.Invariant);
.AddUI(1, "myUI1", Partitioning.Invariant)
.AddUI(2, "myUI2", Partitioning.Invariant);
var data =
new ContentData()
.AddField("my-ui1", new ContentFieldData())
.AddField("my-ui2", new ContentFieldData()
.AddField("myUI1", new ContentFieldData())
.AddField("myUI2", new ContentFieldData()
.AddInvariant(null));
var dataErrors = new List<ValidationError>();
@ -83,8 +83,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
dataErrors.Should().BeEquivalentTo(
new[]
{
new ValidationError("Value must not be defined.", "my-ui1"),
new ValidationError("Value must not be defined.", "my-ui2")
new ValidationError("Value must not be defined.", "myUI1"),
new ValidationError("Value must not be defined.", "myUI2")
});
}
@ -93,16 +93,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{
var schema =
new Schema("my-schema")
.AddArray(1, "my-array", Partitioning.Invariant, array => array
.AddUI(101, "my-ui"));
.AddArray(1, "myArray", Partitioning.Invariant, array => array
.AddUI(101, "myUI"));
var data =
new ContentData()
.AddField("my-array", new ContentFieldData()
.AddField("myArray", new ContentFieldData()
.AddInvariant(
JsonValue.Array(
JsonValue.Object()
.Add("my-ui", null))));
.Add("myUI", null))));
var dataErrors = new List<ValidationError>();
@ -111,13 +111,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
dataErrors.Should().BeEquivalentTo(
new[]
{
new ValidationError("Value must not be defined.", "my-array.iv[1].my-ui")
new ValidationError("Value must not be defined.", "myArray.iv[1].myUI")
});
}
private static NestedField<UIFieldProperties> Field(UIFieldProperties properties)
{
return new NestedField<UIFieldProperties>(1, "my-ui", properties);
return new NestedField<UIFieldProperties>(1, "myUI", properties);
}
}
}

118
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueObjectValuesValidator.cs

@ -0,0 +1,118 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Core.ValidateContent.Validators;
using Squidex.Infrastructure.Json.Objects;
using Xunit;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{
public class UniqueObjectValuesValidatorTests : IClassFixture<TranslationsFixture>
{
private readonly List<string> errors = new List<string>();
[Fact]
public async Task Should_not_add_errors_if_value_is_invalid()
{
var sut = new UniqueObjectValuesValidator(new[] { "myString" });
await sut.ValidateAsync(1, errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_errors_if_value_is_null()
{
var sut = new UniqueObjectValuesValidator(new[] { "myString" });
await sut.ValidateAsync(null, errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_objects_contain_not_duplicates()
{
var sut = new UniqueObjectValuesValidator(new[] { "myString" });
await sut.ValidateAsync(new[]
{
new JsonObject()
.Add("myString", "1"),
new JsonObject()
.Add("myString", "2")
},
errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_objects_contain_unchecked_duplicates()
{
var sut = new UniqueObjectValuesValidator(new[] { "myString" });
await sut.ValidateAsync(new[]
{
new JsonObject()
.Add("other", "1"),
new JsonObject()
.Add("other", "1")
},
errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_add_error_if_objects_contain_duplicates()
{
var sut = new UniqueObjectValuesValidator(new[] { "myString" });
await sut.ValidateAsync(new[]
{
new JsonObject()
.Add("myString", "1"),
new JsonObject()
.Add("myString", "1")
},
errors);
errors.Should().BeEquivalentTo(
new[] { "Must not contain items with duplicate 'myString' fields." });
}
[Fact]
public async Task Should_add_errors_if_objects_contain_multiple_duplicates()
{
var sut = new UniqueObjectValuesValidator(new[] { "myString", "myNumber" });
await sut.ValidateAsync(new[]
{
new JsonObject()
.Add("myString", "1")
.Add("myNumber", 1),
new JsonObject()
.Add("myString", "1")
.Add("myNumber", 1),
},
errors);
errors.Should().BeEquivalentTo(
new[]
{
"Must not contain items with duplicate 'myString' fields.",
"Must not contain items with duplicate 'myNumber' fields."
});
}
}
}

60
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/UniqueValidatorTests.cs

@ -21,36 +21,6 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{
private readonly List<string> errors = new List<string>();
[Fact]
public async Task Should_add_error_if_other_content_with_string_value_found()
{
var filter = string.Empty;
var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f));
await sut.ValidateAsync("hi", errors, updater: c => c.Nested("property").Nested("iv"));
errors.Should().BeEquivalentTo(
new[] { "property.iv: Another content with the same value exists." });
Assert.Equal("Data.property.iv == 'hi'", filter);
}
[Fact]
public async Task Should_add_error_if_other_content_with_double_value_found()
{
var filter = string.Empty;
var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f));
await sut.ValidateAsync(12.5, errors, updater: c => c.Nested("property").Nested("iv"));
errors.Should().BeEquivalentTo(
new[] { "property.iv: Another content with the same value exists." });
Assert.Equal("Data.property.iv == 12.5", filter);
}
[Fact]
public async Task Should_not_check_uniqueness_if_localized_string()
{
@ -99,6 +69,36 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
Assert.Empty(errors);
}
[Fact]
public async Task Should_add_error_if_other_content_with_string_value_found()
{
var filter = string.Empty;
var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f));
await sut.ValidateAsync("hi", errors, updater: c => c.Nested("property").Nested("iv"));
errors.Should().BeEquivalentTo(
new[] { "property.iv: Another content with the same value exists." });
Assert.Equal("Data.property.iv == 'hi'", filter);
}
[Fact]
public async Task Should_add_error_if_other_content_with_double_value_found()
{
var filter = string.Empty;
var sut = new UniqueValidator(FoundDuplicates(DomainId.NewGuid(), f => filter = f));
await sut.ValidateAsync(12.5, errors, updater: c => c.Nested("property").Nested("iv"));
errors.Should().BeEquivalentTo(
new[] { "property.iv: Another content with the same value exists." });
Assert.Equal("Data.property.iv == 12.5", filter);
}
private static CheckUniqueness FoundDuplicates(DomainId id, Action<string>? filter = null)
{
return filterNode =>

4
frontend/app/features/content/shared/forms/field-editor.component.ts

@ -7,7 +7,7 @@
import { Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { AbstractControl, FormControl } from '@angular/forms';
import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, MathHelper, RootFieldDto, Types } from '@app/shared';
import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, MathHelper, RootFieldDto, Types, value$ } from '@app/shared';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@ -68,7 +68,7 @@ export class FieldEditorComponent implements OnChanges {
previousControl.form['_clearChangeFns']();
}
this.isEmpty = this.formModel.form.valueChanges.pipe(map(x => Types.isUndefined(x) || Types.isNull(x)));
this.isEmpty = value$(this.formModel.form).pipe(map(x => Types.isUndefined(x) || Types.isNull(x)));
}
}

3
frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html

@ -16,6 +16,7 @@
[languages]="(languagesState.isoLanguages | async)!"
[field]="field"
[fieldForm]="editForm.form"
[schema]="schema"
[isEditable]="true"
[isLocalizable]="isLocalizable"
[settings]="settings">
@ -55,7 +56,7 @@
<input type="text" class="form-control" formControlName="name" maxlength="40" #nameInput placeholder="{{ 'schemas.field.namePlaceholder' | sqxTranslate }}" sqxFocusOnInit>
</div>
<div class="form-group" *ngIf="!parent && (addFieldForm.isContentField | async)">
<div class="form-group" *ngIf="schema.type !== 'Component' && !parent && (addFieldForm.isContentField | async)">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="isLocalizable" formControlName="isLocalizable">
<label class="form-check-label" for="isLocalizable">

1
frontend/app/features/schemas/pages/schema/fields/field.component.html

@ -98,6 +98,7 @@
[languages]="languages"
[fieldForm]="editForm.form"
[field]="field"
[schema]="schema"
[isEditable]="isEditable"
[isLocalizable]="isLocalizable"
[settings]="settings">

7
frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.ts

@ -7,10 +7,10 @@
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FieldDto } from '@app/shared';
import { FieldDto, SchemaDto } from '@app/shared';
@Component({
selector: 'sqx-field-form-common[field][fieldForm]',
selector: 'sqx-field-form-common[field][fieldForm][schema]',
styleUrls: ['./field-form-common.component.scss'],
templateUrl: './field-form-common.component.html',
})
@ -20,4 +20,7 @@ export class FieldFormCommonComponent {
@Input()
public field: FieldDto;
@Input()
public schema: SchemaDto;
}

7
frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.ts

@ -7,10 +7,10 @@
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FieldDto } from '@app/shared';
import { FieldDto, SchemaDto } from '@app/shared';
@Component({
selector: 'sqx-field-form-ui[field][fieldForm]',
selector: 'sqx-field-form-ui[field][fieldForm][schema]',
styleUrls: ['./field-form-ui.component.scss'],
templateUrl: './field-form-ui.component.html',
})
@ -20,4 +20,7 @@ export class FieldFormUIComponent {
@Input()
public field: FieldDto;
@Input()
public schema: SchemaDto;
}

4
frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html

@ -91,7 +91,8 @@
[isLocalizable]="isLocalizable"
[fieldForm]="fieldForm"
[field]="field"
[properties]="field.rawProperties">
[properties]="field.rawProperties"
[schema]="schema">
</sqx-number-validation>
</ng-container>
<ng-container *ngSwitchCase="'References'">
@ -110,6 +111,7 @@
[fieldForm]="fieldForm"
[field]="field"
[properties]="field.rawProperties"
[schema]="schema"
[settings]="settings">
</sqx-string-validation>
</ng-container>

7
frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts

@ -7,10 +7,10 @@
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AppSettingsDto, FieldDto, LanguageDto } from '@app/shared';
import { AppSettingsDto, FieldDto, LanguageDto, SchemaDto } from '@app/shared';
@Component({
selector: 'sqx-field-form-validation[field][fieldForm][languages][settings]',
selector: 'sqx-field-form-validation[field][fieldForm][languages][schema][settings]',
styleUrls: ['./field-form-validation.component.scss'],
templateUrl: './field-form-validation.component.html',
})
@ -21,6 +21,9 @@ export class FieldFormValidationComponent {
@Input()
public field: FieldDto;
@Input()
public schema: SchemaDto;
@Input()
public settings: AppSettingsDto;

7
frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.html

@ -31,7 +31,8 @@
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 0">
<sqx-field-form-common
[fieldForm]="fieldForm"
[field]="field">
[field]="field"
[schema]="schema">
</sqx-field-form-common>
</div>
@ -40,6 +41,7 @@
[languages]="languages"
[fieldForm]="fieldForm"
[field]="field"
[schema]="schema"
[isLocalizable]="isLocalizable"
[settings]="settings" >
</sqx-field-form-validation>
@ -48,6 +50,7 @@
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 2">
<sqx-field-form-ui
[fieldForm]="fieldForm"
[field]="field">
[field]="field"
[schema]="schema">
</sqx-field-form-ui>
</div>

7
frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.ts

@ -7,10 +7,10 @@
import { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AppSettingsDto, FieldDto, LanguageDto } from '@app/shared';
import { AppSettingsDto, FieldDto, LanguageDto, SchemaDto } from '@app/shared';
@Component({
selector: 'sqx-field-form[field][fieldForm][languages][settings]',
selector: 'sqx-field-form[field][fieldForm][languages][schema][settings]',
styleUrls: ['./field-form.component.scss'],
templateUrl: './field-form.component.html',
})
@ -27,6 +27,9 @@ export class FieldFormComponent implements AfterViewInit {
@Input()
public field: FieldDto;
@Input()
public schema: SchemaDto;
@Input()
public settings: AppSettingsDto;

8
frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html

@ -16,4 +16,12 @@
</div>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.array.uniqueFields' | sqxTranslate }}</label>
<div class="col-9">
<sqx-tag-editor formControlName="uniqueFields"></sqx-tag-editor>
</div>
</div>
</div>

8
frontend/app/features/schemas/pages/schema/fields/types/components-validation.component.html

@ -26,5 +26,13 @@
</sqx-tag-editor>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.array.uniqueFields' | sqxTranslate }}</label>
<div class="col-9">
<sqx-tag-editor formControlName="uniqueFields"></sqx-tag-editor>
</div>
</div>
</div>

9
frontend/app/features/schemas/pages/schema/fields/types/number-validation.component.ts

@ -7,10 +7,10 @@
import { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FieldDto, LanguageDto, NumberFieldPropertiesDto, RootFieldDto, Types } from '@app/shared';
import { FieldDto, LanguageDto, NumberFieldPropertiesDto, RootFieldDto, SchemaDto, Types } from '@app/shared';
@Component({
selector: 'sqx-number-validation[field][fieldForm][properties]',
selector: 'sqx-number-validation[field][fieldForm][properties][schema]',
styleUrls: ['number-validation.component.scss'],
templateUrl: 'number-validation.component.html',
})
@ -21,6 +21,9 @@ export class NumberValidationComponent {
@Input()
public field: FieldDto;
@Input()
public schema: SchemaDto;
@Input()
public properties: NumberFieldPropertiesDto;
@ -31,6 +34,6 @@ export class NumberValidationComponent {
public isLocalizable?: boolean | null;
public get showUnique() {
return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable;
return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component';
}
}

9
frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts

@ -7,11 +7,11 @@
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { AppSettingsDto, fadeAnimation, FieldDto, hasNoValue$, hasValue$, LanguageDto, ModalModel, PatternDto, ResourceOwner, RootFieldDto, StringFieldPropertiesDto, STRING_CONTENT_TYPES, Types, value$ } from '@app/shared';
import { AppSettingsDto, fadeAnimation, FieldDto, hasNoValue$, hasValue$, LanguageDto, ModalModel, PatternDto, ResourceOwner, RootFieldDto, SchemaDto, StringFieldPropertiesDto, STRING_CONTENT_TYPES, Types, value$ } from '@app/shared';
import { Observable } from 'rxjs';
@Component({
selector: 'sqx-string-validation[field][fieldForm][properties]',
selector: 'sqx-string-validation[field][fieldForm][properties][schema]',
styleUrls: ['string-validation.component.scss'],
templateUrl: 'string-validation.component.html',
animations: [
@ -27,6 +27,9 @@ export class StringValidationComponent extends ResourceOwner implements OnChange
@Input()
public field: FieldDto;
@Input()
public schema: SchemaDto;
@Input()
public properties: StringFieldPropertiesDto;
@ -46,7 +49,7 @@ export class StringValidationComponent extends ResourceOwner implements OnChange
public patternsModal = new ModalModel();
public get showUnique() {
return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable;
return Types.is(this.field, RootFieldDto) && !this.field.isLocalizable && this.schema.type !== 'Component';
}
public ngOnChanges(changes: SimpleChanges) {

18
frontend/app/features/schemas/pages/schema/schema-page.component.html

@ -8,12 +8,12 @@
{{ 'schemas.tabFields' | sqxTranslate }}
</a>
</li>
<li>
<li *ngIf="schema.type !== 'Component'">
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'ui' }" [class.active]="tab === 'ui'">
{{ 'schemas.tabUI' | sqxTranslate }}
</a>
</li>
<li>
<li *ngIf="schema.type !== 'Component'">
<a class="nav-link" [routerLink]="[]" [queryParams]="{ tab: 'scripts' }" [class.active]="tab === 'scripts'">
{{ 'schemas.tabScripts' | sqxTranslate }}
</a>
@ -92,9 +92,17 @@
<ng-container *ngSwitchCase="'more'">
<sqx-list-view innerWidth="50rem">
<div>
<sqx-schema-preview-urls-form [schema]="schema"> </sqx-schema-preview-urls-form>
<sqx-schema-field-rules-form [schema]="schema"> </sqx-schema-field-rules-form>
<sqx-schema-edit-form [schema]="schema"></sqx-schema-edit-form>
<sqx-schema-preview-urls-form *ngIf="schema.type !== 'Component'"
[schema]="schema">
</sqx-schema-preview-urls-form>
<sqx-schema-field-rules-form
[schema]="schema">
</sqx-schema-field-rules-form>
<sqx-schema-edit-form
[schema]="schema">
</sqx-schema-edit-form>
</div>
</sqx-list-view>
</ng-container>

564
frontend/app/framework/angular/forms/validators.spec.ts

@ -9,398 +9,458 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
import { DateTime } from '@app/framework/internal';
import { ValidatorsEx } from './validators';
describe('ValidatorsEx.between', () => {
it('should return null validator if no min value or max value', () => {
const validator = ValidatorsEx.between(undefined, undefined);
describe('ValidatorsEx', () => {
describe('between', () => {
it('should return null validator if no min value or max value', () => {
const validator = ValidatorsEx.between(undefined, undefined);
expect(validator).toBe(Validators.nullValidator);
});
expect(validator).toBe(Validators.nullValidator);
});
it('should return null if value is equal to min and max', () => {
const input = new FormControl(3);
it('should return null if value is equal to min and max', () => {
const input = new FormControl(3);
const error = <any>ValidatorsEx.between(3, 3)(input);
const error = <any>ValidatorsEx.between(3, 3)(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return null if value is valid', () => {
const input = new FormControl(4);
it('should return null if value is valid', () => {
const input = new FormControl(4);
const error = <any>ValidatorsEx.between(1, 5)(input);
const error = <any>ValidatorsEx.between(1, 5)(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return null if value is null', () => {
const input = new FormControl(null);
it('should return null if value is null', () => {
const input = new FormControl(null);
const error = <any>ValidatorsEx.between(1, 5)(input);
const error = <any>ValidatorsEx.between(1, 5)(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return null if value is undefined', () => {
const input = new FormControl(undefined);
it('should return null if value is undefined', () => {
const input = new FormControl(undefined);
const error = <any>ValidatorsEx.between(1, 5)(input);
const error = <any>ValidatorsEx.between(1, 5)(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return error if less than min', () => {
const input = new FormControl(0);
it('should return error if less than min', () => {
const input = new FormControl(0);
const error = <any>ValidatorsEx.between(1, undefined)(input);
const error = <any>ValidatorsEx.between(1, undefined)(input);
expect(error.min).toBeDefined();
});
expect(error.min).toBeDefined();
});
it('should return error if greater than max', () => {
const input = new FormControl(6);
it('should return error if greater than max', () => {
const input = new FormControl(6);
const error = <any>ValidatorsEx.between(undefined, 5)(input);
const error = <any>ValidatorsEx.between(undefined, 5)(input);
expect(error.max).toBeDefined();
});
expect(error.max).toBeDefined();
});
it('should return error if not in range', () => {
const input = new FormControl(1);
it('should return error if not in range', () => {
const input = new FormControl(1);
const error = <any>ValidatorsEx.between(2, 4)(input);
const error = <any>ValidatorsEx.between(2, 4)(input);
expect(error.between).toBeDefined();
});
expect(error.between).toBeDefined();
});
it('should return error if not equal to min and max', () => {
const input = new FormControl(2);
it('should return error if not equal to min and max', () => {
const input = new FormControl(2);
const error = <any>ValidatorsEx.between(3, 3)(input);
const error = <any>ValidatorsEx.between(3, 3)(input);
expect(error.exactly).toBeDefined();
expect(error.exactly).toBeDefined();
});
});
});
describe('ValidatorsEx.betweenLength', () => {
it('should return null validator if no min value or max value', () => {
const validator = ValidatorsEx.betweenLength(undefined, undefined);
describe('betweenLength', () => {
it('should return null validator if no min value or max value', () => {
const validator = ValidatorsEx.betweenLength(undefined, undefined);
expect(validator).toBe(Validators.nullValidator);
});
expect(validator).toBe(Validators.nullValidator);
});
it('should return null if value is equal to min and max', () => {
const input = new FormControl('xxx');
it('should return null if value is equal to min and max', () => {
const input = new FormControl('xxx');
const error = <any>ValidatorsEx.betweenLength(3, 3)(input);
const error = <any>ValidatorsEx.betweenLength(3, 3)(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return null if value is valid', () => {
const input = new FormControl('xxxx');
it('should return null if value is valid', () => {
const input = new FormControl('xxxx');
const error = <any>ValidatorsEx.betweenLength(1, 5)(input);
const error = <any>ValidatorsEx.betweenLength(1, 5)(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return null if value is null', () => {
const input = new FormControl(null);
it('should return null if value is null', () => {
const input = new FormControl(null);
const error = <any>ValidatorsEx.betweenLength(1, 5)(input);
const error = <any>ValidatorsEx.betweenLength(1, 5)(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return null if value is undefined', () => {
const input = new FormControl(undefined);
it('should return null if value is undefined', () => {
const input = new FormControl(undefined);
const error = <any>ValidatorsEx.betweenLength(1, 5)(input);
const error = <any>ValidatorsEx.betweenLength(1, 5)(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return error if less than min', () => {
const input = new FormControl('x');
it('should return error if less than min', () => {
const input = new FormControl('x');
const error = <any>ValidatorsEx.betweenLength(2, undefined)(input);
const error = <any>ValidatorsEx.betweenLength(2, undefined)(input);
expect(error.minlength).toBeDefined();
});
expect(error.minlength).toBeDefined();
});
it('should return error if greater than max', () => {
const input = new FormControl('xxxxxx');
it('should return error if greater than max', () => {
const input = new FormControl('xxxxxx');
const error = <any>ValidatorsEx.betweenLength(undefined, 5)(input);
const error = <any>ValidatorsEx.betweenLength(undefined, 5)(input);
expect(error.maxlength).toBeDefined();
});
expect(error.maxlength).toBeDefined();
});
it('should return error if not in range', () => {
const input = new FormControl('x');
it('should return error if not in range', () => {
const input = new FormControl('x');
const error = <any>ValidatorsEx.betweenLength(2, 4)(input);
const error = <any>ValidatorsEx.betweenLength(2, 4)(input);
expect(error.betweenlength).toBeDefined();
});
expect(error.betweenlength).toBeDefined();
});
it('should return error if not equal to min and max', () => {
const input = new FormControl('xx');
it('should return error if not equal to min and max', () => {
const input = new FormControl('xx');
const error = <any>ValidatorsEx.betweenLength(3, 3)(input);
const error = <any>ValidatorsEx.betweenLength(3, 3)(input);
expect(error.exactlylength).toBeDefined();
expect(error.exactlylength).toBeDefined();
});
});
});
describe('ValidatorsEx.validDateTime', () => {
it('should return null validator if valid is not defined', () => {
const input = new FormControl(null);
describe('validDateTime', () => {
it('should return null validator if valid is not defined', () => {
const input = new FormControl(null);
const error = <any>ValidatorsEx.validDateTime()(input);
const error = <any>ValidatorsEx.validDateTime()(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return null if date time is valid', () => {
const input = new FormControl(DateTime.now().toISOString());
it('should return null if date time is valid', () => {
const input = new FormControl(DateTime.now().toISOString());
const error = ValidatorsEx.validDateTime()(input);
const error = ValidatorsEx.validDateTime()(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return error if value is invalid date', () => {
const input = new FormControl('invalid');
it('should return error if value is invalid date', () => {
const input = new FormControl('invalid');
const error = <any>ValidatorsEx.validDateTime()(input);
const error = <any>ValidatorsEx.validDateTime()(input);
expect(error.validdatetime).toBeDefined();
expect(error.validdatetime).toBeDefined();
});
});
});
describe('ValidatorsEx.validValues', () => {
it('should return null validator if values not defined', () => {
const validator = ValidatorsEx.validValues(null!);
describe('validValues', () => {
it('should return null validator if values not defined', () => {
const validator = ValidatorsEx.validValues(null!);
expect(validator).toBe(Validators.nullValidator);
});
expect(validator).toBe(Validators.nullValidator);
});
it('should return null if value is in allowed values', () => {
const input = new FormControl(10);
it('should return null if value is in allowed values', () => {
const input = new FormControl(10);
const error = ValidatorsEx.validValues([10, 20, 30])(input);
const error = ValidatorsEx.validValues([10, 20, 30])(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return error if value is not in allowed values', () => {
const input = new FormControl(50);
it('should return error if value is not in allowed values', () => {
const input = new FormControl(50);
const error = <any>ValidatorsEx.validValues([10, 20, 30])(input);
const error = <any>ValidatorsEx.validValues([10, 20, 30])(input);
expect(error.validvalues).toBeDefined();
expect(error.validvalues).toBeDefined();
});
});
});
describe('ValidatorsEx.validArrayValues', () => {
it('should return null validator if values not defined', () => {
const validator = ValidatorsEx.validArrayValues(null!);
describe('validArrayValues', () => {
it('should return null validator if values not defined', () => {
const validator = ValidatorsEx.validArrayValues(null!);
expect(validator).toBe(Validators.nullValidator);
});
expect(validator).toBe(Validators.nullValidator);
});
it('should return null if value is in allowed values', () => {
const input = new FormControl([10, 20]);
it('should return null if value is in allowed values', () => {
const input = new FormControl([10, 20]);
const error = ValidatorsEx.validArrayValues([10, 20, 30])(input);
const error = ValidatorsEx.validArrayValues([10, 20, 30])(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return error if value is not in allowed values', () => {
const input = new FormControl([50]);
it('should return error if value is not in allowed values', () => {
const input = new FormControl([50]);
const error = <any>ValidatorsEx.validArrayValues([10, 20, 30])(input);
const error = <any>ValidatorsEx.validArrayValues([10, 20, 30])(input);
expect(error.validarrayvalues).toBeDefined();
expect(error.validarrayvalues).toBeDefined();
});
});
});
describe('ValidatorsEx.match', () => {
it('should revalidate if other control changes', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.');
describe('match', () => {
it('should revalidate if other control changes', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.');
const form = new FormGroup({
password: new FormControl('1'),
passwordConfirm: new FormControl('2', validator),
});
const form = new FormGroup({
password: new FormControl('1'),
passwordConfirm: new FormControl('2', validator),
form.controls['passwordConfirm'].setValue('1');
expect(form.valid).toBeTruthy();
form.controls['password'].setValue('2');
expect(form.controls['password'].valid).toBeTruthy();
expect(form.controls['passwordConfirm'].valid).toBeFalsy();
});
form.controls['passwordConfirm'].setValue('1');
it('should return error if not the same value', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.');
expect(form.valid).toBeTruthy();
const form = new FormGroup({
password: new FormControl('1'),
passwordConfirm: new FormControl('2', validator),
});
form.controls['password'].setValue('2');
expect(validator(form.controls['passwordConfirm'])).toEqual({ match: { message: 'Passwords are not the same.' } });
});
expect(form.controls['password'].valid).toBeTruthy();
expect(form.controls['passwordConfirm'].valid).toBeFalsy();
});
it('should return empty object if values are the same', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.');
it('should return error if not the same value', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.');
const form = new FormGroup({
password: new FormControl('1'),
passwordConfirm: new FormControl('1', validator),
});
const form = new FormGroup({
password: new FormControl('1'),
passwordConfirm: new FormControl('2', validator),
expect(validator(form.controls['passwordConfirm'])).toBeNull();
});
expect(validator(form.controls['passwordConfirm'])).toEqual({ match: { message: 'Passwords are not the same.' } });
});
it('should throw error if other object is not found', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.');
it('should return empty object if values are the same', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.');
const form = new FormGroup({
passwordConfirm: new FormControl('2', validator),
});
const form = new FormGroup({
password: new FormControl('1'),
passwordConfirm: new FormControl('1', validator),
expect(() => validator(form.controls['passwordConfirm'])).toThrow();
});
expect(validator(form.controls['passwordConfirm'])).toBeNull();
it('should return empty object if control has no parent', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.');
const control = new FormControl('2', validator);
expect(validator(control)).toBeNull();
});
});
it('should throw error if other object is not found', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.');
describe('pattern', () => {
it('should return null validator if pattern not defined', () => {
const validator = ValidatorsEx.pattern(undefined!, undefined);
const form = new FormGroup({
passwordConfirm: new FormControl('2', validator),
expect(validator).toBe(Validators.nullValidator);
});
expect(() => validator(form.controls['passwordConfirm'])).toThrow();
});
it('should return null if value is valid pattern', () => {
const input = new FormControl('1234');
it('should return empty object if control has no parent', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.');
const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input);
const control = new FormControl('2', validator);
expect(error).toBeNull();
});
expect(validator(control)).toBeNull();
});
});
it('should return null if value is null string', () => {
const input = new FormControl(null);
describe('ValidatorsEx.pattern', () => {
it('should return null validator if pattern not defined', () => {
const validator = ValidatorsEx.pattern(undefined!, undefined);
const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input);
expect(validator).toBe(Validators.nullValidator);
});
expect(error).toBeNull();
});
it('should return null if value is valid pattern', () => {
const input = new FormControl('1234');
it('should return null if value is empty string', () => {
const input = new FormControl('');
const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input);
const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return null if value is null string', () => {
const input = new FormControl(null);
it('should return error with message if value does not match pattern string', () => {
const input = new FormControl('abc');
const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input);
const error = <any>ValidatorsEx.pattern('[0-9]{1,4}', 'My-Message')(input);
const expected: any = {
patternmessage: {
requiredPattern: '^[0-9]{1,4}$', actualValue: 'abc', message: 'My-Message',
},
};
expect(error).toBeNull();
});
expect(error).toEqual(expected);
});
it('should return null if value is empty string', () => {
const input = new FormControl('');
it('should return error with message if value does not match pattern', () => {
const input = new FormControl('abc');
const error = ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input);
const error = <any>ValidatorsEx.pattern(/^[0-9]{1,4}$/, 'My-Message')(input);
const expected: any = {
patternmessage: {
requiredPattern: '/^[0-9]{1,4}$/', actualValue: 'abc', message: 'My-Message',
},
};
expect(error).toBeNull();
});
expect(error).toEqual(expected);
});
it('should return error with message if value does not match pattern string', () => {
const input = new FormControl('abc');
it('should return error without message if value does not match pattern string', () => {
const input = new FormControl('abc');
const error = <any>ValidatorsEx.pattern('[0-9]{1,4}', 'My-Message')(input);
const expected: any = {
patternmessage: {
requiredPattern: '^[0-9]{1,4}$', actualValue: 'abc', message: 'My-Message',
},
};
const error = <any>ValidatorsEx.pattern('[0-9]{1,4}')(input);
const expected: any = {
pattern: {
requiredPattern: '^[0-9]{1,4}$', actualValue: 'abc',
},
};
expect(error).toEqual(expected);
});
expect(error).toEqual(expected);
});
it('should return error with message if value does not match pattern', () => {
const input = new FormControl('abc');
it('should return error without message if value does not match pattern', () => {
const input = new FormControl('abc');
const error = <any>ValidatorsEx.pattern(/^[0-9]{1,4}$/, 'My-Message')(input);
const expected: any = {
patternmessage: {
requiredPattern: '/^[0-9]{1,4}$/', actualValue: 'abc', message: 'My-Message',
},
};
const error = <any>ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input);
const expected: any = {
pattern: {
requiredPattern: '/^[0-9]{1,4}$/', actualValue: 'abc',
},
};
expect(error).toEqual(expected);
expect(error).toEqual(expected);
});
});
it('should return error without message if value does not match pattern string', () => {
const input = new FormControl('abc');
describe('uniqueStrings', () => {
it('should return null if value is null', () => {
const input = new FormControl(null);
const error = <any>ValidatorsEx.pattern('[0-9]{1,4}')(input);
const expected: any = {
pattern: {
requiredPattern: '^[0-9]{1,4}$', actualValue: 'abc',
},
};
const error = ValidatorsEx.uniqueStrings()(input);
expect(error).toEqual(expected);
});
expect(error).toBeNull();
});
it('should return error without message if value does not match pattern', () => {
const input = new FormControl('abc');
it('should return null if value is not a string array', () => {
const input = new FormControl([1, 2, 3]);
const error = <any>ValidatorsEx.pattern(/^[0-9]{1,4}$/)(input);
const expected: any = {
pattern: {
requiredPattern: '/^[0-9]{1,4}$/', actualValue: 'abc',
},
};
const error = ValidatorsEx.uniqueStrings()(input);
expect(error).toEqual(expected);
});
});
expect(error).toBeNull();
});
describe('ValidatorsEx.uniqueStrings', () => {
it('should return null if value is null', () => {
const input = new FormControl(null);
it('should return null if values are unique', () => {
const input = new FormControl(['1', '2', '3']);
const error = ValidatorsEx.uniqueStrings()(input);
const error = ValidatorsEx.uniqueStrings()(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return null if value is not a string array', () => {
const input = new FormControl([1, 2, 3]);
it('should return error if values are not unique', () => {
const input = new FormControl(['1', '2', '2', '3']);
const error = ValidatorsEx.uniqueStrings()(input);
const error = ValidatorsEx.uniqueStrings()(input);
expect(error).toBeNull();
expect(error).toEqual({ uniquestrings: false });
});
});
it('should return null if values are unique', () => {
const input = new FormControl(['1', '2', '3']);
describe('uniqueObjectValues', () => {
it('should return null if value is null', () => {
const input = new FormControl(null);
const error = ValidatorsEx.uniqueStrings()(input);
const error = ValidatorsEx.uniqueObjectValues(['myString'])(input);
expect(error).toBeNull();
});
expect(error).toBeNull();
});
it('should return null if value is not an object array', () => {
const input = new FormControl([1, 2, 3]);
const error = ValidatorsEx.uniqueObjectValues(['myString'])(input);
expect(error).toBeNull();
});
it('should return null if values array has one item', () => {
const input = new FormControl([{}]);
const error = ValidatorsEx.uniqueObjectValues(['myString'])(input);
expect(error).toBeNull();
});
it('should return error if values are not unique', () => {
const input = new FormControl(['1', '2', '2', '3']);
it('should return null if values array has no duplicate', () => {
const input = new FormControl([{ myString: '1' }, { myString: '2' }]);
const error = ValidatorsEx.uniqueStrings()(input);
const error = ValidatorsEx.uniqueObjectValues(['myString'])(input);
expect(error).toEqual({ uniquestrings: false });
expect(error).toBeNull();
});
it('should return null if values array has unchecked duplicate', () => {
const input = new FormControl([{ other: '1' }, { other: '1' }]);
const error = ValidatorsEx.uniqueObjectValues(['myString'])(input);
expect(error).toBeNull();
});
it('should return error if values array has duplicate', () => {
const input = new FormControl([{ myString: '1' }, { myString: '1' }]);
const error = ValidatorsEx.uniqueObjectValues(['myString'])(input);
expect(error).toEqual({ uniqueobjectvalues: { fields: 'myString' } });
});
it('should return error if values array has multiple duplicates', () => {
const input = new FormControl([{ myString: '1', myNumber: 2 }, { myString: '1', myNumber: 2 }]);
const error = ValidatorsEx.uniqueObjectValues(['myString', 'myNumber'])(input);
expect(error).toEqual({ uniqueobjectvalues: { fields: 'myString, myNumber' } });
});
});
});

41
frontend/app/framework/angular/forms/validators.ts

@ -9,7 +9,7 @@ import { AbstractControl, ValidatorFn, Validators } from '@angular/forms';
import { DateTime, Types } from '@app/framework/internal';
function isEmptyInputValue(value: any): boolean {
return value == null || value.length === 0;
return value == null || value === undefined || value.length === 0;
}
export module ValidatorsEx {
@ -191,4 +191,43 @@ export module ValidatorsEx {
return null;
};
}
export function uniqueObjectValues(fields: ReadonlyArray<string>): ValidatorFn {
return (control: AbstractControl) => {
if (isEmptyInputValue(control.value) || !Types.isArrayOfObject(control.value)) {
return null;
}
const items: any[] = control.value;
const duplicateKeys: object = {};
for (const field of fields) {
const values: any[] = [];
for (const item of items) {
if (item.hasOwnProperty(field)) {
const fieldValue = item[field];
for (const other of values) {
if (Types.equals(other, fieldValue)) {
duplicateKeys[field] = true;
break;
}
}
values.push(fieldValue);
}
}
}
const keys = Object.keys(duplicateKeys);
if (keys.length > 0) {
return { uniqueobjectvalues: { fields: keys.join(', ') } };
}
return null;
};
}
}

2
frontend/app/shared/services/schemas.types.ts

@ -181,6 +181,7 @@ export class ArrayFieldPropertiesDto extends FieldPropertiesDto {
public readonly maxItems?: number;
public readonly minItems?: number;
public readonly uniqueFields?: ReadonlyArray<string>;
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitArray(this);
@ -270,6 +271,7 @@ export class ComponentsFieldPropertiesDto extends FieldPropertiesDto {
public readonly schemaIds?: ReadonlyArray<string>;
public readonly maxItems?: number;
public readonly minItems?: number;
public readonly uniqueFields?: ReadonlyArray<string>;
public get isComplexUI() {
return true;

8
frontend/app/shared/state/contents.forms.visitors.ts

@ -246,6 +246,10 @@ export class FieldsValidators implements FieldPropertiesVisitor<ReadonlyArray<Va
ValidatorsEx.betweenLength(properties.minItems, properties.maxItems),
];
if (properties.uniqueFields && properties.uniqueFields.length > 0) {
validators.push(ValidatorsEx.uniqueObjectValues(properties.uniqueFields));
}
return validators;
}
@ -274,6 +278,10 @@ export class FieldsValidators implements FieldPropertiesVisitor<ReadonlyArray<Va
ValidatorsEx.betweenLength(properties.minItems, properties.maxItems),
];
if (properties.uniqueFields && properties.uniqueFields.length > 0) {
validators.push(ValidatorsEx.uniqueObjectValues(properties.uniqueFields));
}
return validators;
}

2
frontend/app/shared/state/schemas.forms.ts

@ -256,6 +256,7 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> {
public visitArray() {
this.config['maxItems'] = undefined;
this.config['minItems'] = undefined;
this.config['uniqueFields'] = undefined;
}
public visitAssets() {
@ -294,6 +295,7 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> {
this.config['schemaIds'] = undefined;
this.config['maxItems'] = undefined;
this.config['minItems'] = undefined;
this.config['uniqueFields'] = undefined;
}
public visitDateTime() {

Loading…
Cancel
Save