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. 90
      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.countMax": "Max Items",
"schemas.fieldTypes.array.countMin": "Min Items", "schemas.fieldTypes.array.countMin": "Min Items",
"schemas.fieldTypes.array.description": "List of embedded objects.", "schemas.fieldTypes.array.description": "List of embedded objects.",
"schemas.fieldTypes.array.uniqueFields": "Unique Fields",
"schemas.fieldTypes.assets.allowDuplicates": "Allow duplicate values", "schemas.fieldTypes.assets.allowDuplicates": "Allow duplicate values",
"schemas.fieldTypes.assets.count": "Count", "schemas.fieldTypes.assets.count": "Count",
"schemas.fieldTypes.assets.countMax": "Max Assets", "schemas.fieldTypes.assets.countMax": "Max Assets",
@ -1006,6 +1007,7 @@
"validation.patternmessage": "{message}", "validation.patternmessage": "{message}",
"validation.required": "{field|upper} is required.", "validation.required": "{field|upper} is required.",
"validation.requiredTrue": "{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.uniquestrings": "{field|upper} must not contain duplicate values.",
"validation.validarrayvalues": "{field|upper} contains an invalid value: {invalidvalue}.", "validation.validarrayvalues": "{field|upper} contains an invalid value: {invalidvalue}.",
"validation.validdatetime": "{field|upper} is not a valid date time.", "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.countMax": "Max num. Elementi",
"schemas.fieldTypes.array.countMin": "Min num. Elementi", "schemas.fieldTypes.array.countMin": "Min num. Elementi",
"schemas.fieldTypes.array.description": "Lista di oggetti incorporati.", "schemas.fieldTypes.array.description": "Lista di oggetti incorporati.",
"schemas.fieldTypes.array.uniqueFields": "Unique Fields",
"schemas.fieldTypes.assets.allowDuplicates": "Consente valori duplicati", "schemas.fieldTypes.assets.allowDuplicates": "Consente valori duplicati",
"schemas.fieldTypes.assets.count": "Conteggio", "schemas.fieldTypes.assets.count": "Conteggio",
"schemas.fieldTypes.assets.countMax": "Max num di Risorse", "schemas.fieldTypes.assets.countMax": "Max num di Risorse",
@ -1006,6 +1007,7 @@
"validation.patternmessage": "{message}", "validation.patternmessage": "{message}",
"validation.required": "{field|upper} è obbligatorio.", "validation.required": "{field|upper} è obbligatorio.",
"validation.requiredTrue": "{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.uniquestrings": "{field|upper} non deve contenere valori duplicati.",
"validation.validarrayvalues": "{field|upper} contiene valori non validicontains an invalid value: {invalidvalue}.", "validation.validarrayvalues": "{field|upper} contiene valori non validicontains an invalid value: {invalidvalue}.",
"validation.validdatetime": "{field|upper} non è una data e ora valida.", "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.countMax": "Max. aantal items",
"schemas.fieldTypes.array.countMin": "Min. items", "schemas.fieldTypes.array.countMin": "Min. items",
"schemas.fieldTypes.array.description": "Lijst met ingesloten objecten.", "schemas.fieldTypes.array.description": "Lijst met ingesloten objecten.",
"schemas.fieldTypes.array.uniqueFields": "Unique Fields",
"schemas.fieldTypes.assets.allowDuplicates": "Dubbele waarden toestaan", "schemas.fieldTypes.assets.allowDuplicates": "Dubbele waarden toestaan",
"schemas.fieldTypes.assets.count": "Tellen", "schemas.fieldTypes.assets.count": "Tellen",
"schemas.fieldTypes.assets.countMax": "Max. bestanden", "schemas.fieldTypes.assets.countMax": "Max. bestanden",
@ -1006,6 +1007,7 @@
"validation.patternmessage": "{message}", "validation.patternmessage": "{message}",
"validation.required": "{field|upper} is vereist.", "validation.required": "{field|upper} is vereist.",
"validation.requiredTrue": "{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.uniquestrings": "{field|upper} mag geen dubbele waarden bevatten.",
"validation.validarrayvalues": "{field|upper} bevat een ongeldige waarde: {invalidvalue}.", "validation.validarrayvalues": "{field|upper} bevat een ongeldige waarde: {invalidvalue}.",
"validation.validdatetime": "{field|upper} is geen geldige datum en tijd.", "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.countMax": "最大项目数",
"schemas.fieldTypes.array.countMin": "最小项目", "schemas.fieldTypes.array.countMin": "最小项目",
"schemas.fieldTypes.array.description": "嵌入对象列表。", "schemas.fieldTypes.array.description": "嵌入对象列表。",
"schemas.fieldTypes.array.uniqueFields": "Unique Fields",
"schemas.fieldTypes.assets.allowDuplicates": "允许重复值", "schemas.fieldTypes.assets.allowDuplicates": "允许重复值",
"schemas.fieldTypes.assets.count": "计数", "schemas.fieldTypes.assets.count": "计数",
"schemas.fieldTypes.assets.countMax": "最大资源", "schemas.fieldTypes.assets.countMax": "最大资源",
@ -1006,6 +1007,7 @@
"validation.patternmessage": "{message}", "validation.patternmessage": "{message}",
"validation.required": "{field|upper} 是必需的。", "validation.required": "{field|upper} 是必需的。",
"validation.requiredTrue": "{field|upper} 是必需的。", "validation.requiredTrue": "{field|upper} 是必需的。",
"validation.uniqueobjectvalues": "{field|upper} has items with duplicate '{fields}' fields.",
"validation.uniquestrings": "{field|upper} 不得包含重复值。", "validation.uniquestrings": "{field|upper} 不得包含重复值。",
"validation.validarrayvalues": "{field|upper} 包含无效值:{invalidvalue}。", "validation.validarrayvalues": "{field|upper} 包含无效值:{invalidvalue}。",
"validation.validdatetime": "{field|upper} 不是有效的日期时间。", "validation.validdatetime": "{field|upper} 不是有效的日期时间。",

1
backend/i18n/source/backend_en.json

@ -183,6 +183,7 @@
"contents.validation.regexTooSlow": "Regex is too slow.", "contents.validation.regexTooSlow": "Regex is too slow.",
"contents.validation.required": "Field is required.", "contents.validation.required": "Field is required.",
"contents.validation.unique": "Another content with the same value exists.", "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.unknownField": "Not a known {fieldType}.",
"contents.validation.wordCount": "Must have exactly {count} word(s).", "contents.validation.wordCount": "Must have exactly {count} word(s).",
"contents.validation.wordsBetween": "Must have between {min} and {max} 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.countMax": "Max Items",
"schemas.fieldTypes.array.countMin": "Min Items", "schemas.fieldTypes.array.countMin": "Min Items",
"schemas.fieldTypes.array.description": "List of embedded objects.", "schemas.fieldTypes.array.description": "List of embedded objects.",
"schemas.fieldTypes.array.uniqueFields": "Unique Fields",
"schemas.fieldTypes.assets.allowDuplicates": "Allow duplicate values", "schemas.fieldTypes.assets.allowDuplicates": "Allow duplicate values",
"schemas.fieldTypes.assets.count": "Count", "schemas.fieldTypes.assets.count": "Count",
"schemas.fieldTypes.assets.countMax": "Max Assets", "schemas.fieldTypes.assets.countMax": "Max Assets",
@ -1006,6 +1007,7 @@
"validation.patternmessage": "{message}", "validation.patternmessage": "{message}",
"validation.required": "{field|upper} is required.", "validation.required": "{field|upper} is required.",
"validation.requiredTrue": "{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.uniquestrings": "{field|upper} must not contain duplicate values.",
"validation.validarrayvalues": "{field|upper} contains an invalid value: {invalidvalue}.", "validation.validarrayvalues": "{field|upper} contains an invalid value: {invalidvalue}.",
"validation.validdatetime": "{field|upper} is not a valid date time.", "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 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) 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); FieldCollection = new FieldCollection<NestedField>(fields);
} }

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

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
{ {
@ -15,6 +16,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public int? MaxItems { get; init; } public int? MaxItems { get; init; }
public ImmutableList<string>? UniqueFields { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args) public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{ {
return visitor.Visit(this, 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 int? MaxItems { get; init; }
public ImmutableList<string>? UniqueFields { get; init; }
public DomainId SchemaId public DomainId SchemaId
{ {
init 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 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, 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, 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); 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); var nestedValidators = new Dictionary<string, (bool IsOptional, IValidator Validator)>(field.Fields.Count);
foreach (var nestedField in field.Fields) 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); 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)); 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) 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++) 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"))); 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) ResolvedComponents components, ImmutableList<DomainId>? allowedIds)
{ {
if (value is not JsonObject obj) 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"> <data name="contents.validation.unique" xml:space="preserve">
<value>Esiste un altro contenuto con lo stesso valore.</value> <value>Esiste un altro contenuto con lo stesso valore.</value>
</data> </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"> <data name="contents.validation.unknownField" xml:space="preserve">
<value>Non è noto {fieldType}.</value> <value>Non è noto {fieldType}.</value>
</data> </data>

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

@ -634,6 +634,9 @@
<data name="contents.validation.unique" xml:space="preserve"> <data name="contents.validation.unique" xml:space="preserve">
<value>Er bestaat een andere inhoud met dezelfde waarde.</value> <value>Er bestaat een andere inhoud met dezelfde waarde.</value>
</data> </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"> <data name="contents.validation.unknownField" xml:space="preserve">
<value>Onbekend {fieldType}.</value> <value>Onbekend {fieldType}.</value>
</data> </data>

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

@ -634,6 +634,9 @@
<data name="contents.validation.unique" xml:space="preserve"> <data name="contents.validation.unique" xml:space="preserve">
<value>Another content with the same value exists.</value> <value>Another content with the same value exists.</value>
</data> </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"> <data name="contents.validation.unknownField" xml:space="preserve">
<value>Not a known {fieldType}.</value> <value>Not a known {fieldType}.</value>
</data> </data>

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

@ -634,6 +634,9 @@
<data name="contents.validation.unique" xml:space="preserve"> <data name="contents.validation.unique" xml:space="preserve">
<value>存在另一个具有相同值的内容。</value> <value>存在另一个具有相同值的内容。</value>
</data> </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"> <data name="contents.validation.unknownField" xml:space="preserve">
<value>不是已知的 {fieldType}。</value> <value>不是已知的 {fieldType}。</value>
</data> </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.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
@ -22,6 +23,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary> /// </summary>
public int? MaxItems { get; set; } public int? MaxItems { get; set; }
/// <summary>
/// The fields that must be unique.
/// </summary>
public ImmutableList<string>? UniqueFields { get; set; }
public override FieldProperties ToProperties() public override FieldProperties ToProperties()
{ {
var result = SimpleMapper.Map(this, new ArrayFieldProperties()); 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> /// </summary>
public ImmutableList<DomainId>? SchemaIds { get; set; } public ImmutableList<DomainId>? SchemaIds { get; set; }
/// <summary>
/// The fields that must be unique.
/// </summary>
public ImmutableList<string>? UniqueFields { get; set; }
public override FieldProperties ToProperties() public override FieldProperties ToProperties()
{ {
var result = SimpleMapper.Map(this, new ComponentsFieldProperties()); 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)); 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] [Fact]
@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
{ {
var parent_1 = parent_0.AddField(CreateField(1)); 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] [Fact]
@ -238,7 +238,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
private static NestedField<NumberFieldProperties> CreateField(int id) 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 }) .Select(x => new[] { x })
.ToList()!; .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] [Fact]
public void Should_instantiate_field() public void Should_instantiate_field()
{ {
Assert.Equal("my-field", field_0.Name); Assert.Equal("myField", field_0.Name);
} }
[Fact] [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)); 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] [Fact]
@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
{ {
var schema_1 = schema_0.AddField(CreateField(1)); 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] [Fact]
@ -498,7 +498,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas
private static RootField<NumberFieldProperties> CreateField(int id) 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("*")] [InlineData("*")]
public void Should_convert_nested_asset_ids_to_urls(string path) 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); var source = JsonValue.Array(id1, id2);
@ -109,7 +109,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
[InlineData("other.assets")] [InlineData("other.assets")]
public void Should_not_convert_nested_asset_ids_if_field_name_does_not_match(string path) 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); 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 = schema =
new Schema("my-schema") new Schema("my-schema")
.AddString(1, "my-string", Partitioning.Language, .AddString(1, "myString", Partitioning.Language,
new StringFieldProperties { DefaultValue = "en-string" }) new StringFieldProperties { DefaultValue = "en-string" })
.AddNumber(2, "my-number", Partitioning.Invariant, .AddNumber(2, "myNumber", Partitioning.Invariant,
new NumberFieldProperties()) new NumberFieldProperties())
.AddDateTime(3, "my-datetime", Partitioning.Invariant, .AddDateTime(3, "myDatetime", Partitioning.Invariant,
new DateTimeFieldProperties { DefaultValue = now }) new DateTimeFieldProperties { DefaultValue = now })
.AddBoolean(4, "my-boolean", Partitioning.Invariant, .AddBoolean(4, "myBoolean", Partitioning.Invariant,
new BooleanFieldProperties { DefaultValue = true }); new BooleanFieldProperties { DefaultValue = true });
} }
@ -44,23 +44,23 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues
{ {
var data = var data =
new ContentData() new ContentData()
.AddField("my-string", .AddField("myString",
new ContentFieldData() new ContentFieldData()
.AddLocalized("de", "de-string")) .AddLocalized("de", "de-string"))
.AddField("my-number", .AddField("myNumber",
new ContentFieldData() new ContentFieldData()
.AddInvariant(456)); .AddInvariant(456));
data.GenerateDefaultValues(schema, languagesConfig.ToResolver()); 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("de-string", data["myString"]!["de"].ToString());
Assert.Equal("en-string", data["my-string"]!["en"].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] [Fact]
@ -68,17 +68,17 @@ namespace Squidex.Domain.Apps.Core.Operations.DefaultValues
{ {
var data = var data =
new ContentData() new ContentData()
.AddField("my-string", .AddField("myString",
new ContentFieldData() new ContentFieldData()
.AddLocalized("de", string.Empty)) .AddLocalized("de", string.Empty))
.AddField("my-number", .AddField("myNumber",
new ContentFieldData() new ContentFieldData()
.AddInvariant(456)); .AddInvariant(456));
data.GenerateDefaultValues(schema, languagesConfig.ToResolver()); data.GenerateDefaultValues(schema, languagesConfig.ToResolver());
Assert.Equal(string.Empty, data["my-string"]!["de"].ToString()); Assert.Equal(string.Empty, data["myString"]!["de"].ToString());
Assert.Equal("en-string", data["my-string"]!["en"].ToString()); Assert.Equal("en-string", data["myString"]!["en"].ToString());
} }
[Fact] [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 public class SchemaSynchronizerTests
{ {
private readonly Func<long> idGenerator; private readonly Func<long> idGenerator;
private readonly NamedId<long> stringId = NamedId.Of(13L, "my-value"); private readonly NamedId<long> stringId = NamedId.Of(13L, "myValue");
private readonly NamedId<long> nestedId = NamedId.Of(141L, "my-value"); private readonly NamedId<long> nestedId = NamedId.Of(141L, "myValue");
private readonly NamedId<long> arrayId = NamedId.Of(14L, "11-array"); private readonly NamedId<long> arrayId = NamedId.Of(14L, "11Array");
private int fields = 50; private int fields = 50;
public SchemaSynchronizerTests() 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] [Fact]
public void Should_return_empty_list_from_non_references_field() 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(); 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 id1 = DomainId.NewGuid();
var id2 = 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 = var value =
JsonValue.Array( JsonValue.Array(
@ -305,14 +305,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds
public static IEnumerable<object[]> ReferencingNestedFields() public static IEnumerable<object[]> ReferencingNestedFields()
{ {
yield return new object[] { Fields.References(1, "my-refs") }; yield return new object[] { Fields.References(1, "myRefs") };
yield return new object[] { Fields.Assets(1, "my-assets") }; yield return new object[] { Fields.Assets(1, "myAssets") };
} }
public static IEnumerable<object[]> ReferencingFields() public static IEnumerable<object[]> ReferencingFields()
{ {
yield return new object[] { Fields.References(1, "my-refs", Partitioning.Invariant) }; yield return new object[] { Fields.References(1, "myRefs", Partitioning.Invariant) };
yield return new object[] { Fields.Assets(1, "my-assets", Partitioning.Invariant) }; yield return new object[] { Fields.Assets(1, "myAssets", Partitioning.Invariant) };
} }
private static HashSet<DomainId> RandomIds() 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.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Xunit; using Xunit;
@ -25,7 +25,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new ArrayFieldProperties()); var sut = Field(new ArrayFieldProperties());
Assert.Equal("my-array", sut.Name); Assert.Equal("myArray", sut.Name);
} }
[Fact] [Fact]
@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new ArrayFieldProperties()); var sut = Field(new ArrayFieldProperties());
await sut.ValidateAsync(CreateValue(JsonValue.Object()), errors); await sut.ValidateAsync(CreateValue(Object()), errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -53,7 +53,17 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new ArrayFieldProperties { MinItems = 2, MaxItems = 2 }); 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); Assert.Empty(errors);
} }
@ -96,7 +106,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var sut = Field(new ArrayFieldProperties { MinItems = 3 }); 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( errors.Should().BeEquivalentTo(
new[] { "Must have at least 3 item(s)." }); 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 }); 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( errors.Should().BeEquivalentTo(
new[] { "Must not have more than 1 item(s)." }); 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) 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()); var sut = Field(new AssetsFieldProperties());
Assert.Equal("my-assets", sut.Name); Assert.Equal("myAssets", sut.Name);
} }
[Fact] [Fact]
@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<AssetsFieldProperties> Field(AssetsFieldProperties properties) 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()); var sut = Field(new BooleanFieldProperties());
Assert.Equal("my-boolean", sut.Name); Assert.Equal("myBoolean", sut.Name);
} }
[Fact] [Fact]
@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<BooleanFieldProperties> Field(BooleanFieldProperties properties) 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()); var (_, sut, _) = Field(new ComponentFieldProperties());
Assert.Equal("my-component", sut.Name); Assert.Equal("myComponent", sut.Name);
} }
[Fact] [Fact]
@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var (id, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); 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); 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); 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( errors.Should().BeEquivalentTo(
new[] { "component-field: Field is required." }); new[] { "componentField: Field is required." });
} }
[Fact] [Fact]
@ -123,7 +123,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var (_, sut, components) = Field(new ComponentFieldProperties { SchemaId = schemaId1 }); 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); 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 (_, 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); await sut.ValidateAsync(value, errors, components: components);
@ -164,10 +164,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var schema = var schema =
new Schema("my-component") new Schema("my-component")
.AddNumber(1, "component-field", Partitioning.Invariant, .AddNumber(1, "componentField", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = isRequired }); 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> 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()); var (_, sut, _) = Field(new ComponentsFieldProperties());
Assert.Equal("my-components", sut.Name); Assert.Equal("myComponents", sut.Name);
} }
[Fact] [Fact]
@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); 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); 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 }); 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); 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); 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( errors.Should().BeEquivalentTo(
new[] { "[1].component-field: Field is required." }); new[] { "[1].componentField: Field is required." });
} }
[Fact] [Fact]
@ -144,7 +144,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var (id, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1, MinItems = 3 }); 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( errors.Should().BeEquivalentTo(
new[] { "Must have at least 3 item(s)." }); 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 }); 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( errors.Should().BeEquivalentTo(
new[] { "Must not have more than 1 item(s)." }); 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] [Fact]
public async Task Should_resolve_schema_id_from_name() public async Task Should_resolve_schema_id_from_name()
{ {
var (_, sut, components) = Field(new ComponentsFieldProperties { SchemaId = schemaId1 }); 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); 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 (_, 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); await sut.ValidateAsync(value, errors, components: components);
@ -214,10 +225,10 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var schema = var schema =
new Schema("my-component") new Schema("my-component")
.AddNumber(1, "component-field", Partitioning.Invariant, .AddNumber(1, "componentField", Partitioning.Invariant,
new NumberFieldProperties { IsRequired = isRequired }); 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> 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>._)) A.CallTo(() => validatorFactory.CreateValueValidators(A<ValidationContext>._, A<IField>._, A<ValidatorFactory>._))
.Returns(Enumerable.Repeat(validator, 1)); .Returns(Enumerable.Repeat(validator, 1));
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant, schema = schema.AddNumber(1, "myField", Partitioning.Invariant,
new NumberFieldProperties()); new NumberFieldProperties());
var data = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddInvariant(1000)); .AddInvariant(1000));
@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> 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>._)) A.CallTo(() => validatorFactory.CreateFieldValidators(A<ValidationContext>._, A<IField>._, A<ValidatorFactory>._))
.Returns(Enumerable.Repeat(validator, 1)); .Returns(Enumerable.Repeat(validator, 1));
schema = schema.AddNumber(1, "my-field", Partitioning.Invariant, schema = schema.AddNumber(1, "myField", Partitioning.Invariant,
new NumberFieldProperties()); new NumberFieldProperties());
var data = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddInvariant(1000)); .AddInvariant(1000));
@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> 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] [Fact]
public async Task Should_add_error_if_validating_data_with_invalid_field() 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 }); new NumberFieldProperties { MaxValue = 100 });
var data = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddInvariant(1000)); .AddInvariant(1000));
@ -125,18 +125,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> 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] [Fact]
public async Task Should_add_error_if_non_localizable_data_field_contains_language() 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 = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddLocalized("es", 1) .AddLocalized("es", 1)
.AddLocalized("it", 1)); .AddLocalized("it", 1));
@ -146,15 +146,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Not a known invariant value.", "my-field.es"), new ValidationError("Not a known invariant value.", "myField.es"),
new ValidationError("Not a known invariant value.", "my-field.it") new ValidationError("Not a known invariant value.", "myField.it")
}); });
} }
[Fact] [Fact]
public async Task Should_add_error_if_validating_data_with_invalid_localizable_field() 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 }); new NumberFieldProperties { IsRequired = true });
var data = var data =
@ -165,15 +165,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Field is required.", "my-field.de"), new ValidationError("Field is required.", "myField.de"),
new ValidationError("Field is required.", "my-field.en") new ValidationError("Field is required.", "myField.en")
}); });
} }
[Fact] [Fact]
public async Task Should_add_error_if_required_data_field_is_not_in_bag() 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 }); new NumberFieldProperties { IsRequired = true });
var data = var data =
@ -184,14 +184,14 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Field is required.", "my-field.iv") new ValidationError("Field is required.", "myField.iv")
}); });
} }
[Fact] [Fact]
public async Task Should_add_error_if_required_data_string_field_is_not_in_bag() 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 }); new StringFieldProperties { IsRequired = true });
var data = var data =
@ -202,18 +202,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Field is required.", "my-field.iv") new ValidationError("Field is required.", "myField.iv")
}); });
} }
[Fact] [Fact]
public async Task Should_add_error_if_data_contains_invalid_language() 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 = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddLocalized("de", 1) .AddLocalized("de", 1)
.AddLocalized("ru", 1)); .AddLocalized("ru", 1));
@ -223,7 +223,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> 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) .Set(Language.IT, true)
.Remove(Language.EN); .Remove(Language.EN);
schema = schema.AddString(1, "my-field", Partitioning.Language, schema = schema.AddString(1, "myField", Partitioning.Language,
new StringFieldProperties { IsRequired = true }); new StringFieldProperties { IsRequired = true });
var data = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddLocalized("es", "value")); .AddLocalized("es", "value"));
@ -253,11 +253,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
[Fact] [Fact]
public async Task Should_add_error_if_data_contains_unsupported_language() 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 = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddLocalized("es", 1) .AddLocalized("es", 1)
.AddLocalized("it", 1)); .AddLocalized("it", 1));
@ -267,8 +267,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Not a known language.", "my-field.es"), new ValidationError("Not a known language.", "myField.es"),
new ValidationError("Not a known language.", "my-field.it") new ValidationError("Not a known language.", "myField.it")
}); });
} }
@ -292,12 +292,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
[Fact] [Fact]
public async Task Should_add_error_if_validating_partial_data_with_invalid_field() 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 }); new NumberFieldProperties { MaxValue = 100 });
var data = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddInvariant(1000)); .AddInvariant(1000));
@ -306,18 +306,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> 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] [Fact]
public async Task Should_add_error_if_non_localizable_partial_data_field_contains_language() 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 = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddLocalized("es", 1) .AddLocalized("es", 1)
.AddLocalized("it", 1)); .AddLocalized("it", 1));
@ -327,15 +327,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Not a known invariant value.", "my-field.es"), new ValidationError("Not a known invariant value.", "myField.es"),
new ValidationError("Not a known invariant value.", "my-field.it") new ValidationError("Not a known invariant value.", "myField.it")
}); });
} }
[Fact] [Fact]
public async Task Should_not_add_error_if_validating_partial_data_with_invalid_localizable_field() 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 }); new NumberFieldProperties { IsRequired = true });
var data = var data =
@ -349,7 +349,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
[Fact] [Fact]
public async Task Should_not_add_error_if_required_partial_data_field_is_not_in_bag() 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 }); new NumberFieldProperties { IsRequired = true });
var data = var data =
@ -363,11 +363,11 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
[Fact] [Fact]
public async Task Should_add_error_if_partial_data_contains_invalid_language() 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 = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddLocalized("de", 1) .AddLocalized("de", 1)
.AddLocalized("ru", 1)); .AddLocalized("ru", 1));
@ -377,18 +377,18 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Not a known language.", "my-field.ru") new ValidationError("Not a known language.", "myField.ru")
}); });
} }
[Fact] [Fact]
public async Task Should_add_error_if_partial_data_contains_unsupported_language() 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 = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddLocalized("es", 1) .AddLocalized("es", 1)
.AddLocalized("it", 1)); .AddLocalized("it", 1));
@ -398,25 +398,25 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Not a known language.", "my-field.es"), new ValidationError("Not a known language.", "myField.es"),
new ValidationError("Not a known language.", "my-field.it") new ValidationError("Not a known language.", "myField.it")
}); });
} }
[Fact] [Fact]
public async Task Should_add_error_if_array_field_has_required_nested_field() public async Task Should_add_error_if_array_field_has_required_nested_field()
{ {
schema = schema.AddArray(1, "my-field", Partitioning.Invariant, f => f. schema = schema.AddArray(1, "myField", Partitioning.Invariant, f => f.
AddNumber(2, "my-nested", new NumberFieldProperties { IsRequired = true })); AddNumber(2, "myNested", new NumberFieldProperties { IsRequired = true }));
var data = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddInvariant( .AddInvariant(
JsonValue.Array( JsonValue.Array(
JsonValue.Object(), JsonValue.Object(),
JsonValue.Object().Add("my-nested", 1), JsonValue.Object().Add("myNested", 1),
JsonValue.Object()))); JsonValue.Object())));
await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema); await data.ValidatePartialAsync(languagesConfig.ToResolver(), errors, schema);
@ -424,8 +424,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Field is required.", "my-field.iv[1].my-nested"), new ValidationError("Field is required.", "myField.iv[1].myNested"),
new ValidationError("Field is required.", "my-field.iv[3].my-nested") new ValidationError("Field is required.", "myField.iv[3].myNested")
}); });
} }
@ -445,12 +445,12 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
[Fact] [Fact]
public async Task Should_not_add_error_if_nested_separator_not_defined() public async Task Should_not_add_error_if_nested_separator_not_defined()
{ {
schema = schema.AddArray(1, "my-field", Partitioning.Invariant, f => f. schema = schema.AddArray(1, "myField", Partitioning.Invariant, f => f.
AddUI(2, "my-nested")); AddUI(2, "myNested"));
var data = var data =
new ContentData() new ContentData()
.AddField("my-field", .AddField("myField",
new ContentFieldData() new ContentFieldData()
.AddInvariant( .AddInvariant(
JsonValue.Array( 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()); var sut = Field(new DateTimeFieldProperties());
Assert.Equal("my-datetime", sut.Name); Assert.Equal("myDatetime", sut.Name);
} }
[Fact] [Fact]
@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<DateTimeFieldProperties> Field(DateTimeFieldProperties properties) 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()); var sut = Field(new GeolocationFieldProperties());
Assert.Equal("my-geolocation", sut.Name); Assert.Equal("myGeolocation", sut.Name);
} }
[Fact] [Fact]
@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<GeolocationFieldProperties> Field(GeolocationFieldProperties properties) 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()); var sut = Field(new JsonFieldProperties());
Assert.Equal("my-json", sut.Name); Assert.Equal("myJson", sut.Name);
} }
[Fact] [Fact]
@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<JsonFieldProperties> Field(JsonFieldProperties properties) 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()); var sut = Field(new NumberFieldProperties());
Assert.Equal("my-number", sut.Name); Assert.Equal("myNumber", sut.Name);
} }
[Fact] [Fact]
@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<NumberFieldProperties> Field(NumberFieldProperties properties) 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()); var sut = Field(new ReferencesFieldProperties());
Assert.Equal("my-refs", sut.Name); Assert.Equal("myRefs", sut.Name);
} }
[Fact] [Fact]
@ -173,7 +173,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<ReferencesFieldProperties> Field(ReferencesFieldProperties properties) 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()); var sut = Field(new StringFieldProperties());
Assert.Equal("my-string", sut.Name); Assert.Equal("myString", sut.Name);
} }
[Fact] [Fact]
@ -144,7 +144,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<StringFieldProperties> Field(StringFieldProperties properties) 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()); var sut = Field(new TagsFieldProperties());
Assert.Equal("my-tags", sut.Name); Assert.Equal("myTags", sut.Name);
} }
[Fact] [Fact]
@ -154,7 +154,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
private static RootField<TagsFieldProperties> Field(TagsFieldProperties properties) 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()); var sut = Field(new UIFieldProperties());
Assert.Equal("my-ui", sut.Name); Assert.Equal("myUI", sut.Name);
} }
[Fact] [Fact]
@ -67,13 +67,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var schema = var schema =
new Schema("my-schema") new Schema("my-schema")
.AddUI(1, "my-ui1", Partitioning.Invariant) .AddUI(1, "myUI1", Partitioning.Invariant)
.AddUI(2, "my-ui2", Partitioning.Invariant); .AddUI(2, "myUI2", Partitioning.Invariant);
var data = var data =
new ContentData() new ContentData()
.AddField("my-ui1", new ContentFieldData()) .AddField("myUI1", new ContentFieldData())
.AddField("my-ui2", new ContentFieldData() .AddField("myUI2", new ContentFieldData()
.AddInvariant(null)); .AddInvariant(null));
var dataErrors = new List<ValidationError>(); var dataErrors = new List<ValidationError>();
@ -83,8 +83,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
dataErrors.Should().BeEquivalentTo( dataErrors.Should().BeEquivalentTo(
new[] new[]
{ {
new ValidationError("Value must not be defined.", "my-ui1"), new ValidationError("Value must not be defined.", "myUI1"),
new ValidationError("Value must not be defined.", "my-ui2") new ValidationError("Value must not be defined.", "myUI2")
}); });
} }
@ -93,16 +93,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
{ {
var schema = var schema =
new Schema("my-schema") new Schema("my-schema")
.AddArray(1, "my-array", Partitioning.Invariant, array => array .AddArray(1, "myArray", Partitioning.Invariant, array => array
.AddUI(101, "my-ui")); .AddUI(101, "myUI"));
var data = var data =
new ContentData() new ContentData()
.AddField("my-array", new ContentFieldData() .AddField("myArray", new ContentFieldData()
.AddInvariant( .AddInvariant(
JsonValue.Array( JsonValue.Array(
JsonValue.Object() JsonValue.Object()
.Add("my-ui", null)))); .Add("myUI", null))));
var dataErrors = new List<ValidationError>(); var dataErrors = new List<ValidationError>();
@ -111,13 +111,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent
dataErrors.Should().BeEquivalentTo( dataErrors.Should().BeEquivalentTo(
new[] 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) 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>(); 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] [Fact]
public async Task Should_not_check_uniqueness_if_localized_string() 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); 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) private static CheckUniqueness FoundDuplicates(DomainId id, Action<string>? filter = null)
{ {
return filterNode => 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 { Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
import { AbstractControl, FormControl } from '@angular/forms'; 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 { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -68,7 +68,7 @@ export class FieldEditorComponent implements OnChanges {
previousControl.form['_clearChangeFns'](); 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)!" [languages]="(languagesState.isoLanguages | async)!"
[field]="field" [field]="field"
[fieldForm]="editForm.form" [fieldForm]="editForm.form"
[schema]="schema"
[isEditable]="true" [isEditable]="true"
[isLocalizable]="isLocalizable" [isLocalizable]="isLocalizable"
[settings]="settings"> [settings]="settings">
@ -55,7 +56,7 @@
<input type="text" class="form-control" formControlName="name" maxlength="40" #nameInput placeholder="{{ 'schemas.field.namePlaceholder' | sqxTranslate }}" sqxFocusOnInit> <input type="text" class="form-control" formControlName="name" maxlength="40" #nameInput placeholder="{{ 'schemas.field.namePlaceholder' | sqxTranslate }}" sqxFocusOnInit>
</div> </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"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="isLocalizable" formControlName="isLocalizable"> <input class="form-check-input" type="checkbox" id="isLocalizable" formControlName="isLocalizable">
<label class="form-check-label" for="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" [languages]="languages"
[fieldForm]="editForm.form" [fieldForm]="editForm.form"
[field]="field" [field]="field"
[schema]="schema"
[isEditable]="isEditable" [isEditable]="isEditable"
[isLocalizable]="isLocalizable" [isLocalizable]="isLocalizable"
[settings]="settings"> [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 { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { FieldDto } from '@app/shared'; import { FieldDto, SchemaDto } from '@app/shared';
@Component({ @Component({
selector: 'sqx-field-form-common[field][fieldForm]', selector: 'sqx-field-form-common[field][fieldForm][schema]',
styleUrls: ['./field-form-common.component.scss'], styleUrls: ['./field-form-common.component.scss'],
templateUrl: './field-form-common.component.html', templateUrl: './field-form-common.component.html',
}) })
@ -20,4 +20,7 @@ export class FieldFormCommonComponent {
@Input() @Input()
public field: FieldDto; 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 { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { FieldDto } from '@app/shared'; import { FieldDto, SchemaDto } from '@app/shared';
@Component({ @Component({
selector: 'sqx-field-form-ui[field][fieldForm]', selector: 'sqx-field-form-ui[field][fieldForm][schema]',
styleUrls: ['./field-form-ui.component.scss'], styleUrls: ['./field-form-ui.component.scss'],
templateUrl: './field-form-ui.component.html', templateUrl: './field-form-ui.component.html',
}) })
@ -20,4 +20,7 @@ export class FieldFormUIComponent {
@Input() @Input()
public field: FieldDto; 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" [isLocalizable]="isLocalizable"
[fieldForm]="fieldForm" [fieldForm]="fieldForm"
[field]="field" [field]="field"
[properties]="field.rawProperties"> [properties]="field.rawProperties"
[schema]="schema">
</sqx-number-validation> </sqx-number-validation>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'References'"> <ng-container *ngSwitchCase="'References'">
@ -110,6 +111,7 @@
[fieldForm]="fieldForm" [fieldForm]="fieldForm"
[field]="field" [field]="field"
[properties]="field.rawProperties" [properties]="field.rawProperties"
[schema]="schema"
[settings]="settings"> [settings]="settings">
</sqx-string-validation> </sqx-string-validation>
</ng-container> </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 { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { AppSettingsDto, FieldDto, LanguageDto } from '@app/shared'; import { AppSettingsDto, FieldDto, LanguageDto, SchemaDto } from '@app/shared';
@Component({ @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'], styleUrls: ['./field-form-validation.component.scss'],
templateUrl: './field-form-validation.component.html', templateUrl: './field-form-validation.component.html',
}) })
@ -21,6 +21,9 @@ export class FieldFormValidationComponent {
@Input() @Input()
public field: FieldDto; public field: FieldDto;
@Input()
public schema: SchemaDto;
@Input() @Input()
public settings: AppSettingsDto; 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"> <div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 0">
<sqx-field-form-common <sqx-field-form-common
[fieldForm]="fieldForm" [fieldForm]="fieldForm"
[field]="field"> [field]="field"
[schema]="schema">
</sqx-field-form-common> </sqx-field-form-common>
</div> </div>
@ -40,6 +41,7 @@
[languages]="languages" [languages]="languages"
[fieldForm]="fieldForm" [fieldForm]="fieldForm"
[field]="field" [field]="field"
[schema]="schema"
[isLocalizable]="isLocalizable" [isLocalizable]="isLocalizable"
[settings]="settings" > [settings]="settings" >
</sqx-field-form-validation> </sqx-field-form-validation>
@ -48,6 +50,7 @@
<div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 2"> <div class="table-items-row-details-tab" [class.hidden]="selectedTab !== 2">
<sqx-field-form-ui <sqx-field-form-ui
[fieldForm]="fieldForm" [fieldForm]="fieldForm"
[field]="field"> [field]="field"
[schema]="schema">
</sqx-field-form-ui> </sqx-field-form-ui>
</div> </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 { AfterViewInit, Component, EventEmitter, Input, Output } from '@angular/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { AppSettingsDto, FieldDto, LanguageDto } from '@app/shared'; import { AppSettingsDto, FieldDto, LanguageDto, SchemaDto } from '@app/shared';
@Component({ @Component({
selector: 'sqx-field-form[field][fieldForm][languages][settings]', selector: 'sqx-field-form[field][fieldForm][languages][schema][settings]',
styleUrls: ['./field-form.component.scss'], styleUrls: ['./field-form.component.scss'],
templateUrl: './field-form.component.html', templateUrl: './field-form.component.html',
}) })
@ -27,6 +27,9 @@ export class FieldFormComponent implements AfterViewInit {
@Input() @Input()
public field: FieldDto; public field: FieldDto;
@Input()
public schema: SchemaDto;
@Input() @Input()
public settings: AppSettingsDto; public settings: AppSettingsDto;

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

@ -16,4 +16,12 @@
</div> </div>
</div> </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> </div>

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

@ -26,5 +26,13 @@
</sqx-tag-editor> </sqx-tag-editor>
</div> </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> </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 { Component, Input } from '@angular/core';
import { FormGroup } from '@angular/forms'; 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({ @Component({
selector: 'sqx-number-validation[field][fieldForm][properties]', selector: 'sqx-number-validation[field][fieldForm][properties][schema]',
styleUrls: ['number-validation.component.scss'], styleUrls: ['number-validation.component.scss'],
templateUrl: 'number-validation.component.html', templateUrl: 'number-validation.component.html',
}) })
@ -21,6 +21,9 @@ export class NumberValidationComponent {
@Input() @Input()
public field: FieldDto; public field: FieldDto;
@Input()
public schema: SchemaDto;
@Input() @Input()
public properties: NumberFieldPropertiesDto; public properties: NumberFieldPropertiesDto;
@ -31,6 +34,6 @@ export class NumberValidationComponent {
public isLocalizable?: boolean | null; public isLocalizable?: boolean | null;
public get showUnique() { 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 { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { FormGroup } from '@angular/forms'; 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'; import { Observable } from 'rxjs';
@Component({ @Component({
selector: 'sqx-string-validation[field][fieldForm][properties]', selector: 'sqx-string-validation[field][fieldForm][properties][schema]',
styleUrls: ['string-validation.component.scss'], styleUrls: ['string-validation.component.scss'],
templateUrl: 'string-validation.component.html', templateUrl: 'string-validation.component.html',
animations: [ animations: [
@ -27,6 +27,9 @@ export class StringValidationComponent extends ResourceOwner implements OnChange
@Input() @Input()
public field: FieldDto; public field: FieldDto;
@Input()
public schema: SchemaDto;
@Input() @Input()
public properties: StringFieldPropertiesDto; public properties: StringFieldPropertiesDto;
@ -46,7 +49,7 @@ export class StringValidationComponent extends ResourceOwner implements OnChange
public patternsModal = new ModalModel(); public patternsModal = new ModalModel();
public get showUnique() { 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) { public ngOnChanges(changes: SimpleChanges) {

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

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

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

@ -9,7 +9,8 @@ import { FormControl, FormGroup, Validators } from '@angular/forms';
import { DateTime } from '@app/framework/internal'; import { DateTime } from '@app/framework/internal';
import { ValidatorsEx } from './validators'; import { ValidatorsEx } from './validators';
describe('ValidatorsEx.between', () => { describe('ValidatorsEx', () => {
describe('between', () => {
it('should return null validator if no min value or max value', () => { it('should return null validator if no min value or max value', () => {
const validator = ValidatorsEx.between(undefined, undefined); const validator = ValidatorsEx.between(undefined, undefined);
@ -79,9 +80,9 @@ describe('ValidatorsEx.between', () => {
expect(error.exactly).toBeDefined(); expect(error.exactly).toBeDefined();
}); });
}); });
describe('ValidatorsEx.betweenLength', () => { describe('betweenLength', () => {
it('should return null validator if no min value or max value', () => { it('should return null validator if no min value or max value', () => {
const validator = ValidatorsEx.betweenLength(undefined, undefined); const validator = ValidatorsEx.betweenLength(undefined, undefined);
@ -151,9 +152,9 @@ describe('ValidatorsEx.betweenLength', () => {
expect(error.exactlylength).toBeDefined(); expect(error.exactlylength).toBeDefined();
}); });
}); });
describe('ValidatorsEx.validDateTime', () => { describe('validDateTime', () => {
it('should return null validator if valid is not defined', () => { it('should return null validator if valid is not defined', () => {
const input = new FormControl(null); const input = new FormControl(null);
@ -177,9 +178,9 @@ describe('ValidatorsEx.validDateTime', () => {
expect(error.validdatetime).toBeDefined(); expect(error.validdatetime).toBeDefined();
}); });
}); });
describe('ValidatorsEx.validValues', () => { describe('validValues', () => {
it('should return null validator if values not defined', () => { it('should return null validator if values not defined', () => {
const validator = ValidatorsEx.validValues(null!); const validator = ValidatorsEx.validValues(null!);
@ -201,9 +202,9 @@ describe('ValidatorsEx.validValues', () => {
expect(error.validvalues).toBeDefined(); expect(error.validvalues).toBeDefined();
}); });
}); });
describe('ValidatorsEx.validArrayValues', () => { describe('validArrayValues', () => {
it('should return null validator if values not defined', () => { it('should return null validator if values not defined', () => {
const validator = ValidatorsEx.validArrayValues(null!); const validator = ValidatorsEx.validArrayValues(null!);
@ -225,9 +226,9 @@ describe('ValidatorsEx.validArrayValues', () => {
expect(error.validarrayvalues).toBeDefined(); expect(error.validarrayvalues).toBeDefined();
}); });
}); });
describe('ValidatorsEx.match', () => { describe('match', () => {
it('should revalidate if other control changes', () => { it('should revalidate if other control changes', () => {
const validator = ValidatorsEx.match('password', 'Passwords are not the same.'); const validator = ValidatorsEx.match('password', 'Passwords are not the same.');
@ -285,9 +286,9 @@ describe('ValidatorsEx.match', () => {
expect(validator(control)).toBeNull(); expect(validator(control)).toBeNull();
}); });
}); });
describe('ValidatorsEx.pattern', () => { describe('pattern', () => {
it('should return null validator if pattern not defined', () => { it('should return null validator if pattern not defined', () => {
const validator = ValidatorsEx.pattern(undefined!, undefined); const validator = ValidatorsEx.pattern(undefined!, undefined);
@ -369,9 +370,9 @@ describe('ValidatorsEx.pattern', () => {
expect(error).toEqual(expected); expect(error).toEqual(expected);
}); });
}); });
describe('ValidatorsEx.uniqueStrings', () => { describe('uniqueStrings', () => {
it('should return null if value is null', () => { it('should return null if value is null', () => {
const input = new FormControl(null); const input = new FormControl(null);
@ -403,4 +404,63 @@ describe('ValidatorsEx.uniqueStrings', () => {
expect(error).toEqual({ uniquestrings: false }); expect(error).toEqual({ uniquestrings: false });
}); });
});
describe('uniqueObjectValues', () => {
it('should return null if value is null', () => {
const input = new FormControl(null);
const error = ValidatorsEx.uniqueObjectValues(['myString'])(input);
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 null if values array has no duplicate', () => {
const input = new FormControl([{ myString: '1' }, { myString: '2' }]);
const error = ValidatorsEx.uniqueObjectValues(['myString'])(input);
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'; import { DateTime, Types } from '@app/framework/internal';
function isEmptyInputValue(value: any): boolean { function isEmptyInputValue(value: any): boolean {
return value == null || value.length === 0; return value == null || value === undefined || value.length === 0;
} }
export module ValidatorsEx { export module ValidatorsEx {
@ -191,4 +191,43 @@ export module ValidatorsEx {
return null; 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 maxItems?: number;
public readonly minItems?: number; public readonly minItems?: number;
public readonly uniqueFields?: ReadonlyArray<string>;
public accept<T>(visitor: FieldPropertiesVisitor<T>): T { public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitArray(this); return visitor.visitArray(this);
@ -270,6 +271,7 @@ export class ComponentsFieldPropertiesDto extends FieldPropertiesDto {
public readonly schemaIds?: ReadonlyArray<string>; public readonly schemaIds?: ReadonlyArray<string>;
public readonly maxItems?: number; public readonly maxItems?: number;
public readonly minItems?: number; public readonly minItems?: number;
public readonly uniqueFields?: ReadonlyArray<string>;
public get isComplexUI() { public get isComplexUI() {
return true; 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), ValidatorsEx.betweenLength(properties.minItems, properties.maxItems),
]; ];
if (properties.uniqueFields && properties.uniqueFields.length > 0) {
validators.push(ValidatorsEx.uniqueObjectValues(properties.uniqueFields));
}
return validators; return validators;
} }
@ -274,6 +278,10 @@ export class FieldsValidators implements FieldPropertiesVisitor<ReadonlyArray<Va
ValidatorsEx.betweenLength(properties.minItems, properties.maxItems), ValidatorsEx.betweenLength(properties.minItems, properties.maxItems),
]; ];
if (properties.uniqueFields && properties.uniqueFields.length > 0) {
validators.push(ValidatorsEx.uniqueObjectValues(properties.uniqueFields));
}
return validators; return validators;
} }

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

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

Loading…
Cancel
Save