Browse Source

Text validation.

pull/572/head
Sebastian 5 years ago
parent
commit
f3caeddd16
  1. 13
      backend/i18n/frontend_en.json
  2. 13
      backend/i18n/frontend_nl.json
  3. 15
      backend/i18n/source/backend_en.json
  4. 13
      backend/i18n/source/frontend_en.json
  5. 6
      backend/i18n/source/frontend_nl.json
  6. 16
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringContentType.cs
  7. 10
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
  8. 104
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs
  9. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/CollectionValidator.cs
  10. 8
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringLengthValidator.cs
  11. 118
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/StringTextValidator.cs
  12. 87
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs
  13. 45
      backend/src/Squidex.Shared/Texts.nl.resx
  14. 45
      backend/src/Squidex.Shared/Texts.resx
  15. 25
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs
  16. 22
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs
  17. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringLengthValidatorTests.cs
  18. 199
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/StringTextValidatorTests.cs
  19. 30
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/DateTimeFieldPropertiesTests.cs
  20. 58
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/NumberFieldPropertiesTests.cs
  21. 48
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/FieldProperties/StringFieldPropertiesTests.cs
  22. 6
      frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html
  23. 15
      frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html
  24. 4
      frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts
  25. 15
      frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html
  26. 6
      frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.ts
  27. 29
      frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html
  28. 4
      frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.ts
  29. 29
      frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.html
  30. 4
      frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.ts
  31. 4
      frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.ts
  32. 35
      frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html
  33. 19
      frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.ts
  34. 22
      frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.html
  35. 4
      frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts
  36. 4
      frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html
  37. 3
      frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts
  38. 5
      frontend/app/framework/angular/pager.component.html
  39. 4
      frontend/app/framework/angular/pager.component.ts
  40. 3
      frontend/app/shared/components/search/queries/sorting.component.html
  41. 4
      frontend/app/shared/components/search/queries/sorting.component.ts
  42. 6
      frontend/app/shared/services/schemas.service.ts
  43. 73
      frontend/app/shared/services/schemas.types.ts
  44. 5
      frontend/app/shared/state/query.ts
  45. 14
      frontend/app/theme/icomoon/demo.html
  46. BIN
      frontend/app/theme/icomoon/fonts/icomoon.eot
  47. 2
      frontend/app/theme/icomoon/fonts/icomoon.svg
  48. BIN
      frontend/app/theme/icomoon/fonts/icomoon.ttf
  49. BIN
      frontend/app/theme/icomoon/fonts/icomoon.woff
  50. 2
      frontend/app/theme/icomoon/selection.json
  51. 15
      frontend/app/theme/icomoon/style.css

13
backend/i18n/frontend_en.json

@ -648,11 +648,11 @@
"schemas.fieldTypes.assets.description": "Images, videos, documents.", "schemas.fieldTypes.assets.description": "Images, videos, documents.",
"schemas.fieldTypes.assets.fileExtensions": "File Extensions", "schemas.fieldTypes.assets.fileExtensions": "File Extensions",
"schemas.fieldTypes.assets.mustBeImage": "Must be Image", "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.previewMode": "PreviewMode",
"schemas.fieldTypes.assets.previewModeHint": "The preview mode for assets in content lists.", "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.resolve": "Resolve first asset",
"schemas.fieldTypes.assets.resolveHint": "Show the first referenced asset in the content list.", "schemas.fieldTypes.assets.resolveHint": "Show the first referenced asset in the content list.",
"schemas.fieldTypes.assets.size": "Size", "schemas.fieldTypes.assets.size": "Size",
@ -674,6 +674,10 @@
"schemas.fieldTypes.references.countMin": "Min Items", "schemas.fieldTypes.references.countMin": "Min Items",
"schemas.fieldTypes.references.description": "Links to other content 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.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.description": "Titles, names, paragraphs.",
"schemas.fieldTypes.string.length": "Length", "schemas.fieldTypes.string.length": "Length",
"schemas.fieldTypes.string.lengthMax": "Max Length", "schemas.fieldTypes.string.lengthMax": "Max Length",
@ -681,6 +685,9 @@
"schemas.fieldTypes.string.pattern": "Regex Pattern", "schemas.fieldTypes.string.pattern": "Regex Pattern",
"schemas.fieldTypes.string.patternMessage": "Pattern Message", "schemas.fieldTypes.string.patternMessage": "Pattern Message",
"schemas.fieldTypes.string.suggestions": "Suggestions", "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.count": "Items",
"schemas.fieldTypes.tags.countMax": "Max Items", "schemas.fieldTypes.tags.countMax": "Max Items",
"schemas.fieldTypes.tags.countMin": "Min Items", "schemas.fieldTypes.tags.countMin": "Min Items",

13
backend/i18n/frontend_nl.json

@ -648,11 +648,11 @@
"schemas.fieldTypes.assets.description": "Afbeeldingen, video's, documenten.", "schemas.fieldTypes.assets.description": "Afbeeldingen, video's, documenten.",
"schemas.fieldTypes.assets.fileExtensions": "Bestandsextensies", "schemas.fieldTypes.assets.fileExtensions": "Bestandsextensies",
"schemas.fieldTypes.assets.mustBeImage": "Moet afbeelding zijn", "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.previewMode": "PreviewMode",
"schemas.fieldTypes.assets.previewModeHint": "De voorbeeldmodus voor items in inhoudslijsten.", "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.resolve": "Eerste item oplossen",
"schemas.fieldTypes.assets.resolveHint": "Toon het eerste item waarnaar wordt verwezen in de inhoudslijst.", "schemas.fieldTypes.assets.resolveHint": "Toon het eerste item waarnaar wordt verwezen in de inhoudslijst.",
"schemas.fieldTypes.assets.size": "Grootte", "schemas.fieldTypes.assets.size": "Grootte",
@ -674,6 +674,10 @@
"schemas.fieldTypes.references.countMin": "Min. items", "schemas.fieldTypes.references.countMin": "Min. items",
"schemas.fieldTypes.references.description": "Links naar andere inhoudsitems.", "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.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.description": "Titels, namen, alinea's.",
"schemas.fieldTypes.string.length": "Lengte", "schemas.fieldTypes.string.length": "Lengte",
"schemas.fieldTypes.string.lengthMax": "Max. lengte", "schemas.fieldTypes.string.lengthMax": "Max. lengte",
@ -681,6 +685,9 @@
"schemas.fieldTypes.string.pattern": "Regex-patroon", "schemas.fieldTypes.string.pattern": "Regex-patroon",
"schemas.fieldTypes.string.patternMessage": "Patroonbericht", "schemas.fieldTypes.string.patternMessage": "Patroonbericht",
"schemas.fieldTypes.string.suggestions": "Suggesties", "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.count": "Artikelen",
"schemas.fieldTypes.tags.countMax": "Max. aantal items", "schemas.fieldTypes.tags.countMax": "Max. aantal items",
"schemas.fieldTypes.tags.countMin": "Min. items", "schemas.fieldTypes.tags.countMin": "Min. items",

15
backend/i18n/source/backend_en.json

@ -43,6 +43,7 @@
"common.clientd": "Client ID", "common.clientd": "Client ID",
"common.clientId": "Client ID", "common.clientId": "Client ID",
"common.clientSecret": "Client Secret", "common.clientSecret": "Client Secret",
"common.contentType": "Content type",
"common.contributorId": "Contributor ID or email", "common.contributorId": "Contributor ID or email",
"common.data": "Data", "common.data": "Data",
"common.defaultValue": "Default value", "common.defaultValue": "Default value",
@ -69,18 +70,22 @@
"common.language": "Language code", "common.language": "Language code",
"common.login": "Login", "common.login": "Login",
"common.logout": "Logout", "common.logout": "Logout",
"common.maxCharacters": "Max characters",
"common.maxHeight": "Max height", "common.maxHeight": "Max height",
"common.maxItems": "Max items", "common.maxItems": "Max items",
"common.maxLength": "Max length", "common.maxLength": "Max length",
"common.maxSize": "Max size", "common.maxSize": "Max size",
"common.maxValue": "Max value", "common.maxValue": "Max value",
"common.maxWidth": "Max width", "common.maxWidth": "Max width",
"common.maxWords": "Max words",
"common.minCharacters": "Min characters",
"common.minHeight": "Min height", "common.minHeight": "Min height",
"common.minItems": "Min items", "common.minItems": "Min items",
"common.minLength": "Min length", "common.minLength": "Min length",
"common.minSize": "Min size", "common.minSize": "Min size",
"common.minValue": "Min value", "common.minValue": "Min value",
"common.minWidth": "Min width", "common.minWidth": "Min width",
"common.minWords": "Min words",
"common.name": "Name", "common.name": "Name",
"common.notFoundValue": "- not found -", "common.notFoundValue": "- not found -",
"common.numDays": "Num days", "common.numDays": "Num days",
@ -147,24 +152,32 @@
"contents.validation.itemCount": "Must have exactly {count} item(s).", "contents.validation.itemCount": "Must have exactly {count} item(s).",
"contents.validation.itemCountBetween": "Must have between {min} and {max} 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.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.maximumHeight": "Height {height}px must be less than {max}px.",
"contents.validation.maximumSize": "Size of {size} must be less than {max}.", "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.maximumWidth": "Width {width}px must be less than {max}px.",
"contents.validation.maxItems": "Must not have more than {max} item(s).", "contents.validation.maxItems": "Must not have more than {max} item(s).",
"contents.validation.maxLength": "Must not have more than {max} character(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.min": "Must be greater or equal to {min}.",
"contents.validation.minimumHeight": "Height {height}px must be greater than {min}px.", "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.minimumSize": "Size of {size} must be greater than {min}.",
"contents.validation.minimumWidth": "Width {width}px must be greater than {min}px.", "contents.validation.minimumWidth": "Width {width}px must be greater than {min}px.",
"contents.validation.minItems": "Must have at least {min} item(s).", "contents.validation.minItems": "Must have at least {min} item(s).",
"contents.validation.minLength": "Must have at least {min} character(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.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.notAllowed": "Not an allowed value.",
"contents.validation.pattern": "Must follow the pattern.", "contents.validation.pattern": "Must follow the pattern.",
"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.unknownField": "Not a known {fieldType}.", "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.workflowErorPublishing": "Content workflow prevents publishing.",
"contents.workflowErrorUpdate": "The workflow does not allow updates at status {status}", "contents.workflowErrorUpdate": "The workflow does not allow updates at status {status}",
"exception.invalidJsonQuery": "Json query not valid: {message}", "exception.invalidJsonQuery": "Json query not valid: {message}",
@ -228,12 +241,10 @@
"schemas.nameAlreadyExists": "A schema with the same name already exists.", "schemas.nameAlreadyExists": "A schema with the same name already exists.",
"schemas.noPermission": "You do not have permission for this schema.", "schemas.noPermission": "You do not have permission for this schema.",
"schemas.notFoundId": "Schema {id} does not exist.", "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.number.inlineEditorError": "Inline editing is not allowed for Radio editor.",
"schemas.onlyArraysHaveNested": "Only array fields can have nested fields.", "schemas.onlyArraysHaveNested": "Only array fields can have nested fields.",
"schemas.onylArraysInRoot": "Nested field cannot be array fields.", "schemas.onylArraysInRoot": "Nested field cannot be array fields.",
"schemas.references.resolveError": "Can only resolve references when MaxItems is 1.", "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.string.inlineEditorError": "Inline editing is only allowed for dropdowns, slugs and input fields.",
"schemas.stringEditorsNeedAllowedValuesError": "Radio buttons or dropdown list need allowed values.", "schemas.stringEditorsNeedAllowedValuesError": "Radio buttons or dropdown list need allowed values.",
"schemas.tags.editorNeedsAllowedValues": "Checkboxes or dropdown list need allowed values.", "schemas.tags.editorNeedsAllowedValues": "Checkboxes or dropdown list need allowed values.",

13
backend/i18n/source/frontend_en.json

@ -648,11 +648,11 @@
"schemas.fieldTypes.assets.description": "Images, videos, documents.", "schemas.fieldTypes.assets.description": "Images, videos, documents.",
"schemas.fieldTypes.assets.fileExtensions": "File Extensions", "schemas.fieldTypes.assets.fileExtensions": "File Extensions",
"schemas.fieldTypes.assets.mustBeImage": "Must be Image", "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.previewMode": "PreviewMode",
"schemas.fieldTypes.assets.previewModeHint": "The preview mode for assets in content lists.", "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.resolve": "Resolve first asset",
"schemas.fieldTypes.assets.resolveHint": "Show the first referenced asset in the content list.", "schemas.fieldTypes.assets.resolveHint": "Show the first referenced asset in the content list.",
"schemas.fieldTypes.assets.size": "Size", "schemas.fieldTypes.assets.size": "Size",
@ -674,6 +674,10 @@
"schemas.fieldTypes.references.countMin": "Min Items", "schemas.fieldTypes.references.countMin": "Min Items",
"schemas.fieldTypes.references.description": "Links to other content 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.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.description": "Titles, names, paragraphs.",
"schemas.fieldTypes.string.length": "Length", "schemas.fieldTypes.string.length": "Length",
"schemas.fieldTypes.string.lengthMax": "Max Length", "schemas.fieldTypes.string.lengthMax": "Max Length",
@ -681,6 +685,9 @@
"schemas.fieldTypes.string.pattern": "Regex Pattern", "schemas.fieldTypes.string.pattern": "Regex Pattern",
"schemas.fieldTypes.string.patternMessage": "Pattern Message", "schemas.fieldTypes.string.patternMessage": "Pattern Message",
"schemas.fieldTypes.string.suggestions": "Suggestions", "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.count": "Items",
"schemas.fieldTypes.tags.countMax": "Max Items", "schemas.fieldTypes.tags.countMax": "Max Items",
"schemas.fieldTypes.tags.countMin": "Min Items", "schemas.fieldTypes.tags.countMin": "Min Items",

6
backend/i18n/source/frontend_nl.json

@ -648,11 +648,11 @@
"schemas.fieldTypes.assets.description": "Afbeeldingen, video's, documenten.", "schemas.fieldTypes.assets.description": "Afbeeldingen, video's, documenten.",
"schemas.fieldTypes.assets.fileExtensions": "Bestandsextensies", "schemas.fieldTypes.assets.fileExtensions": "Bestandsextensies",
"schemas.fieldTypes.assets.mustBeImage": "Moet afbeelding zijn", "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.previewMode": "PreviewMode",
"schemas.fieldTypes.assets.previewModeHint": "De voorbeeldmodus voor items in inhoudslijsten.", "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.resolve": "Eerste item oplossen",
"schemas.fieldTypes.assets.resolveHint": "Toon het eerste item waarnaar wordt verwezen in de inhoudslijst.", "schemas.fieldTypes.assets.resolveHint": "Toon het eerste item waarnaar wordt verwezen in de inhoudslijst.",
"schemas.fieldTypes.assets.size": "Grootte", "schemas.fieldTypes.assets.size": "Grootte",

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

10
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? 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 IsUnique { get; set; }
public bool InlineEditable { get; set; } public bool InlineEditable { get; set; }
@ -28,6 +36,8 @@ namespace Squidex.Domain.Apps.Core.Schemas
public string? PatternMessage { get; set; } public string? PatternMessage { get; set; }
public StringContentType ContentType { get; set; }
public StringFieldEditor Editor { get; set; } public StringFieldEditor Editor { get; set; }
public override T Accept<T>(IFieldPropertiesVisitor<T> visitor) public override T Accept<T>(IFieldPropertiesVisitor<T> visitor)

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

@ -35,9 +35,11 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public IEnumerable<IValidator> Visit(IArrayField field) public IEnumerable<IValidator> 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<string, (bool IsOptional, IValidator Validator)>(field.Fields.Count); var nestedSchema = new Dictionary<string, (bool IsOptional, IValidator Validator)>(field.Fields.Count);
@ -52,12 +54,14 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public IEnumerable<IValidator> Visit(IField<AssetsFieldProperties> field) public IEnumerable<IValidator> Visit(IField<AssetsFieldProperties> 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<Guid>(); yield return new UniqueValuesValidator<Guid>();
} }
@ -65,7 +69,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public IEnumerable<IValidator> Visit(IField<BooleanFieldProperties> field) public IEnumerable<IValidator> Visit(IField<BooleanFieldProperties> field)
{ {
if (field.Properties.IsRequired) var properties = field.Properties;
if (properties.IsRequired)
{ {
yield return new RequiredValidator(); yield return new RequiredValidator();
} }
@ -73,20 +79,24 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public IEnumerable<IValidator> Visit(IField<DateTimeFieldProperties> field) public IEnumerable<IValidator> Visit(IField<DateTimeFieldProperties> field)
{ {
if (field.Properties.IsRequired) var properties = field.Properties;
if (properties.IsRequired)
{ {
yield return new RequiredValidator(); yield return new RequiredValidator();
} }
if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) if (properties.MinValue.HasValue || properties.MaxValue.HasValue)
{ {
yield return new RangeValidator<Instant>(field.Properties.MinValue, field.Properties.MaxValue); yield return new RangeValidator<Instant>(properties.MinValue, properties.MaxValue);
} }
} }
public IEnumerable<IValidator> Visit(IField<GeolocationFieldProperties> field) public IEnumerable<IValidator> Visit(IField<GeolocationFieldProperties> field)
{ {
if (field.Properties.IsRequired) var properties = field.Properties;
if (properties.IsRequired)
{ {
yield return new RequiredValidator(); yield return new RequiredValidator();
} }
@ -94,7 +104,9 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public IEnumerable<IValidator> Visit(IField<JsonFieldProperties> field) public IEnumerable<IValidator> Visit(IField<JsonFieldProperties> field)
{ {
if (field.Properties.IsRequired) var properties = field.Properties;
if (properties.IsRequired)
{ {
yield return new RequiredValidator(); yield return new RequiredValidator();
} }
@ -102,30 +114,34 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public IEnumerable<IValidator> Visit(IField<NumberFieldProperties> field) public IEnumerable<IValidator> Visit(IField<NumberFieldProperties> field)
{ {
if (field.Properties.IsRequired) var properties = field.Properties;
if (properties.IsRequired)
{ {
yield return new RequiredValidator(); yield return new RequiredValidator();
} }
if (field.Properties.MinValue.HasValue || field.Properties.MaxValue.HasValue) if (properties.MinValue.HasValue || properties.MaxValue.HasValue)
{ {
yield return new RangeValidator<double>(field.Properties.MinValue, field.Properties.MaxValue); yield return new RangeValidator<double>(properties.MinValue, properties.MaxValue);
} }
if (field.Properties.AllowedValues != null) if (properties.AllowedValues != null)
{ {
yield return new AllowedValuesValidator<double>(field.Properties.AllowedValues); yield return new AllowedValuesValidator<double>(properties.AllowedValues);
} }
} }
public IEnumerable<IValidator> Visit(IField<ReferencesFieldProperties> field) public IEnumerable<IValidator> Visit(IField<ReferencesFieldProperties> 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<Guid>(); yield return new UniqueValuesValidator<Guid>();
} }
@ -133,37 +149,65 @@ namespace Squidex.Domain.Apps.Core.ValidateContent
public IEnumerable<IValidator> Visit(IField<StringFieldProperties> field) public IEnumerable<IValidator> Visit(IField<StringFieldProperties> field)
{ {
if (field.Properties.IsRequired) var properties = field.Properties;
if (properties.IsRequired)
{ {
yield return new RequiredStringValidator(true); 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<string, string>? 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<string>(field.Properties.AllowedValues); yield return new PatternValidator(properties.Pattern, properties.PatternMessage);
}
if (properties.AllowedValues != null)
{
yield return new AllowedValuesValidator<string>(properties.AllowedValues);
} }
} }
public IEnumerable<IValidator> Visit(IField<TagsFieldProperties> field) public IEnumerable<IValidator> Visit(IField<TagsFieldProperties> 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<string>(field.Properties.AllowedValues)); yield return new CollectionItemValidator(new AllowedValuesValidator<string>(properties.AllowedValues));
} }
yield return new CollectionItemValidator(new RequiredStringValidator(true)); yield return new CollectionItemValidator(new RequiredStringValidator(true));

6
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) 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)); 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 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 })); 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 })); addError(context.Path, T.Get("contents.validation.maxItems", new { max = maxItems }));
} }

8
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? minLength;
private readonly int? maxLength; 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)); 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 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 })); 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 })); addError(context.Path, T.Get("contents.validation.maxLength", new { max = maxLength }));
} }

118
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<string, string>? transform;
private readonly int? minCharacters;
private readonly int? maxCharacters;
private readonly int? minWords;
private readonly int? maxWords;
public StringTextValidator(Func<string, string>? 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;
}
}
}

87
backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/FieldPropertiesValidator.cs

@ -5,7 +5,9 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
@ -33,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
public IEnumerable<ValidationError> Visit(ArrayFieldProperties properties) public IEnumerable<ValidationError> 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)), yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)),
nameof(properties.MinItems), nameof(properties.MinItems),
@ -43,28 +45,28 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
public IEnumerable<ValidationError> Visit(AssetsFieldProperties properties) public IEnumerable<ValidationError> 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)), yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)),
nameof(properties.MinItems), nameof(properties.MinItems),
nameof(properties.MaxItems)); 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)), yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxHeight), nameof(properties.MinHeight)),
nameof(properties.MaxHeight), nameof(properties.MaxHeight),
nameof(properties.MinHeight)); 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)), yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxWidth), nameof(properties.MinWidth)),
nameof(properties.MaxWidth), nameof(properties.MaxWidth),
nameof(properties.MinWidth)); 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)), yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxSize), nameof(properties.MinSize)),
nameof(properties.MaxSize), nameof(properties.MaxSize),
@ -96,19 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
nameof(properties.Editor)); nameof(properties.Editor));
} }
if (properties.DefaultValue.HasValue && properties.MinValue.HasValue && properties.DefaultValue.Value < properties.MinValue.Value) if (IsMaxGreaterThanMin(properties.MaxValue, properties.MinValue))
{
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)
{ {
yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxValue), nameof(properties.MinValue)), yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxValue), nameof(properties.MinValue)),
nameof(properties.MinValue), nameof(properties.MinValue),
@ -154,39 +144,19 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
nameof(properties.Editor)); 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"), yield return new ValidationError(T.Get("schemas.stringEditorsNeedAllowedValuesError"),
nameof(properties.AllowedValues)); nameof(properties.AllowedValues));
} }
if (properties.DefaultValue.HasValue && properties.MinValue.HasValue && properties.DefaultValue.Value < properties.MinValue.Value) if (properties.MaxValue.HasValue && properties.MinValue.HasValue && properties.MinValue >= properties.MaxValue)
{
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)
{ {
yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxValue), nameof(properties.MinValue)), yield return new ValidationError(Not.GreaterThan(nameof(properties.MaxValue), nameof(properties.MinValue)),
nameof(properties.MinValue), nameof(properties.MinValue),
nameof(properties.MaxValue)); 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) if (properties.InlineEditable && properties.Editor == NumberFieldEditor.Radio)
{ {
yield return new ValidationError(T.Get("schemas.number.inlineEditorError"), yield return new ValidationError(T.Get("schemas.number.inlineEditorError"),
@ -203,7 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
nameof(properties.Editor)); 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)), yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)),
nameof(properties.MinItems), nameof(properties.MinItems),
@ -226,7 +196,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
nameof(properties.Editor)); 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"), yield return new ValidationError(T.Get("schemas.stringEditorsNeedAllowedValuesError"),
nameof(properties.AllowedValues)); nameof(properties.AllowedValues));
@ -238,19 +214,25 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
nameof(properties.Pattern)); 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)), yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxLength), nameof(properties.MinLength)),
nameof(properties.MinLength), nameof(properties.MinLength),
nameof(properties.MaxLength)); 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"), yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxWords), nameof(properties.MinWords)),
nameof(properties.AllowedValues), nameof(properties.MinWords),
nameof(properties.MinLength), nameof(properties.MaxWords));
nameof(properties.MaxLength)); }
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) 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)); 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"), yield return new ValidationError(T.Get("schemas.tags.editorNeedsAllowedValues"),
nameof(properties.AllowedValues)); 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)), yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxItems), nameof(properties.MinItems)),
nameof(properties.MinItems), nameof(properties.MinItems),
@ -291,5 +273,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
nameof(properties.Editor)); nameof(properties.Editor));
} }
} }
private static bool IsMaxGreaterThanMin<T>(T? min, T? max) where T : struct, IComparable
{
return max.HasValue && min.HasValue && min.Value.CompareTo(max.Value) < 0;
}
} }
} }

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

@ -190,6 +190,9 @@
<data name="common.clientSecret" xml:space="preserve"> <data name="common.clientSecret" xml:space="preserve">
<value>Clientgeheim</value> <value>Clientgeheim</value>
</data> </data>
<data name="common.contentType" xml:space="preserve">
<value>Content type</value>
</data>
<data name="common.contributorId" xml:space="preserve"> <data name="common.contributorId" xml:space="preserve">
<value>Bijdrager-ID of e-mailadres</value> <value>Bijdrager-ID of e-mailadres</value>
</data> </data>
@ -268,6 +271,9 @@
<data name="common.logout" xml:space="preserve"> <data name="common.logout" xml:space="preserve">
<value>Uitloggen</value> <value>Uitloggen</value>
</data> </data>
<data name="common.maxCharacters" xml:space="preserve">
<value>Max characters</value>
</data>
<data name="common.maxHeight" xml:space="preserve"> <data name="common.maxHeight" xml:space="preserve">
<value>Max. hoogte</value> <value>Max. hoogte</value>
</data> </data>
@ -286,6 +292,12 @@
<data name="common.maxWidth" xml:space="preserve"> <data name="common.maxWidth" xml:space="preserve">
<value>Max. breedte</value> <value>Max. breedte</value>
</data> </data>
<data name="common.maxWords" xml:space="preserve">
<value>Max words</value>
</data>
<data name="common.minCharacters" xml:space="preserve">
<value>Min characters</value>
</data>
<data name="common.minHeight" xml:space="preserve"> <data name="common.minHeight" xml:space="preserve">
<value>Min. hoogte</value> <value>Min. hoogte</value>
</data> </data>
@ -304,6 +316,9 @@
<data name="common.minWidth" xml:space="preserve"> <data name="common.minWidth" xml:space="preserve">
<value>Min. breedte</value> <value>Min. breedte</value>
</data> </data>
<data name="common.minWords" xml:space="preserve">
<value>Min words</value>
</data>
<data name="common.name" xml:space="preserve"> <data name="common.name" xml:space="preserve">
<value>Naam</value> <value>Naam</value>
</data> </data>
@ -502,6 +517,9 @@
<data name="contents.validation.max" xml:space="preserve"> <data name="contents.validation.max" xml:space="preserve">
<value>Moet kleiner zijn dan of gelijk zijn aan {max}.</value> <value>Moet kleiner zijn dan of gelijk zijn aan {max}.</value>
</data> </data>
<data name="contents.validation.maxCharacters" xml:space="preserve">
<value>Must not have more than {max} text character(s).</value>
</data>
<data name="contents.validation.maximumHeight" xml:space="preserve"> <data name="contents.validation.maximumHeight" xml:space="preserve">
<value>Hoogte {hoogte} px moet kleiner zijn dan {max} px.</value> <value>Hoogte {hoogte} px moet kleiner zijn dan {max} px.</value>
</data> </data>
@ -517,6 +535,9 @@
<data name="contents.validation.maxLength" xml:space="preserve"> <data name="contents.validation.maxLength" xml:space="preserve">
<value>Mag niet meer dan {max} teken (s) bevatten.</value> <value>Mag niet meer dan {max} teken (s) bevatten.</value>
</data> </data>
<data name="contents.validation.maxWords" xml:space="preserve">
<value>Must not have more than {max} word(s).</value>
</data>
<data name="contents.validation.min" xml:space="preserve"> <data name="contents.validation.min" xml:space="preserve">
<value>Moet groter zijn dan of gelijk zijn aan {min}.</value> <value>Moet groter zijn dan of gelijk zijn aan {min}.</value>
</data> </data>
@ -535,9 +556,21 @@
<data name="contents.validation.minLength" xml:space="preserve"> <data name="contents.validation.minLength" xml:space="preserve">
<value>Moet minstens {min} teken (s) bevatten.</value> <value>Moet minstens {min} teken (s) bevatten.</value>
</data> </data>
<data name="contents.validation.minNormalCharacters" xml:space="preserve">
<value>Must have at least {min} text character(s).</value>
</data>
<data name="contents.validation.minWords" xml:space="preserve">
<value>Must have at least {min} word(s).</value>
</data>
<data name="contents.validation.mustBeEmpty" xml:space="preserve"> <data name="contents.validation.mustBeEmpty" xml:space="preserve">
<value>Waarde mag niet worden gedefinieerd.</value> <value>Waarde mag niet worden gedefinieerd.</value>
</data> </data>
<data name="contents.validation.normalCharacterCount" xml:space="preserve">
<value>Must have exactly {count} text character(s).</value>
</data>
<data name="contents.validation.normalCharactersBetween" xml:space="preserve">
<value>Must have between {min} and {max} text character(s).</value>
</data>
<data name="contents.validation.notAllowed" xml:space="preserve"> <data name="contents.validation.notAllowed" xml:space="preserve">
<value>Geen toegestane waarde.</value> <value>Geen toegestane waarde.</value>
</data> </data>
@ -556,6 +589,12 @@
<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>
<data name="contents.validation.wordCount" xml:space="preserve">
<value>Must have exactly {count} word(s).</value>
</data>
<data name="contents.validation.wordsBetween" xml:space="preserve">
<value>Must have between {min} and {max} word(s).</value>
</data>
<data name="contents.workflowErorPublishing" xml:space="preserve"> <data name="contents.workflowErorPublishing" xml:space="preserve">
<value>Contentworkflow verhindert publiceren.</value> <value>Contentworkflow verhindert publiceren.</value>
</data> </data>
@ -745,9 +784,6 @@
<data name="schemas.notFoundId" xml:space="preserve"> <data name="schemas.notFoundId" xml:space="preserve">
<value>Schema {id} bestaat niet.</value> <value>Schema {id} bestaat niet.</value>
</data> </data>
<data name="schemas.number.eitherMinMaxOrAllowedValuesError" xml:space="preserve">
<value>Ofwel toegestane waarden of min en max lengte kunnen worden gedefinieerd.</value>
</data>
<data name="schemas.number.inlineEditorError" xml:space="preserve"> <data name="schemas.number.inlineEditorError" xml:space="preserve">
<value>Inline bewerken is niet toegestaan ​​voor Radio-editor.</value> <value>Inline bewerken is niet toegestaan ​​voor Radio-editor.</value>
</data> </data>
@ -760,9 +796,6 @@
<data name="schemas.references.resolveError" xml:space="preserve"> <data name="schemas.references.resolveError" xml:space="preserve">
<value>Kan alleen verwijzingen omzetten als MaxItems 1 is.</value> <value>Kan alleen verwijzingen omzetten als MaxItems 1 is.</value>
</data> </data>
<data name="schemas.string.eitherMinMaxOrAllowedValuesError" xml:space="preserve">
<value>Ofwel toegestane waarden of min en max waarde kunnen worden gedefinieerd.</value>
</data>
<data name="schemas.string.inlineEditorError" xml:space="preserve"> <data name="schemas.string.inlineEditorError" xml:space="preserve">
<value>Inline bewerken is alleen toegestaan ​​voor dropdowns, slugs en invoervelden.</value> <value>Inline bewerken is alleen toegestaan ​​voor dropdowns, slugs en invoervelden.</value>
</data> </data>

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

@ -190,6 +190,9 @@
<data name="common.clientSecret" xml:space="preserve"> <data name="common.clientSecret" xml:space="preserve">
<value>Client Secret</value> <value>Client Secret</value>
</data> </data>
<data name="common.contentType" xml:space="preserve">
<value>Content type</value>
</data>
<data name="common.contributorId" xml:space="preserve"> <data name="common.contributorId" xml:space="preserve">
<value>Contributor ID or email</value> <value>Contributor ID or email</value>
</data> </data>
@ -268,6 +271,9 @@
<data name="common.logout" xml:space="preserve"> <data name="common.logout" xml:space="preserve">
<value>Logout</value> <value>Logout</value>
</data> </data>
<data name="common.maxCharacters" xml:space="preserve">
<value>Max characters</value>
</data>
<data name="common.maxHeight" xml:space="preserve"> <data name="common.maxHeight" xml:space="preserve">
<value>Max height</value> <value>Max height</value>
</data> </data>
@ -286,6 +292,12 @@
<data name="common.maxWidth" xml:space="preserve"> <data name="common.maxWidth" xml:space="preserve">
<value>Max width</value> <value>Max width</value>
</data> </data>
<data name="common.maxWords" xml:space="preserve">
<value>Max words</value>
</data>
<data name="common.minCharacters" xml:space="preserve">
<value>Min characters</value>
</data>
<data name="common.minHeight" xml:space="preserve"> <data name="common.minHeight" xml:space="preserve">
<value>Min height</value> <value>Min height</value>
</data> </data>
@ -304,6 +316,9 @@
<data name="common.minWidth" xml:space="preserve"> <data name="common.minWidth" xml:space="preserve">
<value>Min width</value> <value>Min width</value>
</data> </data>
<data name="common.minWords" xml:space="preserve">
<value>Min words</value>
</data>
<data name="common.name" xml:space="preserve"> <data name="common.name" xml:space="preserve">
<value>Name</value> <value>Name</value>
</data> </data>
@ -502,6 +517,9 @@
<data name="contents.validation.max" xml:space="preserve"> <data name="contents.validation.max" xml:space="preserve">
<value>Must be less or equal to {max}.</value> <value>Must be less or equal to {max}.</value>
</data> </data>
<data name="contents.validation.maxCharacters" xml:space="preserve">
<value>Must not have more than {max} text character(s).</value>
</data>
<data name="contents.validation.maximumHeight" xml:space="preserve"> <data name="contents.validation.maximumHeight" xml:space="preserve">
<value>Height {height}px must be less than {max}px.</value> <value>Height {height}px must be less than {max}px.</value>
</data> </data>
@ -517,6 +535,9 @@
<data name="contents.validation.maxLength" xml:space="preserve"> <data name="contents.validation.maxLength" xml:space="preserve">
<value>Must not have more than {max} character(s).</value> <value>Must not have more than {max} character(s).</value>
</data> </data>
<data name="contents.validation.maxWords" xml:space="preserve">
<value>Must not have more than {max} word(s).</value>
</data>
<data name="contents.validation.min" xml:space="preserve"> <data name="contents.validation.min" xml:space="preserve">
<value>Must be greater or equal to {min}.</value> <value>Must be greater or equal to {min}.</value>
</data> </data>
@ -535,9 +556,21 @@
<data name="contents.validation.minLength" xml:space="preserve"> <data name="contents.validation.minLength" xml:space="preserve">
<value>Must have at least {min} character(s).</value> <value>Must have at least {min} character(s).</value>
</data> </data>
<data name="contents.validation.minNormalCharacters" xml:space="preserve">
<value>Must have at least {min} text character(s).</value>
</data>
<data name="contents.validation.minWords" xml:space="preserve">
<value>Must have at least {min} word(s).</value>
</data>
<data name="contents.validation.mustBeEmpty" xml:space="preserve"> <data name="contents.validation.mustBeEmpty" xml:space="preserve">
<value>Value must not be defined.</value> <value>Value must not be defined.</value>
</data> </data>
<data name="contents.validation.normalCharacterCount" xml:space="preserve">
<value>Must have exactly {count} text character(s).</value>
</data>
<data name="contents.validation.normalCharactersBetween" xml:space="preserve">
<value>Must have between {min} and {max} text character(s).</value>
</data>
<data name="contents.validation.notAllowed" xml:space="preserve"> <data name="contents.validation.notAllowed" xml:space="preserve">
<value>Not an allowed value.</value> <value>Not an allowed value.</value>
</data> </data>
@ -556,6 +589,12 @@
<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>
<data name="contents.validation.wordCount" xml:space="preserve">
<value>Must have exactly {count} word(s).</value>
</data>
<data name="contents.validation.wordsBetween" xml:space="preserve">
<value>Must have between {min} and {max} word(s).</value>
</data>
<data name="contents.workflowErorPublishing" xml:space="preserve"> <data name="contents.workflowErorPublishing" xml:space="preserve">
<value>Content workflow prevents publishing.</value> <value>Content workflow prevents publishing.</value>
</data> </data>
@ -745,9 +784,6 @@
<data name="schemas.notFoundId" xml:space="preserve"> <data name="schemas.notFoundId" xml:space="preserve">
<value>Schema {id} does not exist.</value> <value>Schema {id} does not exist.</value>
</data> </data>
<data name="schemas.number.eitherMinMaxOrAllowedValuesError" xml:space="preserve">
<value>Either allowed values or min and max length can be defined.</value>
</data>
<data name="schemas.number.inlineEditorError" xml:space="preserve"> <data name="schemas.number.inlineEditorError" xml:space="preserve">
<value>Inline editing is not allowed for Radio editor.</value> <value>Inline editing is not allowed for Radio editor.</value>
</data> </data>
@ -760,9 +796,6 @@
<data name="schemas.references.resolveError" xml:space="preserve"> <data name="schemas.references.resolveError" xml:space="preserve">
<value>Can only resolve references when MaxItems is 1.</value> <value>Can only resolve references when MaxItems is 1.</value>
</data> </data>
<data name="schemas.string.eitherMinMaxOrAllowedValuesError" xml:space="preserve">
<value>Either allowed values or min and max value can be defined.</value>
</data>
<data name="schemas.string.inlineEditorError" xml:space="preserve"> <data name="schemas.string.inlineEditorError" xml:space="preserve">
<value>Inline editing is only allowed for dropdowns, slugs and input fields.</value> <value>Inline editing is only allowed for dropdowns, slugs and input fields.</value>
</data> </data>

25
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/StringFieldPropertiesDto.cs

@ -38,6 +38,26 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary> /// </summary>
public int? MaxLength { get; set; } public int? MaxLength { get; set; }
/// <summary>
/// The minimum allowed of normal characters for the field value.
/// </summary>
public int? MinCharacters { get; set; }
/// <summary>
/// The maximum allowed of normal characters for the field value.
/// </summary>
public int? MaxCharacters { get; set; }
/// <summary>
/// The minimum allowed number of words for the field value.
/// </summary>
public int? MinWords { get; set; }
/// <summary>
/// The maximum allowed number of words for the field value.
/// </summary>
public int? MaxWords { get; set; }
/// <summary> /// <summary>
/// The allowed values for the field value. /// The allowed values for the field value.
/// </summary> /// </summary>
@ -53,6 +73,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
/// </summary> /// </summary>
public bool InlineEditable { get; set; } public bool InlineEditable { get; set; }
/// <summary>
/// How the string content should be interpreted.
/// </summary>
public StringContentType ContentType { get; set; }
/// <summary> /// <summary>
/// The editor that is used to manage this field. /// The editor that is used to manage this field.
/// </summary> /// </summary>

22
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)." }); 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] [Fact]
public async Task Should_add_error_if_string_not_allowed() public async Task Should_add_error_if_string_not_allowed()
{ {

6
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] [Fact]
public async Task Should_not_add_error_if_value_is_null() 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); await sut.ValidateAsync(null, errors);
@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
[Fact] [Fact]
public async Task Should_not_add_error_if_value_is_empty() 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); await sut.ValidateAsync(string.Empty, errors);
@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
} }
[Fact] [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); var sut = new StringLengthValidator(2000, 2000);

199
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<TranslationsFixture>
{
private readonly List<string> errors = new List<string>();
[Theory]
[InlineData(20, 10)]
public void Should_throw_error_if_min_characters_greater_than_max(int? min, int? max)
{
Assert.Throws<ArgumentException>(() => 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<ArgumentException>(() => 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();
}
}
}

30
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] [Fact]
public void Should_add_error_if_default_value_is_less_than_min() public void Should_add_error_if_min_value_greater_than_max_value()
{
var sut = new DateTimeFieldProperties { MinValue = FutureDays(10), DefaultValue = FutureDays(5) };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
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<ValidationError>
{
new ValidationError("Default value must be less or equal to max value.", "DefaultValue")
});
}
[Fact]
public void Should_add_error_if_min_greater_than_max()
{ {
var sut = new DateTimeFieldProperties { MinValue = FutureDays(10), MaxValue = FutureDays(5) }; var sut = new DateTimeFieldProperties { MinValue = FutureDays(10), MaxValue = FutureDays(5) };

58
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] [Fact]
public void Should_add_error_if_default_value_is_less_than_min() public void Should_add_error_if_min_value_greater_than_max_value()
{
var sut = new NumberFieldProperties { MinValue = 10, DefaultValue = 5 };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
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<ValidationError>
{
new ValidationError("Default value must be less or equal to max value.", "DefaultValue")
});
}
[Fact]
public void Should_add_error_if_min_greater_than_max()
{ {
var sut = new NumberFieldProperties { MinValue = 10, MaxValue = 5 }; 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<ValidationError>
{
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<ValidationError>
{
new ValidationError("Either allowed values or min and max value can be defined.", "AllowedValues", "MinValue", "MaxValue")
});
}
[Fact] [Fact]
public void Should_add_error_if_radio_button_has_no_allowed_values() public void Should_add_error_if_radio_button_has_no_allowed_values()
{ {

48
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<TranslationsFixture> public class StringFieldPropertiesTests : IClassFixture<TranslationsFixture>
{ {
[Fact] [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 }; var sut = new StringFieldProperties { MinLength = 10, MaxLength = 5 };
@ -43,33 +43,53 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards.FieldProperties
} }
[Fact] [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(); var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
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] [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(); var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
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] [Fact]
public void Should_add_error_if_radio_button_has_no_allowed_values() 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<ValidationError>
{
new ValidationError("Content type is not a valid value.", "ContentType")
});
}
[Fact] [Fact]
public void Should_add_error_if_pattern_is_not_valid_regex() public void Should_add_error_if_pattern_is_not_valid_regex()
{ {

6
frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html

@ -5,9 +5,9 @@
<div class="col-6"> <div class="col-6">
<select type="text" class="form-control" id="{{field.fieldId}}_previewMode" formControlName="previewMode"> <select type="text" class="form-control" id="{{field.fieldId}}_previewMode" formControlName="previewMode">
<option value="ImageAndFileName">{{ 'schemas.fieldTypes.assets.previewModeTumbnailName' | sqxTranslate }}</option> <option value="ImageAndFileName">{{ 'schemas.fieldTypes.assets.previewImageAndFileName' | sqxTranslate }}</option>
<option value="Image">{{ 'schemas.fieldTypes.assets.previewModeTumbnailOrName' | sqxTranslate }}</option> <option value="Image">{{ 'schemas.fieldTypes.assets.previewImage' | sqxTranslate }}</option>
<option value="FileName">{{ 'schemas.fieldTypes.assets.previewModeName' | sqxTranslate }}</option> <option value="FileName">{{ 'schemas.fieldTypes.assets.previewFileName' | sqxTranslate }}</option>
</select> </select>
<sqx-form-hint> <sqx-form-hint>

15
frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html

@ -14,19 +14,12 @@
<label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label> <label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Checkbox'"> <label class="btn btn-radio" *ngFor="let editor of editors" [class.active]="fieldForm.controls['editor'].value === editor">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Checkbox"> <input type="radio" class="radio-input" name="editor" formControlName="editor" [value]="editor">
<i class="icon-control-Checkbox"></i> <i class="icon-control-{{editor}}"></i>
<span class="radio-label">Checkbox</span> <span class="radio-label">{{editor}}</span>
</label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Toggle'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Toggle">
<i class="icon-control-Toggle"></i>
<span class="radio-label">Toggle</span>
</label> </label>
</div> </div>
</div> </div>

4
frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.ts

@ -7,7 +7,7 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { BooleanFieldPropertiesDto, FieldDto } from '@app/shared'; import { BooleanFieldPropertiesDto, BOOLEAN_FIELD_EDITORS, FieldDto } from '@app/shared';
@Component({ @Component({
selector: 'sqx-boolean-ui', selector: 'sqx-boolean-ui',
@ -24,6 +24,8 @@ export class BooleanUIComponent implements OnInit {
@Input() @Input()
public properties: BooleanFieldPropertiesDto; public properties: BooleanFieldPropertiesDto;
public editors = BOOLEAN_FIELD_EDITORS;
public ngOnInit() { public ngOnInit() {
this.fieldForm.setControl('editor', this.fieldForm.setControl('editor',
new FormControl(this.properties.editor, [ new FormControl(this.properties.editor, [

15
frontend/app/features/schemas/pages/schema/fields/types/date-time-ui.component.html

@ -14,19 +14,12 @@
<label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label> <label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Date'"> <label class="btn btn-radio" *ngFor="let editor of editors" [class.active]="fieldForm.controls['editor'].value === editor">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Date"> <input type="radio" class="radio-input" name="editor" formControlName="editor" [value]="editor">
<i class="icon-control-Date"></i> <i class="icon-control-{{editor}}"></i>
<span class="radio-label">Date</span> <span class="radio-label">{{editor}}</span>
</label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'DateTime'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="DateTime">
<i class="icon-control-DateTime"></i>
<span class="radio-label" clas>DateTime</span>
</label> </label>
</div> </div>
</div> </div>

6
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 { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; 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'; import { Observable } from 'rxjs';
@Component({ @Component({
@ -25,7 +25,9 @@ export class DateTimeUIComponent implements OnInit {
public field: FieldDto; public field: FieldDto;
@Input() @Input()
public properties: NumberFieldPropertiesDto; public properties: DateTimeFieldPropertiesDto;
public editors = DATETIME_FIELD_EDITORS;
public hideAllowedValues: Observable<boolean>; public hideAllowedValues: Observable<boolean>;

29
frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.html

@ -14,33 +14,12 @@
<label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label> <label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Input'"> <label class="btn btn-radio" *ngFor="let editor of editors" [class.active]="fieldForm.controls['editor'].value === editor">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Input"> <input type="radio" class="radio-input" name="editor" formControlName="editor" [value]="editor">
<i class="icon-control-Input"></i> <i class="icon-control-{{editor}}"></i>
<span class="radio-label">Input</span> <span class="radio-label">{{editor}}</span>
</label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Dropdown'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Dropdown">
<i class="icon-control-Dropdown"></i>
<span class="radio-label" clas>Dropdown</span>
</label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Radio'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Radio">
<i class="icon-control-Radio"></i>
<span class="radio-label">Radio</span>
</label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Stars'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Stars">
<i class="icon-control-Stars"></i>
<span class="radio-label">Stars</span>
</label> </label>
</div> </div>
</div> </div>

4
frontend/app/features/schemas/pages/schema/fields/types/number-ui.component.ts

@ -7,7 +7,7 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; 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 { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -28,6 +28,8 @@ export class NumberUIComponent extends ResourceOwner implements OnInit {
@Input() @Input()
public properties: NumberFieldPropertiesDto; public properties: NumberFieldPropertiesDto;
public editors = NUMBER_FIELD_EDITORS;
public hideAllowedValues: Observable<boolean>; public hideAllowedValues: Observable<boolean>;
public hideInlineEditable: Observable<boolean>; public hideInlineEditable: Observable<boolean>;

29
frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.html

@ -3,33 +3,12 @@
<label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label> <label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'List'"> <label class="btn btn-radio" *ngFor="let editor of editors" [class.active]="fieldForm.controls['editor'].value === editor">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="List"> <input type="radio" class="radio-input" name="editor" formControlName="editor" [value]="editor">
<i class="icon-control-Checkboxes"></i> <i class="icon-control-{{editor}}"></i>
<span class="radio-label">List</span> <span class="radio-label">{{editor}}</span>
</label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Dropdown'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Dropdown">
<i class="icon-control-Dropdown"></i>
<span class="radio-label">Dropdown</span>
</label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Tags'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Tags">
<i class="icon-control-Tags"></i>
<span class="radio-label">Tags</span>
</label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Checkboxes'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Checkboxes">
<i class="icon-control-Checkboxes"></i>
<span class="radio-label">Checkboxes</span>
</label> </label>
</div> </div>
</div> </div>

4
frontend/app/features/schemas/pages/schema/fields/types/references-ui.component.ts

@ -7,7 +7,7 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { FieldDto, ReferencesFieldPropertiesDto } from '@app/shared'; import { FieldDto, ReferencesFieldPropertiesDto, REFERENCES_FIELD_EDITORS } from '@app/shared';
@Component({ @Component({
selector: 'sqx-references-ui', selector: 'sqx-references-ui',
@ -24,6 +24,8 @@ export class ReferencesUIComponent implements OnInit {
@Input() @Input()
public properties: ReferencesFieldPropertiesDto; public properties: ReferencesFieldPropertiesDto;
public editors = REFERENCES_FIELD_EDITORS;
public ngOnInit() { public ngOnInit() {
this.fieldForm.setControl('editor', this.fieldForm.setControl('editor',
new FormControl(this.properties.editor, [ new FormControl(this.properties.editor, [

4
frontend/app/features/schemas/pages/schema/fields/types/string-ui.component.ts

@ -7,7 +7,7 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; 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 { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -26,6 +26,8 @@ export class StringUIComponent extends ResourceOwner implements OnInit {
@Input() @Input()
public properties: StringFieldPropertiesDto; public properties: StringFieldPropertiesDto;
public editors = STRING_FIELD_EDITORS;
public hideAllowedValues: Observable<boolean>; public hideAllowedValues: Observable<boolean>;
public hideInlineEditable: Observable<boolean>; public hideInlineEditable: Observable<boolean>;

35
frontend/app/features/schemas/pages/schema/fields/types/string-validation.component.html

@ -9,6 +9,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.length' | sqxTranslate }}</label> <label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.length' | sqxTranslate }}</label>
@ -60,4 +61,38 @@
<input type="text" class="form-control" id="{{field.fieldId}}_fieldDefaultValue" formControlName="defaultValue"> <input type="text" class="form-control" id="{{field.fieldId}}_fieldDefaultValue" formControlName="defaultValue">
</div> </div>
</div> </div>
<div class="form-group row mt-4">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.contentType' | sqxTranslate }}</label>
<div class="col-6">
<select class="form-control" formControlName="contentType">
<option *ngFor="let contentType of contentTypes" [ngValue]="contentType">{{contentType}}</option>
</select>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.characters' | sqxTranslate }}</label>
<div class="col-3 minmax-col">
<input type="number" class="form-control" formControlName="minCharacters" placeholder="{{ 'schemas.fieldTypes.string.charactersMin' | sqxTranslate }}">
<label class="col-form-label minmax-label">-</label>
</div>
<div class="col-3">
<input type="number" class="form-control" formControlName="maxCharacters" placeholder="{{ 'schemas.fieldTypes.string.charactersMax' | sqxTranslate }}">
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.words' | sqxTranslate }}</label>
<div class="col-3 minmax-col">
<input type="number" class="form-control" formControlName="minWords" placeholder="{{ 'schemas.fieldTypes.string.wordsMin' | sqxTranslate }}">
<label class="col-form-label minmax-label">-</label>
</div>
<div class="col-3">
<input type="number" class="form-control" formControlName="maxWords" placeholder="{{ 'schemas.fieldTypes.string.wordsMax' | sqxTranslate }}">
</div>
</div>
</div> </div>

19
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 { Component, Input, OnChanges, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; 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'; import { Observable } from 'rxjs';
@Component({ @Component({
@ -31,6 +31,8 @@ export class StringValidationComponent extends ResourceOwner implements OnChange
@Input() @Input()
public patterns: ReadonlyArray<PatternDto>; public patterns: ReadonlyArray<PatternDto>;
public contentTypes = STRING_CONTENT_TYPES;
public showDefaultValue: Observable<boolean>; public showDefaultValue: Observable<boolean>;
public showPatternMessage: Observable<boolean>; public showPatternMessage: Observable<boolean>;
public showPatternSuggestions: Observable<boolean>; public showPatternSuggestions: Observable<boolean>;
@ -54,6 +56,21 @@ export class StringValidationComponent extends ResourceOwner implements OnChange
this.fieldForm.setControl('minLength', this.fieldForm.setControl('minLength',
new FormControl(this.properties.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', this.fieldForm.setControl('pattern',
new FormControl(this.properties.pattern)); new FormControl(this.properties.pattern));

22
frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.html

@ -14,26 +14,12 @@
<label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label> <label class="col-3 col-form-label">{{ 'schemas.field.editor' | sqxTranslate }}</label>
<div class="col-9"> <div class="col-9">
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Tags'"> <label class="btn btn-radio" *ngFor="let editor of editors" [class.active]="fieldForm.controls['editor'].value === editor">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Tags"> <input type="radio" class="radio-input" name="editor" formControlName="editor" [value]="editor">
<i class="icon-control-Tags"></i> <i class="icon-control-{{editor}}"></i>
<span class="radio-label">Tags</span> <span class="radio-label">{{editor}}</span>
</label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Dropdown'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Dropdown">
<i class="icon-control-Dropdown"></i>
<span class="radio-label" clas>Dropdown</span>
</label>
<label class="btn btn-radio" [class.active]="fieldForm.controls['editor'].value === 'Checkboxes'">
<input type="radio" class="radio-input" name="editor" formControlName="editor" value="Checkboxes">
<i class="icon-control-Checkboxes"></i>
<span class="radio-label">Checkboxes</span>
</label> </label>
</div> </div>
</div> </div>

4
frontend/app/features/schemas/pages/schema/fields/types/tags-ui.component.ts

@ -7,7 +7,7 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { FieldDto, TagsFieldPropertiesDto } from '@app/shared'; import { FieldDto, TagsFieldPropertiesDto, TAGS_FIELD_EDITORS } from '@app/shared';
@Component({ @Component({
selector: 'sqx-tags-ui', selector: 'sqx-tags-ui',
@ -24,6 +24,8 @@ export class TagsUIComponent implements OnInit {
@Input() @Input()
public properties: TagsFieldPropertiesDto; public properties: TagsFieldPropertiesDto;
public editors = TAGS_FIELD_EDITORS;
public ngOnInit() { public ngOnInit() {
this.fieldForm.setControl('editor', this.fieldForm.setControl('editor',
new FormControl(this.properties.editor, [ new FormControl(this.properties.editor, [

4
frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html

@ -13,9 +13,7 @@
<sqx-control-errors for="action"></sqx-control-errors> <sqx-control-errors for="action"></sqx-control-errors>
<select class="form-control" formControlName="action"> <select class="form-control" formControlName="action">
<option>Disable</option> <option *ngFor="let fieldAction of fieldActions" [ngValue]="fieldAction">{{fieldAction}}</option>
<option>Hide</option>
<option>Require</option>
</select> </select>
</div> </div>

3
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 { Component, Input, OnChanges } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { ConfigureFieldRulesForm, SchemaDetailsDto, SchemasState } from '@app/shared'; import { ConfigureFieldRulesForm, FIELD_RULE_ACTIONS, SchemaDetailsDto, SchemasState } from '@app/shared';
@Component({ @Component({
selector: 'sqx-schema-field-rules-form', selector: 'sqx-schema-field-rules-form',
@ -21,6 +21,7 @@ export class SchemaFieldRulesFormComponent implements OnChanges {
public editForm = new ConfigureFieldRulesForm(this.formBuilder); public editForm = new ConfigureFieldRulesForm(this.formBuilder);
public fieldNames: ReadonlyArray<string>; public fieldNames: ReadonlyArray<string>;
public fieldActions = FIELD_RULE_ACTIONS;
public isEditable = false; public isEditable = false;

5
frontend/app/framework/angular/pager.component.html

@ -1,10 +1,7 @@
<div class="clearfix" *ngIf="pager && (!autoHide || pager.canGoPrev || pager.canGoNext)"> <div class="clearfix" *ngIf="pager && (!autoHide || pager.canGoPrev || pager.canGoNext)">
<div class="float-right pagination"> <div class="float-right pagination">
<select class="form-control form-control-sm" [ngModel]="pager.pageSize" (ngModelChange)="setPageSize($event)"> <select class="form-control form-control-sm" [ngModel]="pager.pageSize" (ngModelChange)="setPageSize($event)">
<option [ngValue]="10">10</option> <option *ngFor="let pageSize of pageSizes" [ngValue]="pageSize">{{pageSize}}</option>
<option [ngValue]="20">20</option>
<option [ngValue]="30">30</option>
<option [ngValue]="50">50</option>
</select> </select>
<span class="page-info"> <span class="page-info">

4
frontend/app/framework/angular/pager.component.ts

@ -8,6 +8,8 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { Pager } from '@app/framework/internal'; import { Pager } from '@app/framework/internal';
export const PAGE_SIZES: ReadonlyArray<number> = [10, 20, 30, 50];
@Component({ @Component({
selector: 'sqx-pager', selector: 'sqx-pager',
styleUrls: ['./pager.component.scss'], styleUrls: ['./pager.component.scss'],
@ -24,6 +26,8 @@ export class PagerComponent {
@Input() @Input()
public autoHide = false; public autoHide = false;
public pageSizes = PAGE_SIZES;
public goPrev() { public goPrev() {
this.pagerChange.emit(this.pager.goPrev()); this.pagerChange.emit(this.pager.goPrev());
} }

3
frontend/app/shared/components/search/queries/sorting.component.html

@ -8,8 +8,7 @@
</sqx-query-path> </sqx-query-path>
<select class="form-control ml-1" [ngModel]="sorting.order" (ngModelChange)="changeOrder($event)"> <select class="form-control ml-1" [ngModel]="sorting.order" (ngModelChange)="changeOrder($event)">
<option>ascending</option> <option *ngFor="let mode of modes" [ngValue]="mode">{{mode}}</option>
<option>descending</option>
</select> </select>
</div> </div>
</div> </div>

4
frontend/app/shared/components/search/queries/sorting.component.ts

@ -6,7 +6,7 @@
*/ */
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; 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({ @Component({
selector: 'sqx-sorting', selector: 'sqx-sorting',
@ -27,6 +27,8 @@ export class SortingComponent {
@Input() @Input()
public sorting: QuerySorting; public sorting: QuerySorting;
public modes = SORT_MODES;
public changeOrder(order: any) { public changeOrder(order: any) {
this.sorting.order = order; this.sorting.order = order;

6
frontend/app/shared/services/schemas.service.ts

@ -99,6 +99,12 @@ export type TableField = RootFieldDto | string;
export type FieldRuleAction = 'Disable' | 'Hide' | 'Require'; export type FieldRuleAction = 'Disable' | 'Hide' | 'Require';
export type FieldRule = { field: string, action: FieldRuleAction, condition: string }; export type FieldRule = { field: string, action: FieldRuleAction, condition: string };
export const FIELD_RULE_ACTIONS: ReadonlyArray<FieldRuleAction> = [
'Disable',
'Hide',
'Require'
];
export class SchemaDetailsDto extends SchemaDto { export class SchemaDetailsDto extends SchemaDto {
public readonly contentFields: ReadonlyArray<RootFieldDto>; public readonly contentFields: ReadonlyArray<RootFieldDto>;

73
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<AssetPreviewMode> = [
'ImageAndFileName',
'Image',
'FileName'
];
export class AssetsFieldPropertiesDto extends FieldPropertiesDto { export class AssetsFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Assets'; public readonly fieldType = 'Assets';
public readonly previewMode: 'ImageAndFileName' | 'Image' | 'FileName'; public readonly previewMode: AssetPreviewMode;
public readonly allowDuplicates?: boolean; public readonly allowDuplicates?: boolean;
public readonly allowedExtensions?: ReadonlyArray<string>; public readonly allowedExtensions?: ReadonlyArray<string>;
public readonly resolveFirst: boolean; public readonly resolveFirst: boolean;
@ -195,6 +203,11 @@ export class AssetsFieldPropertiesDto extends FieldPropertiesDto {
export type BooleanFieldEditor = 'Checkbox' | 'Toggle'; export type BooleanFieldEditor = 'Checkbox' | 'Toggle';
export const BOOLEAN_FIELD_EDITORS: ReadonlyArray<BooleanFieldEditor> = [
'Checkbox',
'Toggle'
];
export class BooleanFieldPropertiesDto extends FieldPropertiesDto { export class BooleanFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Boolean'; public readonly fieldType = 'Boolean';
@ -213,6 +226,11 @@ export class BooleanFieldPropertiesDto extends FieldPropertiesDto {
export type DateTimeFieldEditor = 'DateTime' | 'Date'; export type DateTimeFieldEditor = 'DateTime' | 'Date';
export const DATETIME_FIELD_EDITORS: ReadonlyArray<DateTimeFieldEditor> = [
'DateTime',
'Date'
];
export class DateTimeFieldPropertiesDto extends FieldPropertiesDto { export class DateTimeFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'DateTime'; public readonly fieldType = 'DateTime';
@ -233,6 +251,10 @@ export class DateTimeFieldPropertiesDto extends FieldPropertiesDto {
export type GeolocationFieldEditor = 'Map'; export type GeolocationFieldEditor = 'Map';
export const GEOLOCATION_FIELD_EDITORS: ReadonlyArray<GeolocationFieldEditor> = [
'Map'
];
export class GeolocationFieldPropertiesDto extends FieldPropertiesDto { export class GeolocationFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Geolocation'; public readonly fieldType = 'Geolocation';
@ -261,6 +283,13 @@ export class JsonFieldPropertiesDto extends FieldPropertiesDto {
export type NumberFieldEditor = 'Input' | 'Radio' | 'Dropdown' | 'Stars'; export type NumberFieldEditor = 'Input' | 'Radio' | 'Dropdown' | 'Stars';
export const NUMBER_FIELD_EDITORS: ReadonlyArray<NumberFieldEditor> = [
'Input',
'Radio',
'Dropdown',
'Stars'
];
export class NumberFieldPropertiesDto extends FieldPropertiesDto { export class NumberFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Number'; public readonly fieldType = 'Number';
@ -283,6 +312,13 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto {
export type ReferencesFieldEditor = 'List' | 'Dropdown' | 'Checkboxes' | 'Tags'; export type ReferencesFieldEditor = 'List' | 'Dropdown' | 'Checkboxes' | 'Tags';
export const REFERENCES_FIELD_EDITORS: ReadonlyArray<ReferencesFieldEditor> = [
'List',
'Dropdown',
'Checkboxes',
'Tags'
];
export class ReferencesFieldPropertiesDto extends FieldPropertiesDto { export class ReferencesFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'References'; 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<StringFieldEditor> = [
'Input',
'TextArea',
'RichText',
'Slug',
'Markdown',
'Dropdown',
'Radio',
'Html',
'StockPhoto',
'Color'
];
export const STRING_CONTENT_TYPES: ReadonlyArray<StringContentType> = [
'Unspecified',
'Markdown',
'Html'
];
export class StringFieldPropertiesDto extends FieldPropertiesDto { export class StringFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'String'; public readonly fieldType = 'String';
public readonly allowedValues?: ReadonlyArray<string>; public readonly allowedValues?: ReadonlyArray<string>;
public readonly defaultValue?: string; public readonly defaultValue?: string;
public readonly editor: StringEditor = 'Input'; public readonly editor: StringFieldEditor = 'Input';
public readonly inlineEditable: boolean = false; public readonly inlineEditable: boolean = false;
public readonly isUnique: boolean = false; public readonly isUnique: boolean = false;
public readonly maxLength?: number; public readonly maxLength?: number;
public readonly minLength?: 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 pattern?: string;
public readonly patternMessage?: string; public readonly patternMessage?: string;
@ -332,6 +393,12 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto {
export type TagsFieldEditor = 'Tags' | 'Checkboxes' | 'Dropdown'; export type TagsFieldEditor = 'Tags' | 'Checkboxes' | 'Dropdown';
export const TAGS_FIELD_EDITORS: ReadonlyArray<TagsFieldEditor> = [
'Tags',
'Checkboxes',
'Dropdown'
];
export class TagsFieldPropertiesDto extends FieldPropertiesDto { export class TagsFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'Tags'; public readonly fieldType = 'Tags';

5
frontend/app/shared/state/query.ts

@ -87,6 +87,11 @@ export interface QuerySorting {
order: SortMode; order: SortMode;
} }
export const SORT_MODES: ReadonlyArray<SortMode> = [
'ascending',
'descending'
];
export type SortMode = 'ascending' | 'descending'; export type SortMode = 'ascending' | 'descending';
export interface Query { export interface Query {

14
frontend/app/theme/icomoon/demo.html

@ -450,6 +450,20 @@
<input type="text" readonly value="" class="liga unitRight" /> <input type="text" readonly value="" class="liga unitRight" />
</div> </div>
</div> </div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-control-List"></span>
<span class="mls"> icon-control-List</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e962" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe962;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
<input type="text" readonly value="" class="liga unitRight" />
</div>
</div>
<div class="glyph fs2"> <div class="glyph fs2">
<div class="clearfix bshadow0 pbs"> <div class="clearfix bshadow0 pbs">
<span class="icon-control-Html"></span> <span class="icon-control-Html"></span>

BIN
frontend/app/theme/icomoon/fonts/icomoon.eot

Binary file not shown.

2
frontend/app/theme/icomoon/fonts/icomoon.svg

@ -105,7 +105,7 @@
<glyph unicode="&#xe95f;" glyph-name="comments" d="M854 256.667v512h-684v-598l86 86h598zM854 852.667c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" /> <glyph unicode="&#xe95f;" glyph-name="comments" d="M854 256.667v512h-684v-598l86 86h598zM854 852.667c46 0 84-38 84-84v-512c0-46-38-86-84-86h-598l-170-170v768c0 46 38 84 84 84h684z" />
<glyph unicode="&#xe960;" glyph-name="control-Html" d="M159.073 285.257l-159.073 134.055 159.073 133.819 36.818-37.29-117.062-96.057 117.062-97.237zM493.247 414.828q0-33.042-9.441-57.115-9.205-24.073-25.961-40.122-16.521-15.813-39.178-23.601-22.657-7.552-49.327-7.552-26.197 0-48.855 4.012-22.421 3.776-42.954 10.385v323.338h57.587v-78.356l-2.36-47.203q12.981 16.757 30.21 26.905 17.465 10.149 41.774 10.149 21.241 0 37.762-8.496t27.614-24.309q11.329-15.577 17.229-37.998 5.9-22.185 5.9-50.035zM432.828 412.468q0 19.825-2.832 33.75t-8.26 22.893q-5.192 8.969-12.981 12.981-7.552 4.248-17.465 4.248-14.633 0-28.086-11.801-13.217-11.801-28.086-32.098v-104.79q6.844-2.596 16.757-4.248 10.149-1.652 20.533-1.652 13.689 0 24.781 5.664 11.329 5.664 19.117 16.049 8.024 10.385 12.273 25.253 4.248 15.105 4.248 33.75zM700.682 437.249q0.472 13.453-1.416 22.893-1.652 9.441-5.664 15.577-3.776 6.136-9.441 8.968t-12.981 2.832q-12.745 0-26.433-10.621-13.453-10.385-29.738-34.458v-151.756h-59.003v239.789h52.159l2.124-34.93q5.9 9.205 13.217 16.521 7.552 7.316 16.521 12.509 9.205 5.428 20.297 8.26t24.309 2.832q18.173 0 32.098-6.372 14.161-6.136 23.601-18.409 9.677-12.273 14.161-30.918 4.72-18.409 4.012-42.718zM864.927 553.132l159.073-133.819-159.073-134.055-36.582 37.29 116.826 96.293-116.826 97.001z" /> <glyph unicode="&#xe960;" glyph-name="control-Html" d="M159.073 285.257l-159.073 134.055 159.073 133.819 36.818-37.29-117.062-96.057 117.062-97.237zM493.247 414.828q0-33.042-9.441-57.115-9.205-24.073-25.961-40.122-16.521-15.813-39.178-23.601-22.657-7.552-49.327-7.552-26.197 0-48.855 4.012-22.421 3.776-42.954 10.385v323.338h57.587v-78.356l-2.36-47.203q12.981 16.757 30.21 26.905 17.465 10.149 41.774 10.149 21.241 0 37.762-8.496t27.614-24.309q11.329-15.577 17.229-37.998 5.9-22.185 5.9-50.035zM432.828 412.468q0 19.825-2.832 33.75t-8.26 22.893q-5.192 8.969-12.981 12.981-7.552 4.248-17.465 4.248-14.633 0-28.086-11.801-13.217-11.801-28.086-32.098v-104.79q6.844-2.596 16.757-4.248 10.149-1.652 20.533-1.652 13.689 0 24.781 5.664 11.329 5.664 19.117 16.049 8.024 10.385 12.273 25.253 4.248 15.105 4.248 33.75zM700.682 437.249q0.472 13.453-1.416 22.893-1.652 9.441-5.664 15.577-3.776 6.136-9.441 8.968t-12.981 2.832q-12.745 0-26.433-10.621-13.453-10.385-29.738-34.458v-151.756h-59.003v239.789h52.159l2.124-34.93q5.9 9.205 13.217 16.521 7.552 7.316 16.521 12.509 9.205 5.428 20.297 8.26t24.309 2.832q18.173 0 32.098-6.372 14.161-6.136 23.601-18.409 9.677-12.273 14.161-30.918 4.72-18.409 4.012-42.718zM864.927 553.132l159.073-133.819-159.073-134.055-36.582 37.29 116.826 96.293-116.826 97.001z" />
<glyph unicode="&#xe961;" glyph-name="drag2" d="M170 298.667v86h684v-86h-684zM854 554.667v-86h-684v86h684z" /> <glyph unicode="&#xe961;" glyph-name="drag2" d="M170 298.667v86h684v-86h-684zM854 554.667v-86h-684v86h684z" />
<glyph unicode="&#xe962;" glyph-name="control-Checkboxes" d="M384 771.657h614.4c14.157 0 25.6 11.443 25.6 25.6s-11.443 25.6-25.6 25.6h-614.4c-14.131 0-25.6-11.443-25.6-25.6s11.469-25.6 25.6-25.6zM998.4 464.457h-614.4c-14.131 0-25.6-11.443-25.6-25.6s11.469-25.6 25.6-25.6h614.4c14.157 0 25.6 11.443 25.6 25.6s-11.443 25.6-25.6 25.6zM998.4 106.057h-614.4c-38.406-15.539-22.811-37.543 0-51.2h614.4c14.157 0 25.6 11.443 25.6 25.6s-11.443 25.6-25.6 25.6zM0 950.857v-307.2h307.2v307.2zM47.4 903.457h212.4v-212.4h-212.4zM0 234.057v-307.2h307.2v307.2zM47.4 186.657h212.4v-212.4h-212.4zM0 592.457v-307.2h307.2v307.2zM47.4 545.057h212.4v-212.4h-212.4zM89.6 861.257h128v-128h-128v128z" /> <glyph unicode="&#xe962;" glyph-name="control-Checkboxes, control-List" d="M384 771.657h614.4c14.157 0 25.6 11.443 25.6 25.6s-11.443 25.6-25.6 25.6h-614.4c-14.131 0-25.6-11.443-25.6-25.6s11.469-25.6 25.6-25.6zM998.4 464.457h-614.4c-14.131 0-25.6-11.443-25.6-25.6s11.469-25.6 25.6-25.6h614.4c14.157 0 25.6 11.443 25.6 25.6s-11.443 25.6-25.6 25.6zM998.4 106.057h-614.4c-38.406-15.539-22.811-37.543 0-51.2h614.4c14.157 0 25.6 11.443 25.6 25.6s-11.443 25.6-25.6 25.6zM0 950.857v-307.2h307.2v307.2zM47.4 903.457h212.4v-212.4h-212.4zM0 234.057v-307.2h307.2v307.2zM47.4 186.657h212.4v-212.4h-212.4zM0 592.457v-307.2h307.2v307.2zM47.4 545.057h212.4v-212.4h-212.4zM89.6 861.257h128v-128h-128v128z" />
<glyph unicode="&#xe963;" glyph-name="control-Tags" horiz-adv-x="1140" d="M498.787 620.534v49.548h112.31v66.065c0 49.548-39.639 89.187-89.187 89.187s-89.187-39.639-89.187-89.187v-541.729l89.187-161.858 89.187 161.858v426.116zM360.052 234.057h-66.065c-59.458 0-105.703 46.245-105.703 105.703v254.348c0 59.458 46.245 105.703 105.703 105.703h66.065v42.942h-66.065c-82.581 0-148.645-66.065-148.645-148.645v-254.348c0-82.581 66.065-148.645 148.645-148.645h66.065zM852.232 689.902c-26.426 33.032-66.065 52.852-109.006 52.852h-59.458v-42.942h39.639c42.942 0 82.581-19.819 109.006-52.852l145.342-181.677-142.039-178.374c-26.426-33.032-69.368-52.852-112.31-52.852h-36.335v-42.942h56.155c42.942 0 85.884 19.819 112.31 52.852l178.374 221.316z" /> <glyph unicode="&#xe963;" glyph-name="control-Tags" horiz-adv-x="1140" d="M498.787 620.534v49.548h112.31v66.065c0 49.548-39.639 89.187-89.187 89.187s-89.187-39.639-89.187-89.187v-541.729l89.187-161.858 89.187 161.858v426.116zM360.052 234.057h-66.065c-59.458 0-105.703 46.245-105.703 105.703v254.348c0 59.458 46.245 105.703 105.703 105.703h66.065v42.942h-66.065c-82.581 0-148.645-66.065-148.645-148.645v-254.348c0-82.581 66.065-148.645 148.645-148.645h66.065zM852.232 689.902c-26.426 33.032-66.065 52.852-109.006 52.852h-59.458v-42.942h39.639c42.942 0 82.581-19.819 109.006-52.852l145.342-181.677-142.039-178.374c-26.426-33.032-69.368-52.852-112.31-52.852h-36.335v-42.942h56.155c42.942 0 85.884 19.819 112.31 52.852l178.374 221.316z" />
<glyph unicode="&#xe964;" glyph-name="show" d="M256 857.6c-28.314 0-51.2-22.886-51.2-51.2v-256h51.2v256h307.2v-153.6c0-28.314 22.886-51.2 51.2-51.2h153.6v-512h-512v460.8h-51.2v-460.8c0-28.314 22.886-51.2 51.2-51.2h512c28.314 0 51.2 22.886 51.2 51.2v548.2l-219.8 219.8h-292.2zM614.4 770.2l117.4-117.4h-117.4zM408.906 372.28l-35.3 37 138.1 131.9 138-131.9-35.3-37-102.7 98.1zM511.706 186.88l-138.1 131.9 35.3 37 102.8-98.1 102.7 98.1 35.3-37z" /> <glyph unicode="&#xe964;" glyph-name="show" d="M256 857.6c-28.314 0-51.2-22.886-51.2-51.2v-256h51.2v256h307.2v-153.6c0-28.314 22.886-51.2 51.2-51.2h153.6v-512h-512v460.8h-51.2v-460.8c0-28.314 22.886-51.2 51.2-51.2h512c28.314 0 51.2 22.886 51.2 51.2v548.2l-219.8 219.8h-292.2zM614.4 770.2l117.4-117.4h-117.4zM408.906 372.28l-35.3 37 138.1 131.9 138-131.9-35.3-37-102.7 98.1zM511.706 186.88l-138.1 131.9 35.3 37 102.8-98.1 102.7 98.1 35.3-37z" />
<glyph unicode="&#xe965;" glyph-name="show-all" d="M256 857.6c-28.314 0-51.2-22.886-51.2-51.2v-256h51.2v256h307.2v-153.6c0-28.314 22.886-51.2 51.2-51.2h153.6v-512h-512v460.8h-51.2v-460.8c0-28.314 22.886-51.2 51.2-51.2h512c28.314 0 51.2 22.886 51.2 51.2v548.2l-219.8 219.8h-292.2zM614.4 770.2l117.4-117.4h-117.4zM348.394 944.012c-28.314 0-51.2-22.886-51.2-51.2v-23.7h51.2v23.7h307.2l204.8-204.8v-512h-23.8v-51.2h23.8c28.314 0 51.2 22.886 51.2 51.2v548.2l-219.8 219.8h-292.2zM408.906 372.28l-35.3 37 138.1 131.9 138-131.9-35.3-37-102.7 98.1zM511.706 186.88l-138.1 131.9 35.3 37 102.8-98.1 102.7 98.1 35.3-37z" /> <glyph unicode="&#xe965;" glyph-name="show-all" d="M256 857.6c-28.314 0-51.2-22.886-51.2-51.2v-256h51.2v256h307.2v-153.6c0-28.314 22.886-51.2 51.2-51.2h153.6v-512h-512v460.8h-51.2v-460.8c0-28.314 22.886-51.2 51.2-51.2h512c28.314 0 51.2 22.886 51.2 51.2v548.2l-219.8 219.8h-292.2zM614.4 770.2l117.4-117.4h-117.4zM348.394 944.012c-28.314 0-51.2-22.886-51.2-51.2v-23.7h51.2v23.7h307.2l204.8-204.8v-512h-23.8v-51.2h23.8c28.314 0 51.2 22.886 51.2 51.2v548.2l-219.8 219.8h-292.2zM408.906 372.28l-35.3 37 138.1 131.9 138-131.9-35.3-37-102.7 98.1zM511.706 186.88l-138.1 131.9 35.3 37 102.8-98.1 102.7 98.1 35.3-37z" />

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

BIN
frontend/app/theme/icomoon/fonts/icomoon.ttf

Binary file not shown.

BIN
frontend/app/theme/icomoon/fonts/icomoon.woff

Binary file not shown.

2
frontend/app/theme/icomoon/selection.json

File diff suppressed because one or more lines are too long

15
frontend/app/theme/icomoon/style.css

@ -1,10 +1,10 @@
@font-face { @font-face {
font-family: 'icomoon'; font-family: 'icomoon';
src: url('fonts/icomoon.eot?uop9lr'); src: url('fonts/icomoon.eot?epjxtr');
src: url('fonts/icomoon.eot?uop9lr#iefix') format('embedded-opentype'), src: url('fonts/icomoon.eot?epjxtr#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?uop9lr') format('truetype'), url('fonts/icomoon.ttf?epjxtr') format('truetype'),
url('fonts/icomoon.woff?uop9lr') format('woff'), url('fonts/icomoon.woff?epjxtr') format('woff'),
url('fonts/icomoon.svg?uop9lr#icomoon') format('svg'); url('fonts/icomoon.svg?epjxtr#icomoon') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
@ -13,7 +13,7 @@
[class^="icon-"], [class*=" icon-"] { [class^="icon-"], [class*=" icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */ /* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important; font-family: 'icomoon' !important;
speak: none; speak: never;
font-style: normal; font-style: normal;
font-weight: normal; font-weight: normal;
font-variant: normal; font-variant: normal;
@ -118,6 +118,9 @@
.icon-control-Checkboxes:before { .icon-control-Checkboxes:before {
content: "\e962"; content: "\e962";
} }
.icon-control-List:before {
content: "\e962";
}
.icon-control-Html:before { .icon-control-Html:before {
content: "\e960"; content: "\e960";
} }

Loading…
Cancel
Save