mirror of https://github.com/Squidex/squidex.git
Browse Source
* Simple SDK. * Fixes. * More progress with embedding. * Check extension. * Fix tests. * Fix code. * Open api tests and many fixes. * More fixes. * Final fixes. * Optional tag restoring. * Fix backup tests.pull/837/head
committed by
GitHub
91 changed files with 39327 additions and 477 deletions
@ -0,0 +1,53 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Text; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Infrastructure.Json; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps |
||||
|
{ |
||||
|
public sealed class CalculateTokens : IContentEnricherStep |
||||
|
{ |
||||
|
private readonly IJsonSerializer jsonSerializer; |
||||
|
private readonly IUrlGenerator urlGenerator; |
||||
|
|
||||
|
public CalculateTokens(IUrlGenerator urlGenerator, IJsonSerializer jsonSerializer) |
||||
|
{ |
||||
|
this.jsonSerializer = jsonSerializer; |
||||
|
this.urlGenerator = urlGenerator; |
||||
|
} |
||||
|
|
||||
|
public Task EnrichAsync(Context context, IEnumerable<ContentEntity> contents, ProvideSchema schemas, |
||||
|
CancellationToken ct) |
||||
|
{ |
||||
|
if (context.IsFrontendClient) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
var url = urlGenerator.Root(); |
||||
|
|
||||
|
foreach (var content in contents) |
||||
|
{ |
||||
|
var token = new |
||||
|
{ |
||||
|
a = content.AppId.Name, |
||||
|
s = content.SchemaId.Name, |
||||
|
i = content.Id.ToString(), |
||||
|
u = url |
||||
|
}; |
||||
|
|
||||
|
var json = jsonSerializer.Serialize(token); |
||||
|
|
||||
|
content.EditToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); |
||||
|
} |
||||
|
|
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,144 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using Microsoft.AspNetCore.Builder; |
|
||||
using Microsoft.AspNetCore.Http; |
|
||||
using Microsoft.Extensions.DependencyInjection; |
|
||||
|
|
||||
namespace Squidex.Web.Pipeline |
|
||||
{ |
|
||||
public static class SameSiteCookiesServiceCollectionExtensions |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// -1 defines the unspecified value, which tells ASPNET Core to NOT
|
|
||||
/// send the SameSite attribute. With ASPNET Core 3.1 the
|
|
||||
/// <seealso cref="SameSiteMode" /> enum will have a definition for
|
|
||||
/// Unspecified.
|
|
||||
/// </summary>
|
|
||||
private const SameSiteMode Unspecified = (SameSiteMode)(-1); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Configures a cookie policy to properly set the SameSite attribute
|
|
||||
/// for Browsers that handle unknown values as Strict. Ensure that you
|
|
||||
/// add the <seealso cref="Microsoft.AspNetCore.CookiePolicy.CookiePolicyMiddleware" />
|
|
||||
/// into the pipeline before sending any cookies!.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// Minimum ASPNET Core Version required for this code:
|
|
||||
/// - 2.1.14
|
|
||||
/// - 2.2.8
|
|
||||
/// - 3.0.1
|
|
||||
/// - 3.1.0-preview1
|
|
||||
/// Starting with version 80 of Chrome (to be released in February 2020)
|
|
||||
/// cookies with NO SameSite attribute are treated as SameSite=Lax.
|
|
||||
/// In order to always get the cookies send they need to be set to
|
|
||||
/// SameSite=None. But since the current standard only defines Lax and
|
|
||||
/// Strict as valid values there are some browsers that treat invalid
|
|
||||
/// values as SameSite=Strict. We therefore need to check the browser
|
|
||||
/// and either send SameSite=None or prevent the sending of SameSite=None.
|
|
||||
/// Relevant links:
|
|
||||
/// - https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
|
|
||||
/// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00
|
|
||||
/// - https://www.chromium.org/updates/same-site
|
|
||||
/// - https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
|
|
||||
/// - https://bugs.webkit.org/show_bug.cgi?id=198181.
|
|
||||
/// </remarks>
|
|
||||
/// <param name="services">The service collection to register <see cref="CookiePolicyOptions" /> into.</param>
|
|
||||
/// <returns>The modified <see cref="IServiceCollection" />.</returns>
|
|
||||
public static IServiceCollection AddNonBreakingSameSiteCookies(this IServiceCollection services) |
|
||||
{ |
|
||||
services.Configure<CookiePolicyOptions>(options => |
|
||||
{ |
|
||||
options.MinimumSameSitePolicy = Unspecified; |
|
||||
|
|
||||
options.OnAppendCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); |
|
||||
options.OnDeleteCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions); |
|
||||
}); |
|
||||
|
|
||||
return services; |
|
||||
} |
|
||||
|
|
||||
private static void CheckSameSite(HttpContext httpContext, CookieOptions options) |
|
||||
{ |
|
||||
if (options.SameSite == SameSiteMode.None) |
|
||||
{ |
|
||||
var userAgent = httpContext.Request.Headers["User-Agent"].ToString(); |
|
||||
|
|
||||
if (DisallowsSameSiteNone(userAgent)) |
|
||||
{ |
|
||||
options.SameSite = Unspecified; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Checks if the UserAgent is known to interpret an unknown value as Strict.
|
|
||||
/// For those the <see cref="CookieOptions.SameSite" /> property should be
|
|
||||
/// set to <see cref="Unspecified" />.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// This code is taken from Microsoft:
|
|
||||
/// https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/.
|
|
||||
/// </remarks>
|
|
||||
/// <param name="userAgent">The user agent string to check.</param>
|
|
||||
/// <returns>Whether the specified user agent (browser) accepts SameSite=None or not.</returns>
|
|
||||
private static bool DisallowsSameSiteNone(string userAgent) |
|
||||
{ |
|
||||
// Cover all iOS based browsers here.
|
|
||||
// This includes:
|
|
||||
// - Safari on iOS 12 for iPhone, iPod Touch, iPad
|
|
||||
// - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
|
|
||||
// - Chrome on iOS 12 for iPhone, iPod Touch, iPad
|
|
||||
// All of which are broken by SameSite=None, because they use the
|
|
||||
// iOS networking stack.
|
|
||||
// Notes from Thinktecture:
|
|
||||
// Regarding https://caniuse.com/#search=samesite iOS versions lower
|
|
||||
// than 12 are not supporting SameSite at all. Starting with version 13
|
|
||||
// unknown values are NOT treated as strict anymore. Therefore we only
|
|
||||
// need to check version 12.
|
|
||||
if (userAgent.Contains("CPU iPhone OS 12", StringComparison.Ordinal) || |
|
||||
userAgent.Contains("iPad; CPU OS 12", StringComparison.Ordinal)) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
// Cover Mac OS X based browsers that use the Mac OS networking stack.
|
|
||||
// This includes:
|
|
||||
// - Safari on Mac OS X.
|
|
||||
// This does not include:
|
|
||||
// - Chrome on Mac OS X
|
|
||||
// because they do not use the Mac OS networking stack.
|
|
||||
// Notes from Thinktecture:
|
|
||||
// Regarding https://caniuse.com/#search=samesite MacOS X versions lower
|
|
||||
// than 10.14 are not supporting SameSite at all. Starting with version
|
|
||||
// 10.15 unknown values are NOT treated as strict anymore. Therefore we
|
|
||||
// only need to check version 10.14.
|
|
||||
if (userAgent.Contains("Safari", StringComparison.Ordinal) && |
|
||||
userAgent.Contains("Macintosh; Intel Mac OS X 10_14", StringComparison.Ordinal) && |
|
||||
userAgent.Contains("Version/", StringComparison.Ordinal)) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
// Cover Chrome 50-69, because some versions are broken by SameSite=None
|
|
||||
// and none in this range require it.
|
|
||||
// Note: this covers some pre-Chromium Edge versions,
|
|
||||
// but pre-Chromium Edge does not require SameSite=None.
|
|
||||
// Notes from Thinktecture:
|
|
||||
// We can not validate this assumption, but we trust Microsofts
|
|
||||
// evaluation. And overall not sending a SameSite value equals to the same
|
|
||||
// behavior as SameSite=None for these old versions anyways.
|
|
||||
if (userAgent.Contains("Chrome/5", StringComparison.Ordinal) || |
|
||||
userAgent.Contains("Chrome/6", StringComparison.Ordinal)) |
|
||||
{ |
|
||||
return true; |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,36 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using Squidex.Areas.Api.Config.OpenApi; |
|
||||
using Squidex.Web; |
|
||||
using Squidex.Web.Pipeline; |
|
||||
|
|
||||
namespace Squidex.Areas.Api |
|
||||
{ |
|
||||
public static class Startup |
|
||||
{ |
|
||||
public static void ConfigureApi(this IApplicationBuilder app) |
|
||||
{ |
|
||||
app.Map(Constants.PrefixApi, builder => |
|
||||
{ |
|
||||
builder.UseAccessTokenQueryString(); |
|
||||
|
|
||||
builder.UseRouting(); |
|
||||
|
|
||||
builder.UseAuthentication(); |
|
||||
builder.UseAuthorization(); |
|
||||
|
|
||||
builder.UseSquidexOpenApi(); |
|
||||
|
|
||||
builder.UseEndpoints(endpoints => |
|
||||
{ |
|
||||
endpoints.MapControllers(); |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,37 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Areas.Frontend.Middlewares |
||||
|
{ |
||||
|
public sealed class EmbedMiddleware |
||||
|
{ |
||||
|
private readonly RequestDelegate next; |
||||
|
|
||||
|
public EmbedMiddleware(RequestDelegate next) |
||||
|
{ |
||||
|
this.next = next; |
||||
|
} |
||||
|
|
||||
|
public Task InvokeAsync(HttpContext context) |
||||
|
{ |
||||
|
var request = context.Request; |
||||
|
|
||||
|
if (request.Path.StartsWithSegments("/embed", StringComparison.Ordinal, out var remaining)) |
||||
|
{ |
||||
|
request.Path = remaining; |
||||
|
|
||||
|
var uiOptions = new OptionsFeature(); |
||||
|
uiOptions.Options["embedded"] = true; |
||||
|
uiOptions.Options["embedPath"] = "/embed"; |
||||
|
|
||||
|
context.Features.Set(uiOptions); |
||||
|
} |
||||
|
|
||||
|
return next(context); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Microsoft.AspNetCore.Mvc; |
||||
|
using Squidex.Shared.Identity; |
||||
|
|
||||
|
namespace Squidex.Areas.IdentityServer.Controllers.Info |
||||
|
{ |
||||
|
public sealed class InfoController : IdentityServerController |
||||
|
{ |
||||
|
[Route("info")] |
||||
|
[HttpGet] |
||||
|
public IActionResult Info() |
||||
|
{ |
||||
|
var displayName = User.Claims.DisplayName(); |
||||
|
|
||||
|
return Ok(new { displayName }); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,41 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using Squidex.Web; |
|
||||
|
|
||||
namespace Squidex.Areas.IdentityServer |
|
||||
{ |
|
||||
public static class Startup |
|
||||
{ |
|
||||
public static void ConfigureIdentityServer(this IApplicationBuilder app) |
|
||||
{ |
|
||||
var environment = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>(); |
|
||||
|
|
||||
app.Map(Constants.PrefixIdentityServer, builder => |
|
||||
{ |
|
||||
if (environment.IsDevelopment()) |
|
||||
{ |
|
||||
builder.UseDeveloperExceptionPage(); |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
builder.UseExceptionHandler("/error"); |
|
||||
} |
|
||||
|
|
||||
builder.UseRouting(); |
|
||||
|
|
||||
builder.UseAuthentication(); |
|
||||
builder.UseAuthorization(); |
|
||||
|
|
||||
builder.UseEndpoints(endpoints => |
|
||||
{ |
|
||||
endpoints.MapControllers(); |
|
||||
}); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,28 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using Orleans; |
|
||||
using Squidex.Areas.OrleansDashboard.Middlewares; |
|
||||
using Squidex.Web; |
|
||||
|
|
||||
namespace Squidex.Areas.OrleansDashboard |
|
||||
{ |
|
||||
public static class Startup |
|
||||
{ |
|
||||
public static void ConfigureOrleansDashboard(this IApplicationBuilder app) |
|
||||
{ |
|
||||
app.Map(Constants.PrefixOrleans, builder => |
|
||||
{ |
|
||||
builder.UseAuthentication(); |
|
||||
builder.UseAuthorization(); |
|
||||
|
|
||||
builder.UseMiddleware<OrleansDashboardAuthenticationMiddleware>(); |
|
||||
builder.UseOrleansDashboard(); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,27 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using Squidex.Areas.Portal.Middlewares; |
|
||||
using Squidex.Web; |
|
||||
|
|
||||
namespace Squidex.Areas.Portal |
|
||||
{ |
|
||||
public static class Startup |
|
||||
{ |
|
||||
public static void ConfigurePortal(this IApplicationBuilder app) |
|
||||
{ |
|
||||
app.Map(Constants.PrefixPortal, builder => |
|
||||
{ |
|
||||
builder.UseAuthentication(); |
|
||||
builder.UseAuthorization(); |
|
||||
|
|
||||
builder.UseMiddleware<PortalDashboardAuthenticationMiddleware>(); |
|
||||
builder.UseMiddleware<PortalRedirectMiddleware>(); |
|
||||
}); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,76 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="utf-8"> |
||||
|
|
||||
|
<meta name="viewport" content="width=device-width,initial-scale=1"> |
||||
|
<meta name="mobile-web-app-capable" content="yes"> |
||||
|
<meta name="apple-mobile-web-app-capable" content="yes"> |
||||
|
|
||||
|
<style> |
||||
|
.element { |
||||
|
border: 2px dashed red; |
||||
|
border-radius: 0; |
||||
|
box-sizing: border-box; |
||||
|
padding: 20px; |
||||
|
position: fixed; |
||||
|
text-align: center; |
||||
|
text-decoration: none; |
||||
|
width: 200px; |
||||
|
} |
||||
|
|
||||
|
.element-left { |
||||
|
left: -100px; |
||||
|
} |
||||
|
|
||||
|
.element-right { |
||||
|
right: -100px; |
||||
|
} |
||||
|
|
||||
|
.element-top { |
||||
|
top: -30px; |
||||
|
} |
||||
|
|
||||
|
.element-bottom { |
||||
|
bottom: -30px; |
||||
|
} |
||||
|
|
||||
|
.element-left, |
||||
|
.element-center, |
||||
|
.element-right { |
||||
|
top: 50%; |
||||
|
margin-top: -30px; |
||||
|
} |
||||
|
|
||||
|
.element-top, |
||||
|
.element-center, |
||||
|
.element-bottom { |
||||
|
left: 50%; |
||||
|
margin-left: -100px; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<script src="embed-sdk.js"></script> |
||||
|
</head> |
||||
|
<body> |
||||
|
<div class="element element-top" squidex-token="ewogICJ1IjogImh0dHBzOi8vbG9jYWxob3N0OjUwMDEiLAogICJhIjogInRlc3QiLAogICJzIjogInRlc3QiLAogICJpIjogIjI0ZWQxMWJhLWJjNzItNDMzZS04ODRjLWE0NGE3ODIwYjU1MSIgIAp9"> |
||||
|
Top Aligned |
||||
|
</div> |
||||
|
|
||||
|
<div class="element element-right" squidex-token="ewogICJ1IjogImh0dHBzOi8vbG9jYWxob3N0OjUwMDEiLAogICJhIjogInRlc3QiLAogICJzIjogInRlc3QiLAogICJpIjogIjI0ZWQxMWJhLWJjNzItNDMzZS04ODRjLWE0NGE3ODIwYjU1MSIgIAp9"> |
||||
|
Right Aligned |
||||
|
</div> |
||||
|
|
||||
|
<div class="element element-bottom" squidex-token="ewogICJ1IjogImh0dHBzOi8vbG9jYWxob3N0OjUwMDEiLAogICJhIjogInRlc3QiLAogICJzIjogInRlc3QiLAogICJpIjogIjI0ZWQxMWJhLWJjNzItNDMzZS04ODRjLWE0NGE3ODIwYjU1MSIgIAp9"> |
||||
|
Bottom Aligned |
||||
|
</div> |
||||
|
|
||||
|
<div class="element element-left" squidex-token="ewogICJ1IjogImh0dHBzOi8vbG9jYWxob3N0OjUwMDEiLAogICJhIjogInRlc3QiLAogICJzIjogInRlc3QiLAogICJpIjogIjI0ZWQxMWJhLWJjNzItNDMzZS04ODRjLWE0NGE3ODIwYjU1MSIgIAp9"> |
||||
|
Left Aligned |
||||
|
</div> |
||||
|
|
||||
|
<div class="element element-center" squidex-token="ewogICJ1IjogImh0dHBzOi8vbG9jYWxob3N0OjUwMDEiLAogICJhIjogInRlc3QiLAogICJzIjogInRlc3QiLAogICJpIjogIjI0ZWQxMWJhLWJjNzItNDMzZS04ODRjLWE0NGE3ODIwYjU1MSIgIAp9"> |
||||
|
Center |
||||
|
</div> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1 @@ |
|||||
|
.squidex *,.squidex-overlay{box-sizing:border-box}.squidex-overlay{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif;font-weight:400;position:fixed}.squidex-overlay-border{border:2px solid #3284f4;border-radius:0;pointer-events:none;position:fixed;z-index:100000}.squidex-overlay-toolbar{color:#fff;cursor:pointer;font-size:13px;font-weight:400;position:fixed;white-space:nowrap;z-index:100000}.squidex-overlay-links,.squidex-overlay-schema{display:inline-block;height:30px;padding:5px 8px 3px}.squidex-overlay-schema{background-color:#4a93f5;border:0;border-radius:0}.squidex-overlay-links{background-color:#3284f4;border:0;border-radius:0}.squidex-overlay-links a{color:#fff!important;margin-left:7px;text-decoration:none;text-overflow:inherit}.squidex-overlay-links a:hover{text-decoration:underline!important}.squidex-iframe{background-color:#fff;border:1px solid #dedfe3;border-radius:2px;bottom:50px;box-shadow:0 0 20px rgba(0,0,0,.2);left:50px;position:fixed;right:50px;top:50px}.squidex-iframe iframe{bottom:0;left:0;min-height:100%;min-width:100%;position:absolute;right:0;top:0}.squidex-iframe-close{-ms-flex-align:center;-ms-flex-pack:center;align-items:center;background-color:#000;border:0;border-radius:20px;box-shadow:0 0 4px rgba(0,0,0,.9);cursor:pointer;display:-ms-flexbox;display:flex;justify-content:center;max-height:30px;max-width:30px;min-height:30px;min-width:30px;opacity:.7;position:absolute;right:-15px;top:-15px;transition:opacity .3s ease-in;z-index:1000}.squidex-iframe-close:hover{box-shadow:0 0 4px #000;opacity:1}.squidex-iframe-close svg{fill:#fff} |
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,84 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using FakeItEasy; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.Contents.Queries.Steps; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Caching; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.Queries |
||||
|
{ |
||||
|
public class EnrichForCachingTests |
||||
|
{ |
||||
|
private readonly ISchemaEntity schema; |
||||
|
private readonly IRequestCache requestCache = A.Fake<IRequestCache>(); |
||||
|
private readonly Context requestContext; |
||||
|
private readonly NamedId<DomainId> appId = NamedId.Of(DomainId.NewGuid(), "my-app"); |
||||
|
private readonly NamedId<DomainId> schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); |
||||
|
private readonly ProvideSchema schemaProvider; |
||||
|
private readonly EnrichForCaching sut; |
||||
|
|
||||
|
public EnrichForCachingTests() |
||||
|
{ |
||||
|
requestContext = new Context(Mocks.ApiUser(), Mocks.App(appId)); |
||||
|
|
||||
|
schema = Mocks.Schema(appId, schemaId); |
||||
|
schemaProvider = x => Task.FromResult((schema, ResolvedComponents.Empty)); |
||||
|
|
||||
|
sut = new EnrichForCaching(requestCache); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_cache_headers() |
||||
|
{ |
||||
|
var headers = new List<string>(); |
||||
|
|
||||
|
A.CallTo(() => requestCache.AddHeader(A<string>._)) |
||||
|
.Invokes(new Action<string>(header => headers.Add(header))); |
||||
|
|
||||
|
await sut.EnrichAsync(requestContext, default); |
||||
|
|
||||
|
Assert.Equal(new List<string> |
||||
|
{ |
||||
|
"X-Flatten", |
||||
|
"X-Languages", |
||||
|
"X-NoCleanup", |
||||
|
"X-NoEnrichment", |
||||
|
"X-NoResolveLanguages", |
||||
|
"X-ResolveFlow", |
||||
|
"X-Resolve-Urls", |
||||
|
"X-Unpublished" |
||||
|
}, headers); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_add_app_version_and_schema_as_dependency() |
||||
|
{ |
||||
|
var content = CreateContent(); |
||||
|
|
||||
|
await sut.EnrichAsync(requestContext, Enumerable.Repeat(content, 1), schemaProvider, default); |
||||
|
|
||||
|
A.CallTo(() => requestCache.AddDependency(content.UniqueId, content.Version)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => requestCache.AddDependency(schema.UniqueId, schema.Version)) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => requestCache.AddDependency(requestContext.App.UniqueId, requestContext.App.Version)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
private ContentEntity CreateContent() |
||||
|
{ |
||||
|
return new ContentEntity { AppId = appId, Id = DomainId.NewGuid(), SchemaId = schemaId, Version = 13 }; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using NSwag; |
||||
|
using TestSuite.Fixtures; |
||||
|
using Xunit; |
||||
|
|
||||
|
#pragma warning disable SA1300 // Element should begin with upper-case letter
|
||||
|
|
||||
|
namespace TestSuite.ApiTests |
||||
|
{ |
||||
|
public class OpenApiTests : IClassFixture<ClientManagerFixture> |
||||
|
{ |
||||
|
public ClientManagerFixture _ { get; } |
||||
|
|
||||
|
public OpenApiTests(ClientManagerFixture fixture) |
||||
|
{ |
||||
|
_ = fixture; |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_provide_spec() |
||||
|
{ |
||||
|
var url = $"{_.ClientManager.Options.Url}/api/swagger/v1/swagger.json"; |
||||
|
|
||||
|
var document = await OpenApiDocument.FromUrlAsync(url); |
||||
|
|
||||
|
Assert.NotNull(document); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
node_modules |
||||
|
/build |
||||
|
/*.log |
||||
@ -0,0 +1,19 @@ |
|||||
|
# sdk |
||||
|
|
||||
|
## CLI Commands |
||||
|
* `npm install`: Installs dependencies |
||||
|
|
||||
|
* `npm run dev`: Run a development, HMR server |
||||
|
|
||||
|
* `npm run serve`: Run a production-like server |
||||
|
|
||||
|
* `npm run build`: Production-ready build |
||||
|
|
||||
|
* `npm run lint`: Pass TypeScript files using ESLint |
||||
|
|
||||
|
* `npm run test`: Run Jest and Enzyme with |
||||
|
[`enzyme-adapter-preact-pure`](https://github.com/preactjs/enzyme-adapter-preact-pure) for |
||||
|
your tests |
||||
|
|
||||
|
|
||||
|
For detailed explanation on how things work, checkout the [CLI Readme](https://github.com/developit/preact-cli/blob/master/README.md). |
||||
File diff suppressed because it is too large
@ -0,0 +1,42 @@ |
|||||
|
{ |
||||
|
"private": true, |
||||
|
"name": "sdk", |
||||
|
"version": "0.0.0", |
||||
|
"license": "MIT", |
||||
|
"scripts": { |
||||
|
"build": "preact build --prerender false --sw false --esm false --inline-css", |
||||
|
"dev": "preact watch", |
||||
|
"lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'", |
||||
|
"serve": "sirv build --port 8080 --cors --single", |
||||
|
"start": "preact watch" |
||||
|
}, |
||||
|
"eslintConfig": { |
||||
|
"parser": "@typescript-eslint/parser", |
||||
|
"extends": [ |
||||
|
"preact", |
||||
|
"plugin:@typescript-eslint/recommended" |
||||
|
], |
||||
|
"ignorePatterns": [ |
||||
|
"build/" |
||||
|
] |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"preact": "^10.3.1", |
||||
|
"preact-render-to-string": "^5.1.4" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"@types/enzyme": "^3.10.5", |
||||
|
"@types/jest": "^26.0.8", |
||||
|
"@typescript-eslint/eslint-plugin": "^2.25.0", |
||||
|
"@typescript-eslint/parser": "^2.25.0", |
||||
|
"enzyme": "^3.11.0", |
||||
|
"enzyme-adapter-preact-pure": "^3.1.0", |
||||
|
"eslint": "^6.8.0", |
||||
|
"eslint-config-preact": "^1.1.1", |
||||
|
"preact-cli": "^3.0.0", |
||||
|
"sass": "^1.49.0", |
||||
|
"sass-loader": "^10.2.1", |
||||
|
"sirv-cli": "^1.0.0-next.3", |
||||
|
"typescript": "^4.5.2" |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
[{"timestamp":1643040747792,"files":[{"filename":"bundle.8ec41.css","previous":665,"size":0,"diff":-665},{"filename":"index.html","previous":971,"size":982,"diff":11},{"filename":"bundle.*****.js","previous":7778,"size":7944,"diff":166},{"filename":"polyfills.*****.js","previous":2291,"size":2291,"diff":0},{"filename":"bundle.9727e.css","previous":0,"size":686,"diff":686}]},{"timestamp":1643037266163,"files":[{"filename":"bundle.8ec41.css","previous":665,"size":665,"diff":0},{"filename":"index.html","previous":951,"size":971,"diff":20},{"filename":"bundle.*****.js","previous":7778,"size":7778,"diff":0},{"filename":"polyfills.*****.js","previous":2291,"size":2291,"diff":0}]},{"timestamp":1643037028401,"files":[{"filename":"bundle.8ec41.css","previous":665,"size":665,"diff":0},{"filename":"index.html","previous":861,"size":951,"diff":90},{"filename":"bundle.*****.js","previous":7778,"size":7778,"diff":0},{"filename":"polyfills.*****.js","previous":2291,"size":2291,"diff":0}]},{"timestamp":1643036552475,"files":[{"filename":"bundle.8ec41.css","previous":665,"size":665,"diff":0},{"filename":"index.html","previous":860,"size":861,"diff":1},{"filename":"bundle.*****.js","previous":7779,"size":7778,"diff":-1},{"filename":"polyfills.*****.js","previous":2291,"size":2291,"diff":0}]},{"timestamp":1642880373297,"files":[{"filename":"bundle.8ec41.css","previous":665,"size":665,"diff":0},{"filename":"index.html","previous":770,"size":860,"diff":90},{"filename":"bundle.*****.js","previous":7165,"size":7779,"diff":614},{"filename":"polyfills.*****.js","previous":2291,"size":2291,"diff":0}]},{"timestamp":1642793188355,"files":[{"filename":"bundle.8ec41.css","previous":665,"size":665,"diff":0},{"filename":"index.html","previous":770,"size":770,"diff":0},{"filename":"bundle.*****.js","previous":7169,"size":7165,"diff":-4},{"filename":"polyfills.*****.js","previous":2291,"size":2291,"diff":0}]},{"timestamp":1642792886072,"files":[{"filename":"bundle.8ec41.css","previous":665,"size":665,"diff":0},{"filename":"index.html","previous":771,"size":770,"diff":-1},{"filename":"bundle.*****.js","previous":7165,"size":7169,"diff":4},{"filename":"polyfills.*****.js","previous":2291,"size":2291,"diff":0}]},{"timestamp":1642792872086,"files":[{"filename":"bundle.8ec41.css","previous":665,"size":665,"diff":0},{"filename":"index.html","previous":770,"size":771,"diff":1},{"filename":"bundle.*****.js","previous":7122,"size":7165,"diff":43},{"filename":"polyfills.*****.js","previous":2291,"size":2291,"diff":0}]},{"timestamp":1642792547160,"files":[{"filename":"bundle.8ec41.css","previous":665,"size":665,"diff":0},{"filename":"index.html","previous":772,"size":770,"diff":-2},{"filename":"bundle.*****.js","previous":7029,"size":7122,"diff":93},{"filename":"polyfills.*****.js","previous":2291,"size":2291,"diff":0}]},{"timestamp":1642792111367,"files":[{"filename":"bundle.8ec41.css","previous":665,"size":665,"diff":0},{"filename":"bundle.*****.esm.js","previous":6968,"size":0,"diff":-6968},{"filename":"polyfills.*****.esm.js","previous":2191,"size":0,"diff":-2191},{"filename":"sw.js","previous":10444,"size":0,"diff":-10444},{"filename":"sw-esm.js","previous":10447,"size":0,"diff":-10447},{"filename":"bundle.dbe17.js","previous":7062,"size":0,"diff":-7062},{"filename":"polyfills.03377.js","previous":2291,"size":0,"diff":-2291},{"filename":"index.html","previous":806,"size":772,"diff":-34},{"filename":"200.html","previous":806,"size":0,"diff":-806},{"filename":"bundle.*****.js","previous":0,"size":7029,"diff":7029},{"filename":"polyfills.*****.js","previous":0,"size":2291,"diff":2291}]},{"timestamp":1642792036250,"files":[{"filename":"bundle.8ec41.css","previous":0,"size":665,"diff":665},{"filename":"bundle.*****.esm.js","previous":0,"size":6968,"diff":6968},{"filename":"polyfills.*****.esm.js","previous":0,"size":2191,"diff":2191},{"filename":"sw.js","previous":0,"size":10444,"diff":10444},{"filename":"sw-esm.js","previous":0,"size":10447,"diff":10447},{"filename":"bundle.dbe17.js","previous":0,"size":7062,"diff":7062},{"filename":"polyfills.03377.js","previous":0,"size":2291,"diff":2291},{"filename":"index.html","previous":0,"size":806,"diff":806},{"filename":"200.html","previous":0,"size":806,"diff":806}]}] |
||||
@ -0,0 +1,5 @@ |
|||||
|
{ |
||||
|
"presets": [ |
||||
|
"preact-cli/babel" |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
import { h } from 'preact'; |
||||
|
|
||||
|
export interface IFrameProps { |
||||
|
// The url to embed.
|
||||
|
url: string; |
||||
|
|
||||
|
// When closed.
|
||||
|
onClose: () => void; |
||||
|
} |
||||
|
|
||||
|
export const IFrame = (props: IFrameProps) => { |
||||
|
const { url, onClose } = props; |
||||
|
return ( |
||||
|
<div class='squidex-iframe'> |
||||
|
<button class='squidex-iframe-close' onClick={onClose}> |
||||
|
<svg version='1.1' xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24'> |
||||
|
<path d='M18.984 6.422l-5.578 5.578 5.578 5.578-1.406 1.406-5.578-5.578-5.578 5.578-1.406-1.406 5.578-5.578-5.578-5.578 1.406-1.406 5.578 5.578 5.578-5.578z'></path> |
||||
|
</svg> |
||||
|
</button> |
||||
|
|
||||
|
<iframe src={url} frameBorder={0}></iframe> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
@ -0,0 +1,230 @@ |
|||||
|
import { h } from 'preact'; |
||||
|
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; |
||||
|
import { IFrame } from './iframe'; |
||||
|
import { Overlay } from './Overlay'; |
||||
|
import { TokenInfo } from './shared'; |
||||
|
|
||||
|
export interface OverlayContainerProps { |
||||
|
// The base url of the script.
|
||||
|
baseUrl: string | null | undefined; |
||||
|
} |
||||
|
|
||||
|
type AuthState = 'Authenticated' | 'Failed' | 'Pending'; |
||||
|
|
||||
|
const UNSET = { x: Number.NEGATIVE_INFINITY, y: Number.NEGATIVE_INFINITY }; |
||||
|
|
||||
|
export const OverlayContainer = (props: OverlayContainerProps) => { |
||||
|
const div = useRef<any>(); |
||||
|
const [auth, setAuth] = useState<{ [url: string]: AuthState }>({}); |
||||
|
const [target, setTarget] = useState<{ target: HTMLElement, token: TokenInfo }>(); |
||||
|
const [targetUrl, setTargetUrl] = useState<string>(); |
||||
|
const authRef = useRef(auth); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
let previousElement: any; |
||||
|
let previousTarget: HTMLElement | undefined = undefined; |
||||
|
let previousPosition = UNSET; |
||||
|
|
||||
|
const updateTarget = (target?: HTMLElement, token?: TokenInfo) => { |
||||
|
if (target !== previousTarget) { |
||||
|
if (target && token) { |
||||
|
setTarget({ target, token }); |
||||
|
} else { |
||||
|
setTarget(undefined); |
||||
|
} |
||||
|
|
||||
|
previousTarget = target; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
function listen(event: MouseEvent) { |
||||
|
const element = event.target as HTMLElement; |
||||
|
|
||||
|
console.log(element); |
||||
|
|
||||
|
if (!element || isToolbar(element)) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const { token, target } = parseTokenInPath(element, Object.keys(authRef.current)); |
||||
|
const position = { x: event.clientX, y: event.clientY }; |
||||
|
|
||||
|
if (token && target) { |
||||
|
updateTarget(target, token); |
||||
|
|
||||
|
previousPosition = position; |
||||
|
} else if (Math.abs(position.x - previousPosition.x) + Math.abs(position.y - previousPosition.y) > 20) { |
||||
|
updateTarget(undefined, undefined); |
||||
|
} |
||||
|
|
||||
|
previousElement = element; |
||||
|
} |
||||
|
|
||||
|
document.addEventListener('mousemove', listen); |
||||
|
|
||||
|
return () => { |
||||
|
document.removeEventListener('mousemove', listen); |
||||
|
} |
||||
|
}, []); |
||||
|
|
||||
|
const checkAuth = useCallback((url: string | null | undefined) => { |
||||
|
if (!url) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (authRef.current[url]) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const updateAuth = (state: AuthState) => { |
||||
|
const newAuth = { |
||||
|
...authRef.current, |
||||
|
[url]: state |
||||
|
}; |
||||
|
|
||||
|
authRef.current = newAuth; |
||||
|
|
||||
|
setAuth(newAuth); |
||||
|
}; |
||||
|
|
||||
|
if (url.indexOf('http://') === 0) { |
||||
|
updateAuth('Authenticated'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const fetchStatus = async () => { |
||||
|
updateAuth('Pending'); |
||||
|
|
||||
|
try { |
||||
|
const response = await fetch(`${url}/identity-server/info`, { |
||||
|
credentials: 'include' |
||||
|
}); |
||||
|
|
||||
|
const json = await response.json(); |
||||
|
|
||||
|
updateAuth(json.displayName ? 'Authenticated' : 'Failed'); |
||||
|
} catch { |
||||
|
updateAuth('Failed'); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
fetchStatus(); |
||||
|
}, [auth]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
checkAuth(props.baseUrl); |
||||
|
}, [props.baseUrl]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
checkAuth(target?.token.u); |
||||
|
}, [target?.token.u]); |
||||
|
|
||||
|
const isAuthenticated = auth[target?.token.u!] === 'Authenticated' |
||||
|
|
||||
|
return ( |
||||
|
<div class='squidex' ref={div}> |
||||
|
{target && isAuthenticated && |
||||
|
<Overlay onOpen={setTargetUrl} {...target} /> |
||||
|
} |
||||
|
|
||||
|
{targetUrl && |
||||
|
<IFrame url={targetUrl} onClose={() => setTargetUrl(undefined)} /> |
||||
|
} |
||||
|
</div> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
const CDN_URL = 'https://assets.squidex.io'; |
||||
|
|
||||
|
function isToolbar(target: HTMLElement) { |
||||
|
let current = target; |
||||
|
|
||||
|
while (current) { |
||||
|
if (current.className === 'squidex-overlay-toolbar') { |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
current = current.parentElement as HTMLElement; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
function parseTokenInPath(target: HTMLElement, baseUrls: string[]): { token?: TokenInfo, target?: HTMLElement } { |
||||
|
let current = target; |
||||
|
|
||||
|
while (current) { |
||||
|
const token = parseToken(current, baseUrls); |
||||
|
|
||||
|
if (token) { |
||||
|
return { token, target: current }; |
||||
|
} |
||||
|
|
||||
|
current = current.parentElement as HTMLElement; |
||||
|
} |
||||
|
|
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
function parseToken(target: HTMLElement, baseUrls: string[]): TokenInfo | null { |
||||
|
const value = target.getAttribute('squidex-token'); |
||||
|
|
||||
|
if (!value && target.nodeName === 'IMG') { |
||||
|
const src = (target as any)['src'] as string; |
||||
|
|
||||
|
if (src) { |
||||
|
for (const baseUrl of baseUrls) { |
||||
|
if (src.indexOf(baseUrl) === 0) { |
||||
|
const parts = src.substring(baseUrl.length + 1).split('/'); |
||||
|
|
||||
|
if (parts[0] === 'api' && |
||||
|
parts[1] === 'assets' && |
||||
|
parts[2]?.length > 0 && |
||||
|
parts[3]?.length > 0) { |
||||
|
return { |
||||
|
u: baseUrl, |
||||
|
a: parts[2], |
||||
|
i: parts[3] |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (src.indexOf(CDN_URL) === 0) { |
||||
|
const parts = src.substring(CDN_URL.length + 1).split('/'); |
||||
|
|
||||
|
if (parts[0]?.length > 0 && |
||||
|
parts[1]?.length > 0) { |
||||
|
return { |
||||
|
u: CDN_URL, |
||||
|
a: parts[0], |
||||
|
i: parts[1] |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (!value) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const decoded = atob(value); |
||||
|
|
||||
|
let token = JSON.parse(decoded) as TokenInfo; |
||||
|
|
||||
|
if (!token.u || !token.i || !token.a) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
while (token.u.endsWith('/')) { |
||||
|
token.u = token.u.substring(0, token.u.length - 1); |
||||
|
} |
||||
|
|
||||
|
return token; |
||||
|
} catch { |
||||
|
return null; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,147 @@ |
|||||
|
import { h } from 'preact'; |
||||
|
import { useEffect, useState, useMemo, useRef, useLayoutEffect } from 'preact/hooks'; |
||||
|
import { TokenInfo } from './shared'; |
||||
|
|
||||
|
export interface OverlayProps { |
||||
|
// The target element ot attach to.
|
||||
|
target: HTMLElement; |
||||
|
|
||||
|
// The token string.
|
||||
|
token: TokenInfo; |
||||
|
|
||||
|
// When opened.
|
||||
|
onOpen: (url: string) => void; |
||||
|
} |
||||
|
|
||||
|
const PADDING = 2; |
||||
|
|
||||
|
export const Overlay = (props: OverlayProps) => { |
||||
|
const { onOpen, target, token } = props; |
||||
|
const toolbarRef = useRef<HTMLDivElement>(); |
||||
|
const linksRect = useRef<DOMRect>(); |
||||
|
const targetRect = useRef<DOMRect>(); |
||||
|
const [_, render] = useState(0); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
render(x => x + 1); |
||||
|
}, [token]); |
||||
|
|
||||
|
useEffect(() => { |
||||
|
function layout() { |
||||
|
const rect = target.getBoundingClientRect(); |
||||
|
|
||||
|
const current = targetRect.current; |
||||
|
|
||||
|
if (!current || |
||||
|
rect.height !== current.height || |
||||
|
rect.width !== current.width || |
||||
|
rect.x !== current.x || |
||||
|
rect.y !== current.y) { |
||||
|
targetRect.current = rect; |
||||
|
render(x => x + 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
document.body.addEventListener('scroll', layout); |
||||
|
|
||||
|
const timer = setInterval(() => { |
||||
|
layout(); |
||||
|
}, 500); |
||||
|
|
||||
|
layout(); |
||||
|
|
||||
|
return () => { |
||||
|
clearInterval(timer); |
||||
|
|
||||
|
document.body.removeEventListener('scroll', layout); |
||||
|
}; |
||||
|
}, [target]); |
||||
|
|
||||
|
useLayoutEffect(() => { |
||||
|
if (!toolbarRef.current) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const rect = toolbarRef.current.getBoundingClientRect(); |
||||
|
|
||||
|
const current = linksRect.current; |
||||
|
|
||||
|
if (!current || |
||||
|
rect.height !== current.height || |
||||
|
rect.width !== current.width) { |
||||
|
linksRect.current = rect; |
||||
|
render(x => x + 1); |
||||
|
} |
||||
|
}, [render]); |
||||
|
|
||||
|
const externalUrl = useMemo(() => { |
||||
|
let { a, i, s, u } = token; |
||||
|
|
||||
|
if (s) { |
||||
|
return `${u}/app/${a}/content/${s}/${i}`; |
||||
|
} else { |
||||
|
return `${u}/app/${a}/assets/?ref=${i}`; |
||||
|
} |
||||
|
}, [token]); |
||||
|
|
||||
|
const embedUrl = useMemo(() => { |
||||
|
let { a, i, s, u } = token; |
||||
|
|
||||
|
if (s) { |
||||
|
return `${u}/embed/app/${a}/content/${s}/${i}`; |
||||
|
} else { |
||||
|
return `${u}/embed/app/${a}/assets/?ref=${i}`; |
||||
|
} |
||||
|
}, [token]); |
||||
|
|
||||
|
const x = (targetRect.current?.x || 0) - PADDING; |
||||
|
const y = (targetRect.current?.y || 0) - PADDING; |
||||
|
|
||||
|
const overlayWidth = (targetRect.current?.width || 0) + 2 * PADDING; |
||||
|
const overlayHeight = (targetRect.current?.height || 0) + 2 * PADDING; |
||||
|
|
||||
|
const toolbarWidth = (linksRect.current?.width || 0); |
||||
|
const toolbarHeight = (linksRect.current?.height || 0); |
||||
|
|
||||
|
let linkX = x; |
||||
|
let linkY = y - toolbarHeight + 2; |
||||
|
|
||||
|
if (linkY < 0) { |
||||
|
linkY = y + overlayHeight - 2; |
||||
|
} |
||||
|
|
||||
|
if (linkY + toolbarHeight > window.innerHeight) { |
||||
|
linkX = window.innerHeight - toolbarHeight; |
||||
|
} |
||||
|
|
||||
|
if (linkX < 0) { |
||||
|
linkX = 0; |
||||
|
} |
||||
|
|
||||
|
if (linkX + toolbarWidth > window.innerWidth) { |
||||
|
linkX = window.innerWidth - toolbarWidth; |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<div class='squidex-overlay'> |
||||
|
<div class='squidex-overlay-border' style={{ left: x, top: y, width: overlayWidth, height: overlayHeight }}> |
||||
|
</div> |
||||
|
|
||||
|
<div class='squidex-overlay-toolbar' style={{ left: linkX, top: linkY }} ref={toolbarRef as any}> |
||||
|
<div class='squidex-overlay-schema'> |
||||
|
{token?.s || 'Asset'} |
||||
|
</div> |
||||
|
|
||||
|
<div class='squidex-overlay-links' data-links={true}> |
||||
|
<a onClick={() => onOpen(embedUrl)}> |
||||
|
Edit Inline |
||||
|
</a> |
||||
|
|
||||
|
<a href={externalUrl!} target='_blank'> |
||||
|
Edit In Squidex |
||||
|
</a> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
); |
||||
|
}; |
||||
@ -0,0 +1,13 @@ |
|||||
|
export interface TokenInfo { |
||||
|
// The content or asset id.
|
||||
|
i: string; |
||||
|
|
||||
|
// The app name.
|
||||
|
a: string; |
||||
|
|
||||
|
// The schema name.
|
||||
|
s?: string; |
||||
|
|
||||
|
// The base url.
|
||||
|
u: string; |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import './style/index.scss'; |
||||
|
import { renderOverlay } from './render'; |
||||
|
import { getBaseUrl } from './utils'; |
||||
|
|
||||
|
const baseUrl = getBaseUrl(); |
||||
|
|
||||
|
function init() { |
||||
|
renderOverlay(baseUrl); |
||||
|
renderStyles(); |
||||
|
} |
||||
|
|
||||
|
function renderStyles() { |
||||
|
if (!baseUrl) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const styleElement = document.createElement('link'); |
||||
|
|
||||
|
styleElement.rel = 'stylesheet'; |
||||
|
styleElement.href = `${baseUrl}/scripts/embed-sdk.css`; |
||||
|
styleElement.type = 'text/css'; |
||||
|
|
||||
|
document.head?.appendChild(styleElement); |
||||
|
} |
||||
|
|
||||
|
if (document.readyState === 'complete') { |
||||
|
init(); |
||||
|
} else { |
||||
|
window.addEventListener('load', () => init()); |
||||
|
} |
||||
@ -0,0 +1,21 @@ |
|||||
|
{ |
||||
|
"name": "sdk", |
||||
|
"short_name": "sdk", |
||||
|
"start_url": "/", |
||||
|
"display": "standalone", |
||||
|
"orientation": "portrait", |
||||
|
"background_color": "#fff", |
||||
|
"theme_color": "#673ab8", |
||||
|
"icons": [ |
||||
|
{ |
||||
|
"src": "/assets/icons/android-chrome-192x192.png", |
||||
|
"type": "image/png", |
||||
|
"sizes": "192x192" |
||||
|
}, |
||||
|
{ |
||||
|
"src": "/assets/icons/android-chrome-512x512.png", |
||||
|
"type": "image/png", |
||||
|
"sizes": "512x512" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
import { h, render } from 'preact'; |
||||
|
import { OverlayContainer } from './components/overlay-container'; |
||||
|
|
||||
|
let element: HTMLDivElement | null = null; |
||||
|
|
||||
|
export function renderOverlay(baseUrl: string | null | undefined) { |
||||
|
if (!element) { |
||||
|
element = document.body.appendChild(document.createElement('div')); |
||||
|
} |
||||
|
|
||||
|
render(<OverlayContainer baseUrl={baseUrl} />, element) |
||||
|
} |
||||
@ -0,0 +1,125 @@ |
|||||
|
$color-theme-brand: #3389ff; |
||||
|
$color-theme-brand-dark: #3284f4; |
||||
|
$color-theme-brand-darker: #2f7deb; |
||||
|
$color-border: #dedfe3; |
||||
|
|
||||
|
@mixin position($t: null, $r: null, $b: null, $l: null) { |
||||
|
bottom: $b; left: $l; right: $r; top: $t; |
||||
|
} |
||||
|
|
||||
|
@mixin absolute($t: null, $r: null, $b: null, $l: null) { |
||||
|
@include position($t, $r, $b, $l); |
||||
|
position: absolute; |
||||
|
} |
||||
|
|
||||
|
@mixin fixed($t: null, $r: null, $b: null, $l: null) { |
||||
|
@include position($t, $r, $b, $l); |
||||
|
position: fixed; |
||||
|
} |
||||
|
|
||||
|
.squidex { |
||||
|
* { |
||||
|
box-sizing: border-box; |
||||
|
} |
||||
|
|
||||
|
&-overlay { |
||||
|
box-sizing: border-box; |
||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; |
||||
|
font-weight: normal; |
||||
|
position: fixed; |
||||
|
|
||||
|
&-border { |
||||
|
border: 2px solid $color-theme-brand-dark; |
||||
|
border-radius: 0; |
||||
|
position: fixed; |
||||
|
pointer-events: none; |
||||
|
z-index: 100000; |
||||
|
} |
||||
|
|
||||
|
&-toolbar { |
||||
|
color: #fff; |
||||
|
cursor: pointer; |
||||
|
font-size: 13px; |
||||
|
font-weight: normal; |
||||
|
position: fixed; |
||||
|
white-space: nowrap; |
||||
|
z-index: 100000; |
||||
|
} |
||||
|
|
||||
|
&-schema, |
||||
|
&-links { |
||||
|
display: inline-block; |
||||
|
height: 30px; |
||||
|
padding: 3px 8px; |
||||
|
padding-top: 5px; |
||||
|
} |
||||
|
|
||||
|
&-schema { |
||||
|
background-color: lighten($color-theme-brand-dark, 5%); |
||||
|
border: 0; |
||||
|
border-radius: 0; |
||||
|
} |
||||
|
|
||||
|
&-links { |
||||
|
background-color: $color-theme-brand-dark; |
||||
|
border: 0; |
||||
|
border-radius: 0; |
||||
|
|
||||
|
a { |
||||
|
margin-left: 7px; |
||||
|
text-overflow: inherit; |
||||
|
text-decoration: none; |
||||
|
|
||||
|
& { |
||||
|
color: #fff !important; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
text-decoration: underline !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-iframe { |
||||
|
@include fixed(50px, 50px, 50px, 50px); |
||||
|
background-color: white; |
||||
|
border: 1px solid $color-border; |
||||
|
border-radius: 2px; |
||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); |
||||
|
|
||||
|
iframe { |
||||
|
@include absolute(0, 0, 0, 0); |
||||
|
min-width: 100%; |
||||
|
min-height: 100%; |
||||
|
} |
||||
|
|
||||
|
&-close { |
||||
|
@include absolute(-15px, -15px); |
||||
|
align-items: center; |
||||
|
background-color: black; |
||||
|
border-radius: 20px; |
||||
|
border: 0; |
||||
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.9); |
||||
|
cursor: pointer; |
||||
|
display: flex; |
||||
|
max-height: 30px; |
||||
|
max-width: 30px; |
||||
|
min-height: 30px; |
||||
|
min-width: 30px; |
||||
|
justify-content: center; |
||||
|
transition: opacity .3s ease-in; |
||||
|
opacity: .7; |
||||
|
z-index: 1000; |
||||
|
|
||||
|
&:hover { |
||||
|
box-shadow: 0 0 4px rgba(0, 0, 0, 1); |
||||
|
opacity: 1; |
||||
|
} |
||||
|
|
||||
|
svg { |
||||
|
fill: white; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,99 @@ |
|||||
|
<!DOCTYPE html> |
||||
|
<html lang="en"> |
||||
|
<head> |
||||
|
<meta charset="utf-8"> |
||||
|
<title><% preact.title %></title> |
||||
|
<meta name="viewport" content="width=device-width,initial-scale=1"> |
||||
|
<meta name="mobile-web-app-capable" content="yes"> |
||||
|
<meta name="apple-mobile-web-app-capable" content="yes"> |
||||
|
<link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png"> |
||||
|
<% preact.headEnd %> |
||||
|
|
||||
|
<style> |
||||
|
.element { |
||||
|
border: 2px dashed red; |
||||
|
border-radius: 0; |
||||
|
box-sizing: border-box; |
||||
|
padding: 20px; |
||||
|
position: fixed; |
||||
|
text-align: center; |
||||
|
text-decoration: none; |
||||
|
width: 200px; |
||||
|
} |
||||
|
|
||||
|
.element-left { |
||||
|
left: -100px; |
||||
|
} |
||||
|
|
||||
|
.element-right { |
||||
|
right: -100px; |
||||
|
} |
||||
|
|
||||
|
.element-top { |
||||
|
top: -30px; |
||||
|
} |
||||
|
|
||||
|
.element-bottom { |
||||
|
bottom: -30px; |
||||
|
} |
||||
|
|
||||
|
.element-left, |
||||
|
.element-center, |
||||
|
.element-right { |
||||
|
top: 50%; |
||||
|
margin-top: -30px; |
||||
|
} |
||||
|
|
||||
|
.element-top, |
||||
|
.element-center, |
||||
|
.element-bottom { |
||||
|
left: 50%; |
||||
|
margin-left: -100px; |
||||
|
} |
||||
|
|
||||
|
.element-image { |
||||
|
padding: 0; |
||||
|
left: 50%; |
||||
|
margin-left: -300px; |
||||
|
margin-top: -150px; |
||||
|
top: 50%; |
||||
|
width: 600px; |
||||
|
} |
||||
|
|
||||
|
.element-image img { |
||||
|
max-width: 100%; |
||||
|
max-height: none; |
||||
|
display: block; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<script src="https://localhost:5001/scripts/test.js"></script> |
||||
|
</head> |
||||
|
<body> |
||||
|
<% preact.bodyEnd %> |
||||
|
|
||||
|
<div class="element element-top" squidex-token="eyJhIjoic3F1aWRleC13ZWJzaXRlIiwicyI6ImJsb2ciLCJpIjoiZmQxZDkzNjYtNDY3ZS00MmFiLWE5MzYtYmNmNDk3NjIxYjk4IiwidSI6Imh0dHBzOi8vbG9jYWxob3N0OjUwMDEvIn0="> |
||||
|
Top Aligned |
||||
|
</div> |
||||
|
|
||||
|
<div class="element element-right" squidex-token="ewogICJ1IjogImh0dHBzOi8vbG9jYWxob3N0OjUwMDEiLAogICJhIjogInRlc3QiLAogICJzIjogInRlc3QiLAogICJpIjogIjI0ZWQxMWJhLWJjNzItNDMzZS04ODRjLWE0NGE3ODIwYjU1MSIgIAp9"> |
||||
|
Right Aligned |
||||
|
</div> |
||||
|
|
||||
|
<div class="element element-bottom" squidex-token="ewogICJ1IjogImh0dHBzOi8vbG9jYWxob3N0OjUwMDEiLAogICJhIjogInRlc3QiLAogICJzIjogInRlc3QiLAogICJpIjogIjI0ZWQxMWJhLWJjNzItNDMzZS04ODRjLWE0NGE3ODIwYjU1MSIgIAp9"> |
||||
|
Bottom Aligned |
||||
|
</div> |
||||
|
|
||||
|
<div class="element element-left" squidex-token="ewogICJ1IjogImh0dHBzOi8vbG9jYWxob3N0OjUwMDEiLAogICJhIjogInRlc3QiLAogICJzIjogInRlc3QiLAogICJpIjogIjI0ZWQxMWJhLWJjNzItNDMzZS04ODRjLWE0NGE3ODIwYjU1MSIgIAp9"> |
||||
|
Left Aligned |
||||
|
</div> |
||||
|
|
||||
|
<div class="element element-image"> |
||||
|
<img src="https://localhost:5001/api/assets/test/51135569-96e0-45f2-a5c2-8102ad4b4bf3/mydraft.gif?version=0"> |
||||
|
</div> |
||||
|
|
||||
|
<div class="element element-center" squidex-token="ewogICJ1IjogImh0dHBzOi8vbG9jYWxob3N0OjUwMDEiLAogICJhIjogInRlc3QiLAogICJzIjogInRlc3QiLAogICJpIjogIjI0ZWQxMWJhLWJjNzItNDMzZS04ODRjLWE0NGE3ODIwYjU1MSIgIAp9"> |
||||
|
Center |
||||
|
</div> |
||||
|
</body> |
||||
|
</html> |
||||
@ -0,0 +1,52 @@ |
|||||
|
/* |
||||
|
* Squidex Headless CMS |
||||
|
* |
||||
|
* @license |
||||
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. |
||||
|
*/ |
||||
|
|
||||
|
export function isArray(value: any): value is any[] { |
||||
|
return Array.isArray(value); |
||||
|
} |
||||
|
|
||||
|
export function isString(value: any): value is string { |
||||
|
return typeof value === 'string' || value instanceof String; |
||||
|
} |
||||
|
|
||||
|
export function isUndefined(value: any): value is undefined { |
||||
|
return typeof value === 'undefined'; |
||||
|
} |
||||
|
|
||||
|
export function isBoolean(value: any): value is boolean { |
||||
|
return typeof value === 'boolean'; |
||||
|
} |
||||
|
|
||||
|
export function isFunction(value: any): value is Function { |
||||
|
return typeof value === 'function'; |
||||
|
} |
||||
|
|
||||
|
export function isNumber(value: any): value is number { |
||||
|
return typeof value === 'number' && Number.isFinite(value); |
||||
|
} |
||||
|
|
||||
|
export function isObject(value: any): value is Object { |
||||
|
return value && typeof value === 'object' && value.constructor === Object; |
||||
|
} |
||||
|
|
||||
|
export function getBaseUrl() { |
||||
|
let url = (document.currentScript as any)?.['src'] as string; |
||||
|
|
||||
|
if (!isString(url)) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
url = url.trim(); |
||||
|
|
||||
|
let indexOfHash = url.indexOf('/', 'https://'.length); |
||||
|
|
||||
|
if (indexOfHash > 0) { |
||||
|
url = url.substring(0, indexOfHash); |
||||
|
} |
||||
|
|
||||
|
return url; |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
{ |
||||
|
"compilerOptions": { |
||||
|
"allowJs": true, |
||||
|
"esModuleInterop": true, |
||||
|
"jsx": "react", |
||||
|
"jsxFactory": "h", |
||||
|
"jsxFragmentFactory": "Fragment", |
||||
|
"module": "ESNext", |
||||
|
"moduleResolution": "node", |
||||
|
"noEmit": true, |
||||
|
"skipLibCheck": true, |
||||
|
"strict": true, |
||||
|
"target": "ES5" |
||||
|
}, |
||||
|
"include": ["src/**/*", "tests/**/*"] |
||||
|
} |
||||
Loading…
Reference in new issue