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