From f3caeddd1695c5db424f61c0d7578e4a6a396ffe Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 31 Aug 2020 22:14:46 +0200 Subject: [PATCH] Text validation. --- backend/i18n/frontend_en.json | 13 +- backend/i18n/frontend_nl.json | 13 +- backend/i18n/source/backend_en.json | 15 +- backend/i18n/source/frontend_en.json | 13 +- backend/i18n/source/frontend_nl.json | 6 +- .../Schemas/StringContentType.cs | 16 ++ .../Schemas/StringFieldProperties.cs | 10 + .../DefaultFieldValueValidatorsFactory.cs | 104 ++++++--- .../Validators/CollectionValidator.cs | 6 +- .../Validators/StringLengthValidator.cs | 8 +- .../Validators/StringTextValidator.cs | 118 +++++++++++ .../Guards/FieldPropertiesValidator.cs | 87 ++++---- backend/src/Squidex.Shared/Texts.nl.resx | 45 +++- backend/src/Squidex.Shared/Texts.resx | 45 +++- .../Models/Fields/StringFieldPropertiesDto.cs | 25 +++ .../ValidateContent/StringFieldTests.cs | 22 ++ .../Validators/StringLengthValidatorTests.cs | 6 +- .../Validators/StringTextValidatorTests.cs | 199 ++++++++++++++++++ .../DateTimeFieldPropertiesTests.cs | 30 +-- .../NumberFieldPropertiesTests.cs | 58 +---- .../StringFieldPropertiesTests.cs | 48 ++++- .../fields/types/assets-ui.component.html | 6 +- .../fields/types/boolean-ui.component.html | 15 +- .../fields/types/boolean-ui.component.ts | 4 +- .../fields/types/date-time-ui.component.html | 15 +- .../fields/types/date-time-ui.component.ts | 6 +- .../fields/types/number-ui.component.html | 29 +-- .../fields/types/number-ui.component.ts | 4 +- .../fields/types/references-ui.component.html | 29 +-- .../fields/types/references-ui.component.ts | 4 +- .../fields/types/string-ui.component.ts | 4 +- .../types/string-validation.component.html | 35 +++ .../types/string-validation.component.ts | 19 +- .../fields/types/tags-ui.component.html | 22 +- .../schema/fields/types/tags-ui.component.ts | 4 +- .../schema-field-rules-form.component.html | 4 +- .../schema-field-rules-form.component.ts | 3 +- .../framework/angular/pager.component.html | 5 +- .../app/framework/angular/pager.component.ts | 4 + .../search/queries/sorting.component.html | 3 +- .../search/queries/sorting.component.ts | 4 +- .../app/shared/services/schemas.service.ts | 6 + frontend/app/shared/services/schemas.types.ts | 73 ++++++- frontend/app/shared/state/query.ts | 5 + frontend/app/theme/icomoon/demo.html | 14 ++ frontend/app/theme/icomoon/fonts/icomoon.eot | Bin 31928 -> 31928 bytes frontend/app/theme/icomoon/fonts/icomoon.svg | 2 +- frontend/app/theme/icomoon/fonts/icomoon.ttf | Bin 31764 -> 31764 bytes frontend/app/theme/icomoon/fonts/icomoon.woff | Bin 31840 -> 31840 bytes frontend/app/theme/icomoon/selection.json | 2 +- frontend/app/theme/icomoon/style.css | 15 +- 51 files changed, 891 insertions(+), 332 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringContentType.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index c89da2187..397f956c8 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -648,11 +648,11 @@ "schemas.fieldTypes.assets.description": "Images, videos, documents.", "schemas.fieldTypes.assets.fileExtensions": "File Extensions", "schemas.fieldTypes.assets.mustBeImage": "Must be Image", + "schemas.fieldTypes.assets.previewFileName": "Only file name", + "schemas.fieldTypes.assets.previewImage": "Only thumbnail or file name if not an image", + "schemas.fieldTypes.assets.previewImageAndFileName": "Thumbnail and file name", "schemas.fieldTypes.assets.previewMode": "PreviewMode", "schemas.fieldTypes.assets.previewModeHint": "The preview mode for assets in content lists.", - "schemas.fieldTypes.assets.previewModeName": "Only file name", - "schemas.fieldTypes.assets.previewModeTumbnailName": "Thumbnail and file name", - "schemas.fieldTypes.assets.previewModeTumbnailOrName": "Only thumbnail or file name if not an image", "schemas.fieldTypes.assets.resolve": "Resolve first asset", "schemas.fieldTypes.assets.resolveHint": "Show the first referenced asset in the content list.", "schemas.fieldTypes.assets.size": "Size", @@ -674,6 +674,10 @@ "schemas.fieldTypes.references.countMin": "Min Items", "schemas.fieldTypes.references.description": "Links to other content items.", "schemas.fieldTypes.references.resolveHint": "Show the name of the referenced item in content list when MaxItems is set to 1.", + "schemas.fieldTypes.string.characters": "Characters", + "schemas.fieldTypes.string.charactersMax": "Max Characters", + "schemas.fieldTypes.string.charactersMin": "Min Characters", + "schemas.fieldTypes.string.contentType": "Content Type", "schemas.fieldTypes.string.description": "Titles, names, paragraphs.", "schemas.fieldTypes.string.length": "Length", "schemas.fieldTypes.string.lengthMax": "Max Length", @@ -681,6 +685,9 @@ "schemas.fieldTypes.string.pattern": "Regex Pattern", "schemas.fieldTypes.string.patternMessage": "Pattern Message", "schemas.fieldTypes.string.suggestions": "Suggestions", + "schemas.fieldTypes.string.words": "Words", + "schemas.fieldTypes.string.wordsMax": "Max Words", + "schemas.fieldTypes.string.wordsMin": "Min Words", "schemas.fieldTypes.tags.count": "Items", "schemas.fieldTypes.tags.countMax": "Max Items", "schemas.fieldTypes.tags.countMin": "Min Items", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index b3cc68fb0..8f9c44f4f 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -648,11 +648,11 @@ "schemas.fieldTypes.assets.description": "Afbeeldingen, video's, documenten.", "schemas.fieldTypes.assets.fileExtensions": "Bestandsextensies", "schemas.fieldTypes.assets.mustBeImage": "Moet afbeelding zijn", + "schemas.fieldTypes.assets.previewFileName": "Alleen bestandsnaam", + "schemas.fieldTypes.assets.previewImage": "Alleen miniatuur- of bestandsnaam indien geen afbeelding", + "schemas.fieldTypes.assets.previewImageAndFileName": "Miniatuur- en bestandsnaam", "schemas.fieldTypes.assets.previewMode": "PreviewMode", "schemas.fieldTypes.assets.previewModeHint": "De voorbeeldmodus voor items in inhoudslijsten.", - "schemas.fieldTypes.assets.previewModeName": "Alleen bestandsnaam", - "schemas.fieldTypes.assets.previewModeTumbnailName": "Miniatuur- en bestandsnaam", - "schemas.fieldTypes.assets.previewModeTumbnailOrName": "Alleen miniatuur- of bestandsnaam indien geen afbeelding", "schemas.fieldTypes.assets.resolve": "Eerste item oplossen", "schemas.fieldTypes.assets.resolveHint": "Toon het eerste item waarnaar wordt verwezen in de inhoudslijst.", "schemas.fieldTypes.assets.size": "Grootte", @@ -674,6 +674,10 @@ "schemas.fieldTypes.references.countMin": "Min. items", "schemas.fieldTypes.references.description": "Links naar andere inhoudsitems.", "schemas.fieldTypes.references.resolveHint": "Toon de naam van het item waarnaar wordt verwezen in de inhoudslijst wanneer MaxItems is ingesteld op 1.", + "schemas.fieldTypes.string.characters": "Characters", + "schemas.fieldTypes.string.charactersMax": "Max Characters", + "schemas.fieldTypes.string.charactersMin": "Min Characters", + "schemas.fieldTypes.string.contentType": "Content Type", "schemas.fieldTypes.string.description": "Titels, namen, alinea's.", "schemas.fieldTypes.string.length": "Lengte", "schemas.fieldTypes.string.lengthMax": "Max. lengte", @@ -681,6 +685,9 @@ "schemas.fieldTypes.string.pattern": "Regex-patroon", "schemas.fieldTypes.string.patternMessage": "Patroonbericht", "schemas.fieldTypes.string.suggestions": "Suggesties", + "schemas.fieldTypes.string.words": "Words", + "schemas.fieldTypes.string.wordsMax": "Max Words", + "schemas.fieldTypes.string.wordsMin": "Min Words", "schemas.fieldTypes.tags.count": "Artikelen", "schemas.fieldTypes.tags.countMax": "Max. aantal items", "schemas.fieldTypes.tags.countMin": "Min. items", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index dcdd7aff0..bfa249dd6 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -43,6 +43,7 @@ "common.clientd": "Client ID", "common.clientId": "Client ID", "common.clientSecret": "Client Secret", + "common.contentType": "Content type", "common.contributorId": "Contributor ID or email", "common.data": "Data", "common.defaultValue": "Default value", @@ -69,18 +70,22 @@ "common.language": "Language code", "common.login": "Login", "common.logout": "Logout", + "common.maxCharacters": "Max characters", "common.maxHeight": "Max height", "common.maxItems": "Max items", "common.maxLength": "Max length", "common.maxSize": "Max size", "common.maxValue": "Max value", "common.maxWidth": "Max width", + "common.maxWords": "Max words", + "common.minCharacters": "Min characters", "common.minHeight": "Min height", "common.minItems": "Min items", "common.minLength": "Min length", "common.minSize": "Min size", "common.minValue": "Min value", "common.minWidth": "Min width", + "common.minWords": "Min words", "common.name": "Name", "common.notFoundValue": "- not found -", "common.numDays": "Num days", @@ -147,24 +152,32 @@ "contents.validation.itemCount": "Must have exactly {count} item(s).", "contents.validation.itemCountBetween": "Must have between {min} and {max} item(s).", "contents.validation.max": "Must be less or equal to {max}.", + "contents.validation.maxCharacters": "Must not have more than {max} text character(s).", "contents.validation.maximumHeight": "Height {height}px must be less than {max}px.", "contents.validation.maximumSize": "Size of {size} must be less than {max}.", "contents.validation.maximumWidth": "Width {width}px must be less than {max}px.", "contents.validation.maxItems": "Must not have more than {max} item(s).", "contents.validation.maxLength": "Must not have more than {max} character(s).", + "contents.validation.maxWords": "Must not have more than {max} word(s).", "contents.validation.min": "Must be greater or equal to {min}.", "contents.validation.minimumHeight": "Height {height}px must be greater than {min}px.", "contents.validation.minimumSize": "Size of {size} must be greater than {min}.", "contents.validation.minimumWidth": "Width {width}px must be greater than {min}px.", "contents.validation.minItems": "Must have at least {min} item(s).", "contents.validation.minLength": "Must have at least {min} character(s).", + "contents.validation.minNormalCharacters": "Must have at least {min} text character(s).", + "contents.validation.minWords": "Must have at least {min} word(s).", "contents.validation.mustBeEmpty": "Value must not be defined.", + "contents.validation.normalCharacterCount": "Must have exactly {count} text character(s).", + "contents.validation.normalCharactersBetween": "Must have between {min} and {max} text character(s).", "contents.validation.notAllowed": "Not an allowed value.", "contents.validation.pattern": "Must follow the pattern.", "contents.validation.regexTooSlow": "Regex is too slow.", "contents.validation.required": "Field is required.", "contents.validation.unique": "Another content with the same value exists.", "contents.validation.unknownField": "Not a known {fieldType}.", + "contents.validation.wordCount": "Must have exactly {count} word(s).", + "contents.validation.wordsBetween": "Must have between {min} and {max} word(s).", "contents.workflowErorPublishing": "Content workflow prevents publishing.", "contents.workflowErrorUpdate": "The workflow does not allow updates at status {status}", "exception.invalidJsonQuery": "Json query not valid: {message}", @@ -228,12 +241,10 @@ "schemas.nameAlreadyExists": "A schema with the same name already exists.", "schemas.noPermission": "You do not have permission for this schema.", "schemas.notFoundId": "Schema {id} does not exist.", - "schemas.number.eitherMinMaxOrAllowedValuesError": "Either allowed values or min and max length can be defined.", "schemas.number.inlineEditorError": "Inline editing is not allowed for Radio editor.", "schemas.onlyArraysHaveNested": "Only array fields can have nested fields.", "schemas.onylArraysInRoot": "Nested field cannot be array fields.", "schemas.references.resolveError": "Can only resolve references when MaxItems is 1.", - "schemas.string.eitherMinMaxOrAllowedValuesError": "Either allowed values or min and max value can be defined.", "schemas.string.inlineEditorError": "Inline editing is only allowed for dropdowns, slugs and input fields.", "schemas.stringEditorsNeedAllowedValuesError": "Radio buttons or dropdown list need allowed values.", "schemas.tags.editorNeedsAllowedValues": "Checkboxes or dropdown list need allowed values.", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index c89da2187..397f956c8 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -648,11 +648,11 @@ "schemas.fieldTypes.assets.description": "Images, videos, documents.", "schemas.fieldTypes.assets.fileExtensions": "File Extensions", "schemas.fieldTypes.assets.mustBeImage": "Must be Image", + "schemas.fieldTypes.assets.previewFileName": "Only file name", + "schemas.fieldTypes.assets.previewImage": "Only thumbnail or file name if not an image", + "schemas.fieldTypes.assets.previewImageAndFileName": "Thumbnail and file name", "schemas.fieldTypes.assets.previewMode": "PreviewMode", "schemas.fieldTypes.assets.previewModeHint": "The preview mode for assets in content lists.", - "schemas.fieldTypes.assets.previewModeName": "Only file name", - "schemas.fieldTypes.assets.previewModeTumbnailName": "Thumbnail and file name", - "schemas.fieldTypes.assets.previewModeTumbnailOrName": "Only thumbnail or file name if not an image", "schemas.fieldTypes.assets.resolve": "Resolve first asset", "schemas.fieldTypes.assets.resolveHint": "Show the first referenced asset in the content list.", "schemas.fieldTypes.assets.size": "Size", @@ -674,6 +674,10 @@ "schemas.fieldTypes.references.countMin": "Min Items", "schemas.fieldTypes.references.description": "Links to other content items.", "schemas.fieldTypes.references.resolveHint": "Show the name of the referenced item in content list when MaxItems is set to 1.", + "schemas.fieldTypes.string.characters": "Characters", + "schemas.fieldTypes.string.charactersMax": "Max Characters", + "schemas.fieldTypes.string.charactersMin": "Min Characters", + "schemas.fieldTypes.string.contentType": "Content Type", "schemas.fieldTypes.string.description": "Titles, names, paragraphs.", "schemas.fieldTypes.string.length": "Length", "schemas.fieldTypes.string.lengthMax": "Max Length", @@ -681,6 +685,9 @@ "schemas.fieldTypes.string.pattern": "Regex Pattern", "schemas.fieldTypes.string.patternMessage": "Pattern Message", "schemas.fieldTypes.string.suggestions": "Suggestions", + "schemas.fieldTypes.string.words": "Words", + "schemas.fieldTypes.string.wordsMax": "Max Words", + "schemas.fieldTypes.string.wordsMin": "Min Words", "schemas.fieldTypes.tags.count": "Items", "schemas.fieldTypes.tags.countMax": "Max Items", "schemas.fieldTypes.tags.countMin": "Min Items", diff --git a/backend/i18n/source/frontend_nl.json b/backend/i18n/source/frontend_nl.json index b3cc68fb0..5641fb60d 100644 --- a/backend/i18n/source/frontend_nl.json +++ b/backend/i18n/source/frontend_nl.json @@ -648,11 +648,11 @@ "schemas.fieldTypes.assets.description": "Afbeeldingen, video's, documenten.", "schemas.fieldTypes.assets.fileExtensions": "Bestandsextensies", "schemas.fieldTypes.assets.mustBeImage": "Moet afbeelding zijn", + "schemas.fieldTypes.assets.previewFileName": "Alleen bestandsnaam", + "schemas.fieldTypes.assets.previewImage": "Alleen miniatuur- of bestandsnaam indien geen afbeelding", + "schemas.fieldTypes.assets.previewImageAndFileName": "Miniatuur- en bestandsnaam", "schemas.fieldTypes.assets.previewMode": "PreviewMode", "schemas.fieldTypes.assets.previewModeHint": "De voorbeeldmodus voor items in inhoudslijsten.", - "schemas.fieldTypes.assets.previewModeName": "Alleen bestandsnaam", - "schemas.fieldTypes.assets.previewModeTumbnailName": "Miniatuur- en bestandsnaam", - "schemas.fieldTypes.assets.previewModeTumbnailOrName": "Alleen miniatuur- of bestandsnaam indien geen afbeelding", "schemas.fieldTypes.assets.resolve": "Eerste item oplossen", "schemas.fieldTypes.assets.resolveHint": "Toon het eerste item waarnaar wordt verwezen in de inhoudslijst.", "schemas.fieldTypes.assets.size": "Grootte", diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringContentType.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringContentType.cs new file mode 100644 index 000000000..20e76163a --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringContentType.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public enum StringContentType + { + Unspecified, + Html, + Markdown + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs index eb398504e..47932d0c8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs @@ -18,6 +18,14 @@ namespace Squidex.Domain.Apps.Core.Schemas public int? MaxLength { get; set; } + public int? MinCharacters { get; set; } + + public int? MaxCharacters { get; set; } + + public int? MinWords { get; set; } + + public int? MaxWords { get; set; } + public bool IsUnique { get; set; } public bool InlineEditable { get; set; } @@ -28,6 +36,8 @@ namespace Squidex.Domain.Apps.Core.Schemas public string? PatternMessage { get; set; } + public StringContentType ContentType { get; set; } + public StringFieldEditor Editor { get; set; } public override T Accept(IFieldPropertiesVisitor visitor) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs index 38306da6f..f629d8fb7 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs @@ -35,9 +35,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public IEnumerable Visit(IArrayField field) { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + var properties = field.Properties; + + if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); } var nestedSchema = new Dictionary(field.Fields.Count); @@ -52,12 +54,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public IEnumerable Visit(IField field) { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + var properties = field.Properties; + + if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); } - if (!field.Properties.AllowDuplicates) + if (!properties.AllowDuplicates) { yield return new UniqueValuesValidator(); } @@ -65,7 +69,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public IEnumerable Visit(IField field) { - if (field.Properties.IsRequired) + var properties = field.Properties; + + if (properties.IsRequired) { yield return new RequiredValidator(); } @@ -73,20 +79,24 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public IEnumerable Visit(IField field) { - if (field.Properties.IsRequired) + var properties = field.Properties; + + if (properties.IsRequired) { yield return new RequiredValidator(); } - if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) + if (properties.MinValue.HasValue || properties.MaxValue.HasValue) { - yield return new RangeValidator(field.Properties.MinValue, field.Properties.MaxValue); + yield return new RangeValidator(properties.MinValue, properties.MaxValue); } } public IEnumerable Visit(IField field) { - if (field.Properties.IsRequired) + var properties = field.Properties; + + if (properties.IsRequired) { yield return new RequiredValidator(); } @@ -94,7 +104,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public IEnumerable Visit(IField field) { - if (field.Properties.IsRequired) + var properties = field.Properties; + + if (properties.IsRequired) { yield return new RequiredValidator(); } @@ -102,30 +114,34 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public IEnumerable Visit(IField field) { - if (field.Properties.IsRequired) + var properties = field.Properties; + + if (properties.IsRequired) { yield return new RequiredValidator(); } - if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) + if (properties.MinValue.HasValue || properties.MaxValue.HasValue) { - yield return new RangeValidator(field.Properties.MinValue, field.Properties.MaxValue); + yield return new RangeValidator(properties.MinValue, properties.MaxValue); } - if (field.Properties.AllowedValues != null) + if (properties.AllowedValues != null) { - yield return new AllowedValuesValidator(field.Properties.AllowedValues); + yield return new AllowedValuesValidator(properties.AllowedValues); } } public IEnumerable Visit(IField field) { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + var properties = field.Properties; + + if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); } - if (!field.Properties.AllowDuplicates) + if (!properties.AllowDuplicates) { yield return new UniqueValuesValidator(); } @@ -133,37 +149,65 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public IEnumerable Visit(IField field) { - if (field.Properties.IsRequired) + var properties = field.Properties; + + if (properties.IsRequired) { yield return new RequiredStringValidator(true); } - if (field.Properties.MinLength.HasValue || field.Properties.MaxLength.HasValue) + if (properties.MinLength.HasValue || properties.MaxLength.HasValue) { - yield return new StringLengthValidator(field.Properties.MinLength, field.Properties.MaxLength); + yield return new StringLengthValidator(properties.MinLength, properties.MaxLength); } - if (!string.IsNullOrWhiteSpace(field.Properties.Pattern)) + if (properties.MinCharacters.HasValue || + properties.MaxCharacters.HasValue || + properties.MinWords.HasValue || + properties.MaxWords.HasValue) { - yield return new PatternValidator(field.Properties.Pattern, field.Properties.PatternMessage); + Func? transform = null; + + switch (properties.ContentType) + { + case StringContentType.Markdown: + transform = TextHelpers.Markdown2Text; + break; + case StringContentType.Html: + transform = TextHelpers.Html2Text; + break; + } + + yield return new StringTextValidator(transform, + properties.MinCharacters, + properties.MaxCharacters, + properties.MinWords, + properties.MaxWords); } - if (field.Properties.AllowedValues != null) + if (!string.IsNullOrWhiteSpace(properties.Pattern)) { - yield return new AllowedValuesValidator(field.Properties.AllowedValues); + yield return new PatternValidator(properties.Pattern, properties.PatternMessage); + } + + if (properties.AllowedValues != null) + { + yield return new AllowedValuesValidator(properties.AllowedValues); } } public IEnumerable Visit(IField field) { - if (field.Properties.IsRequired || field.Properties.MinItems.HasValue || field.Properties.MaxItems.HasValue) + var properties = field.Properties; + + if (properties.IsRequired || properties.MinItems.HasValue || properties.MaxItems.HasValue) { - yield return new CollectionValidator(field.Properties.IsRequired, field.Properties.MinItems, field.Properties.MaxItems); + yield return new CollectionValidator(properties.IsRequired, properties.MinItems, properties.MaxItems); } - if (field.Properties.AllowedValues != null) + if (properties.AllowedValues != null) { - yield return new CollectionItemValidator(new AllowedValuesValidator(field.Properties.AllowedValues)); + yield return new CollectionItemValidator(new AllowedValuesValidator(properties.AllowedValues)); } yield return new CollectionItemValidator(new RequiredStringValidator(true)); diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs index 6d7719181..94979e7a3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators public CollectionValidator(bool isRequired, int? minItems = null, int? maxItems = null) { - if (minItems.HasValue && maxItems.HasValue && minItems.Value > maxItems.Value) + if (minItems.HasValue && maxItems.HasValue && minItems > maxItems) { throw new ArgumentException("Min length must be greater than max length.", nameof(minItems)); } @@ -55,12 +55,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators } else { - if (minItems.HasValue && items.Count < minItems.Value) + if (minItems.HasValue && items.Count < minItems) { addError(context.Path, T.Get("contents.validation.minItems", new { min = minItems })); } - if (maxItems.HasValue && items.Count > maxItems.Value) + if (maxItems.HasValue && items.Count > maxItems) { addError(context.Path, T.Get("contents.validation.maxItems", new { max = maxItems })); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs index 553c39376..a7b4eb426 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs @@ -16,9 +16,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators private readonly int? minLength; private readonly int? maxLength; - public StringLengthValidator(int? minLength, int? maxLength) + public StringLengthValidator(int? minLength = null, int? maxLength = null) { - if (minLength.HasValue && maxLength.HasValue && minLength.Value > maxLength.Value) + if (minLength > maxLength) { throw new ArgumentException("Min length must be greater than max length.", nameof(minLength)); } @@ -44,12 +44,12 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators } else { - if (minLength.HasValue && stringValue.Length < minLength.Value) + if (minLength.HasValue && stringValue.Length < minLength) { addError(context.Path, T.Get("contents.validation.minLength", new { min = minLength })); } - if (maxLength.HasValue && stringValue.Length > maxLength.Value) + if (maxLength.HasValue && stringValue.Length > maxLength) { addError(context.Path, T.Get("contents.validation.maxLength", new { max = maxLength })); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs new file mode 100644 index 000000000..e57303730 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs @@ -0,0 +1,118 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.Translations; +using Squidex.Text; + +namespace Squidex.Domain.Apps.Core.ValidateContent.Validators +{ + public sealed class StringTextValidator : IValidator + { + private readonly Func? transform; + private readonly int? minCharacters; + private readonly int? maxCharacters; + private readonly int? minWords; + private readonly int? maxWords; + + public StringTextValidator(Func? transform = null, + int? minCharacters = null, + int? maxCharacters = null, + int? minWords = null, + int? maxWords = null) + { + if (minCharacters > maxCharacters) + { + throw new ArgumentException("Min characters must be greater than max characters.", nameof(minCharacters)); + } + + if (minWords > maxWords) + { + throw new ArgumentException("Min words must be greater than max words.", nameof(minWords)); + } + + this.transform = transform; + this.minCharacters = minCharacters; + this.maxCharacters = maxCharacters; + this.minWords = minWords; + this.maxWords = maxWords; + } + + public Task ValidateAsync(object? value, ValidationContext context, AddError addError) + { + if (value is string stringValue && !string.IsNullOrEmpty(stringValue)) + { + if (transform != null) + { + stringValue = transform(stringValue); + } + + if (minWords.HasValue || maxWords.HasValue) + { + var words = stringValue.WordCount(); + + if (minWords.HasValue && maxWords.HasValue) + { + if (minWords == maxWords && minWords != words) + { + addError(context.Path, T.Get("contents.validation.wordCount", new { count = minWords })); + } + else if (words < minWords || words > maxWords) + { + addError(context.Path, T.Get("contents.validation.wordsBetween", new { min = minWords, max = maxWords })); + } + } + else + { + if (minWords.HasValue && words < minWords) + { + addError(context.Path, T.Get("contents.validation.minWords", new { min = minWords })); + } + + if (maxWords.HasValue && words > maxWords) + { + addError(context.Path, T.Get("contents.validation.maxWords", new { max = maxWords })); + } + } + } + + if (minCharacters.HasValue || maxCharacters.HasValue) + { + var characters = stringValue.CharacterCount(); + + if (minCharacters.HasValue && maxCharacters.HasValue) + { + if (minCharacters == maxCharacters && minCharacters != characters) + { + addError(context.Path, T.Get("contents.validation.normalCharacterCount", new { count = minCharacters })); + } + else if (characters < minCharacters || characters > maxCharacters) + { + addError(context.Path, T.Get("contents.validation.normalCharactersBetween", new { min = minCharacters, max = maxCharacters })); + } + } + else + { + if (minCharacters.HasValue && characters < minCharacters) + { + addError(context.Path, T.Get("contents.validation.minNormalCharacters", new { min = minCharacters })); + } + + if (maxCharacters.HasValue && characters > maxCharacters) + { + addError(context.Path, T.Get("contents.validation.maxCharacters", new { max = maxCharacters })); + } + } + } + } + + return Task.CompletedTask; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs index 1e13eb3e3..4b8328ba1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs @@ -5,7 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Collections.Generic; +using System.Linq; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; @@ -33,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards public IEnumerable Visit(ArrayFieldProperties properties) { - if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value > properties.MaxItems.Value) + if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems)) { yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), nameof(properties.MinItems), @@ -43,28 +45,28 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards public IEnumerable Visit(AssetsFieldProperties properties) { - if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value > properties.MaxItems.Value) + if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems)) { yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), nameof(properties.MinItems), nameof(properties.MaxItems)); } - if (properties.MaxHeight.HasValue && properties.MinHeight.HasValue && properties.MinHeight.Value > properties.MaxHeight.Value) + if (IsMaxGreaterThanMin(properties.MaxHeight, properties.MinHeight)) { yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxHeight), nameof(properties.MinHeight)), nameof(properties.MaxHeight), nameof(properties.MinHeight)); } - if (properties.MaxWidth.HasValue && properties.MinWidth.HasValue && properties.MinWidth.Value > properties.MaxWidth.Value) + if (IsMaxGreaterThanMin(properties.MaxWidth, properties.MinWidth)) { yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxWidth), nameof(properties.MinWidth)), nameof(properties.MaxWidth), nameof(properties.MinWidth)); } - if (properties.MaxSize.HasValue && properties.MinSize.HasValue && properties.MinSize.Value >= properties.MaxSize.Value) + if (IsMaxGreaterThanMin(properties.MaxSize, properties.MinSize)) { yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxSize), nameof(properties.MinSize)), nameof(properties.MaxSize), @@ -96,19 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards nameof(properties.Editor)); } - if (properties.DefaultValue.HasValue && properties.MinValue.HasValue && properties.DefaultValue.Value < properties.MinValue.Value) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.DefaultValue), nameof(properties.MinValue)), - nameof(properties.DefaultValue)); - } - - if (properties.DefaultValue.HasValue && properties.MaxValue.HasValue && properties.DefaultValue.Value > properties.MaxValue.Value) - { - yield return new ValidationError(Not.LessEqualsThan(nameof(properties.DefaultValue), nameof(properties.MaxValue)), - nameof(properties.DefaultValue)); - } - - if (properties.MaxValue.HasValue && properties.MinValue.HasValue && properties.MinValue.Value >= properties.MaxValue.Value) + if (IsMaxGreaterThanMin(properties.MaxValue, properties.MinValue)) { yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxValue), nameof(properties.MinValue)), nameof(properties.MinValue), @@ -154,39 +144,19 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards nameof(properties.Editor)); } - if ((properties.Editor == NumberFieldEditor.Radio || properties.Editor == NumberFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) + if ((properties.Editor == NumberFieldEditor.Radio || properties.Editor == NumberFieldEditor.Dropdown) && properties.AllowedValues?.Any() != true) { yield return new ValidationError(T.Get("schemas.stringEditorsNeedAllowedValuesError"), nameof(properties.AllowedValues)); } - if (properties.DefaultValue.HasValue && properties.MinValue.HasValue && properties.DefaultValue.Value < properties.MinValue.Value) - { - yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.DefaultValue), nameof(properties.MinValue)), - nameof(properties.DefaultValue)); - } - - if (properties.DefaultValue.HasValue && properties.MaxValue.HasValue && properties.DefaultValue.Value > properties.MaxValue.Value) - { - yield return new ValidationError(Not.LessEqualsThan(nameof(properties.DefaultValue), nameof(properties.MaxValue)), - nameof(properties.DefaultValue)); - } - - if (properties.MaxValue.HasValue && properties.MinValue.HasValue && properties.MinValue.Value >= properties.MaxValue.Value) + if (properties.MaxValue.HasValue && properties.MinValue.HasValue && properties.MinValue >= properties.MaxValue) { yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxValue), nameof(properties.MinValue)), nameof(properties.MinValue), nameof(properties.MaxValue)); } - if (properties.AllowedValues != null && properties.AllowedValues.Count > 0 && (properties.MinValue.HasValue || properties.MaxValue.HasValue)) - { - yield return new ValidationError(T.Get("schemas.string.eitherMinMaxOrAllowedValuesError"), - nameof(properties.AllowedValues), - nameof(properties.MinValue), - nameof(properties.MaxValue)); - } - if (properties.InlineEditable && properties.Editor == NumberFieldEditor.Radio) { yield return new ValidationError(T.Get("schemas.number.inlineEditorError"), @@ -203,7 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards nameof(properties.Editor)); } - if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value > properties.MaxItems.Value) + if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems > properties.MaxItems) { yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), nameof(properties.MinItems), @@ -226,7 +196,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards nameof(properties.Editor)); } - if ((properties.Editor == StringFieldEditor.Radio || properties.Editor == StringFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) + if (!properties.ContentType.IsEnumValue()) + { + yield return new ValidationError(Not.Valid(nameof(properties.ContentType)), + nameof(properties.ContentType)); + } + + if ((properties.Editor == StringFieldEditor.Radio || properties.Editor == StringFieldEditor.Dropdown) && properties.AllowedValues?.Any() != true) { yield return new ValidationError(T.Get("schemas.stringEditorsNeedAllowedValuesError"), nameof(properties.AllowedValues)); @@ -238,19 +214,25 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards nameof(properties.Pattern)); } - if (properties.MaxLength.HasValue && properties.MinLength.HasValue && properties.MinLength.Value > properties.MaxLength.Value) + if (IsMaxGreaterThanMin(properties.MaxLength, properties.MinLength)) { yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxLength), nameof(properties.MinLength)), nameof(properties.MinLength), nameof(properties.MaxLength)); } - if (properties.AllowedValues != null && properties.AllowedValues.Count > 0 && (properties.MinLength.HasValue || properties.MaxLength.HasValue)) + if (IsMaxGreaterThanMin(properties.MaxWords, properties.MinWords)) { - yield return new ValidationError(T.Get("schemas.number.eitherMinMaxOrAllowedValuesError"), - nameof(properties.AllowedValues), - nameof(properties.MinLength), - nameof(properties.MaxLength)); + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxWords), nameof(properties.MinWords)), + nameof(properties.MinWords), + nameof(properties.MaxWords)); + } + + if (IsMaxGreaterThanMin(properties.MaxCharacters, properties.MinCharacters)) + { + yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxCharacters), nameof(properties.MinCharacters)), + nameof(properties.MinCharacters), + nameof(properties.MaxCharacters)); } if (properties.InlineEditable && properties.Editor != StringFieldEditor.Dropdown && properties.Editor != StringFieldEditor.Input && properties.Editor != StringFieldEditor.Slug) @@ -269,13 +251,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards nameof(properties.Editor)); } - if ((properties.Editor == TagsFieldEditor.Checkboxes || properties.Editor == TagsFieldEditor.Dropdown) && (properties.AllowedValues == null || properties.AllowedValues.Count == 0)) + if ((properties.Editor == TagsFieldEditor.Checkboxes || properties.Editor == TagsFieldEditor.Dropdown) && properties.AllowedValues?.Any() != true) { yield return new ValidationError(T.Get("schemas.tags.editorNeedsAllowedValues"), nameof(properties.AllowedValues)); } - if (properties.MaxItems.HasValue && properties.MinItems.HasValue && properties.MinItems.Value > properties.MaxItems.Value) + if (IsMaxGreaterThanMin(properties.MaxItems, properties.MinItems)) { yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)), nameof(properties.MinItems), @@ -291,5 +273,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards nameof(properties.Editor)); } } + + private static bool IsMaxGreaterThanMin(T? min, T? max) where T : struct, IComparable + { + return max.HasValue && min.HasValue && min.Value.CompareTo(max.Value) < 0; + } } } diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index eb283abce..5693fb6d2 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -190,6 +190,9 @@ Clientgeheim + + Content type + Bijdrager-ID of e-mailadres @@ -268,6 +271,9 @@ Uitloggen + + Max characters + Max. hoogte @@ -286,6 +292,12 @@ Max. breedte + + Max words + + + Min characters + Min. hoogte @@ -304,6 +316,9 @@ Min. breedte + + Min words + Naam @@ -502,6 +517,9 @@ Moet kleiner zijn dan of gelijk zijn aan {max}. + + Must not have more than {max} text character(s). + Hoogte {hoogte} px moet kleiner zijn dan {max} px. @@ -517,6 +535,9 @@ Mag niet meer dan {max} teken (s) bevatten. + + Must not have more than {max} word(s). + Moet groter zijn dan of gelijk zijn aan {min}. @@ -535,9 +556,21 @@ Moet minstens {min} teken (s) bevatten. + + Must have at least {min} text character(s). + + + Must have at least {min} word(s). + Waarde mag niet worden gedefinieerd. + + Must have exactly {count} text character(s). + + + Must have between {min} and {max} text character(s). + Geen toegestane waarde. @@ -556,6 +589,12 @@ Onbekend {fieldType}. + + Must have exactly {count} word(s). + + + Must have between {min} and {max} word(s). + Contentworkflow verhindert publiceren. @@ -745,9 +784,6 @@ Schema {id} bestaat niet. - - Ofwel toegestane waarden of min en max lengte kunnen worden gedefinieerd. - Inline bewerken is niet toegestaan ​​voor Radio-editor. @@ -760,9 +796,6 @@ Kan alleen verwijzingen omzetten als MaxItems 1 is. - - Ofwel toegestane waarden of min en max waarde kunnen worden gedefinieerd. - Inline bewerken is alleen toegestaan ​​voor dropdowns, slugs en invoervelden. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index bbb5c6438..010411938 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -190,6 +190,9 @@ Client Secret + + Content type + Contributor ID or email @@ -268,6 +271,9 @@ Logout + + Max characters + Max height @@ -286,6 +292,12 @@ Max width + + Max words + + + Min characters + Min height @@ -304,6 +316,9 @@ Min width + + Min words + Name @@ -502,6 +517,9 @@ Must be less or equal to {max}. + + Must not have more than {max} text character(s). + Height {height}px must be less than {max}px. @@ -517,6 +535,9 @@ Must not have more than {max} character(s). + + Must not have more than {max} word(s). + Must be greater or equal to {min}. @@ -535,9 +556,21 @@ Must have at least {min} character(s). + + Must have at least {min} text character(s). + + + Must have at least {min} word(s). + Value must not be defined. + + Must have exactly {count} text character(s). + + + Must have between {min} and {max} text character(s). + Not an allowed value. @@ -556,6 +589,12 @@ Not a known {fieldType}. + + Must have exactly {count} word(s). + + + Must have between {min} and {max} word(s). + Content workflow prevents publishing. @@ -745,9 +784,6 @@ Schema {id} does not exist. - - Either allowed values or min and max length can be defined. - Inline editing is not allowed for Radio editor. @@ -760,9 +796,6 @@ Can only resolve references when MaxItems is 1. - - Either allowed values or min and max value can be defined. - Inline editing is only allowed for dropdowns, slugs and input fields. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs index cccf0bec5..efdc6c6bf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs @@ -38,6 +38,26 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public int? MaxLength { get; set; } + /// + /// The minimum allowed of normal characters for the field value. + /// + public int? MinCharacters { get; set; } + + /// + /// The maximum allowed of normal characters for the field value. + /// + public int? MaxCharacters { get; set; } + + /// + /// The minimum allowed number of words for the field value. + /// + public int? MinWords { get; set; } + + /// + /// The maximum allowed number of words for the field value. + /// + public int? MaxWords { get; set; } + /// /// The allowed values for the field value. /// @@ -53,6 +73,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields /// public bool InlineEditable { get; set; } + /// + /// How the string content should be interpreted. + /// + public StringContentType ContentType { get; set; } + /// /// The editor that is used to manage this field. /// diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs index d115d981a..a06ce359b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs @@ -82,6 +82,28 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent new[] { "Must not have more than 5 character(s)." }); } + [Fact] + public async Task Should_add_error_if_string_is_shorter_than_min_characters() + { + var sut = Field(new StringFieldProperties { MinCharacters = 10 }); + + await sut.ValidateAsync(CreateValue("123"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 10 text character(s)." }); + } + + [Fact] + public async Task Should_add_error_if_string_is_longer_than_max_characters() + { + var sut = Field(new StringFieldProperties { MaxCharacters = 5 }); + + await sut.ValidateAsync(CreateValue("12345678"), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 5 text character(s)." }); + } + [Fact] public async Task Should_add_error_if_string_not_allowed() { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs index 3fa8d3dfa..bc15bcfa8 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs @@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators [Fact] public async Task Should_not_add_error_if_value_is_null() { - var sut = new StringLengthValidator(100, 200); + var sut = new StringLengthValidator(); await sut.ValidateAsync(null, errors); @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators [Fact] public async Task Should_not_add_error_if_value_is_empty() { - var sut = new StringLengthValidator(100, 200); + var sut = new StringLengthValidator(); await sut.ValidateAsync(string.Empty, errors); @@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators } [Fact] - public async Task Should_add_error_if_collection_has_not_exact_number_of_items() + public async Task Should_add_error_if_value_has_not_exact_number_of_characters() { var sut = new StringLengthValidator(2000, 2000); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs new file mode 100644 index 000000000..7461934c8 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs @@ -0,0 +1,199 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Core.TestHelpers; +using Squidex.Domain.Apps.Core.ValidateContent.Validators; +using Xunit; + +namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators +{ + public class StringTextValidatorTests : IClassFixture + { + private readonly List errors = new List(); + + [Theory] + [InlineData(20, 10)] + public void Should_throw_error_if_min_characters_greater_than_max(int? min, int? max) + { + Assert.Throws(() => new StringTextValidator(minCharacters: min, maxCharacters: max)); + } + + [Theory] + [InlineData(20, 10)] + public void Should_throw_error_if_min_words_greater_than_max(int? min, int? max) + { + Assert.Throws(() => new StringTextValidator(minWords: min, maxWords: max)); + } + + [Fact] + public async Task Should_not_add_error_if_value_is_null() + { + var sut = new StringTextValidator(minCharacters: 5); + + await sut.ValidateAsync(null, errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_not_add_error_if_value_is_empty() + { + var sut = new StringTextValidator(minCharacters: 5); + + await sut.ValidateAsync(string.Empty, errors); + + Assert.Empty(errors); + } + + [Theory] + [InlineData(null, null)] + [InlineData(1000, null)] + [InlineData(1000, 2000)] + [InlineData(null, 2000)] + public async Task Should_not_add_error_if_value_is_within_character_range(int? min, int? max) + { + var sut = new StringTextValidator(minCharacters: min, maxCharacters: max); + + await sut.ValidateAsync(CreateString(1500), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_exact_number_of_characters() + { + var sut = new StringTextValidator(minCharacters: 5, maxCharacters: 5); + + await sut.ValidateAsync(CreateString(4), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have exactly 5 text character(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_smaller_than_min_characters() + { + var sut = new StringTextValidator(minCharacters: 2000); + + await sut.ValidateAsync(CreateString(1500), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2000 text character(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_greater_than_max_characters() + { + var sut = new StringTextValidator(maxCharacters: 1000); + + await sut.ValidateAsync(CreateString(1500), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1000 text character(s)." }); + } + + [Fact] + public async Task Should_add_error_if_collection_count_is_not_in_character_range() + { + var sut = new StringTextValidator(minCharacters: 2000, maxCharacters: 5000); + + await sut.ValidateAsync(CreateString(1), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have between 2000 and 5000 text character(s)." }); + } + + [Theory] + [InlineData(null, null, 1000)] + [InlineData(1000, null, 1000)] + [InlineData(1000, 2000, 1000)] + [InlineData(null, 2000, 1000)] + public async Task Should_not_add_error_if_value_is_within_word_range(int? min, int? max, int length) + { + var sut = new StringTextValidator(minWords: min, maxWords: max); + + await sut.ValidateAsync(CreateSentence(length), errors); + + Assert.Empty(errors); + } + + [Fact] + public async Task Should_add_error_if_value_has_not_exact_number_of_words() + { + var sut = new StringTextValidator(minWords: 5, maxWords: 5); + + await sut.ValidateAsync(CreateSentence(4), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have exactly 5 word(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_smaller_than_min_words() + { + var sut = new StringTextValidator(minWords: 2000); + + await sut.ValidateAsync(CreateSentence(1500), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have at least 2000 word(s)." }); + } + + [Fact] + public async Task Should_add_error_if_value_is_greater_than_max_words() + { + var sut = new StringTextValidator(maxWords: 1000); + + await sut.ValidateAsync(CreateSentence(1500), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must not have more than 1000 word(s)." }); + } + + [Fact] + public async Task Should_add_error_if_collection_count_is_not_in_word_range() + { + var sut = new StringTextValidator(minWords: 2000, maxWords: 5000); + + await sut.ValidateAsync(CreateSentence(1), errors); + + errors.Should().BeEquivalentTo( + new[] { "Must have between 2000 and 5000 word(s)." }); + } + + private static string CreateString(int size) + { + var sb = new StringBuilder(); + + for (var i = 0; i < size; i++) + { + sb.Append("x"); + } + + return sb.ToString(); + } + + private static string CreateSentence(int size) + { + var sb = new StringBuilder(); + + for (var i = 0; i < size; i++) + { + sb.Append("x"); + sb.Append(" "); + } + + return sb.ToString(); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs index d404383ea..f9d8f5e97 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs @@ -35,35 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties } [Fact] - public void Should_add_error_if_default_value_is_less_than_min() - { - var sut = new DateTimeFieldProperties { MinValue = FutureDays(10), DefaultValue = FutureDays(5) }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List - { - new ValidationError("Default value must be greater or equal to min value.", "DefaultValue") - }); - } - - [Fact] - public void Should_add_error_if_default_value_is_greater_than_min() - { - var sut = new DateTimeFieldProperties { MaxValue = FutureDays(10), DefaultValue = FutureDays(15) }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List - { - new ValidationError("Default value must be less or equal to max value.", "DefaultValue") - }); - } - - [Fact] - public void Should_add_error_if_min_greater_than_max() + public void Should_add_error_if_min_value_greater_than_max_value() { var sut = new DateTimeFieldProperties { MinValue = FutureDays(10), MaxValue = FutureDays(5) }; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs index cd097ef2f..11be9d4f0 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs @@ -34,35 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties } [Fact] - public void Should_add_error_if_default_value_is_less_than_min() - { - var sut = new NumberFieldProperties { MinValue = 10, DefaultValue = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List - { - new ValidationError("Default value must be greater or equal to min value.", "DefaultValue") - }); - } - - [Fact] - public void Should_add_error_if_default_value_is_greater_than_min() - { - var sut = new NumberFieldProperties { MaxValue = 0, DefaultValue = 5 }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List - { - new ValidationError("Default value must be less or equal to max value.", "DefaultValue") - }); - } - - [Fact] - public void Should_add_error_if_min_greater_than_max() + public void Should_add_error_if_min_value_greater_than_max_value() { var sut = new NumberFieldProperties { MinValue = 10, MaxValue = 5 }; @@ -75,34 +47,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties }); } - [Fact] - public void Should_add_error_if_allowed_values_and_max_value_is_specified() - { - var sut = new NumberFieldProperties { MaxValue = 10, AllowedValues = ReadOnlyCollection.Create(4d) }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List - { - new ValidationError("Either allowed values or min and max value can be defined.", "AllowedValues", "MinValue", "MaxValue") - }); - } - - [Fact] - public void Should_add_error_if_allowed_values_and_min_value_is_specified() - { - var sut = new NumberFieldProperties { MinValue = 10, AllowedValues = ReadOnlyCollection.Create(4d) }; - - var errors = FieldPropertiesValidator.Validate(sut).ToList(); - - errors.Should().BeEquivalentTo( - new List - { - new ValidationError("Either allowed values or min and max value can be defined.", "AllowedValues", "MinValue", "MaxValue") - }); - } - [Fact] public void Should_add_error_if_radio_button_has_no_allowed_values() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs index 0c9a0c75f..6ffb7b7ce 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties public class StringFieldPropertiesTests : IClassFixture { [Fact] - public void Should_add_error_if_min_greater_than_max() + public void Should_add_error_if_min_length_greater_than_max() { var sut = new StringFieldProperties { MinLength = 10, MaxLength = 5 }; @@ -43,33 +43,53 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties } [Fact] - public void Should_add_error_if_allowed_values_and_max_value_is_specified() + public void Should_add_error_if_min_characters_greater_than_max() { - var sut = new StringFieldProperties { MinLength = 10, AllowedValues = ReadOnlyCollection.Create("4") }; + var sut = new StringFieldProperties { MinCharacters = 10, MaxCharacters = 5 }; var errors = FieldPropertiesValidator.Validate(sut).ToList(); errors.Should().BeEquivalentTo( new List { - new ValidationError("Either allowed values or min and max length can be defined.", "AllowedValues", "MinLength", "MaxLength") + new ValidationError("Max characters must be greater or equal to min characters.", "MinCharacters", "MaxCharacters") }); } [Fact] - public void Should_add_error_if_allowed_values_and_min_value_is_specified() + public void Should_not_add_error_if_min_characters_equal_to_max_characters() { - var sut = new StringFieldProperties { MaxLength = 10, AllowedValues = ReadOnlyCollection.Create("4") }; + var sut = new StringFieldProperties { MinCharacters = 2, MaxCharacters = 2 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + + [Fact] + public void Should_add_error_if_min_words_greater_than_max() + { + var sut = new StringFieldProperties { MinWords = 10, MaxWords = 5 }; var errors = FieldPropertiesValidator.Validate(sut).ToList(); errors.Should().BeEquivalentTo( new List { - new ValidationError("Either allowed values or min and max length can be defined.", "AllowedValues", "MinLength", "MaxLength") + new ValidationError("Max words must be greater or equal to min words.", "MinWords", "MaxWords") }); } + [Fact] + public void Should_not_add_error_if_min_words_equal_to_max_words() + { + var sut = new StringFieldProperties { MinWords = 2, MaxWords = 2 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + Assert.Empty(errors); + } + [Fact] public void Should_add_error_if_radio_button_has_no_allowed_values() { @@ -98,6 +118,20 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties }); } + [Fact] + public void Should_add_error_if_content_type_is_not_valid() + { + var sut = new StringFieldProperties { ContentType = (StringContentType)123 }; + + var errors = FieldPropertiesValidator.Validate(sut).ToList(); + + errors.Should().BeEquivalentTo( + new List + { + new ValidationError("Content type is not a valid value.", "ContentType") + }); + } + [Fact] public void Should_add_error_if_pattern_is_not_valid_regex() { diff --git a/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html index f331dbc3f..559426c59 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html @@ -5,9 +5,9 @@
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html index 758ffdaff..dff67f7cd 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html @@ -14,19 +14,12 @@
-
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts index a7955680e..da7c676e3 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { BooleanFieldPropertiesDto, FieldDto } from '@app/shared'; +import { BooleanFieldPropertiesDto, BOOLEAN_FIELD_EDITORS, FieldDto } from '@app/shared'; @Component({ selector: 'sqx-boolean-ui', @@ -24,6 +24,8 @@ export class BooleanUIComponent implements OnInit { @Input() public properties: BooleanFieldPropertiesDto; + public editors = BOOLEAN_FIELD_EDITORS; + public ngOnInit() { this.fieldForm.setControl('editor', new FormControl(this.properties.editor, [ diff --git a/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html index 6e9f05c58..ceb950b89 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html @@ -14,19 +14,12 @@
-
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts index 0706bd183..f4c5f3a91 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { FieldDto, FloatConverter, NumberFieldPropertiesDto } from '@app/shared'; +import { DateTimeFieldPropertiesDto, DATETIME_FIELD_EDITORS, FieldDto, FloatConverter } from '@app/shared'; import { Observable } from 'rxjs'; @Component({ @@ -25,7 +25,9 @@ export class DateTimeUIComponent implements OnInit { public field: FieldDto; @Input() - public properties: NumberFieldPropertiesDto; + public properties: DateTimeFieldPropertiesDto; + + public editors = DATETIME_FIELD_EDITORS; public hideAllowedValues: Observable; diff --git a/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html index b6eeded39..f0457d130 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html @@ -14,33 +14,12 @@
-
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.ts index 3a92f7ca2..1659754e6 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { FieldDto, FloatConverter, NumberFieldPropertiesDto, ResourceOwner, value$ } from '@app/shared'; +import { FieldDto, FloatConverter, NumberFieldPropertiesDto, NUMBER_FIELD_EDITORS, ResourceOwner, value$ } from '@app/shared'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -28,6 +28,8 @@ export class NumberUIComponent extends ResourceOwner implements OnInit { @Input() public properties: NumberFieldPropertiesDto; + public editors = NUMBER_FIELD_EDITORS; + public hideAllowedValues: Observable; public hideInlineEditable: Observable; diff --git a/frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.html index eb19e7013..4ceaa3a2a 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.html @@ -3,33 +3,12 @@
-
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.ts index b2921544a..74b092d4c 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { FieldDto, ReferencesFieldPropertiesDto } from '@app/shared'; +import { FieldDto, ReferencesFieldPropertiesDto, REFERENCES_FIELD_EDITORS } from '@app/shared'; @Component({ selector: 'sqx-references-ui', @@ -24,6 +24,8 @@ export class ReferencesUIComponent implements OnInit { @Input() public properties: ReferencesFieldPropertiesDto; + public editors = REFERENCES_FIELD_EDITORS; + public ngOnInit() { this.fieldForm.setControl('editor', new FormControl(this.properties.editor, [ diff --git a/frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.ts index d27172476..7b0e42534 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { FieldDto, ResourceOwner, StringFieldPropertiesDto, value$ } from '@app/shared'; +import { FieldDto, ResourceOwner, StringFieldPropertiesDto, STRING_FIELD_EDITORS, value$ } from '@app/shared'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -26,6 +26,8 @@ export class StringUIComponent extends ResourceOwner implements OnInit { @Input() public properties: StringFieldPropertiesDto; + public editors = STRING_FIELD_EDITORS; + public hideAllowedValues: Observable; public hideInlineEditable: Observable; diff --git a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html index 8cc28e68b..49ca2704a 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html @@ -9,6 +9,7 @@ +
@@ -60,4 +61,38 @@
+ +
+ + +
+ +
+
+
+ + +
+ + + +
+
+ +
+
+
+ + +
+ + + +
+
+ +
+
\ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts index 3eeb5cf27..1494e7107 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { fadeAnimation, FieldDto, hasNoValue$, hasValue$, ModalModel, PatternDto, ResourceOwner, RootFieldDto, StringFieldPropertiesDto, Types, value$ } from '@app/shared'; +import { fadeAnimation, FieldDto, hasNoValue$, hasValue$, ModalModel, PatternDto, ResourceOwner, RootFieldDto, StringFieldPropertiesDto, Types, value$, STRING_CONTENT_TYPES } from '@app/shared'; import { Observable } from 'rxjs'; @Component({ @@ -31,6 +31,8 @@ export class StringValidationComponent extends ResourceOwner implements OnChange @Input() public patterns: ReadonlyArray; + public contentTypes = STRING_CONTENT_TYPES; + public showDefaultValue: Observable; public showPatternMessage: Observable; public showPatternSuggestions: Observable; @@ -54,6 +56,21 @@ export class StringValidationComponent extends ResourceOwner implements OnChange this.fieldForm.setControl('minLength', new FormControl(this.properties.minLength)); + this.fieldForm.setControl('maxWords', + new FormControl(this.properties.maxWords)); + + this.fieldForm.setControl('minWords', + new FormControl(this.properties.minWords)); + + this.fieldForm.setControl('maxCharacters', + new FormControl(this.properties.maxCharacters)); + + this.fieldForm.setControl('minCharacters', + new FormControl(this.properties.minCharacters)); + + this.fieldForm.setControl('contentType', + new FormControl(this.properties.contentType)); + this.fieldForm.setControl('pattern', new FormControl(this.properties.pattern)); diff --git a/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.html index 6bcf67045..8930335c6 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.html @@ -14,26 +14,12 @@
-
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts index 2ae0e7974..f9629c9ad 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; -import { FieldDto, TagsFieldPropertiesDto } from '@app/shared'; +import { FieldDto, TagsFieldPropertiesDto, TAGS_FIELD_EDITORS } from '@app/shared'; @Component({ selector: 'sqx-tags-ui', @@ -24,6 +24,8 @@ export class TagsUIComponent implements OnInit { @Input() public properties: TagsFieldPropertiesDto; + public editors = TAGS_FIELD_EDITORS; + public ngOnInit() { this.fieldForm.setControl('editor', new FormControl(this.properties.editor, [ diff --git a/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html b/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html index 0c496ce93..463d50bc6 100644 --- a/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html +++ b/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html @@ -13,9 +13,7 @@ diff --git a/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts b/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts index 6a36af6a1..81dd7683f 100644 --- a/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts +++ b/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts @@ -7,7 +7,7 @@ import { Component, Input, OnChanges } from '@angular/core'; import { FormBuilder } from '@angular/forms'; -import { ConfigureFieldRulesForm, SchemaDetailsDto, SchemasState } from '@app/shared'; +import { ConfigureFieldRulesForm, FIELD_RULE_ACTIONS, SchemaDetailsDto, SchemasState } from '@app/shared'; @Component({ selector: 'sqx-schema-field-rules-form', @@ -21,6 +21,7 @@ export class SchemaFieldRulesFormComponent implements OnChanges { public editForm = new ConfigureFieldRulesForm(this.formBuilder); public fieldNames: ReadonlyArray; + public fieldActions = FIELD_RULE_ACTIONS; public isEditable = false; diff --git a/frontend/app/framework/angular/pager.component.html b/frontend/app/framework/angular/pager.component.html index 9e5ac1769..784459a91 100644 --- a/frontend/app/framework/angular/pager.component.html +++ b/frontend/app/framework/angular/pager.component.html @@ -1,10 +1,7 @@
diff --git a/frontend/app/shared/components/search/queries/sorting.component.ts b/frontend/app/shared/components/search/queries/sorting.component.ts index 828a204b5..919235d45 100644 --- a/frontend/app/shared/components/search/queries/sorting.component.ts +++ b/frontend/app/shared/components/search/queries/sorting.component.ts @@ -6,7 +6,7 @@ */ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; -import { QueryModel, QuerySorting } from '@app/shared/internal'; +import { QueryModel, QuerySorting, SORT_MODES } from '@app/shared/internal'; @Component({ selector: 'sqx-sorting', @@ -27,6 +27,8 @@ export class SortingComponent { @Input() public sorting: QuerySorting; + public modes = SORT_MODES; + public changeOrder(order: any) { this.sorting.order = order; diff --git a/frontend/app/shared/services/schemas.service.ts b/frontend/app/shared/services/schemas.service.ts index be51f31b1..712d4b837 100644 --- a/frontend/app/shared/services/schemas.service.ts +++ b/frontend/app/shared/services/schemas.service.ts @@ -99,6 +99,12 @@ export type TableField = RootFieldDto | string; export type FieldRuleAction = 'Disable' | 'Hide' | 'Require'; export type FieldRule = { field: string, action: FieldRuleAction, condition: string }; +export const FIELD_RULE_ACTIONS: ReadonlyArray = [ + 'Disable', + 'Hide', + 'Require' +]; + export class SchemaDetailsDto extends SchemaDto { public readonly contentFields: ReadonlyArray; diff --git a/frontend/app/shared/services/schemas.types.ts b/frontend/app/shared/services/schemas.types.ts index 6bfdae632..86fceaf53 100644 --- a/frontend/app/shared/services/schemas.types.ts +++ b/frontend/app/shared/services/schemas.types.ts @@ -165,10 +165,18 @@ export class ArrayFieldPropertiesDto extends FieldPropertiesDto { } } +export type AssetPreviewMode = 'ImageAndFileName' | 'Image' | 'FileName'; + +export const ASSET_PREVIEW_MODES: ReadonlyArray = [ + 'ImageAndFileName', + 'Image', + 'FileName' +]; + export class AssetsFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Assets'; - public readonly previewMode: 'ImageAndFileName' | 'Image' | 'FileName'; + public readonly previewMode: AssetPreviewMode; public readonly allowDuplicates?: boolean; public readonly allowedExtensions?: ReadonlyArray; public readonly resolveFirst: boolean; @@ -195,6 +203,11 @@ export class AssetsFieldPropertiesDto extends FieldPropertiesDto { export type BooleanFieldEditor = 'Checkbox' | 'Toggle'; +export const BOOLEAN_FIELD_EDITORS: ReadonlyArray = [ + 'Checkbox', + 'Toggle' +]; + export class BooleanFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Boolean'; @@ -213,6 +226,11 @@ export class BooleanFieldPropertiesDto extends FieldPropertiesDto { export type DateTimeFieldEditor = 'DateTime' | 'Date'; +export const DATETIME_FIELD_EDITORS: ReadonlyArray = [ + 'DateTime', + 'Date' +]; + export class DateTimeFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'DateTime'; @@ -233,6 +251,10 @@ export class DateTimeFieldPropertiesDto extends FieldPropertiesDto { export type GeolocationFieldEditor = 'Map'; +export const GEOLOCATION_FIELD_EDITORS: ReadonlyArray = [ + 'Map' +]; + export class GeolocationFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Geolocation'; @@ -261,6 +283,13 @@ export class JsonFieldPropertiesDto extends FieldPropertiesDto { export type NumberFieldEditor = 'Input' | 'Radio' | 'Dropdown' | 'Stars'; +export const NUMBER_FIELD_EDITORS: ReadonlyArray = [ + 'Input', + 'Radio', + 'Dropdown', + 'Stars' +]; + export class NumberFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Number'; @@ -283,6 +312,13 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto { export type ReferencesFieldEditor = 'List' | 'Dropdown' | 'Checkboxes' | 'Tags'; +export const REFERENCES_FIELD_EDITORS: ReadonlyArray = [ + 'List', + 'Dropdown', + 'Checkboxes', + 'Tags' +]; + export class ReferencesFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'References'; @@ -306,18 +342,43 @@ export class ReferencesFieldPropertiesDto extends FieldPropertiesDto { } } -export type StringEditor = 'Color' | 'Dropdown' | 'Html' | 'Input' | 'Markdown' | 'Radio' | 'RichText' | 'Slug' | 'StockPhoto' | 'TextArea'; +export type StringFieldEditor = 'Color' | 'Dropdown' | 'Html' | 'Input' | 'Markdown' | 'Radio' | 'RichText' | 'Slug' | 'StockPhoto' | 'TextArea'; +export type StringContentType = 'Unspecified' | 'Markdown' | 'Html'; + +export const STRING_FIELD_EDITORS: ReadonlyArray = [ + 'Input', + 'TextArea', + 'RichText', + 'Slug', + 'Markdown', + 'Dropdown', + 'Radio', + 'Html', + 'StockPhoto', + 'Color' +]; + +export const STRING_CONTENT_TYPES: ReadonlyArray = [ + 'Unspecified', + 'Markdown', + 'Html' +]; export class StringFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'String'; public readonly allowedValues?: ReadonlyArray; public readonly defaultValue?: string; - public readonly editor: StringEditor = 'Input'; + public readonly editor: StringFieldEditor = 'Input'; public readonly inlineEditable: boolean = false; public readonly isUnique: boolean = false; public readonly maxLength?: number; public readonly minLength?: number; + public readonly maxWords?: number; + public readonly minWords?: number; + public readonly maxCharacters?: number; + public readonly minCharacters?: number; + public readonly contentType?: StringContentType; public readonly pattern?: string; public readonly patternMessage?: string; @@ -332,6 +393,12 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto { export type TagsFieldEditor = 'Tags' | 'Checkboxes' | 'Dropdown'; +export const TAGS_FIELD_EDITORS: ReadonlyArray = [ + 'Tags', + 'Checkboxes', + 'Dropdown' +]; + export class TagsFieldPropertiesDto extends FieldPropertiesDto { public readonly fieldType = 'Tags'; diff --git a/frontend/app/shared/state/query.ts b/frontend/app/shared/state/query.ts index 6f253c80e..caa6fa3a7 100644 --- a/frontend/app/shared/state/query.ts +++ b/frontend/app/shared/state/query.ts @@ -87,6 +87,11 @@ export interface QuerySorting { order: SortMode; } +export const SORT_MODES: ReadonlyArray = [ + 'ascending', + 'descending' +]; + export type SortMode = 'ascending' | 'descending'; export interface Query { diff --git a/frontend/app/theme/icomoon/demo.html b/frontend/app/theme/icomoon/demo.html index e5d02a5ea..2d8f8172e 100644 --- a/frontend/app/theme/icomoon/demo.html +++ b/frontend/app/theme/icomoon/demo.html @@ -450,6 +450,20 @@ +
+
+ + icon-control-List +
+
+ + +
+
+ liga: + +
+
diff --git a/frontend/app/theme/icomoon/fonts/icomoon.eot b/frontend/app/theme/icomoon/fonts/icomoon.eot index 0ebed0daadf5818f73d36da7cc6a924e90fb8e3c..b29ef24de3414892fae20389c2be2c2d548bcbcc 100644 GIT binary patch delta 63 zcmdn-lX1sS#tAko$2m5nPiCwZnHaKwMcO68bmNbRGMV%Z9LMANZN4&avoL_b?PAUc LFnV)NxeqG<1RNGG delta 63 zcmdn-lX1sS#tAko_6cs;lNqZ;CWb6vk*w6I-1sA+OeWhc!9Jef<|_j?3j+w;>Mdb{ K(VKJ1eOLk5suY?4 diff --git a/frontend/app/theme/icomoon/fonts/icomoon.svg b/frontend/app/theme/icomoon/fonts/icomoon.svg index 2ad4081c4..4091bbac1 100644 --- a/frontend/app/theme/icomoon/fonts/icomoon.svg +++ b/frontend/app/theme/icomoon/fonts/icomoon.svg @@ -105,7 +105,7 @@ - + diff --git a/frontend/app/theme/icomoon/fonts/icomoon.ttf b/frontend/app/theme/icomoon/fonts/icomoon.ttf index 113fd2b9b645a65a10a8a864f6e98a6526bc386d..f50d96a39a24fed80a3e13cae3c8352af1679f3b 100644 GIT binary patch delta 51 ycmbR8gK^3a#t8u|(k>CE8$%+>WYRZq9FOO>`O3h}!T_=*=g}d{_a`))5{6 delta 51 ycmbR8gK^3a#t8u|l9f7@8$%+>WU}27?Bn@uzA|vLFo3|V-V!Dlz4=6$4=Vt}rw|_i diff --git a/frontend/app/theme/icomoon/fonts/icomoon.woff b/frontend/app/theme/icomoon/fonts/icomoon.woff index 63421fbb43ad9519d07042750719b6a82f59d848..6baddf7d036d7dbf5507609a511b2ea090802048 100644 GIT binary patch delta 51 ycmaFxgYm%+#tA(v(k>CE8z*Fx$)s=KI3CY$^Ob>{g#iR^7jr&<(VIoeeOLkisS%3+ delta 51 ycmaFxgYm%+#tA(vl9f7@8z*Fx$z;1F*vIqRd}ZKfVE}