Browse Source

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

pull/351/head
Sebastian 7 years ago
parent
commit
7d852651a9
  1. 7
      CHANGELOG.md
  2. 2
      README.md
  3. 2
      src/Squidex.Domain.Users/UserValues.cs
  4. 2
      src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs
  5. 65
      src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs
  6. 11
      src/Squidex.Infrastructure/Log/LockingLogStore.cs
  7. 13
      src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  8. 2
      src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  9. 7
      src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs
  10. 11
      src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs
  11. 1
      src/Squidex/Areas/IdentityServer/Startup.cs
  12. 2
      src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml
  13. 4
      src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml
  14. 2
      src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml
  15. 3
      src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml
  16. 5
      src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml
  17. 44
      src/Squidex/Areas/IdentityServer/Views/Extensions.cs
  18. 4
      src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
  19. 4
      src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml
  20. 1
      src/Squidex/Areas/IdentityServer/Views/_ViewImports.cshtml
  21. 1
      src/Squidex/Config/Authentication/OidcServices.cs
  22. 2
      src/Squidex/Config/Constants.cs
  23. 3
      src/Squidex/Config/Domain/EntitiesServices.cs
  24. 31
      src/Squidex/Config/Startup/RebuilderHost.cs
  25. 44
      src/Squidex/Pipeline/AssetRequestSizeLimitAttribute.cs
  26. 7
      src/Squidex/Pipeline/Squid/SquidMiddleware.cs
  27. 71
      src/Squidex/Squidex.csproj
  28. 4
      src/Squidex/WebStartup.cs
  29. 2
      src/Squidex/app/features/content/module.ts
  30. 6
      src/Squidex/app/features/content/shared/field-editor.component.html
  31. 16
      src/Squidex/app/framework/angular/forms/autocomplete.component.ts
  32. 4
      src/Squidex/app/framework/angular/forms/code-editor.component.ts
  33. 17
      src/Squidex/app/framework/angular/forms/color-picker.component.html
  34. 18
      src/Squidex/app/framework/angular/forms/color-picker.component.scss
  35. 59
      src/Squidex/app/framework/angular/forms/color-picker.component.ts
  36. 12
      src/Squidex/app/framework/angular/modals/modal-target.directive.ts
  37. 2
      src/Squidex/app/framework/angular/modals/modal-view.directive.ts
  38. 4
      src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts
  39. 1
      src/Squidex/app/framework/declarations.ts
  40. 7
      src/Squidex/app/framework/module.ts
  41. 45
      src/Squidex/app/framework/utils/math-helper.spec.ts
  42. 89
      src/Squidex/app/framework/utils/math-helper.ts
  43. 2
      src/Squidex/app/framework/utils/modal-positioner.ts
  44. 12
      src/Squidex/app/shared/services/assets.service.ts
  45. 23
      src/Squidex/appsettings.json
  46. 48
      tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs
  47. BIN
      tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg
  48. BIN
      tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png
  49. 8
      tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj
  50. 3
      tools/Migrate_01/MigrationPath.cs
  51. 22
      tools/Migrate_01/RebuildOptions.cs
  52. 57
      tools/Migrate_01/RebuildRunner.cs
  53. 78
      tools/Migrate_01/Rebuilder.cs

7
CHANGELOG.md

@ -1,5 +1,12 @@
# Changelog
## v1.16.2 - 2019-03-16
### Bugfixes
* **UI**: Corrections for auto completions.
* **UI**: Correctly close onboarding tooltips.
## v1.16.1 - 2019-03-08
### Bugfixes

2
README.md

@ -16,7 +16,7 @@ Please join our community forum: https://support.squidex.io
## Status
Current Version 1.16.0. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
Current Version 1.16.2. Roadmap: https://trello.com/b/KakM4F3S/squidex-roadmap
## Prerequisites

2
src/Squidex.Domain.Users/UserValues.cs

@ -44,7 +44,7 @@ namespace Squidex.Domain.Users
if (Hidden.HasValue)
{
yield return new Claim(SquidexClaimTypes.Consent, Hidden.ToString());
yield return new Claim(SquidexClaimTypes.Hidden, Hidden.ToString());
}
if (Consent.HasValue)

2
src/Squidex.Infrastructure/Assets/IAssetThumbnailGenerator.cs

@ -14,6 +14,6 @@ namespace Squidex.Infrastructure.Assets
{
Task<ImageInfo> GetImageInfoAsync(Stream source);
Task CreateThumbnailAsync(Stream source, Stream destination, int? width, int? height, string mode);
Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string mode = null, int? quality = null);
}
}

65
src/Squidex.Infrastructure/Assets/ImageSharp/ImageSharpAssetThumbnailGenerator.cs

@ -9,6 +9,7 @@ using System;
using System.IO;
using System.Threading.Tasks;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Transforms;
using SixLabors.Primitives;
@ -17,49 +18,59 @@ namespace Squidex.Infrastructure.Assets.ImageSharp
{
public sealed class ImageSharpAssetThumbnailGenerator : IAssetThumbnailGenerator
{
public ImageSharpAssetThumbnailGenerator()
{
Configuration.Default.ImageFormatsManager.AddImageFormat(ImageFormats.Jpeg);
Configuration.Default.ImageFormatsManager.AddImageFormat(ImageFormats.Png);
}
public Task CreateThumbnailAsync(Stream source, Stream destination, int? width, int? height, string mode)
public Task CreateThumbnailAsync(Stream source, Stream destination, int? width = null, int? height = null, string mode = null, int? quality = null)
{
return Task.Run(() =>
{
if (width == null && height == null)
if (!width.HasValue && !height.HasValue && !quality.HasValue)
{
source.CopyTo(destination);
return;
}
var isCropUpsize = string.Equals("CropUpsize", mode, StringComparison.OrdinalIgnoreCase);
if (!Enum.TryParse<ResizeMode>(mode, true, out var resizeMode))
{
resizeMode = ResizeMode.Max;
}
if (isCropUpsize)
using (var sourceImage = Image.Load(source, out var format))
{
resizeMode = ResizeMode.Crop;
}
var encoder = Configuration.Default.ImageFormatsManager.FindEncoder(format);
var w = width ?? 0;
var h = height ?? 0;
if (quality.HasValue)
{
encoder = new JpegEncoder { Quality = quality.Value };
}
using (var sourceImage = Image.Load(source, out var format))
{
if (w >= sourceImage.Width && h >= sourceImage.Height && resizeMode == ResizeMode.Crop && !isCropUpsize)
if (encoder == null)
{
resizeMode = ResizeMode.BoxPad;
throw new NotSupportedException();
}
var options = new ResizeOptions { Size = new Size(w, h), Mode = resizeMode };
if (width.HasValue || height.HasValue)
{
var isCropUpsize = string.Equals("CropUpsize", mode, StringComparison.OrdinalIgnoreCase);
if (!Enum.TryParse<ResizeMode>(mode, true, out var resizeMode))
{
resizeMode = ResizeMode.Max;
}
if (isCropUpsize)
{
resizeMode = ResizeMode.Crop;
}
var resizeWidth = width ?? 0;
var resizeHeight = height ?? 0;
if (resizeWidth >= sourceImage.Width && resizeHeight >= sourceImage.Height && resizeMode == ResizeMode.Crop && !isCropUpsize)
{
resizeMode = ResizeMode.BoxPad;
}
var options = new ResizeOptions { Size = new Size(resizeWidth, resizeHeight), Mode = resizeMode };
sourceImage.Mutate(x => x.Resize(options));
}
sourceImage.Mutate(x => x.Resize(options));
sourceImage.Save(destination, format);
sourceImage.Save(destination, encoder);
}
});
}

11
src/Squidex.Infrastructure/Log/LockingLogStore.cs

@ -52,10 +52,17 @@ namespace Squidex.Infrastructure.Log
break;
}
await Task.Delay(2000, cts.Token);
try
{
await Task.Delay(2000, cts.Token);
}
catch (TaskCanceledException)
{
break;
}
}
if (!cts.IsCancellationRequested)
if (releaseToken != null)
{
try
{

13
src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -51,6 +51,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// <param name="version">The optional version of the asset.</param>
/// <param name="width">The target width of the asset, if it is an image.</param>
/// <param name="height">The target height of the asset, if it is an image.</param>
/// <param name="quality">Optional image quality, it is is an jpeg image.</param>
/// <param name="mode">The resize mode when the width and height is defined.</param>
/// <returns>
/// 200 => Asset found and content or (resized) image returned.
@ -64,11 +65,12 @@ namespace Squidex.Areas.Api.Controllers.Assets
[FromQuery] long version = EtagVersion.Any,
[FromQuery] int? width = null,
[FromQuery] int? height = null,
[FromQuery] int? quality = null,
[FromQuery] string mode = null)
{
var entity = await assetRepository.FindAssetAsync(id);
if (entity == null || entity.FileVersion < version || width == 0 || height == 0)
if (entity == null || entity.FileVersion < version || width == 0 || height == 0 || quality == 0)
{
return NotFound();
}
@ -79,10 +81,15 @@ namespace Squidex.Areas.Api.Controllers.Assets
{
var assetId = entity.Id.ToString();
if (entity.IsImage && (width.HasValue || height.HasValue))
if (entity.IsImage && (width.HasValue || height.HasValue || quality.HasValue))
{
var assetSuffix = $"{width}_{height}_{mode}";
if (quality.HasValue)
{
assetSuffix += $"_{quality}";
}
try
{
await assetStore.DownloadAsync(assetId, entity.FileVersion, assetSuffix, bodyStream);
@ -103,7 +110,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
using (Profiler.Trace("ResizeImage"))
{
await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, width, height, mode);
await assetThumbnailGenerator.CreateThumbnailAsync(sourceStream, destinationStream, width, height, mode, quality);
destinationStream.Position = 0;
}

2
src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -171,6 +171,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("apps/{app}/assets/")]
[ProducesResponseType(typeof(AssetCreatedDto), 201)]
[ProducesResponseType(typeof(ErrorDto), 400)]
[AssetRequestSizeLimit]
[ApiPermission(Permissions.AppAssetsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostAsset(string app, [SwaggerIgnore] List<IFormFile> file)
@ -233,6 +234,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
[HttpPut]
[Route("apps/{app}/assets/{id}/")]
[ProducesResponseType(typeof(ErrorDto), 400)]
[AssetRequestSizeLimit]
[ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutAsset(string app, Guid id, [FromBody] UpdateAssetDto request)

7
src/Squidex/Areas/IdentityServer/Controllers/Account/AccountController.cs

@ -57,6 +57,13 @@ namespace Squidex.Areas.IdentityServer.Controllers.Account
this.signInManager = signInManager;
}
[HttpGet]
[Route("account/error/")]
public IActionResult LoginError()
{
throw new InvalidOperationException();
}
[HttpGet]
[Route("account/forbidden/")]
public IActionResult Forbidden()

11
src/Squidex/Areas/IdentityServer/Controllers/Extensions.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
@ -20,7 +21,14 @@ namespace Squidex.Areas.IdentityServer.Controllers
{
var externalLogin = await signInManager.GetExternalLoginInfoAsync(expectedXsrf);
externalLogin.ProviderDisplayName = externalLogin.Principal.FindFirst(ClaimTypes.Email).Value;
var email = externalLogin.Principal.FindFirst(ClaimTypes.Email)?.Value;
if (string.IsNullOrWhiteSpace(email))
{
throw new InvalidOperationException("External provider does not provide email claim.");
}
externalLogin.ProviderDisplayName = email;
return externalLogin;
}
@ -28,6 +36,7 @@ namespace Squidex.Areas.IdentityServer.Controllers
public static async Task<List<ExternalProvider>> GetExternalProvidersAsync(this SignInManager<IdentityUser> signInManager)
{
var externalSchemes = await signInManager.GetExternalAuthenticationSchemesAsync();
var externalProviders =
externalSchemes.Where(x => x.Name != OpenIdConnectDefaults.AuthenticationScheme)
.Select(x => new ExternalProvider(x.Name, x.DisplayName)).ToList();

1
src/Squidex/Areas/IdentityServer/Startup.cs

@ -34,7 +34,6 @@ namespace Squidex.Areas.IdentityServer
identityApp.UseMyIdentityServer();
identityApp.UseStaticFiles();
identityApp.UseMvc();
});
}

2
src/Squidex/Areas/IdentityServer/Views/Account/AccessDenied.cshtml

@ -3,7 +3,7 @@
ViewBag.Title = "Account locked";
}
<img class="splash-image" src="~/../squid.svg?title=STOP%20HERE&text=You%20shall%20not%20pass!" />
<img class="splash-image" src="@Url.RootContentUrl("~/squid.svg?title=STOP%20HERE&text=You%20shall%20not%20pass!")" />
<h1 class="splash-h1">Access denied</h1>

4
src/Squidex/Areas/IdentityServer/Views/Account/LockedOut.cshtml

@ -2,7 +2,7 @@
ViewBag.Title = "Account locked";
}
<img class="splash-image" src="~/../squid.svg?title=STOP HERE&text=You%20shall%20not%20pass!" />
<img class="splash-image" src="@Url.RootContentUrl("~/squid.svg?title=STOP HERE&text=You%20shall%20not%20pass!")" />
<h1 class="splash-h1">Account locked</h1>
@ -11,5 +11,5 @@
</p>
<p class="splash-text">
<a href="~/account/logout-redirect">Logout</a>
<a asp-controller="Account" asp-action="LogoutRedirect">Logout</a>
</p>

2
src/Squidex/Areas/IdentityServer/Views/Account/Login.cshtml

@ -36,7 +36,7 @@
<div class="form-group">
<button class="btn external-button btn-block btn btn-@schema" type="submit" name="provider" value="@provider.AuthenticationScheme">
<i class="icon-@schema external-icon"></i> @type with <strong>@provider.AuthenticationScheme</strong>
<i class="icon-@schema external-icon"></i> @type with <strong>@provider.DisplayName</strong>
</button>
</div>
}

3
src/Squidex/Areas/IdentityServer/Views/Account/LogoutCompleted.cshtml

@ -1,9 +1,8 @@
@{
ViewBag.Theme = "white";
ViewBag.Title = "Logout";
}
<img class="splash-image" src="~/../squid.svg?title=BYE%20BYE&text=Hope%20to%20see%20you%20again%20soon!&face=happy" />
<img class="splash-image" src="@Url.Content("~/squid.svg?title=BYE%20BYE&text=Hope%20to%20see%20you%20again%20soon!&face=happy")" />
<h1 class="splash-h1">Logged out!</h1>

5
src/Squidex/Areas/IdentityServer/Views/Error/Error.cshtml

@ -1,11 +1,10 @@
@model Squidex.Areas.IdentityServer.Controllers.Error.ErrorViewModel
@{
ViewBag.Theme = "white";
ViewBag.Title = "Operation failed";
ViewBag.Title = "Error";
}
<img class="splash-image" src="~/../squid.svg?title=OH%20DAMN&text=I%20am%20sorry%2C%20that%20something%20went%20wrong" />
<img class="splash-image" src="@Url.RootContentUrl("~/squid.svg?title=OH%20DAMN&text=I%20am%20sorry%2C%20that%20something%20went%20wrong")" />
<h1 class="splash-h1">Operation failed</h1>

44
src/Squidex/Areas/IdentityServer/Views/Extensions.cs

@ -0,0 +1,44 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Squidex.Areas.IdentityServer.Views
{
public static class Extensions
{
public static string RootContentUrl(this IUrlHelper urlHelper, string contentPath)
{
if (string.IsNullOrEmpty(contentPath))
{
return null;
}
if (contentPath[0] == '~')
{
var segment = new PathString(contentPath.Substring(1));
var applicationPath = urlHelper.ActionContext.HttpContext.Request.PathBase;
if (applicationPath.HasValue)
{
var indexOfLastPart = applicationPath.Value.LastIndexOf('/');
if (indexOfLastPart >= 0)
{
applicationPath = applicationPath.Value.Substring(0, indexOfLastPart);
}
}
return applicationPath.Add(segment).Value;
}
return contentPath;
}
}
}

4
src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml

@ -1,6 +1,8 @@
@model Squidex.Areas.IdentityServer.Controllers.Profile.ProfileVM
@{
ViewBag.Class = "profile-lg";
ViewBag.Title = "Profile";
}
@ -24,7 +26,7 @@
<div class="row profile-section">
<div class="col profile-picture-col">
<img class="profile-picture" src="@Url.Content($"~/../api/users/{Model.Id}/picture/")" />
<img class="profile-picture" src="@Url.RootContentUrl($"~/api/users/{Model.Id}/picture/")" />
</div>
<div class="col">
<form id="pictureForm" class="profile-picture-form" asp-controller="Profile" asp-action="UploadPicture" method="post" enctype="multipart/form-data">

4
src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml

@ -9,7 +9,7 @@
<title>@ViewBag.Title - Squidex Headless CMS</title>
<environment names="Production">
<link rel="stylesheet" asp-append-version="true" href="~/build/app.css" />
<link rel="stylesheet" asp-append-version="true" href="@Url.RootContentUrl("~/build/app.css")" />
</environment>
@if (IsSectionDefined("header"))
@ -19,7 +19,7 @@
</head>
<body class="white">
<div class="profile @ViewBag.Class">
<img class="profile-logo" src="~/images/logo-small.png" />
<img class="profile-logo" src="@Url.RootContentUrl("~/images/logo-small.png")" />
@RenderBody()
</div>

1
src/Squidex/Areas/IdentityServer/Views/_ViewImports.cshtml

@ -1,3 +1,4 @@
@using Microsoft.AspNetCore.Identity
@using Squidex.Areas.IdentityServer.Views;
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

1
src/Squidex/Config/Authentication/OidcServices.cs

@ -24,6 +24,7 @@ namespace Squidex.Config.Authentication
options.Authority = identityOptions.OidcAuthority;
options.ClientId = identityOptions.OidcClient;
options.ClientSecret = identityOptions.OidcSecret;
options.Scope.Add(Constants.EmailScope);
options.Scope.Add(Constants.PermissionsScope);
options.RequireHttpsMetadata = false;
});

2
src/Squidex/Config/Constants.cs

@ -23,6 +23,8 @@ namespace Squidex.Config
public static readonly string PortalPrefix = "/portal";
public static readonly string EmailScope = "email";
public static readonly string RoleScope = "role";
public static readonly string PermissionsScope = "permissions";

3
src/Squidex/Config/Domain/EntitiesServices.cs

@ -239,6 +239,9 @@ namespace Squidex.Config.Domain
services.AddTransientAs<Rebuilder>()
.AsSelf();
services.AddTransientAs<RebuildRunner>()
.AsSelf();
services.AddTransientAs<MigrationPath>()
.As<IMigrationPath>();

31
src/Squidex/Config/Startup/RebuilderHost.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Migrate_01;
using Squidex.Infrastructure.Log;
namespace Squidex.Config.Startup
{
public sealed class RebuilderHost : SafeHostedService
{
private readonly RebuildRunner rebuildRunner;
public RebuilderHost(IApplicationLifetime lifetime, ISemanticLog log, RebuildRunner rebuildRunner)
: base(lifetime, log)
{
this.rebuildRunner = rebuildRunner;
}
protected override Task StartAsync(ISemanticLog log, CancellationToken ct)
{
return rebuildRunner.RunAsync(ct);
}
}
}

44
src/Squidex/Pipeline/AssetRequestSizeLimitAttribute.cs

@ -0,0 +1,44 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Entities.Assets;
namespace Squidex.Pipeline
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class AssetRequestSizeLimitAttribute : Attribute, IFilterFactory, IOrderedFilter
{
public int Order { get; set; } = 900;
public bool IsReusable => true;
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
var assetOptions = serviceProvider.GetService<IOptions<AssetOptions>>();
if (assetOptions?.Value.MaxSize > 0)
{
var filter = serviceProvider.GetRequiredService<RequestSizeLimitFilter>();
filter.Bytes = assetOptions.Value.MaxSize;
return filter;
}
else
{
var filter = serviceProvider.GetRequiredService<DisableRequestSizeLimitFilter>();
return filter;
}
}
}
}

7
src/Squidex/Pipeline/Squid/SquidMiddleware.cs

@ -84,13 +84,16 @@ namespace Squidex.Pipeline.Squid
svg = svg.Replace("{{TEXT3}}", l3);
svg = svg.Replace("[COLOR]", background);
context.Response.StatusCode = 200;
context.Response.ContentType = "image/svg+xml";
context.Response.Headers["Cache-Control"] = "public, max-age=604800";
await context.Response.WriteAsync(svg);
}
await next(context);
else
{
await next(context);
}
}
private static (string, string, string) SplitText(string text)

71
src/Squidex/Squidex.csproj

@ -14,26 +14,6 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Docs\*.md" />
<EmbeddedResource Include="Areas\IdentityServer\Config\Cert\*.*" />
<EmbeddedResource Include="Areas\Api\Controllers\Users\Assets\Avatar.png" />
<EmbeddedResource Include="Pipeline\Squid\*.svg" />
<Compile Remove="Assets\**" />
<Content Remove="Assets\**" />
<Content Remove="package-lock.json" />
<Content Remove="package.json" />
<Content Remove="tsconfig.json" />
<Content Remove="tslint.json" />
</ItemGroup>
<ItemGroup>
<None Remove="Pipeline\Squid\icon-happy-sm.svg" />
<None Remove="Pipeline\Squid\icon-sad-sm.svg" />
</ItemGroup>
<ItemGroup>
<None Update="dockerfile">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
@ -120,6 +100,57 @@
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Areas\Api\Controllers\Users\Assets\Avatar.png" />
<EmbeddedResource Include="Areas\IdentityServer\Config\Cert\IdentityCert.pfx" />
<EmbeddedResource Include="Areas\IdentityServer\Config\Cert\IdentityCert.snk" />
<EmbeddedResource Include="Docs\schemabody.md" />
<EmbeddedResource Include="Docs\schemaquery.md" />
<EmbeddedResource Include="Docs\security.md" />
<EmbeddedResource Include="Pipeline\Squid\icon-happy-sm.svg" />
<EmbeddedResource Include="Pipeline\Squid\icon-happy.svg" />
<EmbeddedResource Include="Pipeline\Squid\icon-sad-sm.svg" />
<EmbeddedResource Include="Pipeline\Squid\icon-sad.svg" />
<EmbeddedResource Remove="Assets\**" />
<EmbeddedResource Remove="_test-output\**" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Assets\**" />
<Compile Remove="_test-output\**" />
</ItemGroup>
<ItemGroup>
<Content Remove="Assets\**" />
<Content Remove="package-lock.json" />
<Content Remove="package.json" />
<Content Remove="tsconfig.json" />
<Content Remove="tslint.json" />
<Content Remove="_test-output\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Areas\Api\Controllers\Users\Assets\Avatar.png" />
<None Remove="Areas\IdentityServer\Config\Cert\IdentityCert.pfx" />
<None Remove="Areas\IdentityServer\Config\Cert\IdentityCert.snk" />
<None Remove="Docs\schemabody.md" />
<None Remove="Docs\schemaquery.md" />
<None Remove="Docs\security.md" />
<None Remove="Pipeline\Squid\icon-happy-sm.svg" />
<None Remove="Pipeline\Squid\icon-happy.svg" />
<None Remove="Pipeline\Squid\icon-sad-sm.svg" />
<None Remove="Pipeline\Squid\icon-sad.svg" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\**" />
<None Include="package-lock.json" />
<None Include="package.json" />
<None Include="tsconfig.json" />
<None Include="tslint.json" />
<None Remove="_test-output\**" />
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);CS1591;1591;1573;1572;NU1605</NoWarn>
</PropertyGroup>

4
src/Squidex/WebStartup.cs

@ -9,6 +9,7 @@ using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Migrate_01;
using Squidex.Areas.Api;
using Squidex.Areas.Api.Config.Swagger;
using Squidex.Areas.Api.Controllers.Contents;
@ -82,6 +83,8 @@ namespace Squidex
config.GetSection("healthz:gc"));
services.Configure<ETagOptions>(
config.GetSection("etags"));
services.Configure<RebuildOptions>(
config.GetSection("rebuild"));
services.Configure<MyContentsControllerOptions>(
config.GetSection("contentsController"));
@ -100,6 +103,7 @@ namespace Squidex
{
afterServices.AddHostedService<InitializerHost>();
afterServices.AddHostedService<MigratorHost>();
afterServices.AddHostedService<RebuilderHost>();
});
return provider;

2
src/Squidex/app/features/content/module.ts

@ -8,7 +8,6 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DndModule } from 'ng2-dnd';
import { ColorPickerModule } from 'ngx-color-picker';
import {
CanDeactivateGuard,
@ -100,7 +99,6 @@ const routes: Routes = [
@NgModule({
imports: [
ColorPickerModule,
DndModule,
SqxFrameworkModule,
SqxSharedModule,

6
src/Squidex/app/features/content/shared/field-editor.component.html

@ -73,11 +73,7 @@
</div>
</ng-container>
<ng-container *ngSwitchCase="'Color'">
<input class="form-control" type="text" [style.background]="control.value" [formControl]="control" [placeholder]="field.displayPlaceholder"
cpPosition="bottom"
cpOutputFormat="hex"
[colorPicker]="control.value"
(colorPickerChange)="control.setValue($event)" />
<sqx-color-picker [formControl]="control" [placeholder]="field.displayPlaceholder"></sqx-color-picker>
</ng-container>
</ng-container>
</ng-container>

16
src/Squidex/app/framework/angular/forms/autocomplete.component.ts

@ -21,6 +21,8 @@ const KEY_ESCAPE = 27;
const KEY_UP = 38;
const KEY_DOWN = 40;
const NO_EMIT = { emitEvent: false };
export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AutocompleteComponent), multi: true
};
@ -118,9 +120,9 @@ export class AutocompleteComponent extends StatefulControlComponent<State, any[]
this.resetForm();
} else {
if (this.displayProperty && this.displayProperty.length > 0) {
this.queryInput.setValue(obj[this.displayProperty]);
this.queryInput.setValue(obj[this.displayProperty], NO_EMIT);
} else {
this.queryInput.setValue(obj.toString());
this.queryInput.setValue(obj.toString(), NO_EMIT);
}
}
@ -130,9 +132,9 @@ export class AutocompleteComponent extends StatefulControlComponent<State, any[]
public setDisabledState(isDisabled: boolean): void {
if (isDisabled) {
this.reset();
this.queryInput.disable();
this.queryInput.disable(NO_EMIT);
} else {
this.queryInput.enable();
this.queryInput.enable(NO_EMIT);
}
}
@ -165,9 +167,9 @@ export class AutocompleteComponent extends StatefulControlComponent<State, any[]
if (selection) {
try {
if (this.displayProperty && this.displayProperty.length > 0) {
this.queryInput.setValue(selection[this.displayProperty], { emitEvent: false });
this.queryInput.setValue(selection[this.displayProperty], NO_EMIT);
} else {
this.queryInput.setValue(selection.toString(), { emitEvent: false });
this.queryInput.setValue(selection.toString(), NO_EMIT);
}
this.callChange(selection);
} finally {
@ -201,7 +203,7 @@ export class AutocompleteComponent extends StatefulControlComponent<State, any[]
}
private resetForm() {
this.queryInput.setValue('');
this.queryInput.setValue('', NO_EMIT);
}
private reset() {

4
src/Squidex/app/framework/angular/forms/code-editor.component.ts

@ -18,7 +18,7 @@ import {
declare var ace: any;
export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
export const SQX_CODE_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CodeEditorComponent), multi: true
};
@ -26,7 +26,7 @@ export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
selector: 'sqx-code-editor',
styleUrls: ['./code-editor.component.scss'],
templateUrl: './code-editor.component.html',
providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR],
providers: [SQX_CODE_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CodeEditorComponent extends ExternalControlComponent<string> implements AfterViewInit {

17
src/Squidex/app/framework/angular/forms/color-picker.component.html

@ -0,0 +1,17 @@
<input class="form-control" type="text" #input
[style.background]="snapshot.value"
[style.color]="snapshot.foreground"
[placeholder]="placeholder"
[ngModel]="snapshot.value"
(ngModelChange)="writeValue($event)"
(focus)="modal.show()" (blur)="blur()" />
<div *sqxModalView="modal" [sqxModalTarget]="input" position="bottom-left">
<div [style.background]="snapshot.value"
[cpToggle]="true"
[cpDialogDisplay]="'inline'"
[cpCancelButton]="false"
[colorPicker]="snapshot.value"
(colorPickerChange)="writeValue($event)">
</div>
</div>

18
src/Squidex/app/framework/angular/forms/color-picker.component.scss

@ -0,0 +1,18 @@
@import '_mixins';
@import '_vars';
:host /deep/ {
.color-picker {
& {
border-color: $color-border;
}
.hex-text {
.box {
input {
border-color: $color-input;
}
}
}
}
}

59
src/Squidex/app/framework/angular/forms/color-picker.component.ts

@ -0,0 +1,59 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import {
MathHelper,
ModalModel,
StatefulControlComponent
} from '@app/framework/internal';
export const SQX_COLOR_PICKER_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ColorPickerComponent), multi: true
};
interface State {
value?: string;
foreground: string;
}
@Component({
selector: 'sqx-color-picker',
styleUrls: ['./color-picker.component.scss'],
templateUrl: './color-picker.component.html',
providers: [SQX_COLOR_PICKER_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ColorPickerComponent extends StatefulControlComponent<State, string> {
@Input()
public placeholder = '';
public modal = new ModalModel();
constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, { foreground: 'black' });
}
public writeValue(obj: any) {
let foreground = 'black';
if (MathHelper.toLuminance(MathHelper.parseColor(obj)!) < .5) {
foreground = 'white';
}
this.next(s => ({ ...s, value: obj, foreground }));
this.callChange(obj);
}
public blur() {
this.callTouched();
}
}

12
src/Squidex/app/framework/angular/modals/modal-target.directive.ts

@ -15,10 +15,10 @@ import { positionModal } from '@app/shared';
selector: '[sqxModalTarget]'
})
export class ModalTargetDirective extends ResourceOwner implements AfterViewInit, OnDestroy {
private targetElement: any;
private targetElement: Element;
@Input('sqxModalTarget')
public set target(element: any) {
public set target(element: Element) {
if (element !== this.targetElement) {
this.unsubscribeAll();
@ -42,7 +42,7 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit
constructor(
private readonly renderer: Renderer2,
private readonly element: ElementRef
private readonly element: ElementRef<Element>
) {
super();
}
@ -78,7 +78,11 @@ export class ModalTargetDirective extends ResourceOwner implements AfterViewInit
const modalRef = this.element.nativeElement;
const modalRect = this.element.nativeElement.getBoundingClientRect();
const targetRect: ClientRect = this.targetElement.getBoundingClientRect();
if (modalRect.width === 0 || modalRect.height === 0) {
return;
}
const targetRect = this.targetElement.getBoundingClientRect();
let y = 0;
let x = 0;

2
src/Squidex/app/framework/angular/modals/modal-view.directive.ts

@ -87,7 +87,7 @@ export class ModalViewDirective implements OnChanges, OnDestroy {
setTimeout(() => {
this.startListening();
});
}, 1000);
this.changeDetector.detectChanges();
} else if (!isOpen && this.renderedView) {

4
src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts

@ -100,12 +100,12 @@ export class OnboardingTooltipComponent extends StatefulComponent implements OnD
public hideThis() {
this.onboardingService.disable(this.helpId);
this.unsubscribeAll();
this.ngOnDestroy();
}
public hideAll() {
this.onboardingService.disableAll();
this.unsubscribeAll();
this.ngOnDestroy();
}
}

1
src/Squidex/app/framework/declarations.ts

@ -8,6 +8,7 @@
export * from './angular/forms/autocomplete.component';
export * from './angular/forms/checkbox-group.component';
export * from './angular/forms/code-editor.component';
export * from './angular/forms/color-picker.component';
export * from './angular/forms/confirm-click.directive';
export * from './angular/forms/control-errors.component';
export * from './angular/forms/copy.directive';

7
src/Squidex/app/framework/module.ts

@ -9,6 +9,7 @@ import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ColorPickerModule } from 'ngx-color-picker';
import {
AnalyticsService,
@ -19,6 +20,7 @@ import {
ClipboardService,
CodeComponent,
CodeEditorComponent,
ColorPickerComponent,
ConfirmClickDirective,
ControlErrorsComponent,
CopyDirective,
@ -89,11 +91,13 @@ import {
imports: [
FormsModule,
CommonModule,
ReactiveFormsModule
ReactiveFormsModule,
ColorPickerModule
],
declarations: [
AutocompleteComponent,
CheckboxGroupComponent,
ColorPickerComponent,
ConfirmClickDirective,
ControlErrorsComponent,
CodeComponent,
@ -158,6 +162,7 @@ import {
CodeEditorComponent,
CommonModule,
CodeComponent,
ColorPickerComponent,
ConfirmClickDirective,
ControlErrorsComponent,
CopyDirective,

45
src/Squidex/app/framework/utils/math-helper.spec.ts

@ -71,4 +71,49 @@ describe('MathHelper', () => {
expect(MathHelper.roundToMultipleOfTwo(13)).toBe(14);
expect(MathHelper.roundToMultipleOfTwo(12.2)).toBe(12);
});
it('should create color from long string', () => {
const color = MathHelper.parseColor('#336699')!;
expect(color.r).toBe(0.2);
expect(color.g).toBe(0.4);
expect(color.b).toBe(0.6);
expect(color.a).toBe(1.0);
});
it('should create color from short string', () => {
const color = MathHelper.parseColor('#369')!;
expect(color.r).toBe(0.2);
expect(color.g).toBe(0.4);
expect(color.b).toBe(0.6);
expect(color.a).toBe(1.0);
});
it('should create color from rgb string', () => {
const color = MathHelper.parseColor('rgb(51, 102, 153)')!;
expect(color.r).toBe(0.2);
expect(color.g).toBe(0.4);
expect(color.b).toBe(0.6);
expect(color.a).toBe(1.0);
});
it('should create color from rgba string', () => {
const color = MathHelper.parseColor('rgba(51, 102, 153, 0.5)')!;
expect(color.r).toBe(0.2);
expect(color.g).toBe(0.4);
expect(color.b).toBe(0.6);
expect(color.a).toBe(0.5);
});
it('should convert to luminance', () => {
expect(MathHelper.toLuminance(undefined!)).toBe(1);
expect(MathHelper.toLuminance({ r: 0, g: 0, b: 0, a: 1 })).toBe(0);
expect(MathHelper.toLuminance({ r: 1, g: 1, b: 1, a: 1 })).toBe(1);
expect(MathHelper.toLuminance({ r: 0.5, g: 0.5, b: 0.5, a: 1 })).toBe(0.5);
});
});

89
src/Squidex/app/framework/utils/math-helper.ts

@ -7,6 +7,65 @@
/* tslint:disable: no-bitwise */
import { Types } from './types';
interface IColorDefinition {
regex: RegExp;
process(bots: RegExpExecArray): Color;
}
export interface Color {
r: number;
g: number;
b: number;
a: number;
}
const ColorDefinitions: IColorDefinition[] = [
{
regex: /^rgba\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}),\s*([\d\.]{1,})\)$/,
process: (bits) => {
return {
r: parseInt(bits[1], 10) / 255,
g: parseInt(bits[2], 10) / 255,
b: parseInt(bits[3], 10) / 255,
a: parseFloat(bits[4])
};
}
}, {
regex: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,
process: (bits) => {
return {
r: parseInt(bits[1], 10) / 255,
g: parseInt(bits[2], 10) / 255,
b: parseInt(bits[3], 10) / 255,
a: 1
};
}
}, {
regex: /^(\w{2})(\w{2})(\w{2})$/,
process: (bits) => {
return {
r: parseInt(bits[1], 16) / 255,
g: parseInt(bits[2], 16) / 255,
b: parseInt(bits[3], 16) / 255,
a: 1
};
}
}, {
regex: /^(\w{1})(\w{1})(\w{1})$/,
process: (bits) => {
return {
r: parseInt(bits[1] + bits[1], 16) / 255,
g: parseInt(bits[2] + bits[2], 16) / 255,
b: parseInt(bits[3] + bits[3], 16) / 255,
a: 1
};
}
}
];
export module MathHelper {
export const EMPTY_GUID = '00000000-0000-0000-0000-000000000000';
@ -83,4 +142,34 @@ export module MathHelper {
return degree;
}
export function parseColor(value: string): Color | undefined {
if (!Types.isString(value)) {
return undefined;
}
if (value.charAt(0) === '#') {
value = value.substr(1, 6);
}
value = value.replace(/ /g, '').toLowerCase();
for (let colorDefinition of ColorDefinitions) {
const bits = colorDefinition.regex.exec(value);
if (bits) {
return colorDefinition.process(bits);
}
}
return undefined;
}
export function toLuminance(color: Color) {
if (!color) {
return 1;
}
return (0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b) / color.a;
}
}

2
src/Squidex/app/framework/utils/modal-positioner.ts

@ -18,7 +18,7 @@ const POSITION_RIGHT_CENTER = 'right';
const POSITION_RIGHT_TOP = 'right-top';
const POSITION_RIGHT_BOTTOM = 'right-bottom';
export function positionModal(targetRect: ClientRect, modalRect: ClientRect, relativePosition: string, offset: number, fix: boolean, viewportHeight: number, viewportWidth: number): { x: number, y: number } {
export function positionModal(targetRect: ClientRect, modalRect: ClientRect, relativePosition: string, offset: number, fix: boolean, viewportWidth: number, viewportHeight: number): { x: number, y: number } {
let y = 0;
let x = 0;

12
src/Squidex/app/shared/services/assets.service.ts

@ -247,8 +247,10 @@ export class AssetsService {
return throwError(error);
}
}),
tap(() => {
this.analytics.trackEvent('Asset', 'Uploaded', appName);
tap(value => {
if (!Types.isNumber(value)) {
this.analytics.trackEvent('Asset', 'Uploaded', appName);
}
}),
pretifyError('Failed to upload asset. Please reload.'));
}
@ -320,8 +322,10 @@ export class AssetsService {
return throwError(error);
}
}),
tap(() => {
this.analytics.trackEvent('Analytics', 'Replaced', appName);
tap(value => {
if (!Types.isNumber(value)) {
this.analytics.trackEvent('Analytics', 'Replaced', appName);
}
}),
pretifyError('Failed to replace asset. Please reload.'));
}

23
src/Squidex/appsettings.json

@ -341,5 +341,28 @@
* The deepl api key if you want to support automated translations.
*/
"deeplAuthKey": ""
},
"rebuild": {
/*
* Set to true to rebuild apps.
*/
"apps": false,
/*
* Set to true to rebuild assets.
*/
"assets": false,
/*
* Set to true to rebuild contents.
*/
"contents": false,
/*
* Set to true to rebuild rules.
*/
"rules": false,
/*
* Set to true to rebuild schemas.
*/
"schemas": false
}
}

48
tests/Squidex.Infrastructure.Tests/Assets/ImageSharpAssetThumbnailGeneratorTests.cs

@ -15,16 +15,15 @@ namespace Squidex.Infrastructure.Assets
{
public class ImageSharpAssetThumbnailGeneratorTests
{
private const string Image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAZdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuMTM0A1t6AAAADElEQVQYV2P4//8/AAX+Av6nNYGEAAAAAElFTkSuQmCC";
private readonly ImageSharpAssetThumbnailGenerator sut = new ImageSharpAssetThumbnailGenerator();
private readonly MemoryStream target = new MemoryStream();
[Fact]
public async Task Should_return_same_image_if_no_size_is_passed_for_thumbnail()
{
var source = new MemoryStream(Convert.FromBase64String(Image));
var target = new MemoryStream();
var source = GetPng();
await sut.CreateThumbnailAsync(source, target, null, null, "resize");
await sut.CreateThumbnailAsync(source, target);
Assert.Equal(target.Length, source.Length);
}
@ -32,23 +31,42 @@ namespace Squidex.Infrastructure.Assets
[Fact]
public async Task Should_resize_image_to_target()
{
var source = new MemoryStream(Convert.FromBase64String(Image));
var target = new MemoryStream();
var source = GetPng();
await sut.CreateThumbnailAsync(source, target, 100, 100, "resize");
await sut.CreateThumbnailAsync(source, target, 1000, 1000, "resize");
Assert.True(target.Length > source.Length);
}
[Fact]
public async Task Should_change_jpeg_quality_and_write_to_target()
{
var source = GetJpeg();
await sut.CreateThumbnailAsync(source, target, quality: 10);
Assert.True(target.Length < source.Length);
}
[Fact]
public async Task Should_change_png_quality_and_write_to_target()
{
var source = GetPng();
await sut.CreateThumbnailAsync(source, target, quality: 10);
Assert.True(target.Length < source.Length);
}
[Fact]
public async Task Should_return_image_information_if_image_is_valid()
{
var source = new MemoryStream(Convert.FromBase64String(Image));
var source = GetPng();
var imageInfo = await sut.GetImageInfoAsync(source);
Assert.Equal(1, imageInfo.PixelHeight);
Assert.Equal(1, imageInfo.PixelWidth);
Assert.Equal(600, imageInfo.PixelHeight);
Assert.Equal(600, imageInfo.PixelWidth);
}
[Fact]
@ -60,5 +78,15 @@ namespace Squidex.Infrastructure.Assets
Assert.Null(imageInfo);
}
private Stream GetPng()
{
return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.png");
}
private Stream GetJpeg()
{
return GetType().Assembly.GetManifestResourceStream("Squidex.Infrastructure.Assets.Images.logo.jpg");
}
}
}

BIN
tests/Squidex.Infrastructure.Tests/Assets/Images/logo.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
tests/Squidex.Infrastructure.Tests/Assets/Images/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

8
tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj

@ -6,6 +6,10 @@
<RootNamespace>Squidex.Infrastructure</RootNamespace>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<ItemGroup>
<None Remove="Assets\Images\logo.jpg" />
<None Remove="Assets\Images\logo.png" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Squidex.Infrastructure.Azure\Squidex.Infrastructure.Azure.csproj" />
<ProjectReference Include="..\..\src\Squidex.Infrastructure.GoogleCloud\Squidex.Infrastructure.GoogleCloud.csproj" />
@ -34,4 +38,8 @@
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Assets\Images\logo.jpg" />
<EmbeddedResource Include="Assets\Images\logo.png" />
</ItemGroup>
</Project>

3
tools/Migrate_01/MigrationPath.cs

@ -10,6 +10,7 @@ using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Migrate_01.Migrations;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Migrations;
namespace Migrate_01
@ -21,6 +22,8 @@ namespace Migrate_01
public MigrationPath(IServiceProvider serviceProvider)
{
Guard.NotNull(serviceProvider, nameof(serviceProvider));
this.serviceProvider = serviceProvider;
}

22
tools/Migrate_01/RebuildOptions.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Migrate_01
{
public sealed class RebuildOptions
{
public bool Apps { get; set; }
public bool Assets { get; set; }
public bool Contents { get; set; }
public bool Rules { get; set; }
public bool Schemas { get; set; }
}
}

57
tools/Migrate_01/RebuildRunner.cs

@ -0,0 +1,57 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Squidex.Infrastructure;
namespace Migrate_01
{
public sealed class RebuildRunner
{
private readonly Rebuilder rebuilder;
private readonly RebuildOptions rebuildOptions;
public RebuildRunner(Rebuilder rebuilder, IOptions<RebuildOptions> rebuildOptions)
{
Guard.NotNull(rebuilder, nameof(rebuilder));
Guard.NotNull(rebuildOptions, nameof(rebuildOptions));
this.rebuilder = rebuilder;
this.rebuildOptions = rebuildOptions.Value;
}
public async Task RunAsync(CancellationToken ct)
{
if (rebuildOptions.Apps)
{
await rebuilder.RebuildAppsAsync(ct);
}
if (rebuildOptions.Schemas)
{
await rebuilder.RebuildSchemasAsync(ct);
}
if (rebuildOptions.Rules)
{
await rebuilder.RebuildRulesAsync(ct);
}
if (rebuildOptions.Assets)
{
await rebuilder.RebuildAssetsAsync(ct);
}
if (rebuildOptions.Contents)
{
await rebuilder.RebuildContentAsync(ct);
}
}
}
}

78
tools/Migrate_01/Rebuilder.cs

@ -39,80 +39,84 @@ namespace Migrate_01
IStore<Guid> store,
IEventStore eventStore)
{
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(localCache, nameof(localCache));
Guard.NotNull(store, nameof(store));
this.eventStore = eventStore;
this.localCache = localCache;
this.store = store;
}
public async Task RebuildAppsAsync()
public async Task RebuildAppsAsync(CancellationToken ct = default)
{
await store.GetSnapshotStore<AppState>().ClearAsync();
await RebuildManyAsync("^app\\-", id => RebuildAsync<AppState, AppGrain>(id, (e, s) => s.Apply(e)));
await RebuildManyAsync("^app\\-", id => RebuildAsync<AppState, AppGrain>(id, (e, s) => s.Apply(e)), ct);
}
public async Task RebuildSchemasAsync()
public async Task RebuildSchemasAsync(CancellationToken ct = default)
{
await store.GetSnapshotStore<SchemaState>().ClearAsync();
await RebuildManyAsync("^schema\\-", id => RebuildAsync<SchemaState, SchemaGrain>(id, (e, s) => s.Apply(e)));
await RebuildManyAsync("^schema\\-", id => RebuildAsync<SchemaState, SchemaGrain>(id, (e, s) => s.Apply(e)), ct);
}
public async Task RebuildRulesAsync()
public async Task RebuildRulesAsync(CancellationToken ct = default)
{
await store.GetSnapshotStore<RuleState>().ClearAsync();
await RebuildManyAsync("^rule\\-", id => RebuildAsync<RuleState, RuleGrain>(id, (e, s) => s.Apply(e)));
await RebuildManyAsync("^rule\\-", id => RebuildAsync<RuleState, RuleGrain>(id, (e, s) => s.Apply(e)), ct);
}
public async Task RebuildAssetsAsync()
public async Task RebuildAssetsAsync(CancellationToken ct = default)
{
await store.GetSnapshotStore<AssetState>().ClearAsync();
await RebuildManyAsync("^asset\\-", id => RebuildAsync<AssetState, AssetGrain>(id, (e, s) => s.Apply(e)));
await RebuildManyAsync("^asset\\-", id => RebuildAsync<AssetState, AssetGrain>(id, (e, s) => s.Apply(e)), ct);
}
public async Task RebuildContentAsync()
public async Task RebuildContentAsync(CancellationToken ct = default)
{
using (localCache.StartContext())
{
await store.GetSnapshotStore<ContentState>().ClearAsync();
await store.GetSnapshotStore<ContentState>().ClearAsync();
await RebuildManyAsync("^content\\-", async id =>
await RebuildManyAsync("^content\\-", async id =>
{
try
{
try
{
await RebuildAsync<ContentState, ContentGrain>(id, (e, s) => s.Apply(e));
}
catch (DomainObjectNotFoundException)
{
return;
}
});
}
await RebuildAsync<ContentState, ContentGrain>(id, (e, s) => s.Apply(e));
}
catch (DomainObjectNotFoundException)
{
return;
}
}, ct);
}
private async Task RebuildManyAsync(string filter, Func<Guid, Task> action)
private async Task RebuildManyAsync(string filter, Func<Guid, Task> action, CancellationToken ct)
{
var handledIds = new HashSet<Guid>();
var worker = new ActionBlock<Guid>(action, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32 });
await eventStore.QueryAsync(async storedEvent =>
using (localCache.StartContext())
{
var headers = storedEvent.Data.Headers;
var handledIds = new HashSet<Guid>();
var id = headers.AggregateId();
var worker = new ActionBlock<Guid>(action, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32 });
if (handledIds.Add(id))
await eventStore.QueryAsync(async storedEvent =>
{
await worker.SendAsync(id);
}
}, filter, ct: CancellationToken.None);
var headers = storedEvent.Data.Headers;
worker.Complete();
var id = headers.AggregateId();
await worker.Completion;
if (handledIds.Add(id))
{
await worker.SendAsync(id);
}
}, filter, ct);
worker.Complete();
await worker.Completion;
}
}
private async Task RebuildAsync<TState, TGrain>(Guid key, Func<Envelope<IEvent>, TState, TState> func) where TState : IDomainState, new()

Loading…
Cancel
Save