mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
102 changed files with 3157 additions and 925 deletions
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
File diff suppressed because one or more lines are too long
@ -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> |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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) |
||||
|
|
||||
|
 |
||||
@ -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> |
||||
@ -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 |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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)); |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -0,0 +1,2 @@ |
|||||
|
@import 'mixins'; |
||||
|
@import 'vars'; |
||||
@ -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, |
||||
|
) { |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -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; |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue