Browse Source

Video validation.

pull/747/head
Sebastian 4 years ago
parent
commit
c975ff72c3
  1. 2
      backend/i18n/frontend_en.json
  2. 2
      backend/i18n/frontend_it.json
  3. 2
      backend/i18n/frontend_nl.json
  4. 2
      backend/i18n/frontend_zh.json
  5. 2
      backend/i18n/source/backend_en.json
  6. 1
      backend/i18n/source/backend_it.json
  7. 1
      backend/i18n/source/backend_nl.json
  8. 1
      backend/i18n/source/backend_zh.json
  9. 2
      backend/i18n/source/frontend_en.json
  10. 1
      backend/i18n/source/frontend_it.json
  11. 1
      backend/i18n/source/frontend_nl.json
  12. 1
      backend/i18n/source/frontend_zh.json
  13. 67
      backend/src/Squidex.Domain.Apps.Core.Model/Assets/AssetMetadata.cs
  14. 11
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
  15. 81
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/AssetsValidator.cs
  16. 21
      backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs
  17. 8
      backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs
  18. 6
      backend/src/Squidex.Shared/Texts.it.resx
  19. 6
      backend/src/Squidex.Shared/Texts.nl.resx
  20. 6
      backend/src/Squidex.Shared/Texts.resx
  21. 6
      backend/src/Squidex.Shared/Texts.zh.resx
  22. 15
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs
  23. 83
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/Validators/AssetsValidatorTests.cs
  24. 15
      backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestAssets.cs
  25. 29
      frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html
  26. 2
      frontend/app/shared/services/schemas.types.ts
  27. 2
      frontend/app/shared/state/schemas.forms.ts
  28. 12
      frontend/app/theme/_forms.scss

2
backend/i18n/frontend_en.json

@ -769,10 +769,10 @@
"schemas.fieldTypes.assets.countMax": "Max Assets", "schemas.fieldTypes.assets.countMax": "Max Assets",
"schemas.fieldTypes.assets.countMin": "Min Assets", "schemas.fieldTypes.assets.countMin": "Min Assets",
"schemas.fieldTypes.assets.description": "Images, videos, documents.", "schemas.fieldTypes.assets.description": "Images, videos, documents.",
"schemas.fieldTypes.assets.expectedType": "Expected type",
"schemas.fieldTypes.assets.fileExtensions": "File Extensions", "schemas.fieldTypes.assets.fileExtensions": "File Extensions",
"schemas.fieldTypes.assets.folderId": "Folder", "schemas.fieldTypes.assets.folderId": "Folder",
"schemas.fieldTypes.assets.folderIdHint": "The asset folder where the new assets will be uploaded to.", "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.previewFileName": "Only file name",
"schemas.fieldTypes.assets.previewImage": "Only thumbnail or file name if not an image", "schemas.fieldTypes.assets.previewImage": "Only thumbnail or file name if not an image",
"schemas.fieldTypes.assets.previewImageAndFileName": "Thumbnail and file name", "schemas.fieldTypes.assets.previewImageAndFileName": "Thumbnail and file name",

2
backend/i18n/frontend_it.json

@ -769,10 +769,10 @@
"schemas.fieldTypes.assets.countMax": "Max num di Risorse", "schemas.fieldTypes.assets.countMax": "Max num di Risorse",
"schemas.fieldTypes.assets.countMin": "Min num. di Risorse", "schemas.fieldTypes.assets.countMin": "Min num. di Risorse",
"schemas.fieldTypes.assets.description": "Immagini, video, documenti.", "schemas.fieldTypes.assets.description": "Immagini, video, documenti.",
"schemas.fieldTypes.assets.expectedType": "Expected type",
"schemas.fieldTypes.assets.fileExtensions": "Estensioni dei File", "schemas.fieldTypes.assets.fileExtensions": "Estensioni dei File",
"schemas.fieldTypes.assets.folderId": "Folder", "schemas.fieldTypes.assets.folderId": "Folder",
"schemas.fieldTypes.assets.folderIdHint": "The asset folder where the new assets will be uploaded to.", "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.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.previewImage": "Solamente l'anteprima o il nome del file se non è un immagine",
"schemas.fieldTypes.assets.previewImageAndFileName": "L'anteprima e il nome del file", "schemas.fieldTypes.assets.previewImageAndFileName": "L'anteprima e il nome del file",

2
backend/i18n/frontend_nl.json

@ -769,10 +769,10 @@
"schemas.fieldTypes.assets.countMax": "Max. bestanden", "schemas.fieldTypes.assets.countMax": "Max. bestanden",
"schemas.fieldTypes.assets.countMin": "Min. bestanden", "schemas.fieldTypes.assets.countMin": "Min. bestanden",
"schemas.fieldTypes.assets.description": "Afbeeldingen, video's, documenten.", "schemas.fieldTypes.assets.description": "Afbeeldingen, video's, documenten.",
"schemas.fieldTypes.assets.expectedType": "Expected type",
"schemas.fieldTypes.assets.fileExtensions": "Bestandsextensies", "schemas.fieldTypes.assets.fileExtensions": "Bestandsextensies",
"schemas.fieldTypes.assets.folderId": "Folder", "schemas.fieldTypes.assets.folderId": "Folder",
"schemas.fieldTypes.assets.folderIdHint": "The asset folder where the new assets will be uploaded to.", "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.previewFileName": "Alleen bestandsnaam",
"schemas.fieldTypes.assets.previewImage": "Alleen miniatuur- of bestandsnaam indien geen afbeelding", "schemas.fieldTypes.assets.previewImage": "Alleen miniatuur- of bestandsnaam indien geen afbeelding",
"schemas.fieldTypes.assets.previewImageAndFileName": "Miniatuur- en bestandsnaam", "schemas.fieldTypes.assets.previewImageAndFileName": "Miniatuur- en bestandsnaam",

2
backend/i18n/frontend_zh.json

@ -769,10 +769,10 @@
"schemas.fieldTypes.assets.countMax": "最大资源", "schemas.fieldTypes.assets.countMax": "最大资源",
"schemas.fieldTypes.assets.countMin": "最小资源", "schemas.fieldTypes.assets.countMin": "最小资源",
"schemas.fieldTypes.assets.description": "图片、视频、文档。", "schemas.fieldTypes.assets.description": "图片、视频、文档。",
"schemas.fieldTypes.assets.expectedType": "Expected type",
"schemas.fieldTypes.assets.fileExtensions": "文件扩展名", "schemas.fieldTypes.assets.fileExtensions": "文件扩展名",
"schemas.fieldTypes.assets.folderId": "文件夹", "schemas.fieldTypes.assets.folderId": "文件夹",
"schemas.fieldTypes.assets.folderIdHint": "新资源将上传到的资源文件夹。", "schemas.fieldTypes.assets.folderIdHint": "新资源将上传到的资源文件夹。",
"schemas.fieldTypes.assets.mustBeImage": "必须是图片",
"schemas.fieldTypes.assets.previewFileName": "仅文件名", "schemas.fieldTypes.assets.previewFileName": "仅文件名",
"schemas.fieldTypes.assets.previewImage": "如果不是图像,则只有缩略图或文件名", "schemas.fieldTypes.assets.previewImage": "如果不是图像,则只有缩略图或文件名",
"schemas.fieldTypes.assets.previewImageAndFileName": "缩略图和文件名", "schemas.fieldTypes.assets.previewImageAndFileName": "缩略图和文件名",

2
backend/i18n/source/backend_en.json

@ -144,6 +144,7 @@
"contents.statusTransitionNotAllowed": "Cannot change status from {oldStatus} to {newStatus}.", "contents.statusTransitionNotAllowed": "Cannot change status from {oldStatus} to {newStatus}.",
"contents.validation.aspectRatio": "Must have aspect ratio {width}:{height}.", "contents.validation.aspectRatio": "Must have aspect ratio {width}:{height}.",
"contents.validation.assetNotFound": "Id {id} not found.", "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.between": "Must be between {min} and {max}.",
"contents.validation.characterCount": "Must have exactly {count} character(s).", "contents.validation.characterCount": "Must have exactly {count} character(s).",
"contents.validation.charactersBetween": "Must have between {min} and {max} 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.error": "Validation failed with internal error.",
"contents.validation.exactValue": "Must be exactly {value}.", "contents.validation.exactValue": "Must be exactly {value}.",
"contents.validation.extension": "Must be an allowed extension.", "contents.validation.extension": "Must be an allowed extension.",
"contents.validation.image": "Not an image.",
"contents.validation.invalid": "Not a valid value.", "contents.validation.invalid": "Not a valid value.",
"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).",

1
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.error": "Non è stato possibile validare a causa di un errore interno.",
"contents.validation.exactValue": "Deve essere esattamente {value}.", "contents.validation.exactValue": "Deve essere esattamente {value}.",
"contents.validation.extension": "Deve essere un'estensione consentita.", "contents.validation.extension": "Deve essere un'estensione consentita.",
"contents.validation.image": "Non è un'immagine.",
"contents.validation.invalid": "Valore non consentito.", "contents.validation.invalid": "Valore non consentito.",
"contents.validation.itemCount": "Deve avere esattamente {count} elemento(i).", "contents.validation.itemCount": "Deve avere esattamente {count} elemento(i).",
"contents.validation.itemCountBetween": "Deve essere tra {min} e {max} elemento(i).", "contents.validation.itemCountBetween": "Deve essere tra {min} e {max} elemento(i).",

1
backend/i18n/source/backend_nl.json

@ -141,7 +141,6 @@
"contents.validation.error": "Validatie mislukt met interne fout.", "contents.validation.error": "Validatie mislukt met interne fout.",
"contents.validation.exactValue": "Moet exact {waarde} zijn.", "contents.validation.exactValue": "Moet exact {waarde} zijn.",
"contents.validation.extension": "Moet een toegestane extensie zijn.", "contents.validation.extension": "Moet een toegestane extensie zijn.",
"contents.validation.image": "Geen afbeelding.",
"contents.validation.invalid": "Geen geldige waarde.", "contents.validation.invalid": "Geen geldige waarde.",
"contents.validation.itemCount": "Moet exact {count} item (s) bevatten.", "contents.validation.itemCount": "Moet exact {count} item (s) bevatten.",
"contents.validation.itemCountBetween": "Moet tussen {min} en {max} item (s) bevatten.", "contents.validation.itemCountBetween": "Moet tussen {min} en {max} item (s) bevatten.",

1
backend/i18n/source/backend_zh.json

@ -150,7 +150,6 @@
"contents.validation.error": "验证失败,内部错误。", "contents.validation.error": "验证失败,内部错误。",
"contents.validation.exactValue": "必须正好是 {value}。", "contents.validation.exactValue": "必须正好是 {value}。",
"contents.validation.extension": "必须是允许的扩展名。", "contents.validation.extension": "必须是允许的扩展名。",
"contents.validation.image": "不是图片。",
"contents.validation.invalid": "无效值。", "contents.validation.invalid": "无效值。",
"contents.validation.itemCount": "必须正好有 {count} 个项目。", "contents.validation.itemCount": "必须正好有 {count} 个项目。",
"contents.validation.itemCountBetween": "必须介于 {min} 和 {max} 项之间。", "contents.validation.itemCountBetween": "必须介于 {min} 和 {max} 项之间。",

2
backend/i18n/source/frontend_en.json

@ -769,10 +769,10 @@
"schemas.fieldTypes.assets.countMax": "Max Assets", "schemas.fieldTypes.assets.countMax": "Max Assets",
"schemas.fieldTypes.assets.countMin": "Min Assets", "schemas.fieldTypes.assets.countMin": "Min Assets",
"schemas.fieldTypes.assets.description": "Images, videos, documents.", "schemas.fieldTypes.assets.description": "Images, videos, documents.",
"schemas.fieldTypes.assets.expectedType": "Expected type",
"schemas.fieldTypes.assets.fileExtensions": "File Extensions", "schemas.fieldTypes.assets.fileExtensions": "File Extensions",
"schemas.fieldTypes.assets.folderId": "Folder", "schemas.fieldTypes.assets.folderId": "Folder",
"schemas.fieldTypes.assets.folderIdHint": "The asset folder where the new assets will be uploaded to.", "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.previewFileName": "Only file name",
"schemas.fieldTypes.assets.previewImage": "Only thumbnail or file name if not an image", "schemas.fieldTypes.assets.previewImage": "Only thumbnail or file name if not an image",
"schemas.fieldTypes.assets.previewImageAndFileName": "Thumbnail and file name", "schemas.fieldTypes.assets.previewImageAndFileName": "Thumbnail and file name",

1
backend/i18n/source/frontend_it.json

@ -709,7 +709,6 @@
"schemas.fieldTypes.assets.countMin": "Min num. di Risorse", "schemas.fieldTypes.assets.countMin": "Min num. di Risorse",
"schemas.fieldTypes.assets.description": "Immagini, video, documenti.", "schemas.fieldTypes.assets.description": "Immagini, video, documenti.",
"schemas.fieldTypes.assets.fileExtensions": "Estensioni dei File", "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.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.previewImage": "Solamente l'anteprima o il nome del file se non è un immagine",
"schemas.fieldTypes.assets.previewImageAndFileName": "L'anteprima e il nome del file", "schemas.fieldTypes.assets.previewImageAndFileName": "L'anteprima e il nome del file",

1
backend/i18n/source/frontend_nl.json

@ -674,7 +674,6 @@
"schemas.fieldTypes.assets.countMin": "Min. bestanden", "schemas.fieldTypes.assets.countMin": "Min. bestanden",
"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.previewFileName": "Alleen bestandsnaam", "schemas.fieldTypes.assets.previewFileName": "Alleen bestandsnaam",
"schemas.fieldTypes.assets.previewImage": "Alleen miniatuur- of bestandsnaam indien geen afbeelding", "schemas.fieldTypes.assets.previewImage": "Alleen miniatuur- of bestandsnaam indien geen afbeelding",
"schemas.fieldTypes.assets.previewImageAndFileName": "Miniatuur- en bestandsnaam", "schemas.fieldTypes.assets.previewImageAndFileName": "Miniatuur- en bestandsnaam",

1
backend/i18n/source/frontend_zh.json

@ -728,7 +728,6 @@
"schemas.fieldTypes.assets.fileExtensions": "文件扩展名", "schemas.fieldTypes.assets.fileExtensions": "文件扩展名",
"schemas.fieldTypes.assets.folderId": "文件夹", "schemas.fieldTypes.assets.folderId": "文件夹",
"schemas.fieldTypes.assets.folderIdHint": "新资源将上传到的资源文件夹。", "schemas.fieldTypes.assets.folderIdHint": "新资源将上传到的资源文件夹。",
"schemas.fieldTypes.assets.mustBeImage": "必须是图片",
"schemas.fieldTypes.assets.previewFileName": "仅文件名", "schemas.fieldTypes.assets.previewFileName": "仅文件名",
"schemas.fieldTypes.assets.previewImage": "如果不是图像,则只有缩略图或文件名", "schemas.fieldTypes.assets.previewImage": "如果不是图像,则只有缩略图或文件名",
"schemas.fieldTypes.assets.previewImageAndFileName": "缩略图和文件名", "schemas.fieldTypes.assets.previewImageAndFileName": "缩略图和文件名",

67
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 = { '.', '[', ']' }; 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) public AssetMetadata SetFocusX(float value)
{ {
this["focusX"] = JsonValue.Create(value); this[FocusX] = JsonValue.Create(value);
return this; return this;
} }
public AssetMetadata SetFocusY(float value) public AssetMetadata SetFocusY(float value)
{ {
this["focusY"] = JsonValue.Create(value); this[FocusY] = JsonValue.Create(value);
return this; return this;
} }
public AssetMetadata SetPixelWidth(int value) public AssetMetadata SetPixelWidth(int value)
{ {
this["pixelWidth"] = JsonValue.Create(value); this[PixelWidth] = JsonValue.Create(value);
return this; return this;
} }
public AssetMetadata SetPixelHeight(int value) public AssetMetadata SetPixelHeight(int value)
{ {
this["pixelHeight"] = JsonValue.Create(value); this[PixelHeight] = JsonValue.Create(value);
return this; return this;
} }
public float? GetFocusX() public AssetMetadata SetVideoWidth(int value)
{ {
if (TryGetValue("focusX", out var n) && n is JsonNumber number) this[VideoWidth] = JsonValue.Create(value);
{
return (float)number.Value;
}
return null; return this;
} }
public float? GetFocusY() public AssetMetadata SetVideoHeight(int value)
{ {
if (TryGetValue("focusY", out var n) && n is JsonNumber number) this[VideoHeight] = JsonValue.Create(value);
{
return (float)number.Value;
}
return null; return this;
} }
public int? GetPixelWidth() public float? GetFocusX()
{ {
if (TryGetValue("pixelWidth", out var n) && n is JsonNumber number) return GetNumber(FocusX);
{ }
return (int)number.Value;
}
return null; public float? GetFocusY()
{
return GetNumber(FocusY);
}
public int? GetPixelWidth()
{
return GetNumber(PixelWidth);
} }
public int? GetPixelHeight() 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; return (int)number.Value;
} }

11
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas namespace Squidex.Domain.Apps.Core.Schemas
@ -39,12 +41,19 @@ namespace Squidex.Domain.Apps.Core.Schemas
public int? AspectHeight { get; init; } public int? AspectHeight { get; init; }
public bool MustBeImage { get; init; } public AssetType? ExpectedType { get; set; }
public bool AllowDuplicates { get; init; } public bool AllowDuplicates { get; init; }
public bool ResolveFirst { 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 public bool ResolveImage
{ {
init => ResolveFirst = value; init => ResolveFirst = value;

81
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); foundIds.Add(asset.AssetId);
ValidateCommon(asset, path, addError); ValidateCommon(asset, path, addError);
ValidateIsImage(asset, path, addError); ValidateType(asset, path, addError);
if (asset.Type == AssetType.Image) 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<string> path, AddError addError) private void ValidateType(IAssetInfo asset, ImmutableQueue<string> 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<string> path, AddError addError) private void ValidateDimensions(int w, int h, ImmutableQueue<string> path, AddError addError)
{ {
var pixelWidth = asset.Metadata.GetPixelWidth(); var actualRatio = (double)w / h;
var pixelHeight = asset.Metadata.GetPixelHeight();
if (pixelWidth != null && pixelHeight != null) if (properties.MinWidth != null && w < properties.MinWidth)
{ {
var w = pixelWidth.Value; addError(path, T.Get("contents.validation.minimumWidth", new { width = w, min = properties.MinWidth }));
var h = pixelHeight.Value; }
var actualRatio = (double)w / h;
if (properties.MinWidth != null && w < properties.MinWidth) if (properties.MaxWidth != null && w > properties.MaxWidth)
{ {
addError(path, T.Get("contents.validation.minimumWidth", new { width = w, min = properties.MinWidth })); addError(path, T.Get("contents.validation.maximumWidth", new { width = w, max = properties.MaxWidth }));
} }
if (properties.MaxWidth != null && w > properties.MaxWidth) if (properties.MinHeight != null && h < properties.MinHeight)
{ {
addError(path, T.Get("contents.validation.maximumWidth", new { width = w, max = properties.MaxWidth })); addError(path, T.Get("contents.validation.minimumHeight", new { height = h, min = properties.MinHeight }));
} }
if (properties.MinHeight != null && h < properties.MinHeight) if (properties.MaxHeight != null && h > properties.MaxHeight)
{ {
addError(path, T.Get("contents.validation.minimumHeight", new { height = h, min = properties.MinHeight })); addError(path, T.Get("contents.validation.maximumHeight", new { height = h, max = properties.MaxHeight }));
} }
if (properties.MaxHeight != null && h > properties.MaxHeight) if (properties.AspectHeight != null && properties.AspectWidth != null)
{ {
addError(path, T.Get("contents.validation.maximumHeight", new { height = h, max = properties.MaxHeight })); 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; addError(path, T.Get("contents.validation.aspectRatio", new { width = properties.AspectWidth, height = properties.AspectHeight }));
if (Math.Abs(expectedRatio - actualRatio) > double.Epsilon)
{
addError(path, T.Get("contents.validation.aspectRatio", new { width = properties.AspectWidth, height = properties.AspectHeight }));
}
} }
} }
} }

21
backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs

@ -129,13 +129,14 @@ namespace Squidex.Domain.Apps.Entities.Assets
TryAddTimeSpan("duration", file.Properties.Duration); TryAddTimeSpan("duration", file.Properties.Duration);
TryAddInt("bitsPerSample", file.Properties.BitsPerSample);
TryAddInt("audioBitrate", file.Properties.AudioBitrate); TryAddInt("audioBitrate", file.Properties.AudioBitrate);
TryAddInt("audioChannels", file.Properties.AudioChannels); TryAddInt("audioChannels", file.Properties.AudioChannels);
TryAddInt("audioSampleRate", file.Properties.AudioSampleRate); TryAddInt("audioSampleRate", file.Properties.AudioSampleRate);
TryAddInt("bitsPerSample", file.Properties.BitsPerSample);
TryAddInt("imageQuality", file.Properties.PhotoQuality); 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); TryAddString("description", file.Properties.Description);
} }
@ -150,24 +151,24 @@ namespace Squidex.Domain.Apps.Entities.Assets
public IEnumerable<string> Format(IAssetEntity asset) public IEnumerable<string> Format(IAssetEntity asset)
{ {
var metadata = asset.Metadata;
if (asset.Type == AssetType.Video) if (asset.Type == AssetType.Video)
{ {
if (metadata.TryGetNumber("videoWidth", out var w) && var videoWidth = asset.Metadata.GetVideoWidth();
metadata.TryGetNumber("videoHeight", out var h)) 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; yield return duration;
} }
} }
else if (asset.Type == AssetType.Audio) else if (asset.Type == AssetType.Audio)
{ {
if (metadata.TryGetString("duration", out var duration)) if (asset.Metadata.TryGetString("duration", out var duration))
{ {
yield return duration; yield return duration;
} }

8
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.Type == AssetType.Image)
{ {
if (asset.Metadata.TryGetNumber("pixelWidth", out var w) && var pixelWidth = asset.Metadata.GetVideoWidth();
asset.Metadata.TryGetNumber("pixelHeight", out var h)) var pixelHeight = asset.Metadata.GetVideoHeight();
if (pixelWidth != null && pixelHeight != null)
{ {
yield return $"{w}x{h}px"; yield return $"{pixelWidth}x{pixelHeight}px";
} }
} }
} }

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

@ -517,6 +517,9 @@
<data name="contents.validation.assetNotFound" xml:space="preserve"> <data name="contents.validation.assetNotFound" xml:space="preserve">
<value>Id {id} non trovato.</value> <value>Id {id} non trovato.</value>
</data> </data>
<data name="contents.validation.assetType" xml:space="preserve">
<value>Not of expected type: {type}.</value>
</data>
<data name="contents.validation.between" xml:space="preserve"> <data name="contents.validation.between" xml:space="preserve">
<value>Deve essere tra {min} e {max}.</value> <value>Deve essere tra {min} e {max}.</value>
</data> </data>
@ -538,9 +541,6 @@
<data name="contents.validation.extension" xml:space="preserve"> <data name="contents.validation.extension" xml:space="preserve">
<value>Deve essere un'estensione consentita.</value> <value>Deve essere un'estensione consentita.</value>
</data> </data>
<data name="contents.validation.image" xml:space="preserve">
<value>Non è un'immagine.</value>
</data>
<data name="contents.validation.invalid" xml:space="preserve"> <data name="contents.validation.invalid" xml:space="preserve">
<value>Valore non consentito.</value> <value>Valore non consentito.</value>
</data> </data>

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

@ -517,6 +517,9 @@
<data name="contents.validation.assetNotFound" xml:space="preserve"> <data name="contents.validation.assetNotFound" xml:space="preserve">
<value>Id {id} niet gevonden.</value> <value>Id {id} niet gevonden.</value>
</data> </data>
<data name="contents.validation.assetType" xml:space="preserve">
<value>Not of expected type: {type}.</value>
</data>
<data name="contents.validation.between" xml:space="preserve"> <data name="contents.validation.between" xml:space="preserve">
<value>Moet tussen {min} en {max} liggen.</value> <value>Moet tussen {min} en {max} liggen.</value>
</data> </data>
@ -538,9 +541,6 @@
<data name="contents.validation.extension" xml:space="preserve"> <data name="contents.validation.extension" xml:space="preserve">
<value>Moet een toegestane extensie zijn.</value> <value>Moet een toegestane extensie zijn.</value>
</data> </data>
<data name="contents.validation.image" xml:space="preserve">
<value>Geen afbeelding.</value>
</data>
<data name="contents.validation.invalid" xml:space="preserve"> <data name="contents.validation.invalid" xml:space="preserve">
<value>Geen geldige waarde.</value> <value>Geen geldige waarde.</value>
</data> </data>

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

@ -517,6 +517,9 @@
<data name="contents.validation.assetNotFound" xml:space="preserve"> <data name="contents.validation.assetNotFound" xml:space="preserve">
<value>Id {id} not found.</value> <value>Id {id} not found.</value>
</data> </data>
<data name="contents.validation.assetType" xml:space="preserve">
<value>Not of expected type: {type}.</value>
</data>
<data name="contents.validation.between" xml:space="preserve"> <data name="contents.validation.between" xml:space="preserve">
<value>Must be between {min} and {max}.</value> <value>Must be between {min} and {max}.</value>
</data> </data>
@ -538,9 +541,6 @@
<data name="contents.validation.extension" xml:space="preserve"> <data name="contents.validation.extension" xml:space="preserve">
<value>Must be an allowed extension.</value> <value>Must be an allowed extension.</value>
</data> </data>
<data name="contents.validation.image" xml:space="preserve">
<value>Not an image.</value>
</data>
<data name="contents.validation.invalid" xml:space="preserve"> <data name="contents.validation.invalid" xml:space="preserve">
<value>Not a valid value.</value> <value>Not a valid value.</value>
</data> </data>

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

@ -517,6 +517,9 @@
<data name="contents.validation.assetNotFound" xml:space="preserve"> <data name="contents.validation.assetNotFound" xml:space="preserve">
<value>未找到 ID {id}。</value> <value>未找到 ID {id}。</value>
</data> </data>
<data name="contents.validation.assetType" xml:space="preserve">
<value>Not of expected type: {type}.</value>
</data>
<data name="contents.validation.between" xml:space="preserve"> <data name="contents.validation.between" xml:space="preserve">
<value>必须介于 {min} 和 {max} 之间。</value> <value>必须介于 {min} 和 {max} 之间。</value>
</data> </data>
@ -538,9 +541,6 @@
<data name="contents.validation.extension" xml:space="preserve"> <data name="contents.validation.extension" xml:space="preserve">
<value>必须是允许的扩展名。</value> <value>必须是允许的扩展名。</value>
</data> </data>
<data name="contents.validation.image" xml:space="preserve">
<value>不是图片。</value>
</data>
<data name="contents.validation.invalid" xml:space="preserve"> <data name="contents.validation.invalid" xml:space="preserve">
<value>无效值。</value> <value>无效值。</value>
</data> </data>

15
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/AssetsFieldPropertiesDto.cs

@ -6,6 +6,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -85,15 +86,25 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields
public int? AspectHeight { get; set; } public int? AspectHeight { get; set; }
/// <summary> /// <summary>
/// Defines if the asset must be an image. /// The expected type.
/// </summary> /// </summary>
public bool MustBeImage { get; set; } public AssetType? ExpectedType { get; set; }
/// <summary> /// <summary>
/// True to resolve first asset in the content list. /// True to resolve first asset in the content list.
/// </summary> /// </summary>
public bool ResolveFirst { get; set; } public bool ResolveFirst { get; set; }
/// <summary>
/// True to resolve first image in the content list.
/// </summary>
[Obsolete("Use 'expectedType' field now")]
public bool MustBeImage
{
get => ExpectedType == AssetType.Image;
set => ExpectedType = value ? AssetType.Image : ExpectedType;
}
/// <summary> /// <summary>
/// True to resolve first image in the content list. /// True to resolve first image in the content list.
/// </summary> /// </summary>

83
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.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using FluentAssertions; using FluentAssertions;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent;
@ -22,17 +23,24 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
public class AssetsValidatorTests : IClassFixture<TranslationsFixture> public class AssetsValidatorTests : IClassFixture<TranslationsFixture>
{ {
private readonly List<string> errors = new List<string>(); private readonly List<string> errors = new List<string>();
private readonly IAssetInfo document = TestAssets.Document(DomainId.NewGuid()); private static readonly IAssetInfo Document = TestAssets.Document(DomainId.NewGuid());
private readonly IAssetInfo image1 = TestAssets.Image(DomainId.NewGuid()); private static readonly IAssetInfo Image1 = TestAssets.Image(DomainId.NewGuid());
private readonly IAssetInfo image2 = TestAssets.Image(DomainId.NewGuid()); private static readonly IAssetInfo Image2 = TestAssets.Image(DomainId.NewGuid());
private readonly IAssetInfo imageSvg = TestAssets.Svg(DomainId.NewGuid()); private static readonly IAssetInfo ImageSvg = TestAssets.Svg(DomainId.NewGuid());
private static readonly IAssetInfo Video = TestAssets.Video(DomainId.NewGuid());
public static IEnumerable<object[]> AssetsWithDimensions()
{
yield return new object[] { Image1.AssetId };
yield return new object[] { Video.AssetId };
}
[Fact] [Fact]
public async Task Should_not_add_error_if_assets_are_valid() public async Task Should_not_add_error_if_assets_are_valid()
{ {
var sut = Validator(new AssetsFieldProperties()); var sut = Validator(new AssetsFieldProperties());
await sut.ValidateAsync(CreateValue(document.AssetId), errors); await sut.ValidateAsync(CreateValue(Document.AssetId), errors);
Assert.Empty(errors); Assert.Empty(errors);
} }
@ -62,7 +70,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{ {
var sut = Validator(new AssetsFieldProperties { AllowDuplicates = true }); 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); Assert.Empty(errors);
} }
@ -70,9 +78,9 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
[Fact] [Fact]
public async Task Should_not_add_error_if_asset_is_an_image() 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); Assert.Empty(errors);
} }
@ -106,7 +114,7 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
{ {
var sut = Validator(new AssetsFieldProperties { MinSize = 5 * 1024 }); 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( errors.Should().BeEquivalentTo(
new[] { "[1]: Size of 4 kB must be greater than 5 kB." }); 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 }); 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( errors.Should().BeEquivalentTo(
new[] { "[2]: Size of 8 kB must be less than 5 kB." }); 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] [Fact]
public async Task Should_add_error_if_document_is_not_an_image() 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( errors.Should().BeEquivalentTo(
new[] { "[1]: Not an image." }); new[] { "[1]: Not of expected type: Image." });
} }
[Fact] [Theory]
public async Task Should_add_error_if_image_width_is_too_small() [MemberData(nameof(AssetsWithDimensions))]
public async Task Should_add_error_if_asset_width_is_too_small(DomainId videoOrImageId)
{ {
var sut = Validator(new AssetsFieldProperties { MinWidth = 1000 }); 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( errors.Should().BeEquivalentTo(
new[] { "[2]: Width 800px must be greater than 1000px." }); new[] { "[2]: Width 800px must be greater than 1000px." });
} }
[Fact] [Theory]
public async Task Should_add_error_if_image_width_is_too_big() [MemberData(nameof(AssetsWithDimensions))]
public async Task Should_add_error_if_asset_width_is_too_big(DomainId videoOrImageId)
{ {
var sut = Validator(new AssetsFieldProperties { MaxWidth = 700 }); 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( errors.Should().BeEquivalentTo(
new[] { "[2]: Width 800px must be less than 700px." }); new[] { "[2]: Width 800px must be less than 700px." });
} }
[Fact] [Theory]
public async Task Should_add_error_if_image_height_is_too_small() [MemberData(nameof(AssetsWithDimensions))]
public async Task Should_add_error_if_asset_height_is_too_small(DomainId videoOrImageId)
{ {
var sut = Validator(new AssetsFieldProperties { MinHeight = 800 }); 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( errors.Should().BeEquivalentTo(
new[] { "[2]: Height 600px must be greater than 800px." }); new[] { "[2]: Height 600px must be greater than 800px." });
} }
[Fact] [Theory]
public async Task Should_add_error_if_image_height_is_too_big() [MemberData(nameof(AssetsWithDimensions))]
public async Task Should_add_error_if_asset_height_is_too_big(DomainId videoOrImageId)
{ {
var sut = Validator(new AssetsFieldProperties { MaxHeight = 500 }); 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( errors.Should().BeEquivalentTo(
new[] { "[2]: Height 600px must be less than 500px." }); new[] { "[2]: Height 600px must be less than 500px." });
} }
[Fact] [Theory]
public async Task Should_add_error_if_image_has_invalid_aspect_ratio() [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 }); 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( errors.Should().BeEquivalentTo(
new[] { "[2]: Must have aspect ratio 1:1." }); 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 }); var sut = Validator(new AssetsFieldProperties { MinItems = 2 });
await sut.ValidateAsync(CreateValue(image1.AssetId), errors); await sut.ValidateAsync(CreateValue(Image1.AssetId), errors);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new[] { "Must have at least 2 item(s)." }); 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 }); 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( errors.Should().BeEquivalentTo(
new[] { "Must not have more than 1 item(s)." }); 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()); 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( errors.Should().BeEquivalentTo(
new[] { "Must not contain duplicate values." }); 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") }); 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( errors.Should().BeEquivalentTo(
new[] new[]
@ -242,16 +255,16 @@ namespace Squidex.Domain.Apps.Core.Operations.ValidateContent.Validators
return ids.ToList(); return ids.ToList();
} }
private IValidator Validator(AssetsFieldProperties properties) private static IValidator Validator(AssetsFieldProperties properties)
{ {
return new AssetsValidator(properties.IsRequired, properties, FoundAssets()); return new AssetsValidator(properties.IsRequired, properties, FoundAssets());
} }
private CheckAssets FoundAssets() private static CheckAssets FoundAssets()
{ {
return ids => return ids =>
{ {
var result = new List<IAssetInfo> { document, image1, image2, imageSvg }; var result = new List<IAssetInfo> { Document, Image1, Image2, ImageSvg, Video };
return Task.FromResult<IReadOnlyList<IAssetInfo>>(result); return Task.FromResult<IReadOnlyList<IAssetInfo>>(result);
}; };

15
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) public static AssetInfo Svg(DomainId id)
{ {
return new AssetInfo return new AssetInfo

29
frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html

@ -40,13 +40,24 @@
</div> </div>
</div> </div>
<hr />
<div class="form-group row"> <div class="form-group row">
<div class="col-9 offset-3"> <label class="col-3 col-form-label">{{ 'schemas.fieldTypes.assets.expectedType' | sqxTranslate }}</label>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="{{field.fieldId}}_fieldMustBeImage" formControlName="mustBeImage"> <div class="col-9">
<label class="form-check-label" for="{{field.fieldId}}_fieldMustBeImage"> <div class="row g-0">
{{ 'schemas.fieldTypes.assets.mustBeImage' | sqxTranslate }} <div class="col">
</label> <select class="form-select" formControlName="expectedType">
<option></option>
<option ngValue="Image">Image</option>
<option ngValue="Audio">Audio</option>
<option ngValue="Video">Video</option>
<option ngValue="Unknown">Unknown</option>
</select>
</div>
<div class="col col-label">
</div>
</div> </div>
</div> </div>
</div> </div>
@ -114,6 +125,8 @@
</div> </div>
</div> </div>
<hr />
<div class="form-group row"> <div class="form-group row">
<div class="col-9 offset-3"> <div class="col-9 offset-3">
<div class="form-check"> <div class="form-check">
@ -126,9 +139,7 @@
</div> </div>
<div class="form-group row"> <div class="form-group row">
<label class="col-3 col-form-label"> <label class="col-3 col-form-label">{{ 'schemas.fieldTypes.assets.fileExtensions' | sqxTranslate }}</label>
{{ 'schemas.fieldTypes.assets.fileExtensions' | sqxTranslate }}
</label>
<div class="col-9"> <div class="col-9">
<sqx-tag-editor formControlName="allowedExtensions"></sqx-tag-editor> <sqx-tag-editor formControlName="allowedExtensions"></sqx-tag-editor>

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

@ -215,7 +215,7 @@ export class AssetsFieldPropertiesDto extends FieldPropertiesDto {
public readonly minItems?: number; public readonly minItems?: number;
public readonly minSize?: number; public readonly minSize?: number;
public readonly minWidth?: number; public readonly minWidth?: number;
public readonly mustBeImage?: boolean; public readonly expectedType?: string;
public get isSortable() { public get isSortable() {
return false; return false;

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

@ -266,6 +266,7 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> {
this.config['aspectWidth'] = undefined; this.config['aspectWidth'] = undefined;
this.config['defaultValue'] = undefined; this.config['defaultValue'] = undefined;
this.config['defaultValues'] = undefined; this.config['defaultValues'] = undefined;
this.config['expectedType'] = undefined;
this.config['folderId'] = undefined; this.config['folderId'] = undefined;
this.config['maxHeight'] = undefined; this.config['maxHeight'] = undefined;
this.config['maxItems'] = undefined; this.config['maxItems'] = undefined;
@ -275,7 +276,6 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> {
this.config['minItems'] = undefined; this.config['minItems'] = undefined;
this.config['minSize'] = undefined; this.config['minSize'] = undefined;
this.config['minWidth'] = undefined; this.config['minWidth'] = undefined;
this.config['mustBeImage'] = undefined;
this.config['previewMode'] = undefined; this.config['previewMode'] = undefined;
this.config['resolveFirst'] = undefined; this.config['resolveFirst'] = undefined;
} }

12
frontend/app/theme/_forms.scss

@ -114,17 +114,19 @@
} }
.form-alert { .form-alert {
@include absolute(.25rem, 0, auto, auto); @include absolute(1.55rem, 0, auto, auto);
font-size: .9rem; font-size: .9rem;
font-weight: normal; font-weight: normal;
line-height: 1.75;
max-width: 600px;
min-width: 200px;
padding: 1rem; padding: 1rem;
padding-right: 2.5rem; padding-right: 2.5rem;
text-align: left; width: max-content;
z-index: 2000; z-index: 2000;
div {
max-width: 400px;
min-width: 250px;
}
&::after { &::after {
@include absolute(-.75rem, .625rem, auto, auto); @include absolute(-.75rem, .625rem, auto, auto);
@include caret-top($color-theme-error, .4rem); @include caret-top($color-theme-error, .4rem);

Loading…
Cancel
Save