diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs index ed1aebcd1..4c152ee44 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs @@ -82,11 +82,6 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper return JsonValue.Create(value.AsBoolean()); } - if (value.IsNumber()) - { - return JsonValue.Create(value.AsNumber()); - } - if (value.IsDate()) { return JsonValue.Create(value.AsDate().ToString()); @@ -97,6 +92,18 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper return JsonValue.Create(value.AsRegExp().Value?.ToString()); } + if (value.IsNumber()) + { + var number = value.AsNumber(); + + if (double.IsNaN(number) || double.IsPositiveInfinity(number) || double.IsNegativeInfinity(number)) + { + return JsonValue.Zero; + } + + return JsonValue.Create(number); + } + if (value.IsArray()) { var arr = value.AsArray(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs index 1f3de5fa6..d85677c28 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs @@ -51,7 +51,9 @@ namespace Squidex.Domain.Apps.Entities.Backup } catch (HttpRequestException ex) { - throw new BackupRestoreException($"Cannot download the archive. Got status code: {response?.StatusCode}.", ex); + var statusCode = response != null ? (int)response.StatusCode : 0; + + throw new BackupRestoreException($"Cannot download the archive. Got status code {statusCode}: {ex.Message}.", ex); } finally { diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 95cdb4bc5..bca9b8124 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -211,6 +211,13 @@ } }, + "apps": { + // True to delete apps permanently. + // + // This process can take a while and is executed in the background. + "deletePermanent": false + }, + "contents": { // True to enable memory caching. // diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs index 23be4a6bf..edb196750 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ContentDataObjectTests.cs @@ -373,6 +373,33 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting ExecuteScript(original, script); } + [Theory] + [InlineData("NaN")] + [InlineData("Number.POSITIVE_INFINITY")] + [InlineData("Number.NEGATIVE_INFINITY")] + public void Should_not_throw_exceptions_if_invalid_numbers(string input) + { + var original = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(JsonValue.Array())); + + var expected = + new ContentData() + .AddField("number", + new ContentFieldData() + .AddInvariant(JsonValue.Zero)); + + string script = $@" + data.number.iv = {input}; + "; + + var result = ExecuteScript(original, script); + + Assert.Equal(expected, result); + } + [Fact] public void Should_null_propagate_unknown_fields() { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs index 009ab0703..9161814f7 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Scripting/ScriptingCompleterTests.cs @@ -73,7 +73,8 @@ namespace Squidex.Domain.Apps.Core.Operations.Scripting "ctx.schemaId", "ctx.schemaName", "ctx.status", - "ctx.statusOld" + "ctx.statusOld", + "ctx.validate" }); } diff --git a/backend/tools/TestSuite/TestSuite.Shared/Fixtures/SchemaFixtureBase.cs b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/SchemaFixtureBase.cs new file mode 100644 index 000000000..d0c93f857 --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/Fixtures/SchemaFixtureBase.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TestSuite.Fixtures +{ + internal class SchemaFixtureBase + { + } +} diff --git a/backend/tools/TestSuite/TestSuite.Shared/SharedInstances.cs b/backend/tools/TestSuite/TestSuite.Shared/SharedInstances.cs new file mode 100644 index 000000000..a51b0884e --- /dev/null +++ b/backend/tools/TestSuite/TestSuite.Shared/SharedInstances.cs @@ -0,0 +1,133 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; +using Squidex.ClientLibrary; +using Squidex.ClientLibrary.Management; +using TestSuite.Model; + +namespace TestSuite +{ + public static class SharedInstances + { + private static readonly string[] Contributors = + { + "hello@squidex.io" + }; + + private static readonly Task ClientManager = CreateClientManagerInternalAsync(); + + private static readonly ConcurrentDictionary ReferenceSchemas = + new ConcurrentDictionary(); + + private static readonly ConcurrentDictionary DefaultSchemas = + new ConcurrentDictionary(); + + private static readonly Task App = CreateAppInternalAsync(); + + private static Task CreateClientManagerInternalAsync() + { + var clientManager = new ClientManagerWrapper(); + + return clientManager.ConnectAsync(); + } + + private static async Task CreateAppInternalAsync() + { + var wrapper = await ClientManager; + + try + { + await wrapper.Apps.PostAppAsync(new CreateAppDto { Name = wrapper.ClientManager.App }); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) + { + throw; + } + } + + var invite = new AssignContributorDto { Invite = true, Role = "Owner" }; + + foreach (var contributor in Contributors) + { + invite.ContributorId = contributor; + + await wrapper.Apps.PostContributorAsync(wrapper.ClientManager.App, invite); + } + + try + { + await wrapper.Apps.PostLanguageAsync(wrapper.ClientManager.App, new AddLanguageDto + { + Language = "de" + }); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) + { + throw; + } + } + } + + public static Task CreateClientManagerAsync() + { + return ClientManager; + } + + public static async Task> CreateReferenceSchema(string name) + { + var wrapper = await ClientManager; + + async Task CreateAsync() + { + try + { + await TestEntityWithReferences.CreateSchemaAsync(wrapper.Schemas, wrapper.ClientManager.App, name); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) + { + throw; + } + } + } + + await ReferenceSchemas.GetOrAdd(name, _ => CreateAsync()); + + return wrapper.ClientManager.CreateContentsClient(name); + } + + public static async Task> CreateDefaultSchema(string name) + { + var wrapper = await ClientManager; + + async Task CreateAsync() + { + try + { + await TestEntity.CreateSchemaAsync(wrapper.Schemas, wrapper.ClientManager.App, name); + } + catch (SquidexManagementException ex) + { + if (ex.StatusCode != 400) + { + throw; + } + } + } + + await DefaultSchemas.GetOrAdd(name, _ => CreateAsync()); + + return wrapper.ClientManager.CreateContentsClient(name); + } + } +} diff --git a/frontend/src/app/features/settings/pages/contributors/contributors-page.component.html b/frontend/src/app/features/settings/pages/contributors/contributors-page.component.html index 7bd72802c..49ea11334 100644 --- a/frontend/src/app/features/settings/pages/contributors/contributors-page.component.html +++ b/frontend/src/app/features/settings/pages/contributors/contributors-page.component.html @@ -35,21 +35,17 @@ - - - - - - -
-
- - -
- {{ 'contributors.empty' | sqxTranslate }} -
-
+ + +
+ {{ 'contributors.empty' | sqxTranslate }} +
+ + + + +
diff --git a/frontend/src/app/features/settings/pages/workflows/workflows-page.component.html b/frontend/src/app/features/settings/pages/workflows/workflows-page.component.html index dacec5b03..f0e615248 100644 --- a/frontend/src/app/features/settings/pages/workflows/workflows-page.component.html +++ b/frontend/src/app/features/settings/pages/workflows/workflows-page.component.html @@ -21,7 +21,7 @@
- +
{{ 'workflows.empty' | sqxTranslate }}
diff --git a/frontend/src/app/framework/state.ts b/frontend/src/app/framework/state.ts index 7dfb857a1..85d11c264 100644 --- a/frontend/src/app/framework/state.ts +++ b/frontend/src/app/framework/state.ts @@ -101,7 +101,7 @@ const devToolsExtension = window['__REDUX_DEVTOOLS_EXTENSION__']; export class State { private readonly state: BehaviorSubject>; private readonly devTools?: any; - + public get changes(): Observable> { return this.state; } @@ -134,8 +134,14 @@ export class State { if (debugName && devToolsExtension) { const name = `[Squidex] ${debugName}`; - this.devTools = devToolsExtension.connect({ name, features: {} }); - this.devTools.init(initialState); + this.devTools = devToolsExtension.connect({ name, features: { jump: true } }); + this.devTools.init(initialState); + + this.devTools.subscribe((message: any) => { + if (message.type === 'DISPATCH' && message.payload.type === 'JUMP_TO_ACTION') { + this.state.next(JSON.parse(message.state)); + } + }); } }