From c975ff72c34e9be862df9562b4efc40f76a825d7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 4 Aug 2021 18:04:34 +0200 Subject: [PATCH] Video validation. --- backend/i18n/frontend_en.json | 2 +- backend/i18n/frontend_it.json | 2 +- backend/i18n/frontend_nl.json | 2 +- backend/i18n/frontend_zh.json | 2 +- backend/i18n/source/backend_en.json | 2 +- backend/i18n/source/backend_it.json | 1 - backend/i18n/source/backend_nl.json | 1 - backend/i18n/source/backend_zh.json | 1 - backend/i18n/source/frontend_en.json | 2 +- backend/i18n/source/frontend_it.json | 1 - backend/i18n/source/frontend_nl.json | 1 - backend/i18n/source/frontend_zh.json | 1 - .../Assets/AssetMetadata.cs | 67 ++++++++++----- .../Schemas/AssetsFieldProperties.cs | 11 ++- .../Validators/AssetsValidator.cs | 81 ++++++++++-------- .../Assets/FileTagAssetMetadataSource.cs | 21 ++--- .../Assets/ImageAssetMetadataSource.cs | 8 +- backend/src/Squidex.Shared/Texts.it.resx | 6 +- backend/src/Squidex.Shared/Texts.nl.resx | 6 +- backend/src/Squidex.Shared/Texts.resx | 6 +- backend/src/Squidex.Shared/Texts.zh.resx | 6 +- .../Models/Fields/AssetsFieldPropertiesDto.cs | 15 +++- .../Validators/AssetsValidatorTests.cs | 83 +++++++++++-------- .../TestHelpers/TestAssets.cs | 15 ++++ .../types/assets-validation.component.html | 29 +++++-- frontend/app/shared/services/schemas.types.ts | 2 +- frontend/app/shared/state/schemas.forms.ts | 2 +- frontend/app/theme/_forms.scss | 12 +-- 28 files changed, 238 insertions(+), 150 deletions(-) diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 21865a47b..dfc942845 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -769,10 +769,10 @@ "schemas.fieldTypes.assets.countMax": "Max Assets", "schemas.fieldTypes.assets.countMin": "Min Assets", "schemas.fieldTypes.assets.description": "Images, videos, documents.", + "schemas.fieldTypes.assets.expectedType": "Expected type", "schemas.fieldTypes.assets.fileExtensions": "File Extensions", "schemas.fieldTypes.assets.folderId": "Folder", "schemas.fieldTypes.assets.folderIdHint": "The asset folder where the new assets will be uploaded to.", - "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", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 899073d4f..ee30052f1 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -769,10 +769,10 @@ "schemas.fieldTypes.assets.countMax": "Max num di Risorse", "schemas.fieldTypes.assets.countMin": "Min num. di Risorse", "schemas.fieldTypes.assets.description": "Immagini, video, documenti.", + "schemas.fieldTypes.assets.expectedType": "Expected type", "schemas.fieldTypes.assets.fileExtensions": "Estensioni dei File", "schemas.fieldTypes.assets.folderId": "Folder", "schemas.fieldTypes.assets.folderIdHint": "The asset folder where the new assets will be uploaded to.", - "schemas.fieldTypes.assets.mustBeImage": "Deve essere un'immagine", "schemas.fieldTypes.assets.previewFileName": "Solamente il nome del file", "schemas.fieldTypes.assets.previewImage": "Solamente l'anteprima o il nome del file se non è un immagine", "schemas.fieldTypes.assets.previewImageAndFileName": "L'anteprima e il nome del file", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index d02bfce52..f300e995b 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -769,10 +769,10 @@ "schemas.fieldTypes.assets.countMax": "Max. bestanden", "schemas.fieldTypes.assets.countMin": "Min. bestanden", "schemas.fieldTypes.assets.description": "Afbeeldingen, video's, documenten.", + "schemas.fieldTypes.assets.expectedType": "Expected type", "schemas.fieldTypes.assets.fileExtensions": "Bestandsextensies", "schemas.fieldTypes.assets.folderId": "Folder", "schemas.fieldTypes.assets.folderIdHint": "The asset folder where the new assets will be uploaded to.", - "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", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 30aba46fd..5c7674085 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -769,10 +769,10 @@ "schemas.fieldTypes.assets.countMax": "最大资源", "schemas.fieldTypes.assets.countMin": "最小资源", "schemas.fieldTypes.assets.description": "图片、视频、文档。", + "schemas.fieldTypes.assets.expectedType": "Expected type", "schemas.fieldTypes.assets.fileExtensions": "文件扩展名", "schemas.fieldTypes.assets.folderId": "文件夹", "schemas.fieldTypes.assets.folderIdHint": "新资源将上传到的资源文件夹。", - "schemas.fieldTypes.assets.mustBeImage": "必须是图片", "schemas.fieldTypes.assets.previewFileName": "仅文件名", "schemas.fieldTypes.assets.previewImage": "如果不是图像,则只有缩略图或文件名", "schemas.fieldTypes.assets.previewImageAndFileName": "缩略图和文件名", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index 2d46a73b9..6ce133773 100644 --- a/backend/i18n/source/backend_en.json +++ b/backend/i18n/source/backend_en.json @@ -144,6 +144,7 @@ "contents.statusTransitionNotAllowed": "Cannot change status from {oldStatus} to {newStatus}.", "contents.validation.aspectRatio": "Must have aspect ratio {width}:{height}.", "contents.validation.assetNotFound": "Id {id} not found.", + "contents.validation.assetType": "Not of expected type: {type}.", "contents.validation.between": "Must be between {min} and {max}.", "contents.validation.characterCount": "Must have exactly {count} character(s).", "contents.validation.charactersBetween": "Must have between {min} and {max} character(s).", @@ -151,7 +152,6 @@ "contents.validation.error": "Validation failed with internal error.", "contents.validation.exactValue": "Must be exactly {value}.", "contents.validation.extension": "Must be an allowed extension.", - "contents.validation.image": "Not an image.", "contents.validation.invalid": "Not a valid value.", "contents.validation.itemCount": "Must have exactly {count} item(s).", "contents.validation.itemCountBetween": "Must have between {min} and {max} item(s).", diff --git a/backend/i18n/source/backend_it.json b/backend/i18n/source/backend_it.json index e8a7f0942..3f065b3a0 100644 --- a/backend/i18n/source/backend_it.json +++ b/backend/i18n/source/backend_it.json @@ -145,7 +145,6 @@ "contents.validation.error": "Non è stato possibile validare a causa di un errore interno.", "contents.validation.exactValue": "Deve essere esattamente {value}.", "contents.validation.extension": "Deve essere un'estensione consentita.", - "contents.validation.image": "Non è un'immagine.", "contents.validation.invalid": "Valore non consentito.", "contents.validation.itemCount": "Deve avere esattamente {count} elemento(i).", "contents.validation.itemCountBetween": "Deve essere tra {min} e {max} elemento(i).", diff --git a/backend/i18n/source/backend_nl.json b/backend/i18n/source/backend_nl.json index 39161207f..5305b0263 100644 --- a/backend/i18n/source/backend_nl.json +++ b/backend/i18n/source/backend_nl.json @@ -141,7 +141,6 @@ "contents.validation.error": "Validatie mislukt met interne fout.", "contents.validation.exactValue": "Moet exact {waarde} zijn.", "contents.validation.extension": "Moet een toegestane extensie zijn.", - "contents.validation.image": "Geen afbeelding.", "contents.validation.invalid": "Geen geldige waarde.", "contents.validation.itemCount": "Moet exact {count} item (s) bevatten.", "contents.validation.itemCountBetween": "Moet tussen {min} en {max} item (s) bevatten.", diff --git a/backend/i18n/source/backend_zh.json b/backend/i18n/source/backend_zh.json index 34740fe15..12f6647e2 100644 --- a/backend/i18n/source/backend_zh.json +++ b/backend/i18n/source/backend_zh.json @@ -150,7 +150,6 @@ "contents.validation.error": "验证失败,内部错误。", "contents.validation.exactValue": "必须正好是 {value}。", "contents.validation.extension": "必须是允许的扩展名。", - "contents.validation.image": "不是图片。", "contents.validation.invalid": "无效值。", "contents.validation.itemCount": "必须正好有 {count} 个项目。", "contents.validation.itemCountBetween": "必须介于 {min} 和 {max} 项之间。", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 21865a47b..dfc942845 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -769,10 +769,10 @@ "schemas.fieldTypes.assets.countMax": "Max Assets", "schemas.fieldTypes.assets.countMin": "Min Assets", "schemas.fieldTypes.assets.description": "Images, videos, documents.", + "schemas.fieldTypes.assets.expectedType": "Expected type", "schemas.fieldTypes.assets.fileExtensions": "File Extensions", "schemas.fieldTypes.assets.folderId": "Folder", "schemas.fieldTypes.assets.folderIdHint": "The asset folder where the new assets will be uploaded to.", - "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", diff --git a/backend/i18n/source/frontend_it.json b/backend/i18n/source/frontend_it.json index 1bf6e17c7..44d30f88b 100644 --- a/backend/i18n/source/frontend_it.json +++ b/backend/i18n/source/frontend_it.json @@ -709,7 +709,6 @@ "schemas.fieldTypes.assets.countMin": "Min num. di Risorse", "schemas.fieldTypes.assets.description": "Immagini, video, documenti.", "schemas.fieldTypes.assets.fileExtensions": "Estensioni dei File", - "schemas.fieldTypes.assets.mustBeImage": "Deve essere un'immagine", "schemas.fieldTypes.assets.previewFileName": "Solamente il nome del file", "schemas.fieldTypes.assets.previewImage": "Solamente l'anteprima o il nome del file se non è un immagine", "schemas.fieldTypes.assets.previewImageAndFileName": "L'anteprima e il nome del file", diff --git a/backend/i18n/source/frontend_nl.json b/backend/i18n/source/frontend_nl.json index c55d31626..dd27391fe 100644 --- a/backend/i18n/source/frontend_nl.json +++ b/backend/i18n/source/frontend_nl.json @@ -674,7 +674,6 @@ "schemas.fieldTypes.assets.countMin": "Min. bestanden", "schemas.fieldTypes.assets.description": "Afbeeldingen, video's, documenten.", "schemas.fieldTypes.assets.fileExtensions": "Bestandsextensies", - "schemas.fieldTypes.assets.mustBeImage": "Moet afbeelding zijn", "schemas.fieldTypes.assets.previewFileName": "Alleen bestandsnaam", "schemas.fieldTypes.assets.previewImage": "Alleen miniatuur- of bestandsnaam indien geen afbeelding", "schemas.fieldTypes.assets.previewImageAndFileName": "Miniatuur- en bestandsnaam", diff --git a/backend/i18n/source/frontend_zh.json b/backend/i18n/source/frontend_zh.json index e9e53404c..6ce05844a 100644 --- a/backend/i18n/source/frontend_zh.json +++ b/backend/i18n/source/frontend_zh.json @@ -728,7 +728,6 @@ "schemas.fieldTypes.assets.fileExtensions": "文件扩展名", "schemas.fieldTypes.assets.folderId": "文件夹", "schemas.fieldTypes.assets.folderIdHint": "新资源将上传到的资源文件夹。", - "schemas.fieldTypes.assets.mustBeImage": "必须是图片", "schemas.fieldTypes.assets.previewFileName": "仅文件名", "schemas.fieldTypes.assets.previewImage": "如果不是图像,则只有缩略图或文件名", "schemas.fieldTypes.assets.previewImageAndFileName": "缩略图和文件名", diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs index 3148a6c1e..6d534de05 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs @@ -17,67 +17,88 @@ namespace Squidex.Domain.Apps.Core.Assets { private static readonly char[] PathSeparators = { '.', '[', ']' }; + public const string FocusX = "focusX"; + public const string FocusY = "focusY"; + public const string PixelWidth = "pixelWidth"; + public const string PixelHeight = "pixelHeight"; + public const string VideoWidth = "videoWidth"; + public const string VideoHeight = "videoHeight"; + public AssetMetadata SetFocusX(float value) { - this["focusX"] = JsonValue.Create(value); + this[FocusX] = JsonValue.Create(value); return this; } public AssetMetadata SetFocusY(float value) { - this["focusY"] = JsonValue.Create(value); + this[FocusY] = JsonValue.Create(value); return this; } public AssetMetadata SetPixelWidth(int value) { - this["pixelWidth"] = JsonValue.Create(value); + this[PixelWidth] = JsonValue.Create(value); return this; } public AssetMetadata SetPixelHeight(int value) { - this["pixelHeight"] = JsonValue.Create(value); + this[PixelHeight] = JsonValue.Create(value); return this; } - public float? GetFocusX() + public AssetMetadata SetVideoWidth(int value) { - if (TryGetValue("focusX", out var n) && n is JsonNumber number) - { - return (float)number.Value; - } + this[VideoWidth] = JsonValue.Create(value); - return null; + return this; } - public float? GetFocusY() + public AssetMetadata SetVideoHeight(int value) { - if (TryGetValue("focusY", out var n) && n is JsonNumber number) - { - return (float)number.Value; - } + this[VideoHeight] = JsonValue.Create(value); - return null; + return this; } - public int? GetPixelWidth() + public float? GetFocusX() { - if (TryGetValue("pixelWidth", out var n) && n is JsonNumber number) - { - return (int)number.Value; - } + return GetNumber(FocusX); + } - return null; + public float? GetFocusY() + { + return GetNumber(FocusY); + } + + public int? GetPixelWidth() + { + return GetNumber(PixelWidth); } public int? GetPixelHeight() { - if (TryGetValue("pixelHeight", out var n) && n is JsonNumber number) + return GetNumber(PixelHeight); + } + + public int? GetVideoWidth() + { + return GetNumber(VideoWidth); + } + + public int? GetVideoHeight() + { + return GetNumber(VideoHeight); + } + + public int? GetNumber(string name) + { + if (TryGetValue(name, out var n) && n is JsonNumber number) { return (int)number.Value; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs index 6e62c6b91..24b1e06a0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Infrastructure.Collections; namespace Squidex.Domain.Apps.Core.Schemas @@ -39,12 +41,19 @@ namespace Squidex.Domain.Apps.Core.Schemas public int? AspectHeight { get; init; } - public bool MustBeImage { get; init; } + public AssetType? ExpectedType { get; set; } public bool AllowDuplicates { get; init; } public bool ResolveFirst { get; init; } + [Obsolete("Use 'AllowDuplicates' field now")] + public bool MustBeImage + { + init => ExpectedType = value ? AssetType.Image : ExpectedType; + } + + [Obsolete("Use 'ResolveFirst' field now")] public bool ResolveImage { init => ResolveFirst = value; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs index 307d3627f..1363f4940 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs @@ -76,11 +76,27 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators foundIds.Add(asset.AssetId); ValidateCommon(asset, path, addError); - ValidateIsImage(asset, path, addError); + ValidateType(asset, path, addError); if (asset.Type == AssetType.Image) { - ValidateImage(asset, path, addError); + var w = asset.Metadata.GetPixelWidth(); + var h = asset.Metadata.GetPixelHeight(); + + if (w != null && h != null) + { + ValidateDimensions(w.Value, h.Value, path, addError); + } + } + else if (asset.Type == AssetType.Video) + { + var w = asset.Metadata.GetVideoWidth(); + var h = asset.Metadata.GetVideoHeight(); + + if (w != null && h != null) + { + ValidateDimensions(w.Value, h.Value, path, addError); + } } } } @@ -120,54 +136,47 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators } } - private void ValidateIsImage(IAssetInfo asset, ImmutableQueue path, AddError addError) + private void ValidateType(IAssetInfo asset, ImmutableQueue path, AddError addError) { - if (properties.MustBeImage && asset.Type != AssetType.Image && asset.MimeType != "image/svg+xml") + var type = asset.MimeType == "image/svg+xml" ? AssetType.Image : asset.Type; + + if (properties.ExpectedType != null && properties.ExpectedType != type) { - addError(path, T.Get("contents.validation.image")); + addError(path, T.Get("contents.validation.assetType", new { type = properties.ExpectedType })); } } - private void ValidateImage(IAssetInfo asset, ImmutableQueue path, AddError addError) + private void ValidateDimensions(int w, int h, ImmutableQueue path, AddError addError) { - var pixelWidth = asset.Metadata.GetPixelWidth(); - var pixelHeight = asset.Metadata.GetPixelHeight(); + var actualRatio = (double)w / h; - if (pixelWidth != null && pixelHeight != null) + if (properties.MinWidth != null && w < properties.MinWidth) { - var w = pixelWidth.Value; - var h = pixelHeight.Value; - - var actualRatio = (double)w / h; + addError(path, T.Get("contents.validation.minimumWidth", new { width = w, min = properties.MinWidth })); + } - if (properties.MinWidth != null && w < properties.MinWidth) - { - addError(path, T.Get("contents.validation.minimumWidth", new { width = w, min = properties.MinWidth })); - } + if (properties.MaxWidth != null && w > properties.MaxWidth) + { + addError(path, T.Get("contents.validation.maximumWidth", new { width = w, max = properties.MaxWidth })); + } - if (properties.MaxWidth != null && w > properties.MaxWidth) - { - addError(path, T.Get("contents.validation.maximumWidth", new { width = w, max = properties.MaxWidth })); - } + if (properties.MinHeight != null && h < properties.MinHeight) + { + addError(path, T.Get("contents.validation.minimumHeight", new { height = h, min = properties.MinHeight })); + } - if (properties.MinHeight != null && h < properties.MinHeight) - { - addError(path, T.Get("contents.validation.minimumHeight", new { height = h, min = properties.MinHeight })); - } + if (properties.MaxHeight != null && h > properties.MaxHeight) + { + addError(path, T.Get("contents.validation.maximumHeight", new { height = h, max = properties.MaxHeight })); + } - if (properties.MaxHeight != null && h > properties.MaxHeight) - { - addError(path, T.Get("contents.validation.maximumHeight", new { height = h, max = properties.MaxHeight })); - } + if (properties.AspectHeight != null && properties.AspectWidth != null) + { + var expectedRatio = (double)properties.AspectWidth.Value / properties.AspectHeight.Value; - if (properties.AspectHeight != null && properties.AspectWidth != null) + if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) { - var expectedRatio = (double)properties.AspectWidth.Value / properties.AspectHeight.Value; - - if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon) - { - addError(path, T.Get("contents.validation.aspectRatio", new { width = properties.AspectWidth, height = properties.AspectHeight })); - } + addError(path, T.Get("contents.validation.aspectRatio", new { width = properties.AspectWidth, height = properties.AspectHeight })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs index 7f8408163..406ef6338 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs @@ -129,13 +129,14 @@ namespace Squidex.Domain.Apps.Entities.Assets TryAddTimeSpan("duration", file.Properties.Duration); + TryAddInt("bitsPerSample", file.Properties.BitsPerSample); TryAddInt("audioBitrate", file.Properties.AudioBitrate); TryAddInt("audioChannels", file.Properties.AudioChannels); TryAddInt("audioSampleRate", file.Properties.AudioSampleRate); - TryAddInt("bitsPerSample", file.Properties.BitsPerSample); TryAddInt("imageQuality", file.Properties.PhotoQuality); - TryAddInt("videoWidth", file.Properties.VideoWidth); - TryAddInt("videoHeight", file.Properties.VideoHeight); + + TryAddInt(AssetMetadata.VideoWidth, file.Properties.VideoWidth); + TryAddInt(AssetMetadata.VideoHeight, file.Properties.VideoHeight); TryAddString("description", file.Properties.Description); } @@ -150,24 +151,24 @@ namespace Squidex.Domain.Apps.Entities.Assets public IEnumerable Format(IAssetEntity asset) { - var metadata = asset.Metadata; - if (asset.Type == AssetType.Video) { - if (metadata.TryGetNumber("videoWidth", out var w) && - metadata.TryGetNumber("videoHeight", out var h)) + var videoWidth = asset.Metadata.GetVideoWidth(); + var videoHeight = asset.Metadata.GetVideoWidth(); + + if (videoWidth != null && videoHeight != null) { - yield return $"{w}x{h}pt"; + yield return $"{videoHeight}x{videoHeight}pt"; } - if (metadata.TryGetString("duration", out var duration)) + if (asset.Metadata.TryGetString("duration", out var duration)) { yield return duration; } } else if (asset.Type == AssetType.Audio) { - if (metadata.TryGetString("duration", out var duration)) + if (asset.Metadata.TryGetString("duration", out var duration)) { yield return duration; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs index 5a4103615..e2452be8c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs @@ -123,10 +123,12 @@ namespace Squidex.Domain.Apps.Entities.Assets { if (asset.Type == AssetType.Image) { - if (asset.Metadata.TryGetNumber("pixelWidth", out var w) && - asset.Metadata.TryGetNumber("pixelHeight", out var h)) + var pixelWidth = asset.Metadata.GetVideoWidth(); + var pixelHeight = asset.Metadata.GetVideoHeight(); + + if (pixelWidth != null && pixelHeight != null) { - yield return $"{w}x{h}px"; + yield return $"{pixelWidth}x{pixelHeight}px"; } } } diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index b859186db..497b0e674 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -517,6 +517,9 @@ Id {id} non trovato. + + Not of expected type: {type}. + Deve essere tra {min} e {max}. @@ -538,9 +541,6 @@ Deve essere un'estensione consentita. - - Non è un'immagine. - Valore non consentito. diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index 382d31d84..62d20868b 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -517,6 +517,9 @@ Id {id} niet gevonden. + + Not of expected type: {type}. + Moet tussen {min} en {max} liggen. @@ -538,9 +541,6 @@ Moet een toegestane extensie zijn. - - Geen afbeelding. - Geen geldige waarde. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index da8b84c2b..34f2c82f8 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -517,6 +517,9 @@ Id {id} not found. + + Not of expected type: {type}. + Must be between {min} and {max}. @@ -538,9 +541,6 @@ Must be an allowed extension. - - Not an image. - Not a valid value. diff --git a/backend/src/Squidex.Shared/Texts.zh.resx b/backend/src/Squidex.Shared/Texts.zh.resx index 8c3e69eca..c6e626adf 100644 --- a/backend/src/Squidex.Shared/Texts.zh.resx +++ b/backend/src/Squidex.Shared/Texts.zh.resx @@ -517,6 +517,9 @@ 未找到 ID {id}。 + + Not of expected type: {type}. + 必须介于 {min} 和 {max} 之间。 @@ -538,9 +541,6 @@ 必须是允许的扩展名。 - - 不是图片。 - 无效值。 diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs index a0be64a82..877b16658 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Reflection; @@ -85,15 +86,25 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields public int? AspectHeight { get; set; } /// - /// Defines if the asset must be an image. + /// The expected type. /// - public bool MustBeImage { get; set; } + public AssetType? ExpectedType { get; set; } /// /// True to resolve first asset in the content list. /// public bool ResolveFirst { get; set; } + /// + /// True to resolve first image in the content list. + /// + [Obsolete("Use 'expectedType' field now")] + public bool MustBeImage + { + get => ExpectedType == AssetType.Image; + set => ExpectedType = value ? AssetType.Image : ExpectedType; + } + /// /// True to resolve first image in the content list. /// diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs index 305e29d6a..9a8e8874b 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using FluentAssertions; +using Squidex.Domain.Apps.Core.Assets; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Core.ValidateContent; @@ -22,17 +23,24 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators public class AssetsValidatorTests : IClassFixture { private readonly List errors = new List(); - private readonly IAssetInfo document = TestAssets.Document(DomainId.NewGuid()); - private readonly IAssetInfo image1 = TestAssets.Image(DomainId.NewGuid()); - private readonly IAssetInfo image2 = TestAssets.Image(DomainId.NewGuid()); - private readonly IAssetInfo imageSvg = TestAssets.Svg(DomainId.NewGuid()); + private static readonly IAssetInfo Document = TestAssets.Document(DomainId.NewGuid()); + private static readonly IAssetInfo Image1 = TestAssets.Image(DomainId.NewGuid()); + private static readonly IAssetInfo Image2 = TestAssets.Image(DomainId.NewGuid()); + private static readonly IAssetInfo ImageSvg = TestAssets.Svg(DomainId.NewGuid()); + private static readonly IAssetInfo Video = TestAssets.Video(DomainId.NewGuid()); + + public static IEnumerable AssetsWithDimensions() + { + yield return new object[] { Image1.AssetId }; + yield return new object[] { Video.AssetId }; + } [Fact] public async Task Should_not_add_error_if_assets_are_valid() { var sut = Validator(new AssetsFieldProperties()); - await sut.ValidateAsync(CreateValue(document.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId), errors); Assert.Empty(errors); } @@ -62,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { var sut = Validator(new AssetsFieldProperties { AllowDuplicates = true }); - await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Image1.AssetId, Image1.AssetId), errors); Assert.Empty(errors); } @@ -70,9 +78,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators [Fact] public async Task Should_not_add_error_if_asset_is_an_image() { - var sut = Validator(new AssetsFieldProperties { MustBeImage = true }); + var sut = Validator(new AssetsFieldProperties { ExpectedType = AssetType.Image }); - await sut.ValidateAsync(CreateValue(imageSvg.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(ImageSvg.AssetId, Image1.AssetId), errors); Assert.Empty(errors); } @@ -106,7 +114,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { var sut = Validator(new AssetsFieldProperties { MinSize = 5 * 1024 }); - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); errors.Should().BeEquivalentTo( new[] { "[1]: Size of 4 kB must be greater than 5 kB." }); @@ -117,7 +125,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { var sut = Validator(new AssetsFieldProperties { MaxSize = 5 * 1024 }); - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); errors.Should().BeEquivalentTo( new[] { "[2]: Size of 8 kB must be less than 5 kB." }); @@ -126,64 +134,69 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators [Fact] public async Task Should_add_error_if_document_is_not_an_image() { - var sut = Validator(new AssetsFieldProperties { MustBeImage = true }); + var sut = Validator(new AssetsFieldProperties { ExpectedType = AssetType.Image }); - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); errors.Should().BeEquivalentTo( - new[] { "[1]: Not an image." }); + new[] { "[1]: Not of expected type: Image." }); } - [Fact] - public async Task Should_add_error_if_image_width_is_too_small() + [Theory] + [MemberData(nameof(AssetsWithDimensions))] + public async Task Should_add_error_if_asset_width_is_too_small(DomainId videoOrImageId) { var sut = Validator(new AssetsFieldProperties { MinWidth = 1000 }); - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); errors.Should().BeEquivalentTo( new[] { "[2]: Width 800px must be greater than 1000px." }); } - [Fact] - public async Task Should_add_error_if_image_width_is_too_big() + [Theory] + [MemberData(nameof(AssetsWithDimensions))] + public async Task Should_add_error_if_asset_width_is_too_big(DomainId videoOrImageId) { var sut = Validator(new AssetsFieldProperties { MaxWidth = 700 }); - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); errors.Should().BeEquivalentTo( new[] { "[2]: Width 800px must be less than 700px." }); } - [Fact] - public async Task Should_add_error_if_image_height_is_too_small() + [Theory] + [MemberData(nameof(AssetsWithDimensions))] + public async Task Should_add_error_if_asset_height_is_too_small(DomainId videoOrImageId) { var sut = Validator(new AssetsFieldProperties { MinHeight = 800 }); - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); errors.Should().BeEquivalentTo( new[] { "[2]: Height 600px must be greater than 800px." }); } - [Fact] - public async Task Should_add_error_if_image_height_is_too_big() + [Theory] + [MemberData(nameof(AssetsWithDimensions))] + public async Task Should_add_error_if_asset_height_is_too_big(DomainId videoOrImageId) { var sut = Validator(new AssetsFieldProperties { MaxHeight = 500 }); - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); errors.Should().BeEquivalentTo( new[] { "[2]: Height 600px must be less than 500px." }); } - [Fact] - public async Task Should_add_error_if_image_has_invalid_aspect_ratio() + [Theory] + [MemberData(nameof(AssetsWithDimensions))] + public async Task Should_add_error_if_asset_has_invalid_aspect_ratio(DomainId videoOrImageId) { var sut = Validator(new AssetsFieldProperties { AspectWidth = 1, AspectHeight = 1 }); - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, videoOrImageId), errors); errors.Should().BeEquivalentTo( new[] { "[2]: Must have aspect ratio 1:1." }); @@ -194,7 +207,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { var sut = Validator(new AssetsFieldProperties { MinItems = 2 }); - await sut.ValidateAsync(CreateValue(image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Image1.AssetId), errors); errors.Should().BeEquivalentTo( new[] { "Must have at least 2 item(s)." }); @@ -205,7 +218,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { var sut = Validator(new AssetsFieldProperties { MaxItems = 1 }); - await sut.ValidateAsync(CreateValue(image1.AssetId, image2.AssetId), errors); + await sut.ValidateAsync(CreateValue(Image1.AssetId, Image2.AssetId), errors); errors.Should().BeEquivalentTo( new[] { "Must not have more than 1 item(s)." }); @@ -216,7 +229,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { var sut = Validator(new AssetsFieldProperties()); - await sut.ValidateAsync(CreateValue(image1.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Image1.AssetId, Image1.AssetId), errors); errors.Should().BeEquivalentTo( new[] { "Must not contain duplicate values." }); @@ -227,7 +240,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators { var sut = Validator(new AssetsFieldProperties { AllowedExtensions = ImmutableList.Create("mp4") }); - await sut.ValidateAsync(CreateValue(document.AssetId, image1.AssetId), errors); + await sut.ValidateAsync(CreateValue(Document.AssetId, Image1.AssetId), errors); errors.Should().BeEquivalentTo( new[] @@ -242,16 +255,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators return ids.ToList(); } - private IValidator Validator(AssetsFieldProperties properties) + private static IValidator Validator(AssetsFieldProperties properties) { return new AssetsValidator(properties.IsRequired, properties, FoundAssets()); } - private CheckAssets FoundAssets() + private static CheckAssets FoundAssets() { return ids => { - var result = new List { document, image1, image2, imageSvg }; + var result = new List { Document, Image1, Image2, ImageSvg, Video }; return Task.FromResult>(result); }; diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestAssets.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestAssets.cs index b82ae0439..462da7f84 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestAssets.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestAssets.cs @@ -64,6 +64,21 @@ namespace Squidex.Domain.Apps.Core.TestHelpers }; } + public static AssetInfo Video(DomainId id) + { + return new AssetInfo + { + AssetId = id, + FileName = "MyImage.png", + FileSize = 1024 * 8, + Type = AssetType.Video, + Metadata = + new AssetMetadata() + .SetVideoWidth(800) + .SetVideoHeight(600) + }; + } + public static AssetInfo Svg(DomainId id) { return new AssetInfo diff --git a/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html index e3afa2975..8bbc3483d 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html @@ -40,13 +40,24 @@ +
+
-
-
- - + + +
+
+
+ +
+
+
@@ -114,6 +125,8 @@
+
+
@@ -126,9 +139,7 @@
- +
diff --git a/frontend/app/shared/services/schemas.types.ts b/frontend/app/shared/services/schemas.types.ts index 0c72ac145..e5509458b 100644 --- a/frontend/app/shared/services/schemas.types.ts +++ b/frontend/app/shared/services/schemas.types.ts @@ -215,7 +215,7 @@ export class AssetsFieldPropertiesDto extends FieldPropertiesDto { public readonly minItems?: number; public readonly minSize?: number; public readonly minWidth?: number; - public readonly mustBeImage?: boolean; + public readonly expectedType?: string; public get isSortable() { return false; diff --git a/frontend/app/shared/state/schemas.forms.ts b/frontend/app/shared/state/schemas.forms.ts index d0ee7b33a..258092dbf 100644 --- a/frontend/app/shared/state/schemas.forms.ts +++ b/frontend/app/shared/state/schemas.forms.ts @@ -266,6 +266,7 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor { this.config['aspectWidth'] = undefined; this.config['defaultValue'] = undefined; this.config['defaultValues'] = undefined; + this.config['expectedType'] = undefined; this.config['folderId'] = undefined; this.config['maxHeight'] = undefined; this.config['maxItems'] = undefined; @@ -275,7 +276,6 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor { this.config['minItems'] = undefined; this.config['minSize'] = undefined; this.config['minWidth'] = undefined; - this.config['mustBeImage'] = undefined; this.config['previewMode'] = undefined; this.config['resolveFirst'] = undefined; } diff --git a/frontend/app/theme/_forms.scss b/frontend/app/theme/_forms.scss index 9c4729ddf..1ab8fc865 100644 --- a/frontend/app/theme/_forms.scss +++ b/frontend/app/theme/_forms.scss @@ -114,17 +114,19 @@ } .form-alert { - @include absolute(.25rem, 0, auto, auto); + @include absolute(1.55rem, 0, auto, auto); font-size: .9rem; font-weight: normal; - line-height: 1.75; - max-width: 600px; - min-width: 200px; padding: 1rem; padding-right: 2.5rem; - text-align: left; + width: max-content; z-index: 2000; + div { + max-width: 400px; + min-width: 250px; + } + &::after { @include absolute(-.75rem, .625rem, auto, auto); @include caret-top($color-theme-error, .4rem);