Browse Source

Rich text (#1053)

* V1

* Fix tests and missing files.

* Fix parent ID.
pull/1055/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
29caa53c8e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/i18n/frontend_en.json
  2. 1
      backend/i18n/frontend_fr.json
  3. 1
      backend/i18n/frontend_it.json
  4. 1
      backend/i18n/frontend_nl.json
  5. 1
      backend/i18n/frontend_pt.json
  6. 1
      backend/i18n/frontend_zh.json
  7. 1
      backend/i18n/source/backend_en.json
  8. 1
      backend/i18n/source/frontend_en.json
  9. 61
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/RichTextExtensions.cs
  10. 58
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/RichTextMark.cs
  11. 220
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/RichTextNode.cs
  12. 63
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs
  13. 21
      backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx
  14. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/AssetsFieldProperties.cs
  15. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs
  16. 24
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs
  17. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs
  18. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs
  19. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs
  20. 52
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RichTextFieldProperties.cs
  21. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs
  22. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/DefaultValueChecker.cs
  23. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/DefaultValueFactory.cs
  24. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs
  25. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs
  26. 56
      backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/StringReferenceExtractor.cs
  27. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterVisitor.cs
  28. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs
  29. 2
      backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  30. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs
  31. 77
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/DefaultFieldValueValidatorsFactory.cs
  32. 15
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs
  33. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs
  34. 6
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs
  35. 26
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs
  36. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs
  37. 67
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs
  38. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs
  39. 18
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs
  40. 67
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/RichTextGraphType.cs
  41. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs
  42. 57
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculatePreviewText.cs
  43. 24
      backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/Guards/FieldPropertiesValidator.cs
  44. 1
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs
  45. 12
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  46. 3
      backend/src/Squidex.Shared/Texts.fr.resx
  47. 3
      backend/src/Squidex.Shared/Texts.it.resx
  48. 3
      backend/src/Squidex.Shared/Texts.nl.resx
  49. 3
      backend/src/Squidex.Shared/Texts.pt.resx
  50. 3
      backend/src/Squidex.Shared/Texts.resx
  51. 3
      backend/src/Squidex.Shared/Texts.zh.resx
  52. 5
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs
  53. 73
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/RichTextFieldPropertiesDto.cs
  54. 3
      backend/src/Squidex/Config/Domain/ContentsServices.cs
  55. 24
      backend/src/Squidex/Squidex.csproj
  56. 360
      backend/src/Squidex/wwwroot/editor/squidex-editor.js
  57. 28
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ComplexText.html
  58. 256
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ComplexText.json
  59. 23
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ComplexText.md
  60. 1
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ComplexText.min.html
  61. 1
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ComplexText.txt
  62. 56
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/RichTextTests.cs
  63. 8
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/DefaultValueFactoryTests.cs
  64. 247
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/StringFormatterTests.cs
  65. 71
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/StringReferenceExtractorTests.cs
  66. 167
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/RichTextFieldTests.cs
  67. 24
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs
  68. 16
      backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj
  69. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestSchema.cs
  70. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs
  71. 340
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  72. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestAsset.cs
  73. 208
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs
  74. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs
  75. 86
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/CalculatePreviewTextTests.cs
  76. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/JsonFieldPropertiesTests.cs
  77. 97
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/RichTextFieldPropertiesTests.cs
  78. 10
      frontend/src/app/declarations.d.ts
  79. 17
      frontend/src/app/features/content/shared/forms/field-editor.component.html
  80. 3
      frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html
  81. 2
      frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.ts
  82. 7
      frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html
  83. 2
      frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts
  84. 36
      frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-ui.component.html
  85. 2
      frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-ui.component.scss
  86. 41
      frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-ui.component.ts
  87. 55
      frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-validation.component.html
  88. 33
      frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-validation.component.scss
  89. 32
      frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-validation.component.ts
  90. 4
      frontend/src/app/shared/components/forms/rich-editor.component.ts
  91. 31
      frontend/src/app/shared/services/schemas.types.ts
  92. 103
      frontend/src/app/shared/state/contents.forms.spec.ts
  93. 30
      frontend/src/app/shared/state/contents.forms.visitors.spec.ts
  94. 206
      frontend/src/app/shared/state/contents.forms.visitors.ts
  95. 12
      frontend/src/app/shared/state/schemas.forms.ts
  96. 240
      frontend/src/app/theme/icomoon/demo.html
  97. BIN
      frontend/src/app/theme/icomoon/fonts/icomoon.eot
  98. 2
      frontend/src/app/theme/icomoon/fonts/icomoon.svg
  99. BIN
      frontend/src/app/theme/icomoon/fonts/icomoon.ttf
  100. BIN
      frontend/src/app/theme/icomoon/fonts/icomoon.woff

1
backend/i18n/frontend_en.json

@ -925,6 +925,7 @@
"schemas.fieldTypes.references.queryHint": "The initial query that is used in the UI to narrow down the results for the user. In Odata notation.",
"schemas.fieldTypes.references.resolve": "Resolve references",
"schemas.fieldTypes.references.resolveHint": "Show the name of the referenced item in content list when MaxItems is set to 1.",
"schemas.fieldTypes.richText.description": "Text, convertible to HTML and markdown",
"schemas.fieldTypes.string.characters": "Characters",
"schemas.fieldTypes.string.charactersMax": "Max Characters",
"schemas.fieldTypes.string.charactersMin": "Min Characters",

1
backend/i18n/frontend_fr.json

@ -925,6 +925,7 @@
"schemas.fieldTypes.references.queryHint": "The initial query that is used in the UI to narrow down the results for the user. In Odata notation.",
"schemas.fieldTypes.references.resolve": "Résoudre les références",
"schemas.fieldTypes.references.resolveHint": "Afficher le nom de l'élément référencé dans la liste de contenu lorsque MaxItems est défini sur 1.",
"schemas.fieldTypes.richText.description": "Text, convertible to HTML and markdown",
"schemas.fieldTypes.string.characters": "Personnages",
"schemas.fieldTypes.string.charactersMax": "Caractères maximum",
"schemas.fieldTypes.string.charactersMin": "Caractères minimum",

1
backend/i18n/frontend_it.json

@ -925,6 +925,7 @@
"schemas.fieldTypes.references.queryHint": "The initial query that is used in the UI to narrow down the results for the user. In Odata notation.",
"schemas.fieldTypes.references.resolve": "Resolve references",
"schemas.fieldTypes.references.resolveHint": "Mostra il nome dell'elemento collegato (riferimento) nella lista dei contenuti quando il numero massimo di elementi è impostato a 1.",
"schemas.fieldTypes.richText.description": "Text, convertible to HTML and markdown",
"schemas.fieldTypes.string.characters": "Caratteri",
"schemas.fieldTypes.string.charactersMax": "Max numero di Caratteri",
"schemas.fieldTypes.string.charactersMin": "Min numero di Caratteri",

1
backend/i18n/frontend_nl.json

@ -925,6 +925,7 @@
"schemas.fieldTypes.references.queryHint": "The initial query that is used in the UI to narrow down the results for the user. In Odata notation.",
"schemas.fieldTypes.references.resolve": "Referenties tonen",
"schemas.fieldTypes.references.resolveHint": "Toon de naam van het item waarnaar wordt verwezen in de inhoudslijst wanneer MaxItems is ingesteld op 1.",
"schemas.fieldTypes.richText.description": "Text, convertible to HTML and markdown",
"schemas.fieldTypes.string.characters": "Karakters",
"schemas.fieldTypes.string.charactersMax": "Max. karakters",
"schemas.fieldTypes.string.charactersMin": "Min. karakters",

1
backend/i18n/frontend_pt.json

@ -925,6 +925,7 @@
"schemas.fieldTypes.references.queryHint": "The initial query that is used in the UI to narrow down the results for the user. In Odata notation.",
"schemas.fieldTypes.references.resolve": "Resolver referências",
"schemas.fieldTypes.references.resolveHint": "Mostre o nome do item referenciado na lista de conteúdos quando maxItems estiver definido para 1.",
"schemas.fieldTypes.richText.description": "Text, convertible to HTML and markdown",
"schemas.fieldTypes.string.characters": "Personagens",
"schemas.fieldTypes.string.charactersMax": "Personagens Max",
"schemas.fieldTypes.string.charactersMin": "Personagens de Min",

1
backend/i18n/frontend_zh.json

@ -925,6 +925,7 @@
"schemas.fieldTypes.references.queryHint": "The initial query that is used in the UI to narrow down the results for the user. In Odata notation.",
"schemas.fieldTypes.references.resolve": "Resolve references",
"schemas.fieldTypes.references.resolveHint": "当 MaxItems 设置为 1 时,在内容列表中显示引用项的名称。",
"schemas.fieldTypes.richText.description": "Text, convertible to HTML and markdown",
"schemas.fieldTypes.string.characters": "字符",
"schemas.fieldTypes.string.charactersMax": "最大字符数",
"schemas.fieldTypes.string.charactersMin": "最小字符数",

1
backend/i18n/source/backend_en.json

@ -139,6 +139,7 @@
"contents.invalidGeolocationLatitude": "Latitude must be between -90 and 90.",
"contents.invalidGeolocationLongitude": "Longitude must be between -180 and 180.",
"contents.invalidNumber": "Invalid json type, expected number.",
"contents.invalidRichText": "Invalid rich text.",
"contents.invalidString": "Invalid json type, expected string.",
"contents.listReferences": "{count} Reference(s)",
"contents.referenced": "Content is referenced by another content and cannot be deleted or unpublished.",

1
backend/i18n/source/frontend_en.json

@ -925,6 +925,7 @@
"schemas.fieldTypes.references.queryHint": "The initial query that is used in the UI to narrow down the results for the user. In Odata notation.",
"schemas.fieldTypes.references.resolve": "Resolve references",
"schemas.fieldTypes.references.resolveHint": "Show the name of the referenced item in content list when MaxItems is set to 1.",
"schemas.fieldTypes.richText.description": "Text, convertible to HTML and markdown",
"schemas.fieldTypes.string.characters": "Characters",
"schemas.fieldTypes.string.charactersMax": "Max Characters",
"schemas.fieldTypes.string.charactersMin": "Min Characters",

61
backend/src/Squidex.Domain.Apps.Core.Model/Contents/RichTextExtensions.cs

@ -0,0 +1,61 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Contents;
public static class RichTextExtensions
{
public static bool TryGetEnum<T>(this JsonValue value, out T enumValue) where T : struct
{
enumValue = default;
return value.Value is string text && Enum.TryParse(text, true, out enumValue);
}
public static int GetIntAttr(this JsonObject? attrs, string name, int defaultValue = 0)
{
if (attrs?.TryGetValue(name, out var value) == true && value.Value is double attr)
{
return (int)attr;
}
return defaultValue;
}
public static string GetStringAttr(this JsonObject? attrs, string name, string defaultValue = "")
{
if (attrs?.TryGetValue(name, out var value) == true && value.Value is string attr)
{
return attr;
}
return defaultValue;
}
public static bool TryGetArrayOfObject(this JsonValue value, out JsonArray array)
{
array = default!;
if (value.Value is not JsonArray temp)
{
return false;
}
foreach (var item in temp)
{
if (item.Value is not JsonObject)
{
return false;
}
}
array = temp;
return true;
}
}

58
backend/src/Squidex.Domain.Apps.Core.Model/Contents/RichTextMark.cs

@ -0,0 +1,58 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Json.Objects;
using Squidex.Text.RichText.Model;
namespace Squidex.Domain.Apps.Core.Contents;
internal class RichTextMark : IMark
{
private JsonObject? attrs;
public MarkType Type { get; private set; }
public bool TryUse(JsonValue source)
{
Type = MarkType.Undefined;
attrs = null;
if (source.Value is not JsonObject obj)
{
return false;
}
var isValid = true;
foreach (var (key, value) in obj)
{
switch (key)
{
case "type" when value.TryGetEnum<MarkType>(out var type):
Type = type;
break;
case "attrs" when value.Value is JsonObject attrs:
this.attrs = attrs;
break;
}
}
isValid &= Type != MarkType.Undefined;
return isValid;
}
public int GetIntAttr(string name, int defaultValue = 0)
{
return attrs.GetIntAttr(name, defaultValue);
}
public string GetStringAttr(string name, string defaultValue = "")
{
return attrs.GetStringAttr(name, defaultValue);
}
}

220
backend/src/Squidex.Domain.Apps.Core.Model/Contents/RichTextNode.cs

@ -0,0 +1,220 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.ObjectPool;
using Squidex.Text.RichText;
using Squidex.Text.RichText.Model;
namespace Squidex.Domain.Apps.Core.Contents;
public sealed class RichTextNode : INode
{
private readonly RichTextMark mark = new RichTextMark();
private State currentState;
internal struct State
{
public JsonObject? Root;
public NodeType Type;
public JsonArray? Marks;
public JsonObject? Attrs;
public JsonArray? Content;
public string? Text;
public int MarkIndex;
}
public NodeType Type
{
get => currentState.Type;
}
public JsonObject? Root
{
get => currentState.Root;
}
public string? Text
{
get => currentState.Text;
}
public static bool TryCreate(JsonValue source, out RichTextNode node)
{
var candidate = new RichTextNode();
if (candidate.TryUse(source, true))
{
node = candidate;
return true;
}
node = null!;
return false;
}
public static RichTextNode Create(JsonValue source)
{
var node = new RichTextNode();
// We assume that we have made the validation before.
node.TryUse(source, false);
return node;
}
public bool TryUse(JsonValue source, bool recursive = false)
{
State state = default;
if (source.Value is not JsonObject obj)
{
currentState = state;
return false;
}
state.Root = obj;
var isValid = true;
foreach (var (key, value) in obj)
{
switch (key)
{
case "type" when value.TryGetEnum<NodeType>(out var type):
state.Type = type;
break;
case "attrs" when value.Value is JsonObject attrs:
state.Attrs = attrs;
break;
case "marks" when value.TryGetArrayOfObject(out var marks):
state.Marks = marks;
break;
case "content" when value.TryGetArrayOfObject(out var content):
state.Content = content;
break;
case "text" when value.Value is string text:
state.Text = text;
break;
}
}
currentState = state;
isValid &= Type != NodeType.Undefined;
if (isValid && recursive)
{
if (state.Content != null)
{
foreach (var content in state.Content)
{
// We have already validated this before.
isValid &= TryUse((JsonObject)content.Value!, recursive);
}
}
if (state.Marks != null)
{
foreach (var markObj in state.Marks)
{
// We have already validated this before.
isValid &= mark.TryUse((JsonObject)markObj.Value!);
}
}
}
return isValid;
}
public int GetIntAttr(string name, int defaultValue = 0)
{
return currentState.Attrs.GetIntAttr(name, defaultValue);
}
public string GetStringAttr(string name, string defaultValue = "")
{
return currentState.Attrs.GetStringAttr(name, defaultValue);
}
public IMark? GetNextMark()
{
if (currentState.Marks == null || currentState.MarkIndex >= currentState.Marks.Count)
{
return null;
}
// We have already validated this before.
mark.TryUse((JsonObject)currentState.Marks[currentState.MarkIndex++].Value!);
return mark;
}
public void IterateContent<T>(T state, Action<INode, T, bool, bool> action)
{
var prevState = currentState;
if (prevState.Content == null)
{
return;
}
var i = 0;
foreach (var item in prevState.Content)
{
var isFirst = i == 0;
var isLast = i == prevState.Content.Count - 1;
// We have already validated this before.
TryUse((JsonObject)item.Value!, false);
action(this, state, isFirst, isLast);
i++;
}
currentState = prevState;
}
public string ToMarkdown()
{
var sb = DefaultPools.StringBuilder.Get();
try
{
MarkdownVisitor.Render(this, sb);
return sb.ToString();
}
finally
{
DefaultPools.StringBuilder.Return(sb);
}
}
public string ToHtml(int indentation = 4)
{
var sb = DefaultPools.StringBuilder.Get();
try
{
HtmlWriterVisitor.Render(this, sb, new HtmlWriterOptions { Indentation = indentation });
return sb.ToString();
}
finally
{
DefaultPools.StringBuilder.Return(sb);
}
}
public string ToText(int maxLength = int.MaxValue)
{
var sb = DefaultPools.StringBuilder.Get();
try
{
TextVisitor.Render(this, sb, maxLength);
return sb.ToString();
}
finally
{
DefaultPools.StringBuilder.Return(sb);
}
}
}

63
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.Designer.cs

@ -762,6 +762,15 @@ namespace Squidex.Domain.Apps.Core {
}
}
/// <summary>
/// Looks up a localized string similar to Number of spaces to format the output..
/// </summary>
public static string Indentation {
get {
return ResourceManager.GetString("Indentation", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The current item, if the field is part of an array..
/// </summary>
@ -897,6 +906,60 @@ namespace Squidex.Domain.Apps.Core {
}
}
/// <summary>
/// Looks up a localized string similar to The referenced assets..
/// </summary>
public static string RichTextFieldAssets {
get {
return ResourceManager.GetString("RichTextFieldAssets", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The value as HTML..
/// </summary>
public static string RichTextFieldHtml {
get {
return ResourceManager.GetString("RichTextFieldHtml", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The value as markdown..
/// </summary>
public static string RichTextFieldMarkdown {
get {
return ResourceManager.GetString("RichTextFieldMarkdown", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The referenced content items..
/// </summary>
public static string RichTextFieldReferences {
get {
return ResourceManager.GetString("RichTextFieldReferences", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The value as plain text..
/// </summary>
public static string RichTextFieldText {
get {
return ResourceManager.GetString("RichTextFieldText", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The raw value..
/// </summary>
public static string RichTextFieldValue {
get {
return ResourceManager.GetString("RichTextFieldValue", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The ID of the schema..
/// </summary>

21
backend/src/Squidex.Domain.Apps.Core.Model/FieldDescriptions.resx

@ -351,6 +351,9 @@
<data name="GraphqlRequest" xml:space="preserve">
<value>The graphql request.</value>
</data>
<data name="Indentation" xml:space="preserve">
<value>Number of spaces to format the output.</value>
</data>
<data name="ItemData" xml:space="preserve">
<value>The current item, if the field is part of an array.</value>
</data>
@ -396,6 +399,24 @@
<data name="QueryVersion" xml:space="preserve">
<value>The optional version of the content to retrieve an older instance (not cached).</value>
</data>
<data name="RichTextFieldAssets" xml:space="preserve">
<value>The referenced assets.</value>
</data>
<data name="RichTextFieldHtml" xml:space="preserve">
<value>The value as HTML.</value>
</data>
<data name="RichTextFieldMarkdown" xml:space="preserve">
<value>The value as markdown.</value>
</data>
<data name="RichTextFieldReferences" xml:space="preserve">
<value>The referenced content items.</value>
</data>
<data name="RichTextFieldText" xml:space="preserve">
<value>The value as plain text.</value>
</data>
<data name="RichTextFieldValue" xml:space="preserve">
<value>The raw value.</value>
</data>
<data name="SchemaId" xml:space="preserve">
<value>The ID of the schema.</value>
</data>

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

@ -20,7 +20,7 @@ public sealed record AssetsFieldProperties : FieldProperties
public string? FolderId { get; init; }
public string? PreviewFormat { get; set; }
public string? PreviewFormat { get; init; }
public int? MinItems { get; init; }
@ -42,7 +42,7 @@ public sealed record AssetsFieldProperties : FieldProperties
public int? AspectHeight { get; init; }
public AssetType? ExpectedType { get; set; }
public AssetType? ExpectedType { get; init; }
public bool AllowDuplicates { get; init; }
@ -60,7 +60,7 @@ public sealed record AssetsFieldProperties : FieldProperties
init => ResolveFirst = value;
}
public ReadonlyList<string>? AllowedExtensions { get; set; }
public ReadonlyList<string>? AllowedExtensions { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/DateTimeFieldProperties.cs

@ -19,7 +19,7 @@ public sealed record DateTimeFieldProperties : FieldProperties
public Instant? MinValue { get; init; }
public string? Format { get; set; }
public string? Format { get; init; }
public DateTimeCalculatedDefaultValue? CalculatedDefaultValue { get; init; }

24
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Fields.cs

@ -71,6 +71,12 @@ public static class Fields
return new RootField<ReferencesFieldProperties> { Id = id, Name = name, Partitioning = partitioning, Properties = properties ?? new() };
}
public static RootField<RichTextFieldProperties> RichText(long id, string name, Partitioning partitioning,
RichTextFieldProperties? properties = null)
{
return new RootField<RichTextFieldProperties> { Id = id, Name = name, Partitioning = partitioning, Properties = properties ?? new() };
}
public static RootField<StringFieldProperties> String(long id, string name, Partitioning partitioning,
StringFieldProperties? properties = null)
{
@ -143,6 +149,12 @@ public static class Fields
return new NestedField<ReferencesFieldProperties> { Id = id, Name = name, Properties = properties ?? new() };
}
public static NestedField<RichTextFieldProperties> RichText(long id, string name,
RichTextFieldProperties? properties = null)
{
return new NestedField<RichTextFieldProperties> { Id = id, Name = name, Properties = properties ?? new() };
}
public static NestedField<StringFieldProperties> String(long id, string name,
StringFieldProperties? properties = null)
{
@ -228,6 +240,12 @@ public static class Fields
return schema.AddField(References(id, name, partitioning, properties));
}
public static Schema AddRichText(this Schema schema, long id, string name, Partitioning partitioning,
RichTextFieldProperties? properties = null)
{
return schema.AddField(RichText(id, name, partitioning, properties));
}
public static Schema AddString(this Schema schema, long id, string name, Partitioning partitioning,
StringFieldProperties? properties = null)
{
@ -300,6 +318,12 @@ public static class Fields
return field.AddField(References(id, name, properties));
}
public static ArrayField AddRichText(this ArrayField field, long id, string name,
RichTextFieldProperties? properties = null)
{
return field.AddField(RichText(id, name, properties));
}
public static ArrayField AddString(this ArrayField field, long id, string name,
StringFieldProperties? properties = null)
{

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldPropertiesVisitor.cs

@ -29,6 +29,8 @@ public interface IFieldPropertiesVisitor<out T, in TArgs>
T Visit(ReferencesFieldProperties properties, TArgs args);
T Visit(RichTextFieldProperties properties, TArgs args);
T Visit(StringFieldProperties properties, TArgs args);
T Visit(TagsFieldProperties properties, TArgs args);

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/IFieldVisitor.cs

@ -29,6 +29,8 @@ public interface IFieldVisitor<out T, in TArgs>
T Visit(IField<ReferencesFieldProperties> field, TArgs args);
T Visit(IField<RichTextFieldProperties> field, TArgs args);
T Visit(IField<StringFieldProperties> field, TArgs args);
T Visit(IField<TagsFieldProperties> field, TArgs args);

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/JsonFieldProperties.cs

@ -9,7 +9,7 @@ namespace Squidex.Domain.Apps.Core.Schemas;
public sealed record JsonFieldProperties : FieldProperties
{
public string? GraphQLSchema { get; set; }
public string? GraphQLSchema { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{

52
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/RichTextFieldProperties.cs

@ -0,0 +1,52 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Schemas;
public sealed record RichTextFieldProperties : FieldProperties
{
public string? FolderId { get; init; }
public int? MinLength { get; init; }
public int? MaxLength { get; init; }
public int? MinCharacters { get; init; }
public int? MaxCharacters { get; init; }
public int? MinWords { get; init; }
public int? MaxWords { get; init; }
public ReadonlyList<string>? ClassNames { get; init; }
public ReadonlyList<DomainId>? SchemaIds { get; init; }
public override T Accept<T, TArgs>(IFieldPropertiesVisitor<T, TArgs> visitor, TArgs args)
{
return visitor.Visit(this, args);
}
public override T Accept<T, TArgs>(IFieldVisitor<T, TArgs> visitor, IField field, TArgs args)
{
return visitor.Visit((IField<RichTextFieldProperties>)field, args);
}
public override RootField CreateRootField(long id, string name, Partitioning partitioning)
{
return Fields.RichText(id, name, partitioning, this);
}
public override NestedField CreateNestedField(long id, string name)
{
return Fields.RichText(id, name, this);
}
}

2
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/StringFieldProperties.cs

@ -14,7 +14,7 @@ public sealed record StringFieldProperties : FieldProperties
{
public ReadonlyList<string>? AllowedValues { get; init; }
public ReadonlyList<string>? ClassNames { get; set; }
public ReadonlyList<string>? ClassNames { get; init; }
public LocalizedValue<string?> DefaultValues { get; init; }

5
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/DefaultValueChecker.cs

@ -75,6 +75,11 @@ internal sealed class DefaultValueChecker : IFieldPropertiesVisitor<bool, None>
return properties.DefaultValue != null || properties.DefaultValues != null;
}
public bool Visit(RichTextFieldProperties properties, None args)
{
return false;
}
public bool Visit(StringFieldProperties properties, None args)
{
return properties.DefaultValue != null || properties.DefaultValues != null;

5
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/DefaultValueFactory.cs

@ -80,6 +80,11 @@ public sealed class DefaultValueFactory : IFieldPropertiesVisitor<JsonValue, Def
return JsonValue.Null;
}
public JsonValue Visit(RichTextFieldProperties properties, Args args)
{
return JsonValue.Null;
}
public JsonValue Visit(NumberFieldProperties properties, Args args)
{
var value = GetDefaultValue(properties.DefaultValue, properties.DefaultValues, args.Partition);

6
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/StringFormatter.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
@ -103,6 +104,11 @@ public sealed class StringFormatter : IFieldPropertiesVisitor<string, StringForm
return FormatArray(args.Value, "Reference", "References");
}
public string Visit(RichTextFieldProperties properties, Args args)
{
return RichTextNode.Create(args.Value).ToText(100);
}
public string Visit(StringFieldProperties properties, Args args)
{
if (properties.Editor == StringFieldEditor.StockPhoto)

5
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/ReferencesCleaner.cs

@ -80,6 +80,11 @@ internal sealed class ReferencesCleaner : IFieldPropertiesVisitor<JsonValue, Ref
return args.Value;
}
public JsonValue Visit(RichTextFieldProperties properties, Args args)
{
return args.Value;
}
public JsonValue Visit(StringFieldProperties properties, Args args)
{
return args.Value;

56
backend/src/Squidex.Domain.Apps.Core.Operations/ExtractReferenceIds/StringReferenceExtractor.cs

@ -7,6 +7,7 @@
using System.Text.RegularExpressions;
using Squidex.Infrastructure;
using Squidex.Text.RichText.Model;
namespace Squidex.Domain.Apps.Core.ExtractReferenceIds;
@ -71,30 +72,71 @@ public sealed class StringReferenceExtractor
contentsPatterns.Add(new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture));
}
public IEnumerable<DomainId> GetEmbeddedContentIds(string text)
public IEnumerable<DomainId> GetEmbeddedContentIds(INode node)
{
if (string.IsNullOrWhiteSpace(text))
var result = new List<DomainId>();
static void Visit(INode node, List<DomainId> result, List<Regex> patterns)
{
yield break;
if (node.Type == NodeType.Text)
{
IMark? mark = null;
while ((mark = node.GetNextMark()) != null)
{
if (mark.Type == MarkType.Link)
{
var href = mark.GetStringAttr("href", string.Empty);
result.AddRange(GetEmbeddedIds(href, patterns));
}
}
}
node.IterateContent((result, patterns), static (node, s, _, _) => Visit(node, s.result, s.patterns));
}
foreach (var pattern in contentsPatterns)
Visit(node, result, contentsPatterns);
return result;
}
public IEnumerable<DomainId> GetEmbeddedContentIds(string text)
{
return GetEmbeddedIds(text, contentsPatterns);
}
public IEnumerable<DomainId> GetEmbeddedAssetIds(INode node)
{
var result = new List<DomainId>();
static void Visit(INode node, List<DomainId> result, List<Regex> patterns)
{
foreach (Match match in pattern.Matches(text).OfType<Match>())
if (node.Type == NodeType.Image)
{
yield return DomainId.Create(match.Groups["Id"].Value);
var src = node.GetStringAttr("src", string.Empty);
result.AddRange(GetEmbeddedIds(src, patterns));
}
node.IterateContent((result, patterns), static (node, s, _, _) => Visit(node, s.result, s.patterns));
}
Visit(node, result, assetsPatterns);
return result;
}
public IEnumerable<DomainId> GetEmbeddedAssetIds(string text)
{
return GetEmbeddedIds(text, assetsPatterns);
}
private static IEnumerable<DomainId> GetEmbeddedIds(string text, List<Regex> patterns)
{
if (string.IsNullOrWhiteSpace(text))
{
yield break;
}
foreach (var pattern in assetsPatterns)
foreach (var pattern in patterns)
{
foreach (Match match in pattern.Matches(text).OfType<Match>())
{

5
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateFilters/FilterVisitor.cs

@ -92,6 +92,11 @@ internal sealed class FilterVisitor : IFieldVisitor<FilterSchema?, FilterVisitor
return FilterSchema.Number;
}
public FilterSchema? Visit(IField<RichTextFieldProperties> field, Args args)
{
return FilterSchema.Any;
}
public FilterSchema? Visit(IField<StringFieldProperties> field, Args args)
{
if (field.Properties.AllowedValues?.Count > 0)

5
backend/src/Squidex.Domain.Apps.Core.Operations/GenerateJsonSchema/JsonTypeVisitor.cs

@ -159,6 +159,11 @@ internal sealed class JsonTypeVisitor : IFieldVisitor<JsonSchemaProperty?, JsonT
return JsonTypeBuilder.JsonProperty();
}
public JsonSchemaProperty? Visit(IField<RichTextFieldProperties> field, Args args)
{
return JsonTypeBuilder.JsonProperty();
}
public JsonSchemaProperty? Visit(IField<NumberFieldProperties> field, Args args)
{
var property = JsonTypeBuilder.NumberProperty();

2
backend/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -28,7 +28,7 @@
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="NJsonSchema" Version="10.9.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.2.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.3.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Linq.Async" Version="6.0.1" />

4
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/ContentValidator.cs

@ -85,10 +85,10 @@ public sealed class ContentValidator
private void ValidateContentCore(ContentData data)
{
CreatecSchemaValidator().Validate(data, context);
CreateSchemaValidator().Validate(data, context);
}
private IValidator CreatecSchemaValidator()
private IValidator CreateSchemaValidator()
{
return new AggregateValidator(CreateContentValidators());
}

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

@ -36,11 +36,9 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (isRequired || properties.MinItems != null || properties.MaxItems != null)
if (IsRequired(properties, args.Context, out var r) || properties.MinItems != null || properties.MaxItems != null)
{
yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems);
yield return new CollectionValidator(r, properties.MinItems, properties.MaxItems);
}
if (properties.UniqueFields?.Count > 0)
@ -67,9 +65,7 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (isRequired)
if (IsRequired(properties, args.Context, out _))
{
yield return new RequiredValidator();
}
@ -79,9 +75,7 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (isRequired)
if (IsRequired(properties, args.Context, out _))
{
yield return new RequiredValidator();
}
@ -93,11 +87,9 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (isRequired || properties.MinItems != null || properties.MaxItems != null)
if (IsRequired(properties, args.Context, out var r) || properties.MinItems != null || properties.MaxItems != null)
{
yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems);
yield return new CollectionValidator(r, properties.MinItems, properties.MaxItems);
}
if (properties.UniqueFields?.Count > 0)
@ -112,9 +104,7 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (isRequired)
if (IsRequired(properties, args.Context, out _))
{
yield return new RequiredValidator();
}
@ -129,9 +119,7 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (isRequired)
if (IsRequired(properties, args.Context, out _))
{
yield return new RequiredValidator();
}
@ -141,9 +129,7 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (isRequired)
if (IsRequired(properties, args.Context, out _))
{
yield return new RequiredValidator();
}
@ -153,9 +139,7 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (isRequired)
if (IsRequired(properties, args.Context, out _))
{
yield return new RequiredValidator();
}
@ -176,13 +160,38 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
yield break;
}
public IEnumerable<IValidator> Visit(IField<StringFieldProperties> field, Args args)
public IEnumerable<IValidator> Visit(IField<RichTextFieldProperties> field, Args args)
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (IsRequired(properties, args.Context, out _))
{
yield return new RequiredStringValidator(true);
}
if (isRequired)
if (properties.MinLength != null || properties.MaxLength != null)
{
yield return new StringLengthValidator(properties.MinLength, properties.MaxLength);
}
if (properties.MinCharacters != null ||
properties.MaxCharacters != null ||
properties.MinWords != null ||
properties.MaxWords != null)
{
yield return new StringTextValidator(null,
properties.MinCharacters,
properties.MaxCharacters,
properties.MinWords,
properties.MaxWords);
}
}
public IEnumerable<IValidator> Visit(IField<StringFieldProperties> field, Args args)
{
var properties = field.Properties;
if (IsRequired(properties, args.Context, out _))
{
yield return new RequiredStringValidator(true);
}
@ -231,11 +240,9 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
{
var properties = field.Properties;
var isRequired = IsRequired(properties, args.Context);
if (isRequired || properties.MinItems != null || properties.MaxItems != null)
if (IsRequired(properties, args.Context, out var r) || properties.MinItems != null || properties.MaxItems != null)
{
yield return new CollectionValidator(isRequired, properties.MinItems, properties.MaxItems);
yield return new CollectionValidator(r, properties.MinItems, properties.MaxItems);
}
if (properties.AllowedValues != null)
@ -254,7 +261,7 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
}
}
private static bool IsRequired(FieldProperties properties, ValidationContext context)
private static bool IsRequired(FieldProperties properties, ValidationContext context, out bool result)
{
var isRequired = properties.IsRequired;
@ -263,6 +270,8 @@ internal sealed class DefaultFieldValueValidatorsFactory : IFieldVisitor<IEnumer
isRequired = isRequired || properties.IsRequiredOnPublish;
}
result = isRequired;
return isRequired;
}

15
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs

@ -91,6 +91,21 @@ public sealed class JsonValueConverter : IFieldPropertiesVisitor<(object? Result
return (null, new JsonError(T.Get("contents.invalidNumber")));
}
public (object? Result, JsonError? Error) Visit(RichTextFieldProperties properties, Args args)
{
if (args.Value.Type == JsonValueType.Null)
{
return (args.Value, null);
}
if (RichTextNode.TryCreate(args.Value, out var node))
{
return (node.ToText(), null);
}
return (null, new JsonError(T.Get("contents.invalidRichText")));
}
public (object? Result, JsonError? Error) Visit(StringFieldProperties properties, Args args)
{
if (args.Value.Value is string s)

5
backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs

@ -95,6 +95,11 @@ public sealed class JsonValueValidator : IFieldPropertiesVisitor<bool, JsonValue
return IsValidStringList(args.Value);
}
public bool Visit(RichTextFieldProperties properties, Args args)
{
return args.Value.Type == JsonValueType.Null || RichTextNode.TryCreate(args.Value, out _);
}
public bool Visit(StringFieldProperties properties, Args args)
{
return args.Value.Value is string;

6
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs

@ -57,7 +57,7 @@ public sealed class MongoAppRepository : MongoSnapshotStoreBase<App, MongoAppEnt
await Collection.Find(x => (x.IndexedUserIds.Contains(contributorId) || names.Contains(x.IndexedName)) && !x.IndexedDeleted)
.ToListAsync(ct);
return RemoveDuplcateNames(entities);
return RemoveDuplicateNames(entities);
}
}
@ -70,7 +70,7 @@ public sealed class MongoAppRepository : MongoSnapshotStoreBase<App, MongoAppEnt
await Collection.Find(x => x.IndexedTeamId == teamId).SortBy(x => x.IndexedCreated)
.ToListAsync(ct);
return RemoveDuplcateNames(entities);
return RemoveDuplicateNames(entities);
}
}
@ -100,7 +100,7 @@ public sealed class MongoAppRepository : MongoSnapshotStoreBase<App, MongoAppEnt
}
}
private static List<App> RemoveDuplcateNames(List<MongoAppEntity> entities)
private static List<App> RemoveDuplicateNames(List<MongoAppEntity> entities)
{
var byName = new Dictionary<string, App>();

26
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Assets/AssetGraphType.cs

@ -151,6 +151,14 @@ internal sealed class AssetGraphType : SharedObjectGraphType<EnrichedAsset>
Description = FieldDescriptions.AssetFileVersion
});
AddField(new FieldType
{
Name = "parentId",
ResolvedType = Scalars.NonNullString,
Resolver = Resolve(x => x.ParentId.ToString()),
Description = FieldDescriptions.AssetParentId
});
AddField(new FieldType
{
Name = "slug",
@ -202,6 +210,15 @@ internal sealed class AssetGraphType : SharedObjectGraphType<EnrichedAsset>
Description = FieldDescriptions.AssetType
});
AddField(new FieldType
{
Name = "metadata",
Arguments = AssetActions.Metadata.Arguments,
ResolvedType = Scalars.JsonNoop,
Resolver = AssetActions.Metadata.Resolver,
Description = FieldDescriptions.AssetMetadata
});
AddField(new FieldType
{
Name = "metadataText",
@ -218,15 +235,6 @@ internal sealed class AssetGraphType : SharedObjectGraphType<EnrichedAsset>
Description = FieldDescriptions.AssetTags
});
AddField(new FieldType
{
Name = "metadata",
Arguments = AssetActions.Metadata.Arguments,
ResolvedType = Scalars.JsonNoop,
Resolver = AssetActions.Metadata.Resolver,
Description = FieldDescriptions.AssetMetadata
});
AddField(new FieldType
{
Name = "editToken",

8
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Builder.cs

@ -25,6 +25,7 @@ internal sealed class Builder
private readonly Dictionary<FieldInfo, ComponentUnionGraphType> componentUnionTypes = [];
private readonly Dictionary<FieldInfo, EmbeddableStringGraphType> embeddableStringTypes = [];
private readonly Dictionary<FieldInfo, NestedGraphType> nestedTypes = [];
private readonly Dictionary<FieldInfo, RichTextGraphType> richTextTypes = [];
private readonly Dictionary<SchemaInfo, ComponentGraphType> componentTypes = [];
private readonly Dictionary<SchemaInfo, ContentGraphType> contentTypes = [];
private readonly Dictionary<SchemaInfo, ContentResultGraphType> contentResultTypes = [];
@ -96,7 +97,7 @@ internal sealed class Builder
FieldMap = new FieldMap(allSchemas);
if (normalSchemas.Any())
if (normalSchemas.Count != 0)
{
var mutations = new ApplicationMutations(this, normalSchemas);
@ -205,6 +206,11 @@ internal sealed class Builder
return embeddableStringTypes.GetOrAdd(fieldInfo, x => new EmbeddableStringGraphType(this, x, properties));
}
public RichTextGraphType GetRichText(FieldInfo fieldInfo, RichTextFieldProperties properties)
{
return richTextTypes.GetOrAdd(fieldInfo, x => new RichTextGraphType(this, x, properties));
}
public ComponentUnionGraphType GetComponentUnion(FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds)
{
return componentUnionTypes.GetOrAdd(fieldInfo, x => new ComponentUnionGraphType(this, x, schemaIds));

67
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentFields.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
@ -34,6 +35,23 @@ internal static class ContentFields
fieldContext.CacheDuration());
});
public static readonly IFieldResolver ResolveRichTextFieldAssets = Resolvers.Sync<RichTextNode, object>((value, fieldContext, context) =>
{
var ids = context.Resolve<StringReferenceExtractor>().GetEmbeddedAssetIds(value).ToList();
return context.GetAssets(ids,
fieldContext.CacheDuration());
});
public static readonly IFieldResolver ResolveRichTextFieldContents = Resolvers.Sync<RichTextNode, object>((value, fieldContext, context) =>
{
var ids = context.Resolve<StringReferenceExtractor>().GetEmbeddedContentIds(value).ToList();
return context.GetContents(ids,
fieldContext.FieldNames(),
fieldContext.CacheDuration());
});
public static readonly FieldType Id = new FieldType
{
Name = "id",
@ -220,6 +238,55 @@ internal static class ContentFields
Description = FieldDescriptions.StringFieldAssets
};
public static readonly FieldType RichTextFieldValue = new FieldType
{
Name = "value",
ResolvedType = Scalars.JsonNoop,
Resolver = Resolvers.Sync<RichTextNode, JsonObject?>(x => x.Root),
Description = FieldDescriptions.RichTextFieldValue
};
public static readonly FieldType RichTextFieldAssets = new FieldType
{
Name = "assets",
ResolvedType = new NonNullGraphType(SharedTypes.AssetsList),
Resolver = ResolveRichTextFieldAssets,
Description = FieldDescriptions.RichTextFieldAssets
};
public static readonly FieldType RichTextFieldMarkdown = new FieldType
{
Name = "markdown",
ResolvedType = Scalars.NonNullString,
Resolver = Resolvers.Sync<RichTextNode, string>(x => x.ToMarkdown()),
Description = FieldDescriptions.RichTextFieldMarkdown
};
public static readonly FieldType RichTextFieldText = new FieldType
{
Name = "text",
ResolvedType = Scalars.NonNullString,
Resolver = Resolvers.Sync<RichTextNode, string>(x => x.ToText()),
Description = FieldDescriptions.RichTextFieldMarkdown
};
public static readonly FieldType RichTextFieldHtml = new FieldType
{
Name = "html",
Arguments =
[
new QueryArgument(Scalars.Int)
{
Name = "indentation",
Description = FieldDescriptions.Indentation,
DefaultValue = 4
},
],
ResolvedType = Scalars.NonNullString,
Resolver = Resolvers.Sync<RichTextNode, string>((x, ctx, _) => x.ToHtml(ctx.GetArgument<int>("indentation", 4))),
Description = FieldDescriptions.RichTextFieldHtml
};
private static IFieldResolver Resolve<T>(Func<JsonObject, T> resolver)
{
return Resolvers.Sync(resolver);

5
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldInputVisitor.cs

@ -74,6 +74,11 @@ internal sealed class FieldInputVisitor : IFieldVisitor<IGraphType?, FieldInfo>
return Scalars.Strings;
}
public IGraphType? Visit(IField<RichTextFieldProperties> field, FieldInfo args)
{
return Scalars.Json;
}
public IGraphType? Visit(IField<NumberFieldProperties> field, FieldInfo args)
{
return Scalars.Float;

18
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/FieldVisitor.cs

@ -8,6 +8,7 @@
using GraphQL;
using GraphQL.Resolvers;
using GraphQL.Types;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
@ -98,6 +99,18 @@ internal sealed class FieldVisitor : IFieldVisitor<FieldGraphSchema, FieldInfo>
}
});
private static readonly IFieldResolver RichText = CreateValueResolver((value, fieldContext, contex) =>
{
switch (value.Value)
{
case JsonObject obj:
return RichTextNode.Create(obj);
default:
ThrowHelper.NotSupportedException();
return default!;
}
});
private static readonly IFieldResolver Assets = CreateValueResolver((value, fieldContext, context) =>
{
var ids = value.AsIds();
@ -250,6 +263,11 @@ internal sealed class FieldVisitor : IFieldVisitor<FieldGraphSchema, FieldInfo>
return new (new ListGraphType(new NonNullGraphType(type)), References, null);
}
public FieldGraphSchema Visit(IField<RichTextFieldProperties> field, FieldInfo args)
{
return new (builder.GetRichText(args, field.Properties), RichText, null);
}
public FieldGraphSchema Visit(IField<UIFieldProperties> field, FieldInfo args)
{
return default;

67
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/RichTextGraphType.cs

@ -0,0 +1,67 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using GraphQL.Types;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents;
internal sealed class RichTextGraphType : ObjectGraphType<RichTextNode>
{
public RichTextGraphType(Builder builder, FieldInfo fieldInfo, RichTextFieldProperties properties)
{
// The name is used for equal comparison. Therefore it is important to treat it as readonly.
Name = fieldInfo.RichTextType;
AddField(ContentFields.RichTextFieldValue);
AddField(ContentFields.RichTextFieldHtml);
AddField(ContentFields.RichTextFieldMarkdown);
AddField(ContentFields.RichTextFieldText);
AddField(ContentFields.RichTextFieldAssets);
var referenceType = ResolveReferences(builder, fieldInfo, properties.SchemaIds);
if (referenceType != null)
{
AddField(new FieldType
{
Name = "contents",
ResolvedType = new NonNullGraphType(new ListGraphType(new NonNullGraphType(referenceType))),
Resolver = ContentFields.ResolveRichTextFieldContents,
Description = FieldDescriptions.RichTextFieldReferences
});
}
}
private static IGraphType? ResolveReferences(Builder builder, FieldInfo fieldInfo, ReadonlyList<DomainId>? schemaIds)
{
IGraphType? contentType = null;
if (schemaIds?.Count == 1)
{
contentType = builder.GetContentType(schemaIds[0]);
}
if (contentType == null)
{
var union = builder.GetContentUnion(fieldInfo.UnionReferenceType, schemaIds);
if (union.SchemaTypes.Count == 0)
{
return default;
}
contentType = union;
}
return contentType;
}
}

3
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/SchemaInfo.cs

@ -90,6 +90,8 @@ internal sealed class FieldInfo
public string NestedType { get; }
public string RichTextType { get; }
public string UnionComponentType { get; }
public string UnionReferenceType { get; }
@ -109,6 +111,7 @@ internal sealed class FieldInfo
LocalizedTypeDynamic = typeNames[$"{typeName}Dto__Dynamic"];
NestedInputType = typeNames[$"{typeName}ChildInputDto"];
NestedType = typeNames[$"{typeName}ChildDto"];
RichTextType = typeNames[$"{typeName}RichText"];
UnionComponentType = typeNames[$"{typeName}ComponentUnionDto"];
UnionReferenceType = typeNames[$"{typeName}UnionDto"];
}

57
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculatePreviewText.cs

@ -0,0 +1,57 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
public sealed class CalculatePreviewText : IContentEnricherStep
{
public async Task EnrichAsync(Context context, IEnumerable<EnrichedContent> contents, ProvideSchema schemas,
CancellationToken ct)
{
// Reuse the node for all contents.
var node = new RichTextNode();
// Group by schema, so we only fetch the schema once.
foreach (var group in contents.GroupBy(x => x.SchemaId.Id))
{
var (schema, components) = await schemas(group.Key);
AddTexts(schema, node, group);
}
}
private void AddTexts(Schema schema, RichTextNode node, IEnumerable<EnrichedContent> contents)
{
foreach (var content in contents)
{
foreach (var field in schema.Fields.Where(x => x.RawProperties is RichTextFieldProperties))
{
if (!content.Data.TryGetValue(field.Name, out var fieldData) || fieldData is not { Count: > 0 })
{
continue;
}
content.ReferenceData ??= [];
var fieldReference = content.ReferenceData.GetOrAdd(field.Name, _ => new ContentFieldData())!;
foreach (var (partitionKey, partitionValue) in fieldData)
{
// Only handle the content if the text is valid.
if (node.TryUse(partitionValue))
{
fieldReference[partitionKey] = node.ToText(100);
}
}
}
}
}
}

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

@ -206,6 +206,30 @@ public sealed class FieldPropertiesValidator : IFieldPropertiesVisitor<IEnumerab
}
}
public IEnumerable<ValidationError> Visit(RichTextFieldProperties properties, None args)
{
if (IsMaxGreaterThanMin(properties.MaxLength, properties.MinLength))
{
yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxLength), nameof(properties.MinLength)),
nameof(properties.MinLength),
nameof(properties.MaxLength));
}
if (IsMaxGreaterThanMin(properties.MaxWords, properties.MinWords))
{
yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxWords), nameof(properties.MinWords)),
nameof(properties.MinWords),
nameof(properties.MaxWords));
}
if (IsMaxGreaterThanMin(properties.MaxCharacters, properties.MinCharacters))
{
yield return new ValidationError(Not.GreaterEqualsThan(nameof(properties.MaxCharacters), nameof(properties.MinCharacters)),
nameof(properties.MinCharacters),
nameof(properties.MaxCharacters));
}
}
public IEnumerable<ValidationError> Visit(StringFieldProperties properties, None args)
{
if (!properties.Editor.IsEnumValue())

1
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoExtensions.cs

@ -168,7 +168,6 @@ public static class MongoExtensions
var (key, snapshot, _, oldVersion) = job;
try
{
Expression<Func<T, bool>> filter2 =
oldVersion > EtagVersion.Any ?
x => x.DocumentId.Equals(key) && x.Version == oldVersion :

12
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -24,12 +24,12 @@
<PackageReference Include="NodaTime" Version="3.1.9" />
<PackageReference Include="OpenTelemetry.Api" Version="1.6.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="6.2.0" />
<PackageReference Include="Squidex.Caching" Version="6.2.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="6.2.0" />
<PackageReference Include="Squidex.Log" Version="6.2.0" />
<PackageReference Include="Squidex.Messaging" Version="6.2.0" />
<PackageReference Include="Squidex.Text" Version="6.2.0" />
<PackageReference Include="Squidex.Assets" Version="6.3.4" />
<PackageReference Include="Squidex.Caching" Version="6.3.4" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="6.3.4" />
<PackageReference Include="Squidex.Log" Version="6.3.4" />
<PackageReference Include="Squidex.Messaging" Version="6.3.4" />
<PackageReference Include="Squidex.Text" Version="6.3.4" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

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

@ -502,6 +502,9 @@
<data name="contents.invalidNumber" xml:space="preserve">
<value>Type json non valide, nombre attendu.</value>
</data>
<data name="contents.invalidRichText" xml:space="preserve">
<value>Invalid rich text.</value>
</data>
<data name="contents.invalidString" xml:space="preserve">
<value>Type json non valide, chaîne attendue.</value>
</data>

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

@ -502,6 +502,9 @@
<data name="contents.invalidNumber" xml:space="preserve">
<value>Errore nel json,, atteso un number.</value>
</data>
<data name="contents.invalidRichText" xml:space="preserve">
<value>Invalid rich text.</value>
</data>
<data name="contents.invalidString" xml:space="preserve">
<value>Errore nel json, atteso una string.</value>
</data>

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

@ -502,6 +502,9 @@
<data name="contents.invalidNumber" xml:space="preserve">
<value>Ongeldig json-type, verwacht aantal.</value>
</data>
<data name="contents.invalidRichText" xml:space="preserve">
<value>Invalid rich text.</value>
</data>
<data name="contents.invalidString" xml:space="preserve">
<value>Ongeldig json-type, verwachte string.</value>
</data>

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

@ -502,6 +502,9 @@
<data name="contents.invalidNumber" xml:space="preserve">
<value>Json type inválido, esperado número.</value>
</data>
<data name="contents.invalidRichText" xml:space="preserve">
<value>Invalid rich text.</value>
</data>
<data name="contents.invalidString" xml:space="preserve">
<value>Json type inválido, esperado texto.</value>
</data>

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

@ -502,6 +502,9 @@
<data name="contents.invalidNumber" xml:space="preserve">
<value>Invalid json type, expected number.</value>
</data>
<data name="contents.invalidRichText" xml:space="preserve">
<value>Invalid rich text.</value>
</data>
<data name="contents.invalidString" xml:space="preserve">
<value>Invalid json type, expected string.</value>
</data>

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

@ -502,6 +502,9 @@
<data name="contents.invalidNumber" xml:space="preserve">
<value>无效的 json 类型,需要的数字。</value>
</data>
<data name="contents.invalidRichText" xml:space="preserve">
<value>Invalid rich text.</value>
</data>
<data name="contents.invalidString" xml:space="preserve">
<value>无效的 json 类型,需要的字符串。</value>
</data>

5
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Converters/FieldPropertiesDtoFactory.cs

@ -74,6 +74,11 @@ internal sealed class FieldPropertiesDtoFactory : IFieldPropertiesVisitor<FieldP
return ReferencesFieldPropertiesDto.FromDomain(fieldProperties);
}
public FieldPropertiesDto Visit(RichTextFieldProperties fieldProperties, None args)
{
return RichTextFieldPropertiesDto.FromDomain(fieldProperties);
}
public FieldPropertiesDto Visit(StringFieldProperties fieldProperties, None args)
{
return StringFieldPropertiesDto.FromDomain(fieldProperties);

73
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/Fields/RichTextFieldPropertiesDto.cs

@ -0,0 +1,73 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Reflection;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Schemas.Models.Fields;
[OpenApiRequest]
public sealed class RichTextFieldPropertiesDto : FieldPropertiesDto
{
/// <summary>
/// The initial id to the folder when the control supports file uploads.
/// </summary>
public string? FolderId { get; set; }
/// <summary>
/// The minimum allowed length for the field value.
/// </summary>
public int? MinLength { get; set; }
/// <summary>
/// The maximum allowed length for the field value.
/// </summary>
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>
/// The class names for the editor.
/// </summary>
public ReadonlyList<string>? ClassNames { get; set; }
/// <summary>
/// The allowed schema ids that can be embedded.
/// </summary>
public ReadonlyList<DomainId>? SchemaIds { get; init; }
public static RichTextFieldPropertiesDto FromDomain(RichTextFieldProperties fieldProperties)
{
return SimpleMapper.Map(fieldProperties, new RichTextFieldPropertiesDto());
}
public override FieldProperties ToProperties()
{
return SimpleMapper.Map(this, new RichTextFieldProperties());
}
}

3
backend/src/Squidex/Config/Domain/ContentsServices.cs

@ -59,6 +59,9 @@ public static class ContentsServices
services.AddSingletonAs<ConvertData>()
.As<IContentEnricherStep>();
services.AddSingletonAs<CalculatePreviewText>()
.As<IContentEnricherStep>();
services.AddSingletonAs<CalculateTokens>()
.As<IContentEnricherStep>();

24
backend/src/Squidex/Squidex.csproj

@ -63,18 +63,18 @@
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.6.0-beta.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="5.2.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets.Azure" Version="6.2.0" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="6.2.0" />
<PackageReference Include="Squidex.Assets.FTP" Version="6.2.0" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="6.2.0" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="6.2.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="6.2.0" />
<PackageReference Include="Squidex.Assets.S3" Version="6.2.0" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="6.2.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="17.0.0" />
<PackageReference Include="Squidex.Hosting" Version="6.2.0" />
<PackageReference Include="Squidex.Messaging.All" Version="6.2.0" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.2.0" />
<PackageReference Include="Squidex.Assets.Azure" Version="6.3.4" />
<PackageReference Include="Squidex.Assets.GoogleCloud" Version="6.3.4" />
<PackageReference Include="Squidex.Assets.FTP" Version="6.3.4" />
<PackageReference Include="Squidex.Assets.ImageMagick" Version="6.3.4" />
<PackageReference Include="Squidex.Assets.ImageSharp" Version="6.3.4" />
<PackageReference Include="Squidex.Assets.Mongo" Version="6.3.4" />
<PackageReference Include="Squidex.Assets.S3" Version="6.3.4" />
<PackageReference Include="Squidex.Assets.TusAdapter" Version="6.3.4" />
<PackageReference Include="Squidex.ClientLibrary" Version="17.2.0" />
<PackageReference Include="Squidex.Hosting" Version="6.3.4" />
<PackageReference Include="Squidex.Messaging.All" Version="6.3.4" />
<PackageReference Include="Squidex.Messaging.Subscriptions" Version="6.3.4" />
<PackageReference Include="Squidex.OpenIddict.MongoDb" Version="4.0.1-dev" />
<PackageReference Include="Squidex.YDotNet" Version="0.2.9" />
<PackageReference Include="Squidex.YDotNet.Native" Version="0.2.9" />

360
backend/src/Squidex/wwwroot/editor/squidex-editor.js

File diff suppressed because one or more lines are too long

28
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ComplexText.html

@ -0,0 +1,28 @@
<h1>Header</h1>
<p>Content with <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <code>code</code> and <span class="__editor_text-left">a class</span>.</p>
<blockquote>
<p>Quote</p>
</blockquote>
<pre class="language-javascript">
<code data-code-block-language="javascript">Code Block in Javascript</code>
</pre>
<p>Just another paragraph</p>
<hr>
<ul>
<li>
<p>Item 1</p>
</li>
<li>
<p>Item 2</p>
</li>
</ul>
<ol>
<li>
<p>Item A</p>
</li>
<li>
<p>Item B</p>
</li>
</ol>
<p><a href="Link Content" rel="noopener noreferrer nofollow">A link</a></p>
<p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Adams_The_Tetons_and_the_Snake_River.jpg/1280px-Adams_The_Tetons_and_the_Snake_River.jpg" title="My Image"></p>

256
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ComplexText.json

@ -0,0 +1,256 @@
{
"type": "doc",
"content": [
{
"type": "heading",
"attrs": {
"level": 1
},
"content": [
{
"type": "text",
"text": "Header"
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Content with "
},
{
"type": "text",
"marks": [
{
"type": "bold"
}
],
"text": "bold"
},
{
"type": "text",
"text": ", "
},
{
"type": "text",
"marks": [
{
"type": "italic"
}
],
"text": "italic"
},
{
"type": "text",
"text": ", "
},
{
"type": "text",
"marks": [
{
"type": "underline"
}
],
"text": "underline"
},
{
"type": "text",
"text": ", "
},
{
"type": "text",
"marks": [
{
"type": "code"
}
],
"text": "code"
},
{
"type": "text",
"text": " and "
},
{
"type": "text",
"marks": [
{
"type": "className",
"attrs": {
"className": "text-left"
}
}
],
"text": "a class"
},
{
"type": "text",
"text": "."
}
]
},
{
"type": "blockquote",
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Quote"
}
]
}
]
},
{
"type": "codeBlock",
"attrs": {
"language": "javascript",
"wrap": false
},
"content": [
{
"type": "text",
"text": "Code Block in Javascript"
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Just another paragraph"
}
]
},
{
"type": "horizontalRule"
},
{
"type": "bulletList",
"content": [
{
"type": "listItem",
"attrs": {
"closed": false,
"nested": false
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Item 1"
}
]
}
]
},
{
"type": "listItem",
"attrs": {
"closed": false,
"nested": false
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Item 2"
}
]
}
]
}
]
},
{
"type": "orderedList",
"attrs": {
"order": 1
},
"content": [
{
"type": "listItem",
"attrs": {
"closed": false,
"nested": false
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Item A"
}
]
}
]
},
{
"type": "listItem",
"attrs": {
"closed": false,
"nested": false
},
"content": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Item B"
}
]
}
]
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "text",
"marks": [
{
"type": "link",
"attrs": {
"href": "Link Content",
"target": null,
"auto": false
}
}
],
"text": "A link"
}
]
},
{
"type": "paragraph",
"content": [
{
"type": "image",
"attrs": {
"alt": "",
"crop": null,
"height": null,
"width": null,
"rotate": null,
"src": "https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Adams_The_Tetons_and_the_Snake_River.jpg/1280px-Adams_The_Tetons_and_the_Snake_River.jpg",
"title": "My Image",
"fileName": null,
"resizable": false
}
}
]
}
]
}

23
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ComplexText.md

@ -0,0 +1,23 @@
# Header
Content with **bold**, *italic*, underline, `code` and a class.
> Quote
```javascript
Code Block in Javascript
```
Just another paragraph
---
* Item 1
* Item 2
1. Item A
2. Item B
[A link](Link Content)
![](https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Adams_The_Tetons_and_the_Snake_River.jpg/1280px-Adams_The_Tetons_and_the_Snake_River.jpg "My Image")

1
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ComplexText.min.html

@ -0,0 +1 @@
<h1>Header</h1><p>Content with <strong>bold</strong>, <em>italic</em>, <u>underline</u>, <code>code</code> and <span class="__editor_text-left">a class</span>.</p><blockquote><p>Quote</p></blockquote><pre class="language-javascript"><code data-code-block-language="javascript">Code Block in Javascript</code></pre><p>Just another paragraph</p><hr><ul><li><p>Item 1</p></li><li><p>Item 2</p></li></ul><ol><li><p>Item A</p></li><li><p>Item B</p></li></ol><p><a href="Link Content" rel="noopener noreferrer nofollow">A link</a></p><p><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/2/21/Adams_The_Tetons_and_the_Snake_River.jpg/1280px-Adams_The_Tetons_and_the_Snake_River.jpg" title="My Image"></p>

1
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ComplexText.txt

@ -0,0 +1 @@
Header Content with bold, italic, underline, code and a class. Quote Code Block in Javascript Just another paragraph Item 1 Item 2 Item A Item B A link

56
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/RichTextTests.cs

@ -0,0 +1,56 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Model.Contents;
public class RichTextTests
{
private readonly RichTextNode node = new RichTextNode();
public RichTextTests()
{
var json = TestUtils.DefaultSerializer.Deserialize<JsonValue>(File.ReadAllText("Model/Contents/ComplexText.json"));
node.TryUse(json);
}
[Fact]
public void Should_format_to_html()
{
var expected = File.ReadAllText("Model/Contents/ComplexText.html");
Assert.Equal(expected.Trim(), node.ToHtml().Trim());
}
[Fact]
public void Should_format_to_minimized_html()
{
var expected = File.ReadAllText("Model/Contents/ComplexText.min.html");
Assert.Equal(expected.Trim(), node.ToHtml(0).Trim());
}
[Fact]
public void Should_format_to_markdown()
{
var expected = File.ReadAllText("Model/Contents/ComplexText.md");
Assert.Equal(expected.Trim(), node.ToMarkdown().Trim());
}
[Fact]
public void Should_format_to_text()
{
var expected = File.ReadAllText("Model/Contents/ComplexText.txt");
Assert.Equal(expected.Trim(), node.ToText().Trim());
}
}

8
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/DefaultValueFactoryTests.cs

@ -254,6 +254,14 @@ public class DefaultValueFactoryTests
Assert.Equal(new JsonArray(), DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code));
}
[Fact]
public void Should_get_default_value_from_rich_text_field()
{
var field = Fields.RichText(1, "1", Partitioning.Invariant);
Assert.Equal(JsonValue.Null, DefaultValueFactory.CreateDefaultValue(field, now, language.Iso2Code));
}
[Fact]
public void Should_get_default_value_from_string_field()
{

247
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/StringFormatterTests.cs

@ -16,9 +16,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_null_value()
{
var field = Fields.Array(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Null;
var fieldConfig = Fields.Array(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, default);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Empty(formatted);
}
@ -26,9 +27,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_null_json_value()
{
var field = Fields.Array(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Null;
var fieldConfig = Fields.Array(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, JsonValue.Null);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Empty(formatted);
}
@ -36,11 +38,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_array_field_without_items()
{
var value = new JsonArray();
var fieldValue = new JsonArray();
var fieldConfig = Fields.Array(1, "field", Partitioning.Invariant);
var field = Fields.Array(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("0 Items", formatted);
}
@ -48,11 +49,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_array_field_with_single_item()
{
var value = JsonValue.Array(JsonValue.Object());
var field = Fields.Array(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Array(JsonValue.Object());
var fieldConfig = Fields.Array(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("1 Item", formatted);
}
@ -60,11 +60,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_array_field_with_multiple_items()
{
var value = JsonValue.Array(JsonValue.Object(), JsonValue.Object());
var field = Fields.Array(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Array(JsonValue.Object(), JsonValue.Object());
var fieldConfig = Fields.Array(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("2 Items", formatted);
}
@ -72,11 +71,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_array_field_with_wrong_type()
{
var value = JsonValue.True;
var fieldValue = JsonValue.True;
var fieldConfig = Fields.Array(1, "field", Partitioning.Invariant);
var field = Fields.Array(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("0 Items", formatted);
}
@ -84,11 +82,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_assets_field_without_items()
{
var value = new JsonArray();
var field = Fields.Assets(1, "field", Partitioning.Invariant);
var fieldValue = new JsonArray();
var fieldConfig = Fields.Assets(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("0 Assets", formatted);
}
@ -96,11 +93,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_assets_field_with_single_item()
{
var value = JsonValue.Array(JsonValue.Object());
var fieldValue = JsonValue.Array(JsonValue.Object());
var fieldConfig = Fields.Assets(1, "field", Partitioning.Invariant);
var field = Fields.Assets(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("1 Asset", formatted);
}
@ -108,11 +104,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_assets_field_with_multiple_items()
{
var value = JsonValue.Array(JsonValue.Object(), JsonValue.Object());
var field = Fields.Assets(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Array(JsonValue.Object(), JsonValue.Object());
var fieldConfig = Fields.Assets(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("2 Assets", formatted);
}
@ -120,11 +115,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_assets_field_with_wrong_type()
{
var value = JsonValue.True;
var field = Fields.Assets(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.True;
var fieldConfig = Fields.Assets(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("0 Assets", formatted);
}
@ -132,11 +126,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_boolean_field_with_true()
{
var value = JsonValue.True;
var fieldValue = JsonValue.True;
var fieldConfig = Fields.Boolean(1, "field", Partitioning.Invariant);
var field = Fields.Boolean(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("Yes", formatted);
}
@ -144,11 +137,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_boolean_field_with_false()
{
var value = JsonValue.False;
var field = Fields.Boolean(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.False;
var fieldConfig = Fields.Boolean(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("No", formatted);
}
@ -156,11 +148,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_boolean_field_with_wrong_type()
{
var value = JsonValue.Zero;
var fieldValue = JsonValue.Zero;
var fieldConfig = Fields.Boolean(1, "field", Partitioning.Invariant);
var field = Fields.Boolean(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("No", formatted);
}
@ -168,11 +159,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_component_field()
{
var value = JsonValue.Object();
var field = Fields.Component(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Object();
var fieldConfig = Fields.Component(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("{ Component }", formatted);
}
@ -180,11 +170,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_components_field_without_items()
{
var value = new JsonArray();
var field = Fields.Components(1, "field", Partitioning.Invariant);
var fieldValue = new JsonArray();
var fieldConfig = Fields.Components(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("0 Components", formatted);
}
@ -192,11 +181,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_components_field_with_single_item()
{
var value = JsonValue.Array(JsonValue.Object());
var fieldValue = JsonValue.Array(JsonValue.Object());
var fieldConfig = Fields.Components(1, "field", Partitioning.Invariant);
var field = Fields.Components(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("1 Component", formatted);
}
@ -204,11 +192,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_components_field_with_multiple_items()
{
var value = JsonValue.Array(JsonValue.Object(), JsonValue.Object());
var field = Fields.Components(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Array(JsonValue.Object(), JsonValue.Object());
var fieldConfig = Fields.Components(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("2 Components", formatted);
}
@ -216,11 +203,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_datetime_field()
{
var value = JsonValue.Create("2019-01-19T12:00:00Z");
var fieldValue = JsonValue.Create("2019-01-19T12:00:00Z");
var fieldConfig = Fields.DateTime(1, "field", Partitioning.Invariant);
var field = Fields.DateTime(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("2019-01-19T12:00:00Z", formatted);
}
@ -228,11 +214,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_geolocation_field_with_correct_data()
{
var value = JsonValue.Object().Add("latitude", 18.9).Add("longitude", 10.9);
var field = Fields.Geolocation(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Object().Add("latitude", 18.9).Add("longitude", 10.9);
var fieldConfig = Fields.Geolocation(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("18.9, 10.9", formatted);
}
@ -240,11 +225,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_geolocation_field_with_missing_property()
{
var value = JsonValue.Object().Add("latitude", 18.9);
var fieldValue = JsonValue.Object().Add("latitude", 18.9);
var fieldConfig = Fields.Geolocation(1, "field", Partitioning.Invariant);
var field = Fields.Geolocation(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Empty(formatted);
}
@ -252,11 +236,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_geolocation_field_with_invalid_type()
{
var value = JsonValue.Zero;
var field = Fields.Geolocation(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Zero;
var fieldConfig = Fields.Geolocation(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Empty(formatted);
}
@ -264,11 +247,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_json_field()
{
var value = JsonValue.Object();
var fieldValue = JsonValue.Object();
var fieldConfig = Fields.Json(1, "field", Partitioning.Invariant);
var field = Fields.Json(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("<Json />", formatted);
}
@ -276,11 +258,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_number_field()
{
var value = JsonValue.Create(123);
var field = Fields.Number(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Create(123);
var fieldConfig = Fields.Number(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("123", formatted);
}
@ -288,11 +269,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_references_field_without_items()
{
var value = new JsonArray();
var field = Fields.References(1, "field", Partitioning.Invariant);
var fieldValue = new JsonArray();
var fieldConfig = Fields.References(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("0 References", formatted);
}
@ -300,11 +280,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_references_field_with_single_item()
{
var value = JsonValue.Array(JsonValue.Object());
var fieldValue = JsonValue.Array(JsonValue.Object());
var fieldConfig = Fields.References(1, "field", Partitioning.Invariant);
var field = Fields.References(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("1 Reference", formatted);
}
@ -312,11 +291,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_references_field_with_multiple_items()
{
var value = JsonValue.Array(JsonValue.Object(), JsonValue.Object());
var field = Fields.References(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Array(JsonValue.Object(), JsonValue.Object());
var fieldConfig = Fields.References(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("2 References", formatted);
}
@ -324,11 +302,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_references_field_with_wrong_type()
{
var value = JsonValue.True;
var fieldValue = JsonValue.True;
var fieldConfig = Fields.References(1, "field", Partitioning.Invariant);
var field = Fields.References(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("0 References", formatted);
}
@ -336,11 +313,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_string_field()
{
var value = JsonValue.Create("hello");
var field = Fields.String(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Create("hello");
var fieldConfig = Fields.String(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("hello", formatted);
}
@ -348,11 +324,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_string_field_with_photo_editor()
{
var value = JsonValue.Create("hello");
var field = Fields.String(1, "field", Partitioning.Invariant, new StringFieldProperties { Editor = StringFieldEditor.StockPhoto });
var fieldValue = JsonValue.Create("hello");
var fieldConfig = Fields.String(1, "field", Partitioning.Invariant, new StringFieldProperties { Editor = StringFieldEditor.StockPhoto });
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("[Photo]", formatted);
}
@ -360,11 +335,10 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_tags_field()
{
var value = JsonValue.Array("hello", "squidex", "and", "team");
var fieldValue = JsonValue.Array("hello", "squidex", "and", "team");
var fieldConfig = Fields.Tags(1, "field", Partitioning.Invariant);
var field = Fields.Tags(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(field, value);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("hello, squidex, and, team", formatted);
}
@ -372,12 +346,47 @@ public sealed class StringFormatterTests
[Fact]
public void Should_format_tags_field_with_invalid_type()
{
var value = JsonValue.Zero;
var fieldValue = JsonValue.Zero;
var fieldConfig = Fields.Tags(1, "field", Partitioning.Invariant);
var field = Fields.Tags(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
var formatted = StringFormatter.Format(field, value);
Assert.Empty(formatted);
}
[Fact]
public void Should_format_rich_text_if_null()
{
var fieldValue = JsonValue.Null;
var fieldConfig = Fields.RichText(1, "field", Partitioning.Invariant);
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Empty(formatted);
}
[Fact]
public void Should_format_rich_text()
{
var fieldConfig = Fields.RichText(1, "field", Partitioning.Invariant);
var fieldValue = JsonValue.Object()
.Add("type", "doc")
.Add("content", JsonValue.Array(
JsonValue.Object()
.Add("type", "paragraph")
.Add("content", JsonValue.Array(
JsonValue.Object()
.Add("type", "text")
.Add("text", "Paragraph1"))),
JsonValue.Object()
.Add("type", "paragraph")
.Add("content", JsonValue.Array(
JsonValue.Object()
.Add("type", "text")
.Add("text", "Paragraph2")))));
var formatted = StringFormatter.Format(fieldConfig, fieldValue);
Assert.Equal("Paragraph1 Paragraph2", formatted);
}
}

71
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ExtractReferenceIds/StringReferenceExtractorTests.cs

@ -5,8 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Operations.ExtractReferenceIds;
@ -34,28 +36,73 @@ public class StringReferenceExtractorTests
}
[Theory]
[InlineData("before content:a_b-123|after")]
[InlineData("before contents:a_b-123|after")]
[InlineData("before https://cloud.squidex.io/api/content/my-app/my-schema/a_b-123|after")]
[InlineData("before https://contents.squidex.io/my-app/my-schema/a_b-123|after")]
[InlineData("before content:my_content-42|after")]
[InlineData("before contents:my_content-42|after")]
[InlineData("before https://cloud.squidex.io/api/content/my-app/my-schema/my_content-42|after")]
[InlineData("before https://contents.squidex.io/my-app/my-schema/my_content-42|after")]
public void Should_extract_content_id(string input)
{
var ids = sut.GetEmbeddedContentIds(input);
Assert.Contains(DomainId.Create("a_b-123"), ids.ToList());
Assert.Contains(DomainId.Create("my_content-42"), ids.ToList());
}
[Theory]
[InlineData("before asset:a_b-123|after")]
[InlineData("before assets:a_b-123|after")]
[InlineData("before https://cloud.squidex.io/api/assets/a_b-123|after")]
[InlineData("before https://cloud.squidex.io/api/assets/my-app/a_b-123|after")]
[InlineData("before https://assets.squidex.io/a_b-123|after")]
[InlineData("before https://assets.squidex.io/my-app/a_b-123|after")]
[InlineData("content:my_content-42")]
[InlineData("contents:my_content-42")]
[InlineData("https://cloud.squidex.io/api/content/my-app/my-schema/my_content-42")]
[InlineData("https://contents.squidex.io/my-app/my-schema/my_content-42")]
public void Should_extract_content_id_from_rich_text(string href)
{
var source = JsonValue.Object()
.Add("type", "paragraph")
.Add("content", JsonValue.Array(
JsonValue.Object()
.Add("type", "text")
.Add("marks", JsonValue.Array(
JsonValue.Object()
.Add("type", "link")
.Add("attrs", JsonValue.Object()
.Add("href", href))))));
var ids = sut.GetEmbeddedContentIds(RichTextNode.Create(source));
Assert.Contains(DomainId.Create("my_content-42"), ids.ToList());
}
[Theory]
[InlineData("before asset:my_asset-42|after")]
[InlineData("before assets:my_asset-42|after")]
[InlineData("before https://cloud.squidex.io/api/assets/my_asset-42|after")]
[InlineData("before https://cloud.squidex.io/api/assets/my-app/my_asset-42|after")]
[InlineData("before https://assets.squidex.io/my_asset-42|after")]
[InlineData("before https://assets.squidex.io/my-app/my_asset-42|after")]
public void Should_extract_asset_id(string input)
{
var ids = sut.GetEmbeddedAssetIds(input);
Assert.Contains(DomainId.Create("a_b-123"), ids.ToList());
Assert.Contains(DomainId.Create("my_asset-42"), ids.ToList());
}
[Theory]
[InlineData("asset:my_asset-42")]
[InlineData("assets:my_asset-42")]
[InlineData("https://cloud.squidex.io/api/assets/my_asset-42")]
[InlineData("https://cloud.squidex.io/api/assets/my-app/my_asset-42")]
[InlineData("https://assets.squidex.io/my_asset-42")]
[InlineData("https://assets.squidex.io/my-app/my_asset-42")]
public void Should_extract_asset_id_from_rich_text(string src)
{
var source = JsonValue.Object()
.Add("type", "paragraph")
.Add("content", JsonValue.Array(
JsonValue.Object()
.Add("type", "image")
.Add("attrs", JsonValue.Object()
.Add("src", src))));
var ids = sut.GetEmbeddedAssetIds(RichTextNode.Create(source));
Assert.Contains(DomainId.Create("my_asset-42"), ids.ToList());
}
}

167
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/RichTextFieldTests.cs

@ -0,0 +1,167 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Core.Operations.ValidateContent;
public class RichTextFieldTests : IClassFixture<TranslationsFixture>
{
private readonly List<string> errors = [];
[Fact]
public void Should_instantiate_field()
{
var sut = Field(new RichTextFieldProperties());
Assert.Equal("myRichText", sut.Name);
}
[Fact]
public async Task Should_not_add_error_if_value_is_null()
{
var sut = Field(new RichTextFieldProperties());
await sut.ValidateAsync(CreateValue(null), errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_rich_text_is_valid()
{
var sut = Field(new RichTextFieldProperties());
await sut.ValidateAsync(CreateValue("text"), errors);
Assert.Empty(errors);
}
[Fact]
public async Task Should_not_add_error_if_rich_text_is_invalid()
{
var sut = Field(new RichTextFieldProperties());
await sut.ValidateAsync(CreateValue(string.Empty, "unknown"), errors);
errors.Should().BeEquivalentTo(
new[] { "Invalid rich text." });
}
[Fact]
public async Task Should_add_error_if_string_is_required_but_null()
{
var sut = Field(new RichTextFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(null), errors);
errors.Should().BeEquivalentTo(
new[] { "Field is required." });
}
[Fact]
public async Task Should_add_error_if_string_is_required_but_empty()
{
var sut = Field(new RichTextFieldProperties { IsRequired = true });
await sut.ValidateAsync(CreateValue(string.Empty), errors);
errors.Should().BeEquivalentTo(
new[] { "Field is required." });
}
[Fact]
public async Task Should_add_error_if_string_is_shorter_than_min_length()
{
var sut = Field(new RichTextFieldProperties { MinLength = 10 });
await sut.ValidateAsync(CreateValue("123"), errors);
errors.Should().BeEquivalentTo(
new[] { "Must have at least 10 character(s)." });
}
[Fact]
public async Task Should_add_error_if_string_is_longer_than_max_length()
{
var sut = Field(new RichTextFieldProperties { MaxLength = 5 });
await sut.ValidateAsync(CreateValue("12345678"), errors);
errors.Should().BeEquivalentTo(
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 RichTextFieldProperties { 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 RichTextFieldProperties { MaxCharacters = 5 });
await sut.ValidateAsync(CreateValue("12345678"), errors);
errors.Should().BeEquivalentTo(
new[] { "Must not have more than 5 text character(s)." });
}
[Fact]
public async Task Should_add_error_if_string_is_shorter_than_min_words()
{
var sut = Field(new RichTextFieldProperties { MinWords = 10 });
await sut.ValidateAsync(CreateValue("word1 word2 word3"), errors);
errors.Should().BeEquivalentTo(
new[] { "Must have at least 10 word(s)." });
}
[Fact]
public async Task Should_add_error_if_string_is_longer_than_max_words()
{
var sut = Field(new RichTextFieldProperties { MaxWords = 5 });
await sut.ValidateAsync(CreateValue("word1 word2 word3 word4 word5 word6"), errors);
errors.Should().BeEquivalentTo(
new[] { "Must not have more than 5 word(s)." });
}
private static JsonValue CreateValue(string? v, string? type = null)
{
if (v == null)
{
return default;
}
return JsonValue.Object()
.Add("type", "doc")
.Add("content", JsonValue.Array(
JsonValue.Object()
.Add("type", "paragraph")
.Add("content", JsonValue.Array(
JsonValue.Object()
.Add("type", type ?? "text")
.Add("text", v)))));
}
private static RootField<RichTextFieldProperties> Field(RichTextFieldProperties properties)
{
return Fields.RichText(1, "myRichText", Partitioning.Invariant, properties);
}
}

24
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ValidateContent/StringFieldTests.cs

@ -94,12 +94,34 @@ public class StringFieldTests : IClassFixture<TranslationsFixture>
{
var sut = Field(new StringFieldProperties { MaxCharacters = 5 });
await sut.ValidateAsync(CreateValue("12345678"), errors);
await sut.ValidateAsync(CreateValue("123456"), errors);
errors.Should().BeEquivalentTo(
new[] { "Must not have more than 5 text character(s)." });
}
[Fact]
public async Task Should_add_error_if_string_is_shorter_than_min_words()
{
var sut = Field(new StringFieldProperties { MinWords = 10 });
await sut.ValidateAsync(CreateValue("word1 word2 word3"), errors);
errors.Should().BeEquivalentTo(
new[] { "Must have at least 10 word(s)." });
}
[Fact]
public async Task Should_add_error_if_string_is_longer_than_max_words()
{
var sut = Field(new StringFieldProperties { MaxWords = 5 });
await sut.ValidateAsync(CreateValue("word1 word2 word3 word4 word5 word6"), errors);
errors.Should().BeEquivalentTo(
new[] { "Must not have more than 5 word(s)." });
}
[Fact]
public async Task Should_add_error_if_string_not_allowed()
{

16
backend/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj

@ -42,6 +42,21 @@
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<None Update="Model\Contents\ComplexText.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Model\Contents\ComplexText.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Model\Contents\ComplexText.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Model\Contents\ComplexText.md">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Model\Contents\ComplexText.min.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Model\Apps\App_Old.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
@ -76,5 +91,4 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties model_4apps_4app_1json__JsonSchema="" /></VisualStudio></ProjectExtensions>
</Project>

6
backend/tests/Squidex.Domain.Apps.Core.Tests/TestHelpers/TestSchema.cs

@ -95,7 +95,9 @@ public static class TestSchema
.AddTags(211, "nested-tags",
new TagsFieldProperties())
.AddUI(212, "nested-ui",
new UIFieldProperties()))
new UIFieldProperties())
.AddRichText(213, "nested-richtext",
new RichTextFieldProperties()))
.AddAssets(102, "root-assets", Partitioning.Invariant,
new AssetsFieldProperties())
.AddBoolean(103, "root-boolean", Partitioning.Invariant,
@ -124,6 +126,8 @@ public static class TestSchema
new ComponentFieldProperties { SchemaIds = componentIds })
.AddComponents(115, "root-components", Partitioning.Language,
new ComponentsFieldProperties { SchemaIds = componentIds })
.AddRichText(116, "root-richtext", Partitioning.Invariant,
new RichTextFieldProperties())
.Update(new SchemaProperties { Hints = "The User" })
.HideField(104)
.HideField(211, 101)

12
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs

@ -23,7 +23,7 @@ public class GraphQLMutationTests : GraphQLTestBase
public GraphQLMutationTests()
{
content = TestContent.Create(contentId, TestSchemas.Reference1.Id, TestSchemas.Reference2.Id, null);
content = TestContent.Create(contentId);
A.CallTo(() => commandBus.PublishAsync(A<ICommand>.Ignored, A<CancellationToken>._))
.Returns(commandContext);
@ -102,7 +102,7 @@ public class GraphQLMutationTests : GraphQLTestBase
},
Variables = new
{
data = TestContent.Input(content, TestSchemas.Reference1.Id, TestSchemas.Reference2.Id),
data = TestContent.Input(content),
},
Permission = PermissionIds.AppContentsCreate
});
@ -147,7 +147,7 @@ public class GraphQLMutationTests : GraphQLTestBase
},
Variables = new
{
data = TestContent.Input(content, TestSchemas.Reference1.Id, TestSchemas.Reference2.Id)
data = TestContent.Input(content)
},
Permission = PermissionIds.AppContentsCreate
});
@ -251,7 +251,7 @@ public class GraphQLMutationTests : GraphQLTestBase
},
Variables = new
{
data = TestContent.Input(content, TestSchemas.Reference1.Id, TestSchemas.Reference2.Id)
data = TestContent.Input(content)
},
Permission = PermissionIds.AppContentsUpdateOwn
});
@ -350,7 +350,7 @@ public class GraphQLMutationTests : GraphQLTestBase
},
Variables = new
{
data = TestContent.Input(content, TestSchemas.Reference1.Id, TestSchemas.Reference2.Id)
data = TestContent.Input(content)
},
Permission = PermissionIds.AppContentsUpsert
});
@ -454,7 +454,7 @@ public class GraphQLMutationTests : GraphQLTestBase
},
Variables = new
{
data = TestContent.Input(content, TestSchemas.Reference1.Id, TestSchemas.Reference2.Id)
data = TestContent.Input(content)
},
Permission = PermissionIds.AppContentsUpdateOwn
});

340
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -6,10 +6,12 @@
// ==========================================================================
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL;
@ -184,7 +186,7 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_return_multiple_assets_if_querying_assets()
public async Task Should_query_assets()
{
var asset = TestAsset.Create(DomainId.NewGuid());
@ -222,7 +224,7 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_return_multiple_assets_with_total_if_querying_assets_with_total()
public async Task Should_query_assets_with_total()
{
var asset = TestAsset.Create(DomainId.NewGuid());
@ -303,7 +305,7 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_return_single_asset_if_finding_asset()
public async Task Should_find_single_asset()
{
var assetId = DomainId.NewGuid();
var asset = TestAsset.Create(assetId);
@ -340,7 +342,7 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_return_multiple_flat_contents_if_querying_contents()
public async Task Should_query_contents_with_flat_data()
{
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId);
@ -379,7 +381,7 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_return_multiple_contents_if_querying_contents()
public async Task Should_query_contents()
{
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId);
@ -418,7 +420,7 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_return_multiple_contents_with_total_if_querying_contents_with_total()
public async Task Should_query_contents_with_total()
{
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId);
@ -701,13 +703,19 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_also_fetch_embedded_contents_if_field_is_included_in_query()
public async Task Should_resolve_embedded_contents()
{
var contentRefId = DomainId.NewGuid();
var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1");
var data =
new ContentData()
.AddField("my-embeds",
new ContentFieldData()
.AddInvariant(JsonValue.Create($"contents:{contentRefId}")));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, contentRefId);
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentRefId),
@ -765,7 +773,7 @@ public class GraphQLQueriesTests : GraphQLTestBase
{
iv = new
{
text = $"assets:{DomainId.Empty}, contents:{contentRefId}",
text = $"contents:{contentRefId}",
contents = new[]
{
new
@ -791,13 +799,135 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_also_fetch_referenced_contents_if_field_is_included_in_query()
public async Task Should_resolve_richtext_contents()
{
var contentRefId = DomainId.NewGuid();
var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1");
var data =
new ContentData()
.AddField("my-richtext",
new ContentFieldData()
.AddInvariant(JsonValue.Object()
.Add("type", "text")
.Add("marks", JsonValue.Array(
JsonValue.Object()
.Add("type", "link")
.Add("attrs", JsonValue.Object()
.Add("href", $"contents:{contentRefId}"))))));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, contentRefId);
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentRefId),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom(0, contentRef));
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom(1, content));
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
query {
findMySchemaContent(id: '{contentId}') {
id
data {
myRichtext {
iv {
value
contents {
... on Content {
id
}
... on MyReference1 {
data {
reference1Field {
iv
}
}
}
}
}
}
}
}
}",
Args = new
{
contentId
}
});
var expected = new
{
data = new
{
findMySchemaContent = new
{
id = content.Id,
data = new
{
myRichtext = new
{
iv = new
{
value = new
{
type = "text",
marks = new[]
{
new
{
type = "link",
attrs = new
{
href = $"contents:{contentRefId}"
}
}
}
},
contents = new[]
{
new
{
id = contentRefId,
data = new
{
reference1Field = new
{
iv = "reference1"
}
}
}
}
}
}
}
}
}
};
AssertResult(expected, actual);
}
[Fact]
public async Task Should_resolve_referenced_contents()
{
var contentRefId = DomainId.NewGuid();
var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1");
var data =
new ContentData()
.AddField("my-references",
new ContentFieldData()
.AddInvariant(JsonValue.Array(contentRefId)));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentRefId),
@ -870,13 +1000,19 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_also_fetch_referenced_contents_from_flat_data_if_field_is_included_in_query()
public async Task Should_resolve_referenced_contents_from_flat_data()
{
var contentRefId = DomainId.NewGuid();
var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1");
var data =
new ContentData()
.AddField("my-references",
new ContentFieldData()
.AddInvariant(JsonValue.Array(contentRefId)));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, contentRefId);
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentRefId),
@ -932,13 +1068,19 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_cache_referenced_contents_from_flat_data_if_field_is_included_in_query()
public async Task Should_cache_referenced_contents_from_flat_data()
{
var contentRefId = DomainId.NewGuid();
var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1");
var data =
new ContentData()
.AddField("my-references",
new ContentFieldData()
.AddInvariant(JsonValue.Array(contentRefId)));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, contentRefId);
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentRefId),
@ -1003,13 +1145,22 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_also_fetch_referencing_contents_if_field_is_included_in_query()
public async Task Should_resolve_referencing_contents()
{
var contentRefId = DomainId.NewGuid();
var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1");
var data =
new ContentData()
.AddField("my-references",
new ContentFieldData()
.AddInvariant(JsonValue.Array(contentRefId)))
.AddField("my-localized-string",
new ContentFieldData()
.AddLocalized("de-DE", "de-DE"));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, contentRefId);
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentRefId),
@ -1072,13 +1223,22 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_also_fetch_referencing_contents_with_total_if_field_is_included_in_query()
public async Task Should_resolve_referencing_contents_with_total()
{
var contentRefId = DomainId.NewGuid();
var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1");
var data =
new ContentData()
.AddField("my-references",
new ContentFieldData()
.AddInvariant(JsonValue.Array(contentRefId)))
.AddField("my-localized-string",
new ContentFieldData()
.AddLocalized("de-DE", "de-DE"));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, contentRefId);
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentRefId),
@ -1148,13 +1308,19 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_also_fetch_references_contents_if_field_is_included_in_query()
public async Task Should_resolve_references_contents()
{
var contentRefId = DomainId.NewGuid();
var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1");
var data =
new ContentData()
.AddField("my-references",
new ContentFieldData()
.AddInvariant(JsonValue.Array(contentRefId)));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, contentRefId);
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId),
@ -1205,13 +1371,19 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_also_fetch_references_contents_with_total_if_field_is_included_in_query()
public async Task Should_resolve_references_contents_with_total()
{
var contentRefId = DomainId.NewGuid();
var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1");
var data =
new ContentData()
.AddField("my-references",
new ContentFieldData()
.AddInvariant(JsonValue.Array(contentRefId)));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, contentRefId);
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId),
@ -1269,13 +1441,19 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_also_fetch_union_contents_if_field_is_included_in_query()
public async Task Should_resolve_union_contents()
{
var contentRefId = DomainId.NewGuid();
var contentRef = TestContent.CreateSimple(TestSchemas.Reference1.NamedId(), contentRefId, "reference1-field", "reference1");
var data =
new ContentData()
.AddField("my-union",
new ContentFieldData()
.AddInvariant(JsonValue.Array(contentRefId)));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, contentRefId);
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentRefId),
@ -1354,13 +1532,19 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_also_fetch_embedded_assets_if_field_is_included_in_query()
public async Task Should_resolve_embedded_assets()
{
var assetRefId = DomainId.NewGuid();
var assetRef = TestAsset.Create(assetRefId);
var data =
new ContentData()
.AddField("my-embeds",
new ContentFieldData()
.AddInvariant(JsonValue.Create($"assets:{assetRefId}")));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, assetId: assetRefId);
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId),
@ -1409,7 +1593,7 @@ public class GraphQLQueriesTests : GraphQLTestBase
{
iv = new
{
text = $"assets:{assetRefId}, contents:{DomainId.Empty}",
text = $"assets:{assetRefId}",
assets = new[]
{
new
@ -1428,13 +1612,109 @@ public class GraphQLQueriesTests : GraphQLTestBase
}
[Fact]
public async Task Should_also_fetch_referenced_assets_if_field_is_included_in_query()
public async Task Should_resolve_richtext_assets()
{
var assetRefId = DomainId.NewGuid();
var assetRef = TestAsset.Create(assetRefId);
var data =
new ContentData()
.AddField("my-richtext",
new ContentFieldData()
.AddInvariant(JsonValue.Object()
.Add("type", "image")
.Add("attrs", JsonValue.Object()
.Add("src", $"assets:{assetRefId}"))));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom(1, content));
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null,
A<Q>.That.HasIdsWithoutTotal(assetRefId),
A<CancellationToken>._))
.Returns(ResultList.CreateFrom(0, assetRef));
var actual = await ExecuteAsync(new TestQuery
{
Query = @"
query {
findMySchemaContent(id: '{contentId}') {
id
data {
myRichtext {
iv {
value
assets {
id
}
}
}
}
}
}",
Args = new
{
contentId
}
});
var expected = new
{
data = new
{
findMySchemaContent = new
{
id = content.Id,
data = new
{
myRichtext = new
{
iv = new
{
value = new
{
type = "image",
attrs = new
{
src = $"assets:{assetRefId}"
}
},
assets = new[]
{
new
{
id = assetRefId
}
}
}
}
}
}
}
};
AssertResult(expected, actual);
}
[Fact]
public async Task Should_resolve_referenced_assets()
{
var assetRefId = DomainId.NewGuid();
var assetRef = TestAsset.Create(assetRefId);
var data =
new ContentData()
.AddField("my-assets",
new ContentFieldData()
.AddInvariant(JsonValue.Array(assetRefId)));
var contentId = DomainId.NewGuid();
var content = TestContent.Create(contentId, assetId: assetRefId);
var content = TestContent.Create(contentId, data);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(),
A<Q>.That.HasIdsWithoutTotal(contentId),

3
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestAsset.cs

@ -44,6 +44,7 @@ public static class TestAsset
isProtected
pixelWidth
pixelHeight
parentId
tags
type
metadataText
@ -71,6 +72,7 @@ public static class TestAsset
FileSize = 1024,
FileHash = "ABC123",
FileVersion = 123,
ParentId = id,
MimeType = "image/png",
Type = AssetType.Image,
MetadataText = "metadata-text",
@ -123,6 +125,7 @@ public static class TestAsset
isProtected = asset.IsProtected,
pixelWidth = asset.Metadata.GetPixelWidth(),
pixelHeight = asset.Metadata.GetPixelHeight(),
parentId = asset.Id,
tags = asset.TagNames,
type = "IMAGE",
metadataText = asset.MetadataText,

208
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs

@ -46,18 +46,18 @@ public static class TestContent
myJson2 {
iv {
__typename
rootString,
rootInt,
rootFloat,
rootBoolean,
rootArray,
rootString
rootInt
rootFloat
rootBoolean
rootArray
rootObject {
__typename
nestedString,
nestedInt,
nestedFloat,
nestedBoolean,
nestedArray,
nestedString
nestedInt
nestedFloat
nestedBoolean
nestedArray
}
}
}
@ -127,6 +127,15 @@ public static class TestContent
nestedBoolean
}
}
myRichtext {
iv {
value
htmlMinimized: html(indentation: 0)
htmlNormal: html
markdown
text
}
}
}";
public const string AllFlatFields = @"
@ -157,18 +166,18 @@ public static class TestContent
myJsonValue: myJson(path: 'value')
myJson2 {
__typename
rootString,
rootInt,
rootFloat,
rootBoolean,
rootArray,
rootString
rootInt
rootFloat
rootBoolean
rootArray
rootObject {
__typename
nestedString,
nestedInt,
nestedFloat,
nestedBoolean,
nestedArray,
nestedString
nestedInt
nestedFloat
nestedBoolean
nestedArray
}
}
myString
@ -209,9 +218,16 @@ public static class TestContent
nestedNumber
nestedBoolean
}
myRichtext {
value
htmlMinimized: html(indentation: 0)
htmlNormal: html
markdown
text
}
}";
public static EnrichedContent Create(DomainId id, DomainId refId = default, DomainId assetId = default, ContentData? data = null)
public static EnrichedContent Create(DomainId id, ContentData? data = null)
{
var now = SystemClock.Instance.GetCurrentInstant();
@ -226,30 +242,30 @@ public static class TestContent
.AddField("my-string-enum",
new ContentFieldData()
.AddInvariant("EnumA"))
.AddField("my-assets",
new ContentFieldData()
.AddInvariant(JsonValue.Array(assetId.ToString())))
.AddField("my-number",
new ContentFieldData()
.AddInvariant(1.0))
.AddField("my-assets",
new ContentFieldData()
.AddInvariant(JsonValue.Array(TestSchemas.Component.Id)))
.AddField("my-boolean",
new ContentFieldData()
.AddInvariant(true))
.AddField("my-datetime",
new ContentFieldData()
.AddInvariant(now))
.AddField("my-references",
new ContentFieldData()
.AddInvariant(JsonValue.Array(TestSchemas.Reference1.Id)))
.AddField("my-union",
new ContentFieldData()
.AddInvariant(JsonValue.Array(TestSchemas.Reference2.Id)))
.AddField("my-tags",
new ContentFieldData()
.AddInvariant(JsonValue.Array("tag1", "tag2")))
.AddField("my-tags-enum",
new ContentFieldData()
.AddInvariant(JsonValue.Array("EnumA", "EnumB")))
.AddField("my-references",
new ContentFieldData()
.AddInvariant(JsonValue.Array(refId.ToString())))
.AddField("my-union",
new ContentFieldData()
.AddInvariant(JsonValue.Array(refId.ToString())))
.AddField("my-geolocation",
new ContentFieldData()
.AddInvariant(
@ -316,14 +332,15 @@ public static class TestContent
.Add("nested-boolean", true),
JsonValue.Object()
.Add("nested-number", 3.14)
.Add("nested-boolean", false))));
if (assetId != default || refId != default)
{
data.AddField("my-embeds",
new ContentFieldData()
.AddInvariant(JsonValue.Create($"assets:{assetId}, contents:{refId}")));
}
.Add("nested-boolean", false))))
.AddField("my-richtext",
new ContentFieldData()
.AddInvariant(JsonValue.Object()
.Add("type", "heading")
.Add("content", JsonValue.Array(
JsonValue.Object()
.Add("type", "text")
.Add("text", "Rich Text")))));
var content = new EnrichedContent
{
@ -441,7 +458,7 @@ public static class TestContent
};
}
public static object Input(EnrichedContent content, DomainId refId = default, DomainId assetId = default)
public static object Input(EnrichedContent content)
{
var actual = new Dictionary<string, object>
{
@ -487,6 +504,13 @@ public static class TestContent
{
iv = 1.0
},
["myAssets"] = new
{
iv = new[]
{
TestSchemas.Component.Id
}
},
["myBoolean"] = new
{
iv = true
@ -495,6 +519,20 @@ public static class TestContent
{
iv = content.LastModified.ToString()
},
["myReferences"] = new
{
iv = new[]
{
TestSchemas.Reference1.Id
}
},
["myUnion"] = new
{
iv = new[]
{
TestSchemas.Reference2.Id
}
},
["myGeolocation"] = new
{
iv = new
@ -567,46 +605,23 @@ public static class TestContent
nestedBoolean = false
}
}
}
};
if (refId != default)
{
actual["myReferences"] = new
{
iv = new[]
{
refId
}
};
actual["myUnion"] = new
{
iv = new[]
{
refId
}
};
}
if (assetId != default)
{
actual["myAssets"] = new
},
["myRichtext"] = new
{
iv = new[]
iv = new
{
assetId
type = "heading",
content = new[]
{
new
{
type = "text",
text = "Rich Text",
}
}
}
};
}
if (assetId != default || refId != default)
{
actual["myEmbeds"] = new
{
iv = $"assets:{assetId}, contents:{refId}"
};
}
}
};
return actual;
}
@ -776,6 +791,28 @@ public static class TestContent
nestedBoolean = false
}
}
},
["myRichtext"] = new
{
iv = new
{
value = new
{
type = "heading",
content = new[]
{
new
{
type = "text",
text = "Rich Text",
}
}
},
htmlMinimized = "<h1>Rich Text</h1>",
htmlNormal = "<h1>Rich Text</h1>",
markdown = "# Rich Text",
text = "Rich Text"
}
}
};
@ -899,6 +936,25 @@ public static class TestContent
nestedNumber = 3.14,
nestedBoolean = false
}
},
["myRichtext"] = new
{
value = new
{
type = "heading",
content = new[]
{
new
{
type = "text",
text = "Rich Text",
}
}
},
htmlMinimized = "<h1>Rich Text</h1>",
htmlNormal = "<h1>Rich Text</h1>",
markdown = "# Rich Text",
text = "Rich Text"
}
};

6
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestSchemas.cs

@ -132,13 +132,15 @@ public static class TestSchemas
new TagsFieldProperties())
.AddTags(16, "my-tags-enum", Partitioning.Invariant,
new TagsFieldProperties { AllowedValues = enums, CreateEnum = true })
.AddString(17, "my-embeds", Partitioning.Invariant,
new StringFieldProperties { IsEmbeddable = true, SchemaIds = ReadonlyList.Create(Reference1.Id, Reference2.Id) })
.AddRichText(18, "my-richtext", Partitioning.Invariant,
new RichTextFieldProperties { SchemaIds = ReadonlyList.Create(Reference1.Id, Reference2.Id) })
.AddArray(100, "my-array", Partitioning.Invariant, f => f
.AddBoolean(121, "nested-boolean",
new BooleanFieldProperties())
.AddNumber(122, "nested-number",
new NumberFieldProperties()))
.AddString(17, "my-embeds", Partitioning.Invariant,
new StringFieldProperties { IsEmbeddable = true, SchemaIds = ReadonlyList.Create(Reference1.Id, Reference2.Id) })
.SetScripts(new SchemaScripts { Query = "<query-script>" });
}
}

86
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/CalculatePreviewTextTests.cs

@ -0,0 +1,86 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Json.Objects;
namespace Squidex.Domain.Apps.Entities.Contents.Queries;
public class CalculatePreviewTextTests : GivenContext
{
private readonly CalculatePreviewText sut;
public CalculatePreviewTextTests()
{
sut = new CalculatePreviewText();
Schema = Schema.AddRichText(1, "richText", Partitioning.Language);
}
[Fact]
public async Task Should_compute_texts()
{
var content = CreateContentWithData();
await sut.EnrichAsync(ApiContext, new[] { content }, SchemaProvider(), CancellationToken);
Assert.Equal("Text1Text2", content.ReferenceData!["richText"]!["en"]);
Assert.Equal("Text3Text4", content.ReferenceData!["richText"]!["de"]);
}
[Fact]
public async Task Should_not_compute_texts_if_field_not_found()
{
var content = CreateContent();
await sut.EnrichAsync(ApiContext, new[] { content }, SchemaProvider(), CancellationToken);
Assert.Null(content.ReferenceData);
}
private EnrichedContent CreateContentWithData()
{
var content = CreateContent() with
{
Data =
new ContentData()
.AddField("richText",
new ContentFieldData()
.AddLocalized("en",
JsonValue.Object()
.Add("type", "paragraph")
.Add("content", JsonValue.Array(
JsonValue.Object()
.Add("type", "text")
.Add("text", "Text1"),
JsonValue.Object()
.Add("type", "text")
.Add("text", "Text2"))))
.AddLocalized("de",
JsonValue.Object()
.Add("type", "paragraph")
.Add("content", JsonValue.Array(
JsonValue.Object()
.Add("type", "text")
.Add("text", "Text3"),
JsonValue.Object()
.Add("type", "text")
.Add("text", "Text4")))))
};
return content;
}
private ProvideSchema SchemaProvider()
{
return x => Task.FromResult((Schema, ResolvedComponents.Empty));
}
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/JsonFieldPropertiesTests.cs

@ -13,7 +13,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperti
public class JsonFieldPropertiesTests : IClassFixture<TranslationsFixture>
{
[Fact]
public void Should_add_error_if_editor_is_not_valid()
public void Should_not_add_error()
{
var sut = new JsonFieldProperties();

97
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/Guards/FieldProperties/RichTextFieldPropertiesTests.cs

@ -0,0 +1,97 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject.Guards.FieldProperties;
public class RichTextFieldPropertiesTests : IClassFixture<TranslationsFixture>
{
[Fact]
public void Should_not_add_error()
{
var sut = new RichTextFieldProperties();
var errors = FieldPropertiesValidator.Validate(sut).ToList();
Assert.Empty(errors);
}
[Fact]
public void Should_add_error_if_min_length_greater_than_max()
{
var sut = new RichTextFieldProperties { MinLength = 10, MaxLength = 5 };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Max length must be greater or equal to min length.", "MinLength", "MaxLength")
});
}
[Fact]
public void Should_not_add_error_if_min_length_equal_to_max_length()
{
var sut = new RichTextFieldProperties { MinLength = 2, MaxLength = 2 };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
Assert.Empty(errors);
}
[Fact]
public void Should_add_error_if_min_characters_greater_than_max()
{
var sut = new RichTextFieldProperties { MinCharacters = 10, MaxCharacters = 5 };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Max characters must be greater or equal to min characters.", "MinCharacters", "MaxCharacters")
});
}
[Fact]
public void Should_not_add_error_if_min_characters_equal_to_max_characters()
{
var sut = new RichTextFieldProperties { 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 RichTextFieldProperties { MinWords = 10, MaxWords = 5 };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
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 RichTextFieldProperties { MinWords = 2, MaxWords = 2 };
var errors = FieldPropertiesValidator.Validate(sut).ToList();
Assert.Empty(errors);
}
}

10
frontend/src/app/declarations.d.ts

@ -13,7 +13,7 @@ declare class SquidexEditorWrapper {
update(newProps: Partial<EditorProps>): void;
setValue(value: string): void;
setValue(value: EditorValue): void;
setIsDisabled(isDisabled: boolean): void;
@ -22,6 +22,8 @@ declare class SquidexEditorWrapper {
destroy(): void;
}
type EditorValue = string | Node | undefined | null;
type Asset = {
// The alternative text of the image.
alt?: string;
@ -52,13 +54,13 @@ type OnAnnotationUpdate = (annotation: ReadonlyArray<Annotation>) => void;
type OnAnnotationFocus = (annotation: ReadonlyArray<string>) => void;
type OnAssetEdit = (id: string) => void;
type OnAssetUpload = (images: UploadRequest[]) => DelayedPromiseCreator<Asset>[];
type OnChange = (value: string | undefined) => void;
type OnChange = (value: EditorValue) => void;
type OnContentEdit = (schemaName: string, contentId: string) => void;
type OnSelectAIText = () => Promise<string | undefined | null>;
type OnSelectAssets = () => Promise<Asset[]>;
type OnSelectContents = () => Promise<Content[]>;
type SquidexEditorMode = 'Html' | 'Markdown';
type SquidexEditorMode = 'Html' | 'Markdown' | 'State';
interface UploadRequest {
// The file to upload.
@ -73,7 +75,7 @@ interface EditorProps {
mode: SquidexEditorMode;
// The incoming value.
value?: string;
value?: EditorValue;
// The base url.
baseUrl: string;

17
frontend/src/app/features/content/shared/forms/field-editor.component.html

@ -243,6 +243,23 @@
</ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchCase="'RichText'">
<sqx-rich-editor #editor
mode="State"
[annotations]="annotations | async"
(annotationsCreate)="annotationCreate($event)"
(annotationsSelect)="annotationsSelect($event)"
(annotationsUpdate)="annotationsUpdate($event)"
[classNames]="field.rawProperties.classNames"
[formControl]="$any(fieldForm)"
[folderId]="field.rawProperties.folderId"
[hasAnnotations]="!!comments"
[hasChatBot]="hasChatBot"
[language]="language"
[languages]="languages"
[schemaIds]="field.rawProperties.schemaIds">
</sqx-rich-editor>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<ng-container [ngSwitch]="field.rawProperties.editor">
<ng-container *ngSwitchCase="'Tags'">

3
frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html

@ -43,6 +43,9 @@
<ng-container *ngSwitchCase="'References'">
<sqx-references-ui [fieldForm]="fieldForm" [field]="field" [properties]="field.rawProperties"></sqx-references-ui>
</ng-container>
<ng-container *ngSwitchCase="'RichText'">
<sqx-rich-text-ui [fieldForm]="fieldForm" [field]="field" [properties]="field.rawProperties"></sqx-rich-text-ui>
</ng-container>
<ng-container *ngSwitchCase="'String'">
<sqx-string-ui [fieldForm]="fieldForm" [field]="field" [properties]="field.rawProperties"></sqx-string-ui>
</ng-container>

2
frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.ts

@ -19,6 +19,7 @@ import { GeolocationUIComponent } from '../types/geolocation-ui.component';
import { JsonUIComponent } from '../types/json-ui.component';
import { NumberUIComponent } from '../types/number-ui.component';
import { ReferencesUIComponent } from '../types/references-ui.component';
import { RichTextUIComponent } from '../types/rich-text-ui.component';
import { StringUIComponent } from '../types/string-ui.component';
import { TagsUIComponent } from '../types/tags-ui.component';
@ -42,6 +43,7 @@ import { TagsUIComponent } from '../types/tags-ui.component';
NgSwitchCase,
NumberUIComponent,
ReactiveFormsModule,
RichTextUIComponent,
ReferencesUIComponent,
StringUIComponent,
TagsUIComponent,

7
frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html

@ -104,6 +104,13 @@
[properties]="field.rawProperties">
</sqx-references-validation>
</ng-container>
<ng-container *ngSwitchCase="'RichText'">
<sqx-rich-text-validation
[fieldForm]="fieldForm"
[field]="field"
[properties]="field.rawProperties">
</sqx-rich-text-validation>
</ng-container>
<ng-container *ngSwitchCase="'String'">
<sqx-string-validation
[languages]="languages"

2
frontend/src/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts

@ -19,6 +19,7 @@ import { GeolocationValidationComponent } from '../types/geolocation-validation.
import { JsonValidationComponent } from '../types/json-validation.component';
import { NumberValidationComponent } from '../types/number-validation.component';
import { ReferencesValidationComponent } from '../types/references-validation.component';
import { RichTextValidationComponent } from '../types/rich-text-validation.component';
import { StringValidationComponent } from '../types/string-validation.component';
import { TagsValidationComponent } from '../types/tags-validation.component';
@ -42,6 +43,7 @@ import { TagsValidationComponent } from '../types/tags-validation.component';
NumberValidationComponent,
ReactiveFormsModule,
ReferencesValidationComponent,
RichTextValidationComponent,
StringValidationComponent,
TagsValidationComponent,
TranslatePipe,

36
frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-ui.component.html

@ -0,0 +1,36 @@
<div [formGroup]="fieldForm">
<div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.classNames' | sqxTranslate }}</label>
<div class="col-9">
<sqx-tag-editor formControlName="classNames"></sqx-tag-editor>
<sqx-form-hint>
{{ 'schemas.fieldTypes.string.classNamesHint' | sqxTranslate }}
</sqx-form-hint>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_folderId">{{ 'schemas.fieldTypes.string.folderId' | sqxTranslate }}</label>
<div class="col-9">
<sqx-asset-folder-dropdown formControlName="folderId"></sqx-asset-folder-dropdown>
<sqx-form-hint>
{{ 'schemas.fieldTypes.string.folderIdHint' | sqxTranslate }}
</sqx-form-hint>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label" for="{{field.fieldId}}_fieldSchemaIds">{{ 'common.schemas' | sqxTranslate }}</label>
<div class="col-9">
<sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}" formControlName="schemaIds"
[itemConverter]="(schemasSource.normalConverter | async)!"
[itemsSource]="(schemasSource.normalConverter | async)?.suggestions">
</sqx-tag-editor>
</div>
</div>
</div>

2
frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-ui.component.scss

@ -0,0 +1,2 @@
@import 'mixins';
@import 'vars';

41
frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-ui.component.ts

@ -0,0 +1,41 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { AsyncPipe } from '@angular/common';
import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { FieldDto, FormHintComponent, RichTextFieldPropertiesDto, SchemaTagSource, TagEditorComponent, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
selector: 'sqx-rich-text-ui',
styleUrls: ['rich-text-ui.component.scss'],
templateUrl: 'rich-text-ui.component.html',
imports: [
AsyncPipe,
FormHintComponent,
FormsModule,
ReactiveFormsModule,
TagEditorComponent,
TranslatePipe,
],
})
export class RichTextUIComponent {
@Input({ required: true })
public fieldForm!: UntypedFormGroup;
@Input({ required: true })
public field!: FieldDto;
@Input({ required: true })
public properties!: RichTextFieldPropertiesDto;
constructor(
public readonly schemasSource: SchemaTagSource,
) {
}
}

55
frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-validation.component.html

@ -0,0 +1,55 @@
<div [formGroup]="fieldForm">
<div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.length' | sqxTranslate }}</label>
<div class="col-9">
<div class="row g-0">
<div class="col">
<input type="number" class="form-control" formControlName="minLength" placeholder="{{ 'schemas.fieldTypes.string.lengthMin' | sqxTranslate }}">
</div>
<div class="col-auto">
<label class="col-form-label minmax">-</label>
</div>
<div class="col">
<input type="number" class="form-control" formControlName="maxLength" placeholder="{{ 'schemas.fieldTypes.string.lengthMax' | sqxTranslate }}">
</div>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.characters' | sqxTranslate }}</label>
<div class="col-9">
<div class="row g-0">
<div class="col">
<input type="number" class="form-control" formControlName="minCharacters" placeholder="{{ 'schemas.fieldTypes.string.charactersMin' | sqxTranslate }}">
</div>
<div class="col-auto">
<label class="col-form-label minmax">-</label>
</div>
<div class="col">
<input type="number" class="form-control" formControlName="maxCharacters" placeholder="{{ 'schemas.fieldTypes.string.charactersMax' | sqxTranslate }}">
</div>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-3 col-form-label">{{ 'schemas.fieldTypes.string.words' | sqxTranslate }}</label>
<div class="col-9">
<div class="row g-0">
<div class="col">
<input type="number" class="form-control" formControlName="minWords" placeholder="{{ 'schemas.fieldTypes.string.wordsMin' | sqxTranslate }}">
</div>
<div class="col-auto">
<label class="col-form-label minmax">-</label>
</div>
<div class="col">
<input type="number" class="form-control" formControlName="maxWords" placeholder="{{ 'schemas.fieldTypes.string.wordsMax' | sqxTranslate }}">
</div>
</div>
</div>
</div>
</div>

33
frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-validation.component.scss

@ -0,0 +1,33 @@
@import 'mixins';
@import 'vars';
.minmax {
text-align: center;
text-decoration: none;
width: 2rem;
}
.control-dropdown {
max-width: 285px;
min-height: 0;
min-width: 200px;
h4 {
padding: .5rem 0 0 .5rem;
}
&-item {
font-size: $font-smallest;
font-weight: normal;
&:hover {
.text-muted {
color: $color-white !important;
}
}
}
}
.truncate {
@include truncate;
}

32
frontend/src/app/features/schemas/pages/schema/fields/types/rich-text-validation.component.ts

@ -0,0 +1,32 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, Input } from '@angular/core';
import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular/forms';
import { FieldDto, RichTextFieldPropertiesDto, TranslatePipe } from '@app/shared';
@Component({
standalone: true,
selector: 'sqx-rich-text-validation',
styleUrls: ['rich-text-validation.component.scss'],
templateUrl: 'rich-text-validation.component.html',
imports: [
FormsModule,
ReactiveFormsModule,
TranslatePipe,
],
})
export class RichTextValidationComponent {
@Input({ required: true })
public fieldForm!: UntypedFormGroup;
@Input({ required: true })
public field!: FieldDto;
@Input({ required: true })
public properties!: RichTextFieldPropertiesDto;
}

4
frontend/src/app/shared/components/forms/rich-editor.component.ts

@ -38,7 +38,7 @@ export const SQX_RICH_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
ModalDirective,
],
})
export class RichEditorComponent extends StatefulControlComponent<{}, string> implements AfterViewInit, OnDestroy {
export class RichEditorComponent extends StatefulControlComponent<{}, EditorValue> implements AfterViewInit, OnDestroy {
private readonly assetId = new BehaviorSubject<string | null>(null);
private editorWrapper?: SquidexEditorWrapper;
private value?: string;
@ -171,7 +171,7 @@ export class RichEditorComponent extends StatefulControlComponent<{}, string> im
onUpload: (requests: UploadRequest[]) => {
return this.uploadFiles(requests);
},
onChange: (value: string | undefined) => {
onChange: (value: EditorValue) => {
this.callChange(value);
},
onEditAsset: id => {

31
frontend/src/app/shared/services/schemas.types.ts

@ -16,6 +16,7 @@ export type FieldType =
'Geolocation' |
'Number' |
'References' |
'RichText' |
'String' |
'Tags' |
'UI';
@ -51,6 +52,9 @@ export const fieldTypes: ReadonlyArray<{ type: FieldType; description: string }>
}, {
type: 'References',
description: 'i18n:schemas.fieldTypes.references.description',
}, {
type: 'RichText',
description: 'i18n:schemas.fieldTypes.richText.description',
}, {
type: 'Tags',
description: 'i18n:schemas.fieldTypes.tags.description',
@ -99,6 +103,9 @@ export function createProperties(fieldType: FieldType, values?: any): FieldPrope
case 'References':
properties = new ReferencesFieldPropertiesDto();
break;
case 'RichText':
properties = new RichTextFieldPropertiesDto();
break;
case 'String':
properties = new StringFieldPropertiesDto();
break;
@ -140,6 +147,8 @@ export interface FieldPropertiesVisitor<T> {
visitReferences(properties: ReferencesFieldPropertiesDto): T;
visitRichText(properties: RichTextFieldPropertiesDto): T;
visitString(properties: StringFieldPropertiesDto): T;
visitTags(properties: TagsFieldPropertiesDto): T;
@ -407,6 +416,28 @@ export class ReferencesFieldPropertiesDto extends FieldPropertiesDto {
}
}
export class RichTextFieldPropertiesDto extends FieldPropertiesDto {
public readonly fieldType = 'RichText';
public readonly classNames?: ReadonlyArray<string>;
public readonly folderId?: string;
public readonly maxCharacters?: number;
public readonly maxLength?: number;
public readonly maxWords?: number;
public readonly minCharacters?: number;
public readonly minLength?: number;
public readonly minWords?: number;
public readonly schemaIds?: ReadonlyArray<string>;
public get isSortable() {
return false;
}
public accept<T>(visitor: FieldPropertiesVisitor<T>): T {
return visitor.visitRichText(this);
}
}
export type StringFieldEditor = 'Color' | 'Dropdown' | 'Html' | 'Input' | 'Markdown' | 'Radio' | 'RichText' | 'Slug' | 'StockPhoto' | 'TextArea';
export type StringContentType = 'Unspecified' | 'Markdown' | 'Html';

103
frontend/src/app/shared/state/contents.forms.spec.ts

@ -180,7 +180,7 @@ describe('GetContentValue', () => {
const fieldLocalized = createField({ properties: createProperties('Number') });
const fieldAssets = createField({ properties: createProperties('Assets') });
it('should resolve image url and filename from referenced asset', () => {
it('should resolve formatted text as image url and filename from referenced asset', () => {
const content: any = {
referenceData: {
field1: {
@ -193,11 +193,13 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, assetWithImageAndFileName);
expect(result).toEqual({ value: ['url/to/13', 'file13'],
formatted: new HtmlValue('<div class="image"><img src="url/to/13?width=50&height=50&mode=Pad&" /> <span>file13</span></div>', 'url/to/13') });
expect(result).toEqual({
value: undefined,
formatted: new HtmlValue('<div class="image"><img src="url/to/13?width=50&height=50&mode=Pad&" /> <span>file13</span></div>', 'url/to/13'),
});
});
it('should resolve image url only from referenced asset', () => {
it('should resolve formatted text as image url from referenced asset', () => {
const content: any = {
referenceData: {
field1: {
@ -210,11 +212,13 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, assetWithImage);
expect(result).toEqual({ value: ['url/to/13', 'file13'],
formatted: new HtmlValue('<div class="image"><img src="url/to/13?width=50&height=50&mode=Pad&" /></div>', 'url/to/13') });
expect(result).toEqual({
value: undefined,
formatted: new HtmlValue('<div class="image"><img src="url/to/13?width=50&height=50&mode=Pad&" /></div>', 'url/to/13'),
});
});
it('should resolve image url only from referenced asset with custom format', () => {
it('should resolve formatted text as image url from referenced asset with custom format', () => {
const content: any = {
referenceData: {
field1: {
@ -227,11 +231,13 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, assetWithImage);
expect(result).toEqual({ value: ['url/to/13', 'file13'],
formatted: new HtmlValue('<div class="image"><img src="url/to/13?width=100&height=100" /></div>', 'url/to/13') });
expect(result).toEqual({
value: undefined,
formatted: new HtmlValue('<div class="image"><img src="url/to/13?width=100&height=100" /></div>', 'url/to/13'),
});
});
it('should resolve image url only from referenced asset with merged format', () => {
it('should resolve formatted text as image url from referenced asset with merged format', () => {
const content: any = {
referenceData: {
field1: {
@ -244,11 +250,13 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, assetWithImage);
expect(result).toEqual({ value: ['url/to/13', 'file13'],
formatted: new HtmlValue('<div class="image"><img src="url/to/13?width=50&height=50&mode=Pad&bg=red" /></div>', 'url/to/13') });
expect(result).toEqual({
value: undefined,
formatted: new HtmlValue('<div class="image"><img src="url/to/13?width=50&height=50&mode=Pad&bg=red" /></div>', 'url/to/13'),
});
});
it('should resolve filename only from referenced asset', () => {
it('should resolve formatted text as filename from referenced asset', () => {
const content: any = {
referenceData: {
field1: {
@ -261,10 +269,13 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, assetWithFileName);
expect(result).toEqual({ value: ['url/to/13', 'file13'], formatted: 'file13' });
expect(result).toEqual({
value: undefined,
formatted: 'file13',
});
});
it('should resolve filename from referenced asset', () => {
it('should resolve formatted text as filename from referenced asset with fallback format', () => {
const content: any = {
referenceData: {
field1: {
@ -275,10 +286,13 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, fieldAssets);
expect(result).toEqual({ value: ['file13'], formatted: 'file13' });
expect(result).toEqual({
value: undefined,
formatted: 'file13',
});
});
it('should not image url if not found', () => {
it('should not resolve formatted text as image url if not found', () => {
const content: any = {
referenceData: {
field1: {
@ -289,29 +303,35 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, fieldAssets);
expect(result).toEqual({ value: '-', formatted: '-' });
expect(result).toEqual({
value: undefined,
formatted: '',
});
});
it('should resolve string field from references value', () => {
it('should resolve formatted text from invariant reference data', () => {
const content: any = {
referenceData: {
field1: {
iv: '13',
iv: '42',
},
},
};
const result = getContentValue(content, language, fieldInvariant);
expect(result).toEqual({ value: '13', formatted: '13' });
expect(result).toEqual({
value: undefined,
formatted: '42',
});
});
it('should resolve invariant field from references value', () => {
it('should resolve formatted text from localized-invariant reference data', () => {
const content: any = {
referenceData: {
field1: {
iv: {
en: '13',
en: '42',
},
},
},
@ -319,10 +339,13 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, fieldInvariant);
expect(result).toEqual({ value: '13', formatted: '13' });
expect(result).toEqual({
value: undefined,
formatted: '42',
});
});
it('should resolve localized field from references value', () => {
it('should resolve formatted text from localized-localized reference data', () => {
const content: any = {
referenceData: {
field1: {
@ -335,10 +358,10 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, fieldLocalized);
expect(result).toEqual({ value: '13', formatted: '13' });
expect(result).toEqual({ value: undefined, formatted: '13' });
});
it('should return default value if reference field not found', () => {
it('should not resolve formatted text if reference data not found', () => {
const content: any = {
referenceData: {
field1: {
@ -351,10 +374,13 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, fieldLocalized);
expect(result).toEqual({ value: '-', formatted: '-' });
expect(result).toEqual({
value: undefined,
formatted: '',
});
});
it('should resolve invariant field', () => {
it('should resolve value from invariant field', () => {
const content: any = {
data: {
field1: {
@ -365,10 +391,13 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, fieldInvariant);
expect(result).toEqual({ value: 13, formatted: '13' });
expect(result).toEqual({
value: 13,
formatted: '13',
});
});
it('should resolve localized field', () => {
it('should resolve value from localized field', () => {
const content: any = {
data: {
field1: {
@ -379,10 +408,13 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, fieldLocalized);
expect(result).toEqual({ value: 13, formatted: '13' });
expect(result).toEqual({
value: 13,
formatted: '13',
});
});
it('should return default values if field not found', () => {
it('should not resolve value if field not found', () => {
const content: any = {
data: {
other: {
@ -393,7 +425,10 @@ describe('GetContentValue', () => {
const result = getContentValue(content, language, fieldLocalized);
expect(result).toEqual({ value: undefined, formatted: '' });
expect(result).toEqual({
value: undefined,
formatted: '',
});
});
});

30
frontend/src/app/shared/state/contents.forms.visitors.spec.ts

@ -339,7 +339,7 @@ describe('NumberField', () => {
it('should not format to stars if html not allowed', () => {
const field2 = createField({ properties: createProperties('Number', { editor: 'Stars' }) });
expect(FieldFormatter.format(field2, 3, false)).toEqual('3');
expect(FieldFormatter.format(field2, 3, undefined, false)).toEqual('3');
});
it('should return default value from properties', () => {
@ -366,7 +366,7 @@ describe('ReferencesField', () => {
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to plural count for many items', () => {
it('should format to text', () => {
expect(FieldFormatter.format(field, [1, 2, 3])).toBe('3 References');
});
@ -391,6 +391,26 @@ describe('ReferencesField', () => {
});
});
describe('RichTextField', () => {
const field = createField({ properties: createProperties('RichText') });
it('should create validators', () => {
expect(FieldsValidators.create(field, false).length).toBe(0);
});
it('should format to empty string if null', () => {
expect(FieldFormatter.format(field, null)).toBe('');
});
it('should format to plural count for many items', () => {
expect(FieldFormatter.format(field, 'text')).toBe('<Text />');
});
it('should return default value as null', () => {
expect(FieldDefaultValue.get(field, 'iv')).toBeNull();
});
});
describe('StringField', () => {
const field = createField({ properties: createProperties('String', { isRequired: true, pattern: 'pattern', minLength: 1, maxLength: 5, allowedValues: ['a', 'b'] }) });
@ -409,19 +429,19 @@ describe('StringField', () => {
it('should not format to preview image if html not allowed', () => {
const field2 = createField({ properties: createProperties('String', { editor: 'StockPhoto' }) });
expect(FieldFormatter.format(field2, 'https://images.unsplash.com/123?x', false)).toBe('https://images.unsplash.com/123?x');
expect(FieldFormatter.format(field2, 'https://images.unsplash.com/123?x', undefined, false)).toBe('https://images.unsplash.com/123?x');
});
it('should format to preview image', () => {
const field2 = createField({ properties: createProperties('String', { editor: 'StockPhoto' }) });
expect(FieldFormatter.format(field2, 'https://images.unsplash.com/123?x', true)).toEqual(new HtmlValue('<div class="image"><img src="https://images.unsplash.com/123?x&q=80&fm=jpg&crop=entropy&cs=tinysrgb&h=50&fit=max" /></div>', 'https://images.unsplash.com/123?x'));
expect(FieldFormatter.format(field2, 'https://images.unsplash.com/123?x', undefined, true)).toEqual(new HtmlValue('<div class="image"><img src="https://images.unsplash.com/123?x&q=80&fm=jpg&crop=entropy&cs=tinysrgb&h=50&fit=max" /></div>', 'https://images.unsplash.com/123?x'));
});
it('should not format to preview image if not unsplash image', () => {
const field2 = createField({ properties: createProperties('String', { editor: 'StockPhoto' }) });
expect(FieldFormatter.format(field2, 'https://images.com/123?x', true)).toEqual(new HtmlValue('<div class="image"><img src="https://images.com/123?x" /></div>', 'https://images.com/123?x'));
expect(FieldFormatter.format(field2, 'https://images.com/123?x', undefined, true)).toEqual(new HtmlValue('<div class="image"><img src="https://images.com/123?x" /></div>', 'https://images.com/123?x'));
});
it('should return default value from properties', () => {

206
frontend/src/app/shared/state/contents.forms.visitors.ts

@ -7,10 +7,10 @@
import { ValidatorFn, Validators } from '@angular/forms';
import { DateTime, Types, ValidatorsEx } from '@app/framework';
import { ContentDto, ContentReferencesValue } from '../services/contents.service';
import { ContentDto, ContentReferences, ContentReferencesValue } from '../services/contents.service';
import { LanguageDto } from '../services/languages.service';
import { FieldDto, RootFieldDto } from '../services/schemas.service';
import { ArrayFieldPropertiesDto, AssetPreviewMode, AssetsFieldPropertiesDto, BooleanFieldPropertiesDto, ComponentFieldPropertiesDto, ComponentsFieldPropertiesDto, DateTimeFieldPropertiesDto, fieldInvariant, FieldPropertiesVisitor, GeolocationFieldPropertiesDto, JsonFieldPropertiesDto, NumberFieldPropertiesDto, ReferencesFieldPropertiesDto, StringFieldPropertiesDto, TagsFieldPropertiesDto, UIFieldPropertiesDto } from '../services/schemas.types';
import { ArrayFieldPropertiesDto, AssetsFieldPropertiesDto, BooleanFieldPropertiesDto, ComponentFieldPropertiesDto, ComponentsFieldPropertiesDto, DateTimeFieldPropertiesDto, fieldInvariant, FieldPropertiesVisitor, GeolocationFieldPropertiesDto, JsonFieldPropertiesDto, NumberFieldPropertiesDto, ReferencesFieldPropertiesDto, RichTextFieldPropertiesDto, StringFieldPropertiesDto, TagsFieldPropertiesDto, UIFieldPropertiesDto } from '../services/schemas.types';
export class HtmlValue {
constructor(
@ -23,126 +23,126 @@ export class HtmlValue {
export type FieldValue = string | HtmlValue;
export function getContentValue(content: ContentDto, language: LanguageDto, field: RootFieldDto, allowHtml = true): { value: any; formatted: FieldValue } {
if (content.referenceData) {
const reference = content.referenceData[field.name];
function getValue(source: ContentReferences | undefined, language: LanguageDto, field: RootFieldDto, secondLevel = false) {
const fieldValue = source?.[field.name];
const isAssets = field.properties.fieldType === 'Assets';
if (reference && (!isAssets || allowHtml)) {
let fieldValue: ContentReferencesValue;
if (field.isLocalizable) {
fieldValue = reference[language.iso2Code];
} else {
fieldValue = reference[fieldInvariant];
}
let value: string | undefined;
if (Types.isObject(fieldValue)) {
value = (fieldValue as any)[language.iso2Code];
} else {
value = fieldValue;
}
if (!value) {
return { value: '-', formatted: '-' };
}
let formatted: FieldValue = value;
if (isAssets && Types.isArray(value)) {
if (value.length === 2) {
const buildImage = (src: string) => {
let format = (field.properties as any)['previewFormat'] || '';
if (format.indexOf('width') < 0 || format.indexOf('height') < 0) {
format = `width=50&height=50&mode=Pad&${format}`;
}
return `<img src="${src}?${format}" />`;
};
switch ((field.properties as any)['previewMode'] as AssetPreviewMode) {
case 'ImageAndFileName':
formatted = new HtmlValue(`<div class="image">${buildImage(value[0])} <span>${value[1]}</span></div>`, value[0]);
break;
case 'Image':
formatted = new HtmlValue(`<div class="image">${buildImage(value[0])}</div>`, value[0]);
break;
default:
formatted = value[1];
}
} else if (value.length === 1) {
formatted = value[0];
}
}
return { value, formatted };
if (!fieldValue) {
return undefined;
}
}
const contentField = content.data[field.name];
if (contentField) {
let value: any;
let partitionValue: ContentReferencesValue;
if (field.isLocalizable) {
value = contentField[language.iso2Code];
partitionValue = fieldValue[language.iso2Code];
} else {
value = contentField[fieldInvariant];
partitionValue = fieldValue[fieldInvariant];
}
let formatted: any;
if (Types.isUndefined(value)) {
formatted = value || '';
} else {
formatted = FieldFormatter.format(field, value, allowHtml);
if (secondLevel && Types.isObject(partitionValue)) {
partitionValue = (partitionValue as any)[language.iso2Code];
}
return { value, formatted };
return partitionValue;
}
return { value: undefined, formatted: '' };
const actualReference = getValue(content.referenceData, language, field, true);
const actualValue = getValue(content.data, language, field);
if (!actualReference && !actualValue) {
return { value: actualValue, formatted: '' };
}
const formatted = FieldFormatter.format(field, actualValue, actualReference, allowHtml);
return { value: actualValue, formatted };
}
export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
private constructor(
private readonly value: any,
private readonly referenceValue: any,
private readonly allowHtml: boolean,
) {
}
public static format(field: FieldDto, value: any, allowHtml = true) {
if (value === null || value === undefined) {
public static format(field: FieldDto, value: any, referenceValue?: any, allowHtml = true) {
if ((value === null || value === undefined) && !referenceValue) {
return '';
}
return field.properties.accept(new FieldFormatter(value, allowHtml));
return field.properties.accept(new FieldFormatter(value, referenceValue, allowHtml));
}
public visitArray(_: ArrayFieldPropertiesDto): string {
return this.formatArray('Item', 'Items');
}
public visitAssets(properties: AssetsFieldPropertiesDto): FieldValue {
if (this.allowHtml && this.referenceValue) {
const reference = this.referenceValue;
if (Types.isArray(reference) && reference.length === 2) {
const buildImage = (src: string) => {
let format = properties.previewFormat || '';
if (format.indexOf('width') < 0 || format.indexOf('height') < 0) {
format = `width=50&height=50&mode=Pad&${format}`;
}
return `<img src="${src}?${format}" />`;
};
switch (properties.previewMode) {
case 'ImageAndFileName':
return new HtmlValue(`<div class="image">${buildImage(reference[0])} <span>${reference[1]}</span></div>`, reference[0]);
case 'Image':
return new HtmlValue(`<div class="image">${buildImage(reference[0])}</div>`, reference[0]);
default:
return reference[1];
}
}
if (Types.isArray(reference) && reference.length === 1) {
return reference[0];
}
}
public visitAssets(_: AssetsFieldPropertiesDto): string {
return this.formatArray('Asset', 'Assets');
}
public visitArray(_: ArrayFieldPropertiesDto): string {
if (this.referenceValue) {
return this.referenceValue;
}
return this.formatArray('Item', 'Items');
}
public visitComponents(_: ComponentsFieldPropertiesDto): string {
if (this.referenceValue) {
return this.referenceValue;
}
return this.formatArray('Component', 'Components');
}
public visitReferences(_: ReferencesFieldPropertiesDto): string {
if (this.referenceValue) {
return this.referenceValue;
}
return this.formatArray('Reference', 'References');
}
public visitBoolean(_: BooleanFieldPropertiesDto): string {
if (this.referenceValue) {
return this.referenceValue;
}
return Types.booleanToString(this.value);
}
public visitComponent(_: ComponentFieldPropertiesDto): string {
if (this.referenceValue) {
return this.referenceValue;
}
const inner = Types.objectToString(this.value, ['schemaId'], 100);
if (inner.length > 0) {
@ -153,6 +153,10 @@ export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
}
public visitDateTime(properties: DateTimeFieldPropertiesDto): FieldValue {
if (this.referenceValue) {
return this.referenceValue;
}
try {
const parsed = DateTime.parseISO(this.value);
@ -167,14 +171,26 @@ export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
}
public visitJson(_: JsonFieldPropertiesDto): string {
if (this.referenceValue) {
return this.referenceValue;
}
return '<Json />';
}
public visitUI(_: UIFieldPropertiesDto): any {
if (this.referenceValue) {
return this.referenceValue;
}
return '';
}
public visitNumber(properties: NumberFieldPropertiesDto): FieldValue {
if (this.referenceValue) {
return this.referenceValue;
}
if (!Types.isNumber(this.value)) {
return '';
}
@ -197,6 +213,10 @@ export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
}
public visitGeolocation(_: GeolocationFieldPropertiesDto): string {
if (this.referenceValue) {
return this.referenceValue;
}
if (!Types.isObject(this.value)) {
return '';
}
@ -204,7 +224,23 @@ export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
return `${this.value.longitude}, ${this.value.latitude}`;
}
public visitRichText(_: RichTextFieldPropertiesDto): string {
if (this.referenceValue) {
return this.referenceValue;
}
if (this.referenceValue) {
return this.referenceValue;
}
return '<Text />';
}
public visitTags(_: TagsFieldPropertiesDto): string {
if (this.referenceValue) {
return this.referenceValue;
}
if (!Types.isArrayOfString(this.value)) {
return '';
}
@ -213,6 +249,10 @@ export class FieldFormatter implements FieldPropertiesVisitor<FieldValue> {
}
public visitString(properties: StringFieldPropertiesDto): any {
if (this.referenceValue) {
return this.referenceValue;
}
if (!Types.isString(this.value)) {
return '';
}
@ -389,6 +429,10 @@ export class FieldsValidators implements FieldPropertiesVisitor<ReadonlyArray<Va
return [];
}
public visitRichText(_: RichTextFieldPropertiesDto): ReadonlyArray<ValidatorFn> {
return [];
}
public visitUI(_: UIFieldPropertiesDto): ReadonlyArray<ValidatorFn> {
return [];
}
@ -461,6 +505,10 @@ export class FieldDefaultValue implements FieldPropertiesVisitor<any> {
return this.getValue(properties.defaultValue, properties.defaultValues);
}
public visitRichText(_: RichTextFieldPropertiesDto): any {
return null;
}
public visitString(properties: StringFieldPropertiesDto): any {
return this.getValue(properties.defaultValue, properties.defaultValues);
}

12
frontend/src/app/shared/state/schemas.forms.ts

@ -331,6 +331,18 @@ export class EditFieldFormVisitor implements FieldPropertiesVisitor<any> {
this.config['schemaIds'] = new UntypedFormControl(undefined);
}
public visitRichText() {
this.config['classNames'] = new UntypedFormControl(undefined);
this.config['folderId'] = new UntypedFormControl(undefined);
this.config['maxCharacters'] = new UntypedFormControl(undefined);
this.config['maxLength'] = new UntypedFormControl(undefined);
this.config['maxWords'] = new UntypedFormControl(undefined);
this.config['minCharacters'] = new UntypedFormControl(undefined);
this.config['minLength'] = new UntypedFormControl(undefined);
this.config['minWords'] = new UntypedFormControl(undefined);
this.config['schemaIds'] = new UntypedFormControl(undefined);
}
public visitString() {
this.config['allowedValues'] = new UntypedFormControl(undefined);
this.config['classNames'] = new UntypedFormControl(undefined);

240
frontend/src/app/theme/icomoon/demo.html

@ -1138,236 +1138,12 @@
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-download"></span>
<span class="mls"> icon-download</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e93e" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe93e;" 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="clearfix bshadow0 pbs">
<span class="icon-assets"></span>
<span class="mls"> icon-assets</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e948" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe948;" 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="clearfix bshadow0 pbs">
<span class="icon-search-Asset"></span>
<span class="mls"> icon-search-Asset</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e948" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe948;" 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="clearfix bshadow0 pbs">
<span class="icon-bug"></span>
<span class="mls"> icon-bug</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e93d" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe93d;" 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="clearfix bshadow0 pbs">
<span class="icon-caret-down"></span>
<span class="mls"> icon-caret-down</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e92c" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe92c;" 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="clearfix bshadow0 pbs">
<span class="icon-caret-left"></span>
<span class="mls"> icon-caret-left</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e92a" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe92a;" 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="clearfix bshadow0 pbs">
<span class="icon-caret-right"></span>
<span class="mls"> icon-caret-right</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e929" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe929;" 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="clearfix bshadow0 pbs">
<span class="icon-caret-up"></span>
<span class="mls"> icon-caret-up</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e92b" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe92b;" 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="clearfix bshadow0 pbs">
<span class="icon-contents"></span>
<span class="mls"> icon-contents</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e946" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe946;" 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="clearfix bshadow0 pbs">
<span class="icon-trigger-ContentChanged"></span>
<span class="mls"> icon-trigger-ContentChanged</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e946" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe946;" 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="clearfix bshadow0 pbs">
<span class="icon-control-Date"></span>
<span class="mls"> icon-control-Date</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e936" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe936;" 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="clearfix bshadow0 pbs">
<span class="icon-control-DateTime"></span>
<span class="mls"> icon-control-DateTime</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e937" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe937;" 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="clearfix bshadow0 pbs">
<span class="icon-control-Markdown"></span>
<span class="mls"> icon-control-Markdown</span>
<span class="icon-type-RichText"></span>
<span class="mls"> icon-type-RichText</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e938" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe938;" 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="clearfix bshadow0 pbs">
<span class="icon-grid"></span>
<span class="mls"> icon-grid</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="f00a" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xf00a;" 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="clearfix bshadow0 pbs">
<span class="icon-list1"></span>
<span class="mls"> icon-list1</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="f0c9" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xf0c9;" 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="clearfix bshadow0 pbs">
<span class="icon-user-o"></span>
<span class="mls"> icon-user-o</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e932" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe932;" 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="clearfix bshadow0 pbs">
<span class="icon-rules"></span>
<span class="mls"> icon-rules</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e947" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe947;" class="unitRight size1of2 talign-right" />
<input type="text" readonly value="e939" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe939;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>
@ -1376,12 +1152,12 @@
</div>
<div class="glyph fs2">
<div class="clearfix bshadow0 pbs">
<span class="icon-search-Rule"></span>
<span class="mls"> icon-search-Rule</span>
<span class="icon-download"></span>
<span class="mls"> icon-download</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e947" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe947;" class="unitRight size1of2 talign-right" />
<input type="text" readonly value="e93e" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe93e;" class="unitRight size1of2 talign-right" />
</fieldset>
<div class="fs0 bshadow0 clearfix hidden-true">
<span class="unit pvs fgc1">liga: </span>

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

Binary file not shown.

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

@ -64,7 +64,7 @@
<glyph unicode="&#xe936;" glyph-name="control-Date" d="M947.2 848.457h-128v25.6c0 14.131-11.469 25.6-25.6 25.6s-25.6-11.469-25.6-25.6v-25.6h-512v25.6c0 14.131-11.52 25.6-25.6 25.6s-25.6-11.469-25.6-25.6v-25.6h-128c-42.342 0-76.8-34.458-76.8-76.8v-716.8c0-42.342 34.458-76.8 76.8-76.8h870.4c42.342 0 76.8 34.458 76.8 76.8v716.8c0 42.342-34.458 76.8-76.8 76.8zM972.8 54.857c0-14.131-11.469-25.6-25.6-25.6h-870.4c-14.080 0-25.6 11.469-25.6 25.6v537.6h921.6v-537.6zM972.8 643.657h-921.6v128c0 14.080 11.52 25.6 25.6 25.6h128v-76.8c0-14.080 11.52-25.6 25.6-25.6s25.6 11.52 25.6 25.6v76.8h512v-76.8c0-14.080 11.469-25.6 25.6-25.6s25.6 11.52 25.6 25.6v76.8h128c14.131 0 25.6-11.52 25.6-25.6v-128zM332.8 438.857h51.2c14.080 0 25.6 11.52 25.6 25.6s-11.52 25.6-25.6 25.6h-51.2c-14.080 0-25.6-11.52-25.6-25.6s11.52-25.6 25.6-25.6zM486.4 438.857h51.2c14.131 0 25.6 11.52 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.080 0-25.6-11.52-25.6-25.6s11.52-25.6 25.6-25.6zM640 438.857h51.2c14.131 0 25.6 11.52 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.131 0-25.6-11.52-25.6-25.6s11.469-25.6 25.6-25.6zM793.6 438.857h51.2c14.131 0 25.6 11.52 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.131 0-25.6-11.52-25.6-25.6s11.469-25.6 25.6-25.6zM179.2 336.457h51.2c14.080 0 25.6 11.469 25.6 25.6s-11.52 25.6-25.6 25.6h-51.2c-14.080 0-25.6-11.469-25.6-25.6s11.52-25.6 25.6-25.6zM332.8 336.457h51.2c14.080 0 25.6 11.469 25.6 25.6s-11.52 25.6-25.6 25.6h-51.2c-14.080 0-25.6-11.469-25.6-25.6s11.52-25.6 25.6-25.6zM486.4 336.457h51.2c14.131 0 25.6 11.469 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.080 0-25.6-11.469-25.6-25.6s11.52-25.6 25.6-25.6zM640 336.457h51.2c14.131 0 25.6 11.469 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.131 0-25.6-11.469-25.6-25.6s11.469-25.6 25.6-25.6zM793.6 336.457h51.2c14.131 0 25.6 11.469 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.131 0-25.6-11.469-25.6-25.6s11.469-25.6 25.6-25.6zM179.2 234.057h51.2c14.080 0 25.6 11.469 25.6 25.6s-11.52 25.6-25.6 25.6h-51.2c-14.080 0-25.6-11.469-25.6-25.6s11.52-25.6 25.6-25.6zM332.8 234.057h51.2c14.080 0 25.6 11.469 25.6 25.6s-11.52 25.6-25.6 25.6h-51.2c-14.080 0-25.6-11.469-25.6-25.6s11.52-25.6 25.6-25.6zM486.4 234.057h51.2c14.131 0 25.6 11.469 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.080 0-25.6-11.469-25.6-25.6s11.52-25.6 25.6-25.6zM640 234.057h51.2c14.131 0 25.6 11.469 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.131 0-25.6-11.469-25.6-25.6s11.469-25.6 25.6-25.6zM793.6 234.057h51.2c14.131 0 25.6 11.469 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.131 0-25.6-11.469-25.6-25.6s11.469-25.6 25.6-25.6zM179.2 131.657h51.2c14.080 0 25.6 11.469 25.6 25.6s-11.52 25.6-25.6 25.6h-51.2c-14.080 0-25.6-11.469-25.6-25.6s11.52-25.6 25.6-25.6zM332.8 131.657h51.2c14.080 0 25.6 11.469 25.6 25.6s-11.52 25.6-25.6 25.6h-51.2c-14.080 0-25.6-11.469-25.6-25.6s11.52-25.6 25.6-25.6zM486.4 131.657h51.2c14.131 0 25.6 11.469 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.080 0-25.6-11.469-25.6-25.6s11.52-25.6 25.6-25.6zM640 131.657h51.2c14.131 0 25.6 11.469 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.131 0-25.6-11.469-25.6-25.6s11.469-25.6 25.6-25.6zM793.6 131.657h51.2c14.131 0 25.6 11.469 25.6 25.6s-11.469 25.6-25.6 25.6h-51.2c-14.131 0-25.6-11.469-25.6-25.6s11.469-25.6 25.6-25.6z" />
<glyph unicode="&#xe937;" glyph-name="control-DateTime" d="M486.4 541.257h51.2c14.080 0 25.6-11.52 25.6-25.6s-11.52-25.6-25.6-25.6h-51.2c-14.080 0-25.6 11.52-25.6 25.6s11.52 25.6 25.6 25.6zM230.4 336.457c14.080 0 25.6-11.469 25.6-25.6s-11.52-25.6-25.6-25.6h-51.2c-14.080 0-25.6 11.469-25.6 25.6s11.52 25.6 25.6 25.6h51.2zM230.4 438.857c14.080 0 25.6-11.469 25.6-25.6s-11.52-25.6-25.6-25.6h-51.2c-14.080 0-25.6 11.469-25.6 25.6s11.52 25.6 25.6 25.6h51.2zM51.2 208.457v435.2h665.6v-102.4h51.2v281.6c0 42.342-34.458 76.8-76.8 76.8h-128v25.6c0 14.131-11.469 25.6-25.6 25.6s-25.6-11.469-25.6-25.6v-25.6h-256v25.6c0 14.131-11.52 25.6-25.6 25.6s-25.6-11.469-25.6-25.6v-25.6h-128c-42.342 0-76.8-34.458-76.8-76.8v-614.4c0-42.342 34.458-76.8 76.8-76.8h332.8v51.2h-332.8c-14.080 0-25.6 11.469-25.6 25.6zM51.2 822.857c0 14.080 11.52 25.6 25.6 25.6h128v-76.8c0-14.080 11.52-25.6 25.6-25.6s25.6 11.52 25.6 25.6v76.8h256v-76.8c0-14.080 11.469-25.6 25.6-25.6s25.6 11.52 25.6 25.6v76.8h128c14.131 0 25.6-11.52 25.6-25.6v-128h-665.6v128zM384 541.257c14.080 0 25.6-11.52 25.6-25.6s-11.52-25.6-25.6-25.6h-51.2c-14.080 0-25.6 11.52-25.6 25.6s11.52 25.6 25.6 25.6h51.2zM742.4 490.057c-155.546 0-281.6-126.054-281.6-281.6s126.054-281.6 281.6-281.6 281.6 126.054 281.6 281.6-126.054 281.6-281.6 281.6zM742.4-21.943c-127.232 0-230.4 103.168-230.4 230.4s103.168 230.4 230.4 230.4 230.4-103.168 230.4-230.4-103.168-230.4-230.4-230.4zM384 438.857c14.080 0 25.6-11.469 25.6-25.6s-11.52-25.6-25.6-25.6h-51.2c-14.080 0-25.6 11.469-25.6 25.6s11.52 25.6 25.6 25.6h51.2zM384 336.457c14.080 0 25.6-11.469 25.6-25.6s-11.52-25.6-25.6-25.6h-51.2c-14.080 0-25.6 11.469-25.6 25.6s11.52 25.6 25.6 25.6h51.2zM844.8 234.057c14.131 0 25.6-11.469 25.6-25.6s-11.469-25.6-25.6-25.6h-102.4c-14.131 0-25.6 11.469-25.6 25.6v102.4c0 14.131 11.469 25.6 25.6 25.6s25.6-11.469 25.6-25.6v-76.8h76.8z" />
<glyph unicode="&#xe938;" glyph-name="control-Markdown" d="M793.6 341.441h-61.838v28.108h-0.783q-21.135-33.092-62.034-33.092-37.573 0-60.469 26.912-22.896 27.112-22.896 75.554 0 50.635 25.244 81.136t66.144 30.501q38.747 0 54.011-28.308h0.783v121.405h61.838v-302.216zM732.936 440.718v15.35q0 19.935-11.35 33.092t-29.549 13.157q-20.548 0-32.093-16.546-11.546-16.347-11.546-45.053 0-26.912 11.154-41.465t30.919-14.553q18.786 0 30.528 15.35 11.937 15.35 11.937 40.668zM548.594 341.441h-61.643v116.421q0 44.455-32.093 44.455-15.264 0-24.853-13.357t-9.589-33.292v-114.228h-61.839v117.617q0 43.259-31.506 43.259-15.851 0-25.44-12.758-9.393-12.758-9.393-34.687v-113.431h-61.838v204.135h61.838v-31.896h0.783q9.589 16.347 26.81 26.514 17.417 10.366 37.964 10.366 42.465 0 58.12-38.076 22.896 38.076 67.318 38.076 65.361 0 65.361-82.133v-126.987zM0 950.857v-204.8h76.8v-76.8h51.2v76.8h76.8v204.8zM819.2 950.857v-204.8h204.8v204.8zM51.2 899.657h102.4v-102.4h-102.4zM870.4 899.657h102.4v-102.4h-102.4zM281.6 874.057v-51.2h102.4v51.2zM486.4 874.057v-51.2h102.4v51.2zM691.2 874.057v-51.2h102.4v51.2zM896 669.257v-102.4h51.2v102.4zM76.8 566.857v-102.4h51.2v102.4zM896 464.457v-102.4h51.2v102.4zM76.8 362.057v-102.4h51.2v102.4zM896 259.657v-102.4h51.2v102.4zM76.8 157.257v-25.6h-76.8v-204.8h204.8v76.8h76.8v51.2h-76.8v76.8h-76.8v25.6zM819.2 131.657v-76.8h-25.6v-51.2h25.6v-76.8h204.8v204.8zM51.2 80.457h102.4v-102.4h-102.4zM870.4 80.457h102.4v-102.4h-102.4zM384 54.857v-51.2h102.4v51.2zM588.8 54.857v-51.2h102.4v51.2z" />
<glyph unicode="&#xe939;" glyph-name="control-RichText" d="M918 554.667v-128h-128v-298h-128v298h-128v128h384zM106 768.667h556v-128h-214v-512h-128v512h-214v128z" />
<glyph unicode="&#xe939;" glyph-name="control-RichText, type-RichText" d="M918 554.667v-128h-128v-298h-128v298h-128v128h384zM106 768.667h556v-128h-214v-512h-128v512h-214v128z" />
<glyph unicode="&#xe93a;" glyph-name="control-Stars" d="M1020.192 558.176c-8.864 25.568-31.616 44.288-59.008 48.352l-266.432 39.616-115.808 240.448c-12.192 25.248-38.272 41.408-66.944 41.408s-54.752-16.16-66.944-41.408l-115.808-240.448-266.464-39.616c-27.36-4.064-50.112-22.784-58.944-48.352-8.8-25.632-2.144-53.856 17.184-73.12l195.264-194.944-45.28-270.432c-4.608-27.232 7.2-54.56 30.336-70.496 12.704-8.736 27.648-13.184 42.592-13.184 12.288 0 24.608 3.008 35.776 8.992l232.288 125.056 232.32-125.056c11.168-5.984 23.488-8.992 35.744-8.992 14.944 0 29.888 4.448 42.624 13.184 23.136 15.936 34.88 43.264 30.304 70.496l-45.312 270.432 195.328 194.944c19.296 19.296 25.92 47.52 17.184 73.12zM754.816 340.384c-16.384-16.32-23.808-39.328-20.064-61.888l45.312-270.432-232.32 124.992c-11.136 6.016-23.424 8.992-35.776 8.992-12.288 0-24.608-3.008-35.744-8.992l-232.32-124.992 45.312 270.432c3.776 22.56-3.648 45.568-20.032 61.888l-195.264 194.944 266.432 39.68c24.352 3.616 45.312 18.848 55.776 40.576l115.872 240.384 115.84-240.416c10.496-21.728 31.424-36.928 55.744-40.576l266.496-39.68-195.264-194.912z" />
<glyph unicode="&#xe93b;" glyph-name="google" d="M522.2 521.2v-175.6h290.4c-11.8-75.4-87.8-220.8-290.4-220.8-174.8 0-317.4 144.8-317.4 323.2s142.6 323.2 317.4 323.2c99.4 0 166-42.4 204-79l139 133.8c-89.2 83.6-204.8 134-343 134-283 0-512-229-512-512s229-512 512-512c295.4 0 491.6 207.8 491.6 500.2 0 33.6-3.6 59.2-8 84.8l-483.6 0.2z" />
<glyph unicode="&#xe93c;" glyph-name="info" d="M636.518 972.8c68.608 0 102.912-46.694 102.912-100.198 0-66.816-59.597-128.614-137.165-128.614-64.973 0-102.861 38.4-101.069 101.888 0 53.402 45.107 126.925 135.322 126.925zM425.421-51.2c-54.17 0-93.85 33.382-55.962 180.429l62.157 260.71c10.803 41.677 12.595 58.419 0 58.419-16.23 0-86.477-28.774-128.102-57.19l-27.034 45.056c131.686 111.923 283.187 177.51 348.211 177.51 54.118 0 63.13-65.178 36.096-165.376l-71.219-274.022c-12.595-48.384-7.219-65.075 5.427-65.075 16.23 0 69.478 20.070 121.805 61.798l30.72-41.677c-128.102-130.406-268.032-180.582-322.099-180.582z" />

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

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

Binary file not shown.

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

Binary file not shown.

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save