Browse Source

Merge branch 'master' of github.com:Squidex/squidex

pull/499/head
Sebastian 6 years ago
parent
commit
75cd0509bd
  1. 10
      backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs
  2. 4
      backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs
  3. 198
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs
  4. 10
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Scripting/EventScriptExtension.cs
  5. 11
      backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs
  6. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  7. 10
      backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs
  8. 6
      backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs
  9. 3
      backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs
  10. 47
      backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs
  11. 3
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  12. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  13. 10
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs
  14. 26
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs
  15. 11
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs
  16. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs
  17. 14
      backend/src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs
  18. 11
      backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs
  19. 45
      backend/src/Squidex.Web/Services/UrlGenerator.cs
  20. 17
      backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  21. 4
      backend/src/Squidex/Config/Domain/QueryServices.cs
  22. 45
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs
  23. 87
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  24. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  25. 5
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs
  26. 9
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs
  27. 121
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs
  28. 7
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/NotificationEmailSenderTests.cs
  29. 3
      frontend/app-config/webpack.config.js
  30. 4
      frontend/app/framework/angular/forms/editable-title.component.ts
  31. 79
      frontend/app/shared/components/comments/comment.component.html
  32. 8
      frontend/app/shared/components/comments/comment.component.scss
  33. 93
      frontend/app/shared/components/comments/comment.component.ts
  34. 54
      frontend/app/shared/components/comments/comments.component.html
  35. 31
      frontend/app/shared/components/comments/comments.component.ts
  36. 2
      frontend/app/shell/pages/internal/notifications-menu.component.html
  37. 4
      frontend/app/shell/pages/internal/notifications-menu.component.ts
  38. 16
      frontend/app/theme/icomoon/demo.html
  39. BIN
      frontend/app/theme/icomoon/fonts/icomoon.eot
  40. 1
      frontend/app/theme/icomoon/fonts/icomoon.svg
  41. BIN
      frontend/app/theme/icomoon/fonts/icomoon.ttf
  42. BIN
      frontend/app/theme/icomoon/fonts/icomoon.woff
  43. 2
      frontend/app/theme/icomoon/selection.json
  44. 13
      frontend/app/theme/icomoon/style.css

10
backend/src/Squidex.Domain.Apps.Core.Model/Rules/EnrichedEvents/EnrichedAssetEvent.cs

@ -7,6 +7,7 @@
using System;
using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
@ -33,12 +34,17 @@ namespace Squidex.Domain.Apps.Core.Rules.EnrichedEvents
public long FileSize { get; set; }
public bool IsImage { get; set; }
public int? PixelWidth { get; set; }
public int? PixelHeight { get; set; }
public AssetType AssetType { get; set; }
public bool IsImage
{
get { return AssetType == AssetType.Image; }
}
public override long Partition
{
get { return Id.GetHashCode(); }

4
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/FieldConverters.cs

@ -85,7 +85,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
};
}
public static FieldConverter ResolveAssetUrls(IReadOnlyCollection<string>? fields, IAssetUrlGenerator urlGenerator)
public static FieldConverter ResolveAssetUrls(IReadOnlyCollection<string>? fields, IUrlGenerator urlGenerator)
{
if (fields?.Any() != true)
{
@ -122,7 +122,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent
{
var id = array[i].ToString();
array[i] = JsonValue.Create(urlGenerator.GenerateUrl(id));
array[i] = JsonValue.Create(urlGenerator.AssetContent(Guid.Parse(id)));
}
}
}

198
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using Squidex.Domain.Apps.Core.Contents;
@ -26,11 +27,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules
private const string Fallback = "null";
private const string ScriptSuffix = ")";
private const string ScriptPrefix = "Script(";
private static readonly char[] ContentPlaceholderStartOld = "CONTENT_DATA".ToCharArray();
private static readonly char[] ContentPlaceholderStartNew = "{CONTENT_DATA".ToCharArray();
private static readonly Regex ContentDataPlaceholderOld = new Regex(@"^CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}", RegexOptions.Compiled);
private static readonly Regex ContentDataPlaceholderNew = new Regex(@"^\{CONTENT_DATA(\.([0-9A-Za-z\-_]*)){2,}\}", RegexOptions.Compiled);
private readonly List<(char[] Pattern, Func<EnrichedEvent, string> Replacer)> patterns = new List<(char[] Pattern, Func<EnrichedEvent, string> Replacer)>();
private static readonly Regex RegexPatternOld = new Regex(@"^(?<Type>[^_]*)_(?<Path>.*)", RegexOptions.Compiled);
private static readonly Regex RegexPatternNew = new Regex(@"^\{(?<Type>[^_]*)_(?<Path>.*)\}", RegexOptions.Compiled);
private readonly List<(char[] Pattern, Func<EnrichedEvent, string?> Replacer)> patterns = new List<(char[] Pattern, Func<EnrichedEvent, string?> Replacer)>();
private readonly IJsonSerializer jsonSerializer;
private readonly IUrlGenerator urlGenerator;
private readonly IScriptEngine scriptEngine;
@ -47,8 +46,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules
AddPattern("APP_ID", AppId);
AddPattern("APP_NAME", AppName);
AddPattern("ASSET_CONTENT_URL", AssetContentUrl);
AddPattern("CONTENT_ACTION", ContentAction);
AddPattern("CONTENT_STATUS", ContentStatus);
AddPattern("CONTENT_URL", ContentUrl);
AddPattern("MENTIONED_ID", MentionedId);
AddPattern("MENTIONED_NAME", MentionedName);
@ -62,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
AddPattern("USER_EMAIL", UserEmail);
}
private void AddPattern(string placeholder, Func<EnrichedEvent, string> generator)
private void AddPattern(string placeholder, Func<EnrichedEvent, string?> generator)
{
patterns.Add((placeholder.ToCharArray(), generator));
}
@ -102,9 +101,6 @@ namespace Squidex.Domain.Apps.Core.HandleRules
var sb = new StringBuilder();
var cp2 = new ReadOnlySpan<char>(ContentPlaceholderStartNew);
var cp1 = new ReadOnlySpan<char>(ContentPlaceholderStartOld);
for (var i = 0; i < current.Length; i++)
{
var c = current[i];
@ -115,50 +111,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules
current = current.Slice(i);
var test = current.Slice(1);
var tested = false;
var (replacement, length) = GetReplacement(current.Slice(1), @event);
for (var j = 0; j < patterns.Count; j++)
if (length > 0)
{
var (pattern, replacer) = patterns[j];
if (test.StartsWith(pattern, StringComparison.OrdinalIgnoreCase))
{
sb.Append(replacer(@event));
current = current.Slice(pattern.Length + 1);
i = 0;
tested = true;
break;
}
}
sb.Append(replacement);
if (!tested && (test.StartsWith(cp1, StringComparison.OrdinalIgnoreCase) || test.StartsWith(cp2, StringComparison.OrdinalIgnoreCase)))
{
var currentString = test.ToString();
var match = ContentDataPlaceholderOld.Match(currentString);
if (!match.Success)
{
match = ContentDataPlaceholderNew.Match(currentString);
}
if (match.Success)
{
if (@event is EnrichedContentEvent contentEvent)
{
sb.Append(CalculateData(contentEvent.Data, match));
}
else
{
sb.Append(Fallback);
}
current = current.Slice(match.Length + 1);
i = 0;
}
current = current.Slice(length + 1);
i = 0;
}
}
}
@ -168,6 +128,37 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return sb.ToString();
}
private (string Result, int Length) GetReplacement(ReadOnlySpan<char> test, EnrichedEvent @event)
{
for (var j = 0; j < patterns.Count; j++)
{
var (pattern, replacer) = patterns[j];
if (test.StartsWith(pattern, StringComparison.OrdinalIgnoreCase))
{
return (replacer(@event) ?? Fallback, pattern.Length);
}
}
var currentString = test.ToString();
var match = RegexPatternNew.Match(currentString);
if (!match.Success)
{
match = RegexPatternOld.Match(currentString);
}
if (match.Success)
{
var path = match.Groups["Path"].Value.Split('.', StringSplitOptions.RemoveEmptyEntries);
return (CalculateData(@event, path) ?? Fallback, match.Length);
}
return (Fallback, 0);
}
private static string TimestampDate(EnrichedEvent @event)
{
return @event.Timestamp.ToDateTimeUtc().ToString("yyy-MM-dd", CultureInfo.InvariantCulture);
@ -188,74 +179,84 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return @event.AppId.Name;
}
private static string SchemaId(EnrichedEvent @event)
private static string? SchemaId(EnrichedEvent @event)
{
if (@event is EnrichedSchemaEventBase schemaEvent)
{
return schemaEvent.SchemaId.Id.ToString();
}
return Fallback;
return null;
}
private static string SchemaName(EnrichedEvent @event)
private static string? SchemaName(EnrichedEvent @event)
{
if (@event is EnrichedSchemaEventBase schemaEvent)
{
return schemaEvent.SchemaId.Name;
}
return Fallback;
return null;
}
private static string ContentAction(EnrichedEvent @event)
private static string? ContentAction(EnrichedEvent @event)
{
if (@event is EnrichedContentEvent contentEvent)
{
return contentEvent.Type.ToString();
}
return Fallback;
return null;
}
private static string ContentStatus(EnrichedEvent @event)
private static string? ContentStatus(EnrichedEvent @event)
{
if (@event is EnrichedContentEvent contentEvent)
{
return contentEvent.Status.ToString();
}
return Fallback;
return null;
}
private string ContentUrl(EnrichedEvent @event)
private string? AssetContentUrl(EnrichedEvent @event)
{
if (@event is EnrichedAssetEvent assetEvent)
{
return urlGenerator.AssetContent(assetEvent.Id);
}
return null;
}
private string? ContentUrl(EnrichedEvent @event)
{
if (@event is EnrichedContentEvent contentEvent)
{
return urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id);
}
return Fallback;
return null;
}
private static string UserName(EnrichedEvent @event)
private static string? UserName(EnrichedEvent @event)
{
if (@event is EnrichedUserEventBase userEvent)
{
return userEvent.User?.DisplayName() ?? Fallback;
}
return Fallback;
return null;
}
private static string UserId(EnrichedEvent @event)
private static string? UserId(EnrichedEvent @event)
{
if (@event is EnrichedUserEventBase userEvent)
{
return userEvent.User?.Id ?? Fallback;
}
return Fallback;
return null;
}
private static string UserEmail(EnrichedEvent @event)
@ -298,36 +299,63 @@ namespace Squidex.Domain.Apps.Core.HandleRules
return Fallback;
}
private static string CalculateData(NamedContentData data, Match match)
private static string? CalculateData(object @event, string[] path)
{
var captures = match.Groups[2].Captures;
var path = new string[captures.Count];
object? current = @event;
for (var i = 0; i < path.Length; i++)
foreach (var segment in path)
{
path[i] = captures[i].Value;
}
if (current is NamedContentData data)
{
if (!data.TryGetValue(segment, out var temp) || temp == null)
{
return null;
}
if (!data.TryGetValue(path[0], out var field) || field == null)
{
return Fallback;
}
current = temp;
}
else if (current is ContentFieldData field)
{
if (!field.TryGetValue(segment, out var temp) || temp == null)
{
return null;
}
if (!field.TryGetValue(path[1], out var value))
{
return Fallback;
}
current = temp;
}
else if (current is IJsonValue json)
{
if (!json.TryGet(segment, out var temp) || temp == null || temp.Type == JsonValueType.Null)
{
return null;
}
if (path.Skip(2).Any())
{
if (!value.TryGetByPath(path.Skip(2), out value) || value == null || value.Type == JsonValueType.Null)
current = temp;
}
else if (current != null)
{
const BindingFlags bindingFlags =
BindingFlags.FlattenHierarchy |
BindingFlags.Public |
BindingFlags.Instance;
var properties = current.GetType().GetProperties(bindingFlags);
var property = properties.FirstOrDefault(x => x.CanRead && string.Equals(x.Name, segment, StringComparison.OrdinalIgnoreCase));
if (property == null)
{
return null;
}
current = property.GetValue(current);
}
else
{
return Fallback;
return null;
}
}
return value.ToString() ?? Fallback;
return current?.ToString();
}
}
}

10
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Scripting/EventScriptExtension.cs

@ -45,6 +45,16 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Scripting
return JsValue.Null;
}));
context.Engine.SetValue("assetContentUrl", new EventDelegate(() =>
{
if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent)
{
return urlGenerator.AssetContent(assetEvent.Id);
}
return JsValue.Null;
}));
}
}
}

11
backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs

@ -6,18 +6,27 @@
// ==========================================================================
using System;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core
{
public interface IUrlGenerator
{
bool CanGenerateAssetSourceUrl { get; }
string? AssetSource(Guid assetId, long fileVersion);
string? AssetThumbnail(Guid assetId, AssetType assetType);
string AppSettingsUI(NamedId<Guid> appId);
string AssetsUI(NamedId<Guid> appId);
string AssetsUI(NamedId<Guid> appId, string? query = null);
string AssetContent(Guid assetId);
string BackupsUI(NamedId<Guid> appId);
string ClientsUI(NamedId<Guid> appId);
@ -47,5 +56,7 @@ namespace Squidex.Domain.Apps.Core
string SchemaUI(NamedId<Guid> appId, NamedId<Guid> schemaId);
string WorkflowsUI(NamedId<Guid> appId);
string UI();
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs

@ -45,6 +45,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
SimpleMapper.Map(asset, result);
result.AssetType = asset.Type;
switch (@event.Payload)
{
case AssetCreated _:

10
backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/IAssetUrlGenerator.cs → backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentTextCommand.cs

@ -5,10 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Core.ConvertContent
using System;
namespace Squidex.Domain.Apps.Entities.Comments.Commands
{
public interface IAssetUrlGenerator
public abstract class CommentTextCommand : CommentsCommand
{
string GenerateUrl(string assetId);
public string Text { get; set; }
public string[]? Mentions { get; set; }
}
}

6
backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CreateComment.cs

@ -9,14 +9,10 @@ using System;
namespace Squidex.Domain.Apps.Entities.Comments.Commands
{
public sealed class CreateComment : CommentsCommand
public sealed class CreateComment : CommentTextCommand
{
public bool IsMention { get; set; }
public string Text { get; set; }
public string[]? Mentions { get; set; }
public Uri? Url { get; set; }
public CreateComment()

3
backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/UpdateComment.cs

@ -7,8 +7,7 @@
namespace Squidex.Domain.Apps.Entities.Comments.Commands
{
public sealed class UpdateComment : CommentsCommand
public sealed class UpdateComment : CommentTextCommand
{
public string Text { get; set; }
}
}

47
backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs

@ -43,23 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
{
if (commentsCommand is CreateComment createComment && !IsMention(createComment))
{
await MentionUsersAsync(createComment);
if (createComment.Mentions != null)
{
foreach (var userId in createComment.Mentions)
{
var notificationCommand = SimpleMapper.Map(createComment, new CreateComment());
notificationCommand.AppId = null!;
notificationCommand.Mentions = null;
notificationCommand.CommentsId = userId;
notificationCommand.ExpectedVersion = EtagVersion.Any;
notificationCommand.IsMention = true;
context.CommandBus.PublishAsync(notificationCommand).Forget();
}
}
await ReplicateCommandAsync(context, createComment);
}
await ExecuteCommandAsync(context, commentsCommand);
@ -68,6 +52,27 @@ namespace Squidex.Domain.Apps.Entities.Comments
await next(context);
}
private async Task ReplicateCommandAsync(CommandContext context, CommentTextCommand command)
{
await MentionUsersAsync(command);
if (command.Mentions != null)
{
foreach (var userId in command.Mentions)
{
var notificationCommand = SimpleMapper.Map(command, new CreateComment());
notificationCommand.AppId = null!;
notificationCommand.Mentions = null;
notificationCommand.CommentsId = userId;
notificationCommand.ExpectedVersion = EtagVersion.Any;
notificationCommand.IsMention = true;
context.CommandBus.PublishAsync(notificationCommand).Forget();
}
}
}
private async Task ExecuteCommandAsync(CommandContext context, CommentsCommand commentsCommand)
{
var grain = grainFactory.GetGrain<ICommentsGrain>(commentsCommand.CommentsId);
@ -82,11 +87,11 @@ namespace Squidex.Domain.Apps.Entities.Comments
return createComment.IsMention;
}
private async Task MentionUsersAsync(CreateComment createComment)
private async Task MentionUsersAsync(CommentTextCommand command)
{
if (!string.IsNullOrWhiteSpace(createComment.Text))
if (!string.IsNullOrWhiteSpace(command.Text))
{
var emails = MentionRegex.Matches(createComment.Text).Select(x => x.Value.Substring(1)).ToArray();
var emails = MentionRegex.Matches(command.Text).Select(x => x.Value.Substring(1)).ToArray();
if (emails.Length > 0)
{
@ -104,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Comments
if (mentions.Count > 0)
{
createComment.Mentions = mentions.ToArray();
command.Mentions = mentions.ToArray();
}
}
}

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

@ -11,6 +11,7 @@ using System.Threading.Tasks;
using GraphQL;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
@ -92,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
allSchemas,
GetPageSizeForContents(),
GetPageSizeForAssets(),
resolver.Resolve<IGraphQLUrlGenerator>());
resolver.Resolve<IUrlGenerator>());
});
}

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

@ -11,6 +11,7 @@ using System.Linq;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.DataLoader;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Domain.Apps.Entities.Contents.Queries;
@ -26,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private readonly IDataLoaderContextAccessor dataLoaderContextAccessor;
private readonly IDependencyResolver resolver;
public IGraphQLUrlGenerator UrlGenerator { get; }
public IUrlGenerator UrlGenerator { get; }
public ISemanticLog Log { get; }
@ -37,7 +38,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
resolver.Resolve<IAssetQueryService>(),
resolver.Resolve<IContentQueryService>())
{
UrlGenerator = resolver.Resolve<IGraphQLUrlGenerator>();
UrlGenerator = resolver.Resolve<IUrlGenerator>();
dataLoaderContextAccessor = resolver.Resolve<IDataLoaderContextAccessor>();

10
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLModel.cs

@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
IEnumerable<ISchemaEntity> schemas,
int pageSizeContents,
int pageSizeAssets,
IGraphQLUrlGenerator urlGenerator)
IUrlGenerator urlGenerator)
{
this.app = app;
@ -99,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateAssetUrl(app, c.Source);
return context.UrlGenerator.AssetContent(c.Source.Id);
});
return resolver;
@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateAssetSourceUrl(c.Source);
return context.UrlGenerator.AssetSource(c.Source.Id, c.Source.FileVersion);
});
return resolver;
@ -123,7 +123,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateAssetThumbnailUrl(app, c.Source);
return context.UrlGenerator.AssetThumbnail(c.Source.Id, c.Source.Type);
});
return resolver;
@ -135,7 +135,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var context = (GraphQLExecutionContext)c.UserContext;
return context.UrlGenerator.GenerateContentUrl(app, schema, c.Source);
return context.UrlGenerator.ContentUI(app.NamedId(), schema.NamedId(), c.Source.Id);
});
return resolver;

26
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/IGraphQLUrlGenerator.cs

@ -1,26 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Schemas;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public interface IGraphQLUrlGenerator
{
bool CanGenerateAssetSourceUrl { get; }
string? GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset);
string? GenerateAssetSourceUrl(IAssetEntity asset);
string GenerateAssetUrl(IAppEntity app, IAssetEntity asset);
string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content);
}
}

11
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
@ -19,17 +20,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
public sealed class ConvertData : IContentEnricherStep
{
private readonly IAssetUrlGenerator assetUrlGenerator;
private readonly IUrlGenerator urlGenerator;
private readonly IAssetRepository assetRepository;
private readonly IContentRepository contentRepository;
public ConvertData(IAssetUrlGenerator assetUrlGenerator, IAssetRepository assetRepository, IContentRepository contentRepository)
public ConvertData(IUrlGenerator urlGenerator, IAssetRepository assetRepository, IContentRepository contentRepository)
{
Guard.NotNull(assetUrlGenerator);
Guard.NotNull(urlGenerator);
Guard.NotNull(assetRepository);
Guard.NotNull(contentRepository);
this.assetUrlGenerator = assetUrlGenerator;
this.urlGenerator = urlGenerator;
this.assetRepository = assetRepository;
this.contentRepository = contentRepository;
}
@ -137,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
if (assetUrls.Any())
{
yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), assetUrlGenerator);
yield return FieldConverters.ResolveAssetUrls(assetUrls.ToList(), urlGenerator);
}
}
}

12
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs

@ -9,9 +9,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.ExtractReferenceIds;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets;
@ -26,17 +26,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
private static readonly ILookup<Guid, IEnrichedAssetEntity> EmptyAssets = Enumerable.Empty<IEnrichedAssetEntity>().ToLookup(x => x.Id);
private readonly IAssetUrlGenerator assetUrlGenerator;
private readonly IUrlGenerator urlGenerator;
private readonly IAssetQueryService assetQuery;
private readonly IRequestCache requestCache;
public ResolveAssets(IAssetUrlGenerator assetUrlGenerator, IAssetQueryService assetQuery, IRequestCache requestCache)
public ResolveAssets(IUrlGenerator urlGenerator, IAssetQueryService assetQuery, IRequestCache requestCache)
{
Guard.NotNull(assetUrlGenerator);
Guard.NotNull(urlGenerator);
Guard.NotNull(assetQuery);
Guard.NotNull(requestCache);
this.assetUrlGenerator = assetUrlGenerator;
this.urlGenerator = urlGenerator;
this.assetQuery = assetQuery;
this.requestCache = requestCache;
}
@ -90,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
if (referencedImage != null)
{
var url = assetUrlGenerator.GenerateUrl(referencedImage.Id.ToString());
var url = urlGenerator.AssetContent(Guid.Parse(referencedImage.Id.ToString()));
requestCache.AddDependency(referencedImage.Id, referencedImage.Version);

14
backend/src/Squidex.Domain.Apps.Entities/IEmailUrlGenerator.cs

@ -1,14 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities
{
public interface IEmailUrlGenerator
{
string GenerateUIUrl();
}
}

11
backend/src/Squidex.Domain.Apps.Entities/Notifications/NotificationEmailSender.cs

@ -8,6 +8,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Email;
using Squidex.Infrastructure.Log;
@ -18,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Notifications
public sealed class NotificationEmailSender : INotificationSender
{
private readonly IEmailSender emailSender;
private readonly IEmailUrlGenerator emailUrlGenerator;
private readonly IUrlGenerator urlGenerator;
private readonly ISemanticLog log;
private readonly NotificationEmailTextOptions texts;
@ -45,17 +46,17 @@ namespace Squidex.Domain.Apps.Entities.Apps.Notifications
public NotificationEmailSender(
IOptions<NotificationEmailTextOptions> texts,
IEmailSender emailSender,
IEmailUrlGenerator emailUrlGenerator,
IUrlGenerator urlGenerator,
ISemanticLog log)
{
Guard.NotNull(texts);
Guard.NotNull(emailSender);
Guard.NotNull(emailUrlGenerator);
Guard.NotNull(urlGenerator);
Guard.NotNull(log);
this.texts = texts.Value;
this.emailSender = emailSender;
this.emailUrlGenerator = emailUrlGenerator;
this.urlGenerator = urlGenerator;
this.log = log;
}
@ -115,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Notifications
return;
}
vars.URL = emailUrlGenerator.GenerateUIUrl();
vars.URL = urlGenerator.UI();
vars.User = user;

45
backend/src/Squidex.Web/Services/UrlGenerator.cs

@ -9,18 +9,12 @@ using System;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
namespace Squidex.Web.Services
{
public sealed class UrlGenerator : IGraphQLUrlGenerator, IUrlGenerator, IAssetUrlGenerator, IEmailUrlGenerator
public sealed class UrlGenerator : IUrlGenerator
{
private readonly IAssetFileStore assetFileStore;
private readonly UrlsOptions urlsOptions;
@ -39,44 +33,29 @@ namespace Squidex.Web.Services
CanGenerateAssetSourceUrl = allowAssetSourceUrl;
}
public string? GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset)
public string? AssetThumbnail(Guid assetId, AssetType assetType)
{
if (asset.Type != AssetType.Image)
if (assetType != AssetType.Image)
{
return null;
}
return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.FileVersion}&width=100&mode=Max");
return urlsOptions.BuildUrl($"api/assets/{assetId}?width=100&mode=Max");
}
public string GenerateUrl(string assetId)
{
return urlsOptions.BuildUrl($"api/assets/{assetId}");
}
public string GenerateAssetUrl(IAppEntity app, IAssetEntity asset)
{
return urlsOptions.BuildUrl($"api/assets/{asset.Id}?version={asset.FileVersion}");
}
public string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content)
{
return urlsOptions.BuildUrl($"api/content/{app.Name}/{schema.SchemaDef.Name}/{content.Id}");
}
public string GenerateContentUIUrl(NamedId<Guid> appId, NamedId<Guid> schemaId, Guid contentId)
public string AppSettingsUI(NamedId<Guid> appId)
{
return urlsOptions.BuildUrl($"app/{appId.Name}/content/{schemaId.Name}/{contentId}/history");
return urlsOptions.BuildUrl($"app/{appId.Name}/settings", false);
}
public string GenerateUIUrl()
public string AssetContent(Guid assetId)
{
return urlsOptions.BuildUrl("app", false);
return urlsOptions.BuildUrl($"api/assets/{assetId}");
}
public string AppSettingsUI(NamedId<Guid> appId)
public string? AssetSource(Guid assetId, long fileVersion)
{
return urlsOptions.BuildUrl($"app/{appId.Name}/settings", false);
return assetFileStore.GeneratePublicUrl(assetId, fileVersion);
}
public string AssetsUI(NamedId<Guid> appId)
@ -164,9 +143,9 @@ namespace Squidex.Web.Services
return urlsOptions.BuildUrl($"app/{appId.Name}/settings/workflows", false);
}
public string? GenerateAssetSourceUrl(IAssetEntity asset)
public string UI()
{
return assetFileStore.GeneratePublicUrl(asset.Id, asset.FileVersion);
return urlsOptions.BuildUrl("app", false);
}
}
}

17
backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs

@ -11,6 +11,7 @@ using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Users.Models;
using Squidex.Domain.Users;
using Squidex.Infrastructure.Commands;
@ -27,6 +28,7 @@ namespace Squidex.Areas.Api.Controllers.Users
public sealed class UsersController : ApiController
{
private static readonly byte[] AvatarBytes;
private readonly IHttpClientFactory httpClientFactory;
private readonly IUserPictureStore userPictureStore;
private readonly IUserResolver userResolver;
private readonly ISemanticLog log;
@ -45,11 +47,13 @@ namespace Squidex.Areas.Api.Controllers.Users
public UsersController(
ICommandBus commandBus,
IHttpClientFactory httpClientFactory,
IUserPictureStore userPictureStore,
IUserResolver userResolver,
ISemanticLog log)
: base(commandBus)
{
this.httpClientFactory = httpClientFactory;
this.userPictureStore = userPictureStore;
this.userResolver = userResolver;
@ -177,7 +181,7 @@ namespace Squidex.Areas.Api.Controllers.Users
});
}
using (var client = new HttpClient())
using (var client = httpClientFactory.CreateClient())
{
var url = entity.PictureNormalizedUrl();
@ -189,7 +193,16 @@ namespace Squidex.Areas.Api.Controllers.Users
{
var contentType = response.Content.Headers.ContentType.ToString();
return new FileStreamResult(await response.Content.ReadAsStreamAsync(), contentType);
var etag = response.Headers.ETag;
var result = new FileStreamResult(await response.Content.ReadAsStreamAsync(), contentType);
if (!string.IsNullOrWhiteSpace(etag?.Tag))
{
result.EntityTag = new EntityTagHeaderValue(etag.Tag, etag.IsWeak);
}
return result;
}
}
}

4
backend/src/Squidex/Config/Domain/QueryServices.cs

@ -11,8 +11,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Web;
@ -30,7 +28,7 @@ namespace Squidex.Config.Domain
c.GetRequiredService<IOptions<UrlsOptions>>(),
c.GetRequiredService<IAssetFileStore>(),
exposeSourceUrl))
.As<IGraphQLUrlGenerator>().As<IUrlGenerator>().As<IAssetUrlGenerator>().As<IEmailUrlGenerator>();
.As<IUrlGenerator>();
services.AddSingletonAs(x => new FuncDependencyResolver(x.GetRequiredService))
.As<IDependencyResolver>();

45
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/FieldConvertersTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using FakeItEasy;
@ -20,13 +21,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
{
public class FieldConvertersTests
{
private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly IUrlGenerator urlGenerato = A.Fake<IUrlGenerator>();
private readonly Guid id1 = Guid.NewGuid();
private readonly Guid id2 = Guid.NewGuid();
private readonly LanguagesConfig languagesConfig = LanguagesConfig.English.Set(Language.DE);
public FieldConvertersTests()
{
A.CallTo(() => assetUrlGenerator.GenerateUrl(A<string>._))
.ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<string>(0)}");
A.CallTo(() => urlGenerato.AssetContent(A<Guid>._))
.ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<Guid>(0)}");
}
[Fact]
@ -479,13 +482,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source =
new ContentFieldData()
.AddJsonValue(JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array(id1, id2));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array("url/to/1", "url/to/2"));
.AddJsonValue(JsonValue.Array($"url/to/{id1}", $"url/to/{id2}"));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "assets" }), assetUrlGenerator)(source, field);
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "assets" }), urlGenerato)(source, field);
Assert.Equal(expected, result);
}
@ -501,15 +504,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
new ContentFieldData()
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array("1", "2"))));
.Add("assets", JsonValue.Array(id1, id2))));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array("url/to/1", "url/to/2"))));
.Add("assets", JsonValue.Array($"url/to/{id1}", $"url/to/{id2}"))));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "array.assets" }), assetUrlGenerator)(source, field);
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "array.assets" }), urlGenerato)(source, field);
Assert.Equal(expected, result);
}
@ -521,13 +524,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source =
new ContentFieldData()
.AddJsonValue(JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array(id1, id2));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array("url/to/1", "url/to/2"));
.AddJsonValue(JsonValue.Array($"url/to/{id1}", $"url/to/{id2}"));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "*" }), assetUrlGenerator)(source, field);
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "*" }), urlGenerato)(source, field);
Assert.Equal(expected, result);
}
@ -543,15 +546,15 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
new ContentFieldData()
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array("1", "2"))));
.Add("assets", JsonValue.Array(id1, id2))));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array(
JsonValue.Object()
.Add("assets", JsonValue.Array("url/to/1", "url/to/2"))));
.Add("assets", JsonValue.Array($"url/to/{id1}", $"url/to/{id2}"))));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "*" }), assetUrlGenerator)(source, field);
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "*" }), urlGenerato)(source, field);
Assert.Equal(expected, result);
}
@ -563,13 +566,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source =
new ContentFieldData()
.AddJsonValue(JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array(id1, id2));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array(id1, id2));
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "other" }), assetUrlGenerator)(source, field);
var result = FieldConverters.ResolveAssetUrls(new HashSet<string>(new[] { "other" }), urlGenerato)(source, field);
Assert.Equal(expected, result);
}
@ -581,13 +584,13 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
var source =
new ContentFieldData()
.AddJsonValue(JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array(id1, id2));
var expected =
new ContentFieldData()
.AddJsonValue(JsonValue.Array("1", "2"));
.AddJsonValue(JsonValue.Array(id1, id2));
var result = FieldConverters.ResolveAssetUrls(null, assetUrlGenerator)(source, field);
var result = FieldConverters.ResolveAssetUrls(null, urlGenerato)(source, field);
Assert.Equal(expected, result);
}

87
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

@ -12,6 +12,7 @@ using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using NodaTime;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.Scripting;
@ -34,12 +35,19 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
private readonly Instant now = SystemClock.Instance.GetCurrentInstant();
private readonly Guid contentId = Guid.NewGuid();
private readonly Guid assetId = Guid.NewGuid();
private readonly RuleEventFormatter sut;
public RuleEventFormatterTests()
{
A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId))
.Returns("content-url");
A.CallTo(() => urlGenerator.AssetContent(assetId))
.Returns("asset-content-url");
A.CallTo(() => user.Id)
.Returns("123");
.Returns("user123");
A.CallTo(() => user.Email)
.Returns("me@email.com");
@ -47,9 +55,6 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => user.Claims)
.Returns(new List<Claim> { new Claim(SquidexClaimTypes.DisplayName, "me") });
A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId))
.Returns("content-url");
var extensions = new IScriptExtension[]
{
new DateTimeScriptExtension(),
@ -93,7 +98,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Theory]
[InlineData("Name $APP_NAME has id $APP_ID")]
[InlineData("Script(`Name ${event.appId.name} has id ${event.appId.id}`)")]
public void Should_replace_app_information_from_event(string script)
public void Should_format_app_information_from_event(string script)
{
var @event = new EnrichedContentEvent { AppId = appId };
@ -105,7 +110,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Theory]
[InlineData("Name $SCHEMA_NAME has id $SCHEMA_ID")]
[InlineData("Script(`Name ${event.schemaId.name} has id ${event.schemaId.id}`)")]
public void Should_replace_schema_information_from_event(string script)
public void Should_format_schema_information_from_event(string script)
{
var @event = new EnrichedContentEvent { SchemaId = schemaId };
@ -117,7 +122,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Theory]
[InlineData("Date: $TIMESTAMP_DATE, Full: $TIMESTAMP_DATETIME")]
[InlineData("Script(`Date: ${formatDate(event.timestamp, 'yyyy-MM-dd')}, Full: ${formatDate(event.timestamp, 'yyyy-MM-dd-hh-mm-ss')}`)")]
public void Should_replace_timestamp_information_from_event(string script)
public void Should_format_timestamp_information_from_event(string script)
{
var @event = new EnrichedContentEvent { Timestamp = now };
@ -135,7 +140,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
var result = sut.Format(script, @event);
Assert.Equal("From me (me@email.com, 123)", result);
Assert.Equal("From me (me@email.com, user123)", result);
}
[Theory]
@ -147,7 +152,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
var result = sut.Format(script, @event);
Assert.Equal("From me (me@email.com, 123)", result);
Assert.Equal("From me (me@email.com, user123)", result);
}
[Theory]
@ -174,10 +179,70 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Equal("From client:android (client:android, android)", result);
}
[Theory]
[InlineData("Version: $ASSET_VERSION")]
[InlineData("Script(`Version: ${event.version}`)")]
public void Should_format_base_property(string script)
{
var @event = new EnrichedAssetEvent { Version = 13 };
var result = sut.Format(script, @event);
Assert.Equal("Version: 13", result);
}
[Theory]
[InlineData("File: $ASSET_FILENAME")]
[InlineData("Script(`File: ${event.fileName}`)")]
public void Should_format_asset_file_name_from_event(string script)
{
var @event = new EnrichedAssetEvent { FileName = "my-file.png" };
var result = sut.Format(script, @event);
Assert.Equal("File: my-file.png", result);
}
[Theory]
[InlineData("Type: $ASSET_ASSETTYPE")]
[InlineData("Script(`Type: ${event.assetType}`)")]
public void Should_format_asset_asset_type_from_event(string script)
{
var @event = new EnrichedAssetEvent { AssetType = AssetType.Audio };
var result = sut.Format(script, @event);
Assert.Equal("Type: Audio", result);
}
[Theory]
[InlineData("Download at $ASSET_CONTENT_URL")]
[InlineData("Script(`Download at ${assetContentUrl()}`)")]
public void Should_format_asset_content_url_from_event(string script)
{
var @event = new EnrichedAssetEvent { Id = assetId };
var result = sut.Format(script, @event);
Assert.Equal("Download at asset-content-url", result);
}
[Theory]
[InlineData("Download at $ASSET_CONTENT_URL")]
[InlineData("Script(`Download at ${assetContentUrl()}`)")]
public void Should_return_null_when_asset_content_url_not_found(string script)
{
var @event = new EnrichedContentEvent();
var result = sut.Format(script, @event);
Assert.Equal("Download at null", result);
}
[Theory]
[InlineData("Go to $CONTENT_URL")]
[InlineData("Script(`Go to ${contentUrl()}`)")]
public void Should_replace_content_url_from_event(string script)
public void Should_format_content_url_from_event(string script)
{
var @event = new EnrichedContentEvent { AppId = appId, Id = contentId, SchemaId = schemaId };
@ -189,7 +254,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
[Theory]
[InlineData("Go to $CONTENT_URL")]
[InlineData("Script(`Go to ${contentUrl()}`)")]
public void Should_format_content_url_when_not_found(string script)
public void Should_return_null_when_content_url_when_not_found(string script)
{
var @event = new EnrichedAssetEvent();

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -272,10 +272,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
[typeof(IAssetQueryService)] = assetQuery,
[typeof(IContentQueryService)] = contentQuery,
[typeof(IDataLoaderContextAccessor)] = dataLoaderContext,
[typeof(IGraphQLUrlGenerator)] = new FakeUrlGenerator(),
[typeof(IOptions<AssetOptions>)] = Options.Create(new AssetOptions()),
[typeof(IOptions<ContentOptions>)] = Options.Create(new ContentOptions()),
[typeof(ISemanticLog)] = A.Fake<ISemanticLog>(),
[typeof(IUrlGenerator)] = new FakeUrlGenerator(),
[typeof(DataLoaderDocumentListener)] = new DataLoaderDocumentListener(dataLoaderContext)
};

5
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ConvertDataTests.cs

@ -12,7 +12,6 @@ using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
@ -28,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public class ConvertDataTests
{
private readonly ISchemaEntity schema;
private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>();
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
@ -48,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
schema = Mocks.Schema(appId, schemaId, schemaDef);
schemaProvider = x => Task.FromResult(schema);
sut = new ConvertData(assetUrlGenerator, assetRepository, contentRepository);
sut = new ConvertData(urlGenerator, assetRepository, contentRepository);
}
[Fact]

9
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs

@ -12,7 +12,6 @@ using FakeItEasy;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.ConvertContent;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps;
@ -28,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public class ResolveAssetsTests
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IAssetUrlGenerator assetUrlGenerator = A.Fake<IAssetUrlGenerator>();
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>();
private readonly IRequestCache requestCache = A.Fake<IRequestCache>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId = NamedId.Of(Guid.NewGuid(), "my-schema");
@ -56,8 +55,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
})
.SetFieldsInLists("asset1", "asset2");
A.CallTo(() => assetUrlGenerator.GenerateUrl(A<string>._))
.ReturnsLazily(new Func<string, string>(id => $"url/to/{id}"));
A.CallTo(() => urlGenerator.AssetContent(A<Guid>._))
.ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<Guid>(0)}");
schemaProvider = x =>
{
@ -71,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
}
};
sut = new ResolveAssets(assetUrlGenerator, assetQuery, requestCache);
sut = new ResolveAssets(urlGenerator, assetQuery, requestCache);
}
[Fact]

121
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs

@ -5,35 +5,130 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.GraphQL;
using Squidex.Domain.Apps.Entities.Schemas;
using System;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.TestData
{
public sealed class FakeUrlGenerator : IGraphQLUrlGenerator
public sealed class FakeUrlGenerator : IUrlGenerator
{
public bool CanGenerateAssetSourceUrl { get; } = true;
public string GenerateAssetUrl(IAppEntity app, IAssetEntity asset)
public string? AssetThumbnail(Guid assetId, AssetType assetType)
{
return $"assets/{asset.Id}";
return $"assets/{assetId}?width=100";
}
public string GenerateAssetThumbnailUrl(IAppEntity app, IAssetEntity asset)
public string? AssetSource(Guid assetId, long fileVersion)
{
return $"assets/{asset.Id}?width=100";
return $"assets/source/{assetId}";
}
public string GenerateAssetSourceUrl(IAssetEntity asset)
public string AssetContent(Guid assetId)
{
return $"assets/source/{asset.Id}";
return $"assets/{assetId}";
}
public string GenerateContentUrl(IAppEntity app, ISchemaEntity schema, IContentEntity content)
public string ContentUI(NamedId<Guid> appId, NamedId<Guid> schemaId, Guid contentId)
{
return $"contents/{schema.SchemaDef.Name}/{content.Id}";
return $"contents/{schemaId.Name}/{contentId}";
}
public string AppSettingsUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string AssetsUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string AssetsUI(NamedId<Guid> appId, string? query = null)
{
throw new NotSupportedException();
}
public string AssetSource(Guid assetId)
{
throw new NotSupportedException();
}
public string BackupsUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string ClientsUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string ContentsUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string ContentsUI(NamedId<Guid> appId, NamedId<Guid> schemaId)
{
throw new NotSupportedException();
}
public string ContributorsUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string DashboardUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string LanguagesUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string PatternsUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string PlansUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string RolesUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string RulesUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string SchemasUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string SchemaUI(NamedId<Guid> appId, NamedId<Guid> schemaId)
{
throw new NotSupportedException();
}
public string WorkflowsUI(NamedId<Guid> appId)
{
throw new NotSupportedException();
}
public string UI()
{
throw new NotSupportedException();
}
}
}

7
backend/tests/Squidex.Domain.Apps.Entities.Tests/Notifications/NotificationEmailSenderTests.cs

@ -11,6 +11,7 @@ using System.Security.Claims;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core;
using Squidex.Infrastructure.Email;
using Squidex.Infrastructure.Log;
using Squidex.Shared.Identity;
@ -22,7 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Notifications
public class NotificationEmailSenderTests
{
private readonly IEmailSender emailSender = A.Fake<IEmailSender>();
private readonly IEmailUrlGenerator emailUrlGenerator = A.Fake<IEmailUrlGenerator>();
private readonly IUrlGenerator urlGenerator = A.Fake<IUrlGenerator>();
private readonly IUser assigner = A.Fake<IUser>();
private readonly IUser user = A.Fake<IUser>();
private readonly ISemanticLog log = A.Fake<ISemanticLog>();
@ -45,10 +46,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Notifications
A.CallTo(() => user.Claims)
.Returns(assigneeClaims);
A.CallTo(() => emailUrlGenerator.GenerateUIUrl())
A.CallTo(() => urlGenerator.UI())
.Returns(uiUrl);
sut = new NotificationEmailSender(Options.Create(texts), emailSender, emailUrlGenerator, log);
sut = new NotificationEmailSender(Options.Create(texts), emailSender, urlGenerator, log);
}
[Fact]

3
frontend/app-config/webpack.config.js

@ -40,6 +40,7 @@ module.exports = function (env) {
const isTests = env && env.target === 'tests';
const isTestCoverage = env && env.coverage;
const isAnalyzing = isProduction && env.analyze;
const isAot = isProduction;
const configFile = isTests ? 'tsconfig.spec.json' : 'tsconfig.app.json';
@ -296,7 +297,7 @@ module.exports = function (env) {
directTemplateLoading: true,
entryModule: 'app/app.module#AppModule',
sourceMap: !isProduction,
skipCodeGeneration: false,
skipCodeGeneration: !isAot,
tsConfigPath: configFile
})
);

4
frontend/app/framework/angular/forms/editable-title.component.ts

@ -8,7 +8,7 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
const ESCAPE_KEY = 27;
import { Keys } from '@app/framework/internal';
@Component({
selector: 'sqx-editable-title',
@ -56,7 +56,7 @@ export class EditableTitleComponent {
}
public onKeyDown(keyCode: number) {
if (keyCode === ESCAPE_KEY) {
if (keyCode === Keys.ESCAPE) {
this.toggleRename();
}
}

79
frontend/app/shared/components/comments/comment.component.html

@ -1,30 +1,65 @@
<div class="comment row no-gutters">
<div class="col-auto">
<div class="col-auto pr-2">
<img class="user-picture" title="{{comment.user | sqxUserNameRef}}" [src]="comment.user | sqxUserPictureRef" />
</div>
<div class="col pl-2 col-right">
<div class="comment-message">
<div class="user-row">
<div class="user-ref">{{comment.user | sqxUserNameRef}}</div>
</div>
<div [innerHTML]="comment.text | sqxMarkdown"></div>
<div class="comment-created text-muted">
<ng-container *ngIf="canFollow && comment.url">
<a [routerLink]="comment.url">Follow</a>&nbsp;
</ng-container>
{{comment.time | sqxFromNow}}
<ng-container *ngIf="!isEditing; else editing">
<div class="col col-text">
<div class="comment-message">
<div class="user-row">
<div class="user-ref">{{comment.user | sqxUserNameRef}}</div>
</div>
<div [innerHTML]="comment.text | sqxMarkdown"></div>
<div class="comment-created text-muted">
<ng-container *ngIf="canFollow && comment.url">
<a [routerLink]="comment.url">Follow</a>&nbsp;
</ng-container>
{{comment.time | sqxFromNow}}
</div>
</div>
</div>
</ng-container>
<ng-template #editing>
<div class="col">
<form (ngSubmit)="update()">
<textarea class="form-control mb-1" name="{{comment.id}}" sqxFocusOnInit
[(ngModel)]="editingText"
[mention]="mentionUsers"
[mentionConfig]="mentionConfig"
[mentionListTemplate]="mentionListTemplate"
(keydown)="updateWhenEnter($event)"></textarea>
<div>
<button type="button" class="btn btn-sm btn-secondary mr-1" (click)="cancelEdit()">
Cancel
</button>
<button type="submit" class="btn btn-sm btn-primary">
<i class="icon-enter"></i> Save
</button>
</div>
</form>
</div>
</ng-template>
<div class="actions" *ngIf="!isEditing">
<button *ngIf="isEditable && canEdit" type="button" class="btn btn-sm btn-text-secondary" (click)="startEdit()">
<i class="icon-pencil"></i>
</button>
<button *ngIf="isDeletable || canDelete" type="button" class="btn btn-sm btn-text-danger"
(sqxConfirmClick)="delete()"
confirmTitle="Delete comment"
confirmText="Do you really want to delete the comment?"
[confirmRequired]="confirmDelete">
<i class="icon-bin2"></i>
</button>
</div>
</div>
<button *ngIf="comment.user === userToken || canDelete" type="button" class="btn btn-sm btn-text-danger item-remove"
(sqxConfirmClick)="delete.emit()"
confirmTitle="Delete comment"
confirmText="Do you really want to delete the comment?"
[confirmRequired]="confirmDelete">
<i class="icon-bin2"></i>
</button>
</div>
<ng-template #mentionListTemplate let-item="item">
{{item['contributorEmail']}}
</ng-template>

8
frontend/app/shared/components/comments/comment.component.scss

@ -1,5 +1,7 @@
.item-remove {
.actions {
@include absolute(-5px, -15px, auto, auto);
background: $color-table-background;
border: 0;
display: none;
}
@ -17,7 +19,7 @@
}
}
.col-right {
.col-text {
overflow-wrap: break-word;
overflow-x: hidden;
}
@ -41,7 +43,7 @@
}
&:hover {
.item-remove {
.actions {
display: block;
}
}

93
frontend/app/shared/components/comments/comment.component.ts

@ -5,9 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { MentionConfig } from 'angular-mentions';
import { CommentDto } from '@app/shared/internal';
import {
CommentDto,
CommentsState,
ContributorDto,
DialogService,
Keys
} from '@app/shared/internal';
@Component({
selector: 'sqx-comment',
@ -15,15 +22,18 @@ import { CommentDto } from '@app/shared/internal';
templateUrl: './comment.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CommentComponent {
@Output()
public delete = new EventEmitter();
export class CommentComponent implements OnChanges {
@Input()
public canFollow = false;
@Input()
public canDelete = false;
@Input()
public canFollow = false;
public canEdit = false;
@Input()
public commentsState: CommentsState;
@Input()
public confirmDelete = true;
@ -33,4 +43,75 @@ export class CommentComponent {
@Input()
public userToken: string;
@Input()
public mentionUsers: ReadonlyArray<ContributorDto>;
public mentionConfig: MentionConfig = { dropUp: true, labelKey: 'contributorEmail' };
public isDeletable = false;
public isEditable = false;
public isEditing = false;
public editingText: string;
constructor(
private readonly dialogs: DialogService
) {
}
public ngOnChanges() {
const isMyComment = this.comment.user === this.userToken;
this.isDeletable = isMyComment;
this.isEditable = isMyComment;
}
public startEdit() {
this.editingText = this.comment.text;
this.isEditing = true;
}
public cancelEdit() {
this.isEditing = false;
}
public delete() {
if (!this.isDeletable) {
return;
}
this.commentsState.delete(this.comment);
}
public updateWhenEnter(event: KeyboardEvent) {
if (event.keyCode === Keys.ENTER && !event.altKey && !event.shiftKey && !event.defaultPrevented) {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
this.update();
}
}
public update() {
if (!this.isEditable) {
return;
}
const text = this.editingText;
if (!text || text.length === 0) {
this.dialogs.confirm('Delete comment', 'Do you really want to delete the comment?')
.subscribe(() => {
this.delete();
});
} else {
this.commentsState.update(this.comment, text);
this.cancelEdit();
}
}
}

54
frontend/app/shared/components/comments/comments.component.html

@ -4,31 +4,37 @@
</ng-container>
<ng-container content>
<div class="comments-list" #scrollMe [scrollTop]="scrollMe.scrollHeight">
<sqx-comment *ngFor="let comment of commentsState.comments | async; trackBy: trackByComment"
[comment]="comment"
[canDelete]="true"
[canFollow]="false"
(delete)="delete(comment)"
[userToken]="userToken">
</sqx-comment>
</div>
<div class="comments-footer">
<ng-template #mentionListTemplate let-item="item">
{{item['contributorEmail']}}
</ng-template>
<ng-container *ngIf="mentionUsers | async; let users">
<div class="comments-list" #commentsList>
<div (sqxResized)="scrollDown()">
<sqx-comment *ngFor="let comment of commentsState.comments | async; trackBy: trackByComment"
[comment]="comment"
[commentsState]="commentsState"
[mentionUsers]="users"
[canEdit]="true"
[canFollow]="false"
[userToken]="userToken">
</sqx-comment>
</div>
</div>
<form [formGroup]="commentForm.form" (ngSubmit)="comment()">
<input class="form-control" name="text" formControlName="text" placeholder="Create a comment"
[mention]="mentionUsers | async"
[mentionConfig]="mentionConfig"
[mentionListTemplate]="mentionListTemplate"
autocomplete="off"
autocorrect="off"
autocapitalize="off" />
</form>
</div>
<div class="comments-footer">
<ng-template #mentionListTemplate let-item="item">
{{item['contributorEmail']}}
</ng-template>
<form [formGroup]="commentForm.form" (ngSubmit)="comment()">
<input class="form-control" name="text" formControlName="text" placeholder="Create a comment"
[mention]="users"
[mentionConfig]="mentionConfig"
[mentionListTemplate]="mentionListTemplate"
autocomplete="off"
autocorrect="off"
autocapitalize="off" />
</form>
</div>
</ng-container>
</ng-container>
</sqx-panel>

31
frontend/app/shared/components/comments/comments.component.ts

@ -5,9 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, ElementRef, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { MentionConfig } from 'angular-mentions';
import { timer } from 'rxjs';
import { onErrorResumeNext, switchMap } from 'rxjs/operators';
@ -23,12 +24,20 @@ import {
UpsertCommentForm
} from '@app/shared/internal';
import { CommentComponent } from './comment.component';
@Component({
selector: 'sqx-comments',
styleUrls: ['./comments.component.scss'],
templateUrl: './comments.component.html'
})
export class CommentsComponent extends ResourceOwner implements OnInit {
@ViewChild('commentsList', { static: false })
public commentsList: ElementRef<HTMLDivElement>;
@ViewChildren(CommentComponent)
public children: QueryList<CommentComponent>;
@Input()
public commentsId: string;
@ -37,7 +46,7 @@ export class CommentsComponent extends ResourceOwner implements OnInit {
public commentForm = new UpsertCommentForm(this.formBuilder);
public mentionUsers = this.contributorsState.contributors;
public mentionConfig = { dropUp: true, labelKey: 'contributorEmail' };
public mentionConfig: MentionConfig = { dropUp: true, labelKey: 'contributorEmail' };
public userToken: string;
@ -64,12 +73,20 @@ export class CommentsComponent extends ResourceOwner implements OnInit {
this.own(timer(0, 4000).pipe(switchMap(() => this.commentsState.load(true).pipe(onErrorResumeNext()))));
}
public delete(comment: CommentDto) {
this.commentsState.delete(comment);
}
public scrollDown() {
if (this.commentsList && this.commentsList.nativeElement) {
let isEditing = false;
public update(comment: CommentDto, text: string) {
this.commentsState.update(comment, text);
this.children.forEach(x => {
isEditing = isEditing || x.isEditing;
});
if (!isEditing) {
const height = this.commentsList.nativeElement.scrollHeight;
this.commentsList.nativeElement.scrollTop = height;
}
}
}
public comment() {

2
frontend/app/shell/pages/internal/notifications-menu.component.html

@ -17,10 +17,10 @@
<sqx-comment *ngFor="let comment of comments; trackBy: trackByComment"
[comment]="comment"
[commentsState]="commentsState"
[confirmDelete]="false"
[canDelete]="true"
[canFollow]="true"
(delete)="delete(comment)"
[userToken]="userToken">
</sqx-comment>
</ng-container>

4
frontend/app/shell/pages/internal/notifications-menu.component.ts

@ -92,10 +92,6 @@ export class NotificationsMenuComponent extends ResourceOwner implements OnInit
this.own(timer(0, 4000).pipe(switchMap(() => this.commentsState.load(true).pipe(onErrorResumeNext()))));
}
public delete(comment: CommentDto) {
this.commentsState.delete(comment);
}
public trackByComment(comment: CommentDto) {
return comment.id;
}

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

@ -9,10 +9,24 @@
<link rel="stylesheet" href="style.css"></head>
<body>
<div class="bgc1 clearfix">
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;135)</small></h1>
<h1 class="mhmm mvm"><span class="fgc1">Font Name:</span> icomoon <small class="fgc1">(Glyphs:&nbsp;136)</small></h1>
</div>
<div class="clearfix mhl ptl">
<h1 class="mvm mtn fgc1">Grid Size: 24</h1>
<div class="glyph fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-enter"></span>
<span class="mls"> icon-enter</span>
</div>
<fieldset class="fs0 size1of1 clearfix hidden-false">
<input type="text" readonly value="e984" class="unit size1of2" />
<input type="text" maxlength="1" readonly value="&#xe984;" 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 fs1">
<div class="clearfix bshadow0 pbs">
<span class="icon-zoom_out"></span>

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

Binary file not shown.

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

@ -139,6 +139,7 @@
<glyph unicode="&#xe981;" glyph-name="rotate_left" d="M554 764.667q126-16 213-112t87-226-87-226-213-112v86q92 16 153 87t61 165-61 165-153 87v-166l-194 190 194 194v-132zM302 156.667l62 62q46-34 106-44v-86q-96 12-168 68zM260 384.667q10-58 42-106l-60-60q-56 74-68 166h86zM304 574.667q-36-52-44-106h-86q12 90 70 166z" />
<glyph unicode="&#xe982;" glyph-name="zoom_out" d="M298 554.667h214v-42h-214v42zM406 340.667q80 0 136 56t56 136-56 136-136 56-136-56-56-136 56-136 136-56zM662 340.667l212-212-64-64-212 212v34l-12 12q-76-66-180-66-116 0-197 80t-81 196 81 197 197 81 196-81 80-197q0-42-20-95t-46-85l12-12h34z" />
<glyph unicode="&#xe983;" glyph-name="zoom_in" d="M512 512.667h-86v-86h-42v86h-86v42h86v86h42v-86h86v-42zM406 340.667q80 0 136 56t56 136-56 136-136 56-136-56-56-136 56-136 136-56zM662 340.667l212-212-64-64-212 212v34l-12 12q-76-66-180-66-116 0-197 80t-81 196 81 197 197 81 196-81 80-197q0-42-20-95t-46-85l12-12h34z" />
<glyph unicode="&#xe984;" glyph-name="enter" d="M470 554.667l60-60-154-154h392v428h86v-512h-478l154-154-60-60-256 256z" />
<glyph unicode="&#xe9ca;" glyph-name="earth" d="M512 960c-282.77 0-512-229.23-512-512s229.23-512 512-512 512 229.23 512 512-229.23 512-512 512zM512-0.002c-62.958 0-122.872 13.012-177.23 36.452l233.148 262.29c5.206 5.858 8.082 13.422 8.082 21.26v96c0 17.674-14.326 32-32 32-112.99 0-232.204 117.462-233.374 118.626-6 6.002-14.14 9.374-22.626 9.374h-128c-17.672 0-32-14.328-32-32v-192c0-12.122 6.848-23.202 17.69-28.622l110.31-55.156v-187.886c-116.052 80.956-192 215.432-192 367.664 0 68.714 15.49 133.806 43.138 192h116.862c8.488 0 16.626 3.372 22.628 9.372l128 128c6 6.002 9.372 14.14 9.372 22.628v77.412c40.562 12.074 83.518 18.588 128 18.588 70.406 0 137.004-16.26 196.282-45.2-4.144-3.502-8.176-7.164-12.046-11.036-36.266-36.264-56.236-84.478-56.236-135.764s19.97-99.5 56.236-135.764c36.434-36.432 85.218-56.264 135.634-56.26 3.166 0 6.342 0.080 9.518 0.236 13.814-51.802 38.752-186.656-8.404-372.334-0.444-1.744-0.696-3.488-0.842-5.224-81.324-83.080-194.7-134.656-320.142-134.656z" />
<glyph unicode="&#xf00a;" glyph-name="grid" d="M292.571 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM292.571 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 237.714v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM658.286 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 530.286v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857zM1024 822.857v-109.714c0-30.286-24.571-54.857-54.857-54.857h-182.857c-30.286 0-54.857 24.571-54.857 54.857v109.714c0 30.286 24.571 54.857 54.857 54.857h182.857c30.286 0 54.857-24.571 54.857-54.857z" />
<glyph unicode="&#xf0c9;" glyph-name="list1" horiz-adv-x="878" d="M877.714 182.857v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 475.428v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571zM877.714 768v-73.143c0-20-16.571-36.571-36.571-36.571h-804.571c-20 0-36.571 16.571-36.571 36.571v73.143c0 20 16.571 36.571 36.571 36.571h804.571c20 0 36.571-16.571 36.571-36.571z" />

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

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

Binary file not shown.

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

Binary file not shown.

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

File diff suppressed because one or more lines are too long

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

@ -1,10 +1,10 @@
@font-face {
font-family: 'icomoon';
src: url('fonts/icomoon.eot?fxsj8g');
src: url('fonts/icomoon.eot?fxsj8g#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?fxsj8g') format('truetype'),
url('fonts/icomoon.woff?fxsj8g') format('woff'),
url('fonts/icomoon.svg?fxsj8g#icomoon') format('svg');
src: url('fonts/icomoon.eot?uop9lr');
src: url('fonts/icomoon.eot?uop9lr#iefix') format('embedded-opentype'),
url('fonts/icomoon.ttf?uop9lr') format('truetype'),
url('fonts/icomoon.woff?uop9lr') format('woff'),
url('fonts/icomoon.svg?uop9lr#icomoon') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
@ -25,6 +25,9 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-enter:before {
content: "\e984";
}
.icon-zoom_out:before {
content: "\e982";
}

Loading…
Cancel
Save