diff --git a/cli/Squidex.CLI/Directory.Build.props b/cli/Squidex.CLI/Directory.Build.props index fc03e7c8..7ade48b9 100644 --- a/cli/Squidex.CLI/Directory.Build.props +++ b/cli/Squidex.CLI/Directory.Build.props @@ -8,6 +8,6 @@ MIT https://github.com/Squidex/squidex/ Squidex HeadlessCMS - 8.13 + 8.14 diff --git a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/AssertFolders/AssetFoldersModel.cs b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/AssertFolders/AssetFoldersModel.cs deleted file mode 100644 index f4a698a6..00000000 --- a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/AssertFolders/AssetFoldersModel.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.ComponentModel.DataAnnotations; - -namespace Squidex.CLI.Commands.Implementation.Sync.AssertFolders -{ - public sealed class AssetFoldersModel - { - [Required] - public List Paths { get; set; } - } -} diff --git a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/AssertFolders/AssetFoldersSynchronizer.cs b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/AssertFolders/AssetFoldersSynchronizer.cs deleted file mode 100644 index 8ff378e6..00000000 --- a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/AssertFolders/AssetFoldersSynchronizer.cs +++ /dev/null @@ -1,122 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.CLI.Commands.Implementation.FileSystem; - -namespace Squidex.CLI.Commands.Implementation.Sync.AssertFolders -{ - public sealed class AssetFoldersSynchronizer : ISynchronizer - { - private const string Ref = "../__json/assetFolders"; - private readonly ILogger log; - - public int Order => -2000; - - public string Name => "AssetFolders"; - - public AssetFoldersSynchronizer(ILogger log) - { - this.log = log; - } - - public Task CleanupAsync(IFileSystem fs) - { - foreach (var file in GetFiles(fs)) - { - file.Delete(); - } - - return Task.CompletedTask; - } - - public async Task ExportAsync(ISyncService sync, SyncOptions options, ISession session) - { - var model = new AssetFoldersModel - { - Paths = new List() - }; - - async Task QueryAsync(string id) - { - var node = await sync.Folders.GetByIdAsync(id, true); - - foreach (var child in node?.Children?.Values ?? Enumerable.Empty()) - { - model.Paths.Add(child.Path); - - await QueryAsync(child.Id); - } - } - - await log.DoSafeAsync("Exporting folders", async () => - { - await QueryAsync(FolderNode.RootId); - }); - - await sync.WriteWithSchema(new FilePath("assetFolders/assetFolders.json"), model, Ref); - } - - public Task DescribeAsync(ISyncService sync, MarkdownWriter writer) - { - var models = - GetFiles(sync.FileSystem) - .Select(x => sync.Read(x, log)) - .ToList(); - - writer.Paragraph($"{models.SelectMany(x => x.Paths).Distinct().Count()} asset folder(s)."); - - return Task.CompletedTask; - } - - public async Task ImportAsync(ISyncService sync, SyncOptions options, ISession session) - { - var models = - GetFiles(sync.FileSystem) - .Select(x => sync.Read(x, log)) - .ToList(); - - foreach (var model in models) - { - await log.DoSafeAsync("Importing folders", async () => - { - foreach (var path in model.Paths ?? Enumerable.Empty()) - { - await sync.Folders.GetIdAsync(path); - } - }); - } - } - - private static IEnumerable GetFiles(IFileSystem fs) - { - foreach (var file in fs.GetFiles(new FilePath("assetFolders"), ".json")) - { - if (!file.Name.StartsWith("__", StringComparison.OrdinalIgnoreCase)) - { - yield return file; - } - } - } - - public async Task GenerateSchemaAsync(ISyncService sync) - { - await sync.WriteJsonSchemaAsync(new FilePath("assetFolders.json")); - - var sample = new AssetFoldersModel - { - Paths = new List - { - "images", - "documents", - "videos" - } - }; - - await sync.WriteWithSchema(new FilePath("assetFolders", "__assetFolder.json"), sample, Ref); - } - } -} diff --git a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/Assets/UploadPipeline.cs b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/Assets/UploadPipeline.cs index 924a63c5..daed1674 100644 --- a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/Assets/UploadPipeline.cs +++ b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/Assets/UploadPipeline.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks.Dataflow; using Squidex.CLI.Commands.Implementation.FileSystem; +using Squidex.ClientLibrary; using Squidex.ClientLibrary.Management; namespace Squidex.CLI.Commands.Implementation.Sync.Assets @@ -22,7 +23,7 @@ public sealed class UploadPipeline public UploadPipeline(ISession session, ILogger log, IFileSystem fs) { - var tree = new FolderTree(session); + var tree = new AssetFolderTree(session.Assets, session.App); var fileNameStep = new TransformBlock(async asset => { diff --git a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/FolderNode.cs b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/FolderNode.cs deleted file mode 100644 index 9e0b2a44..00000000 --- a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/FolderNode.cs +++ /dev/null @@ -1,28 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -#pragma warning disable SA1313 // Parameter names should begin with lower-case letter - -namespace Squidex.CLI.Commands.Implementation.Sync -{ - public sealed record FolderNode(string Id, string Path) - { - public static readonly string RootId = Guid.Empty.ToString(); - - public Dictionary Children { get; } = new Dictionary(); - - public FolderNode Parent { get; set; } - - public bool HasBeenQueried { get; set; } - - public void Add(FolderNode child, string name) - { - Children[name] = child; - child.Parent = this; - } - } -} diff --git a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/FolderTree.cs b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/FolderTree.cs deleted file mode 100644 index 51829179..00000000 --- a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/FolderTree.cs +++ /dev/null @@ -1,163 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.ClientLibrary.Management; - -namespace Squidex.CLI.Commands.Implementation.Sync -{ - public sealed class FolderTree - { - private static readonly char[] TrimChars = { '/', '\\', ' ', '.' }; - private static readonly char[] SplitChars = { '/', '\\' }; - private static readonly string RootId = Guid.Empty.ToString(); - private readonly Dictionary nodes = new Dictionary(); - private readonly ISession session; - private readonly FolderNode rootNode = new FolderNode(RootId, string.Empty); - - public FolderTree(ISession session) - { - nodes[RootId] = rootNode; - - this.session = session; - } - - public async Task GetPathAsync(string? id) - { - var node = await GetByIdAsync(id); - - return node?.Path; - } - - public async Task GetIdAsync(string? path) - { - var node = await GetByPathAsync(path); - - return node?.Id; - } - - public async Task GetByIdAsync(string? id, bool needsChildren = false) - { - if (string.IsNullOrWhiteSpace(id)) - { - return null; - } - - return await QueryAsync(id, needsChildren); - } - - public async Task GetByPathAsync(string? path) - { - if (path == null || path.Equals(".", StringComparison.OrdinalIgnoreCase)) - { - return null; - } - - path = path.Trim(TrimChars); - - if (path.Length == 0) - { - return null; - } - - var names = path.Split(SplitChars, StringSplitOptions.RemoveEmptyEntries); - - var current = rootNode; - - foreach (var name in names) - { - if (current.Children.TryGetValue(name, out var child)) - { - current = child; - continue; - } - - var node = await QueryAsync(current.Id, true); - - if (node.Children.TryGetValue(name, out child)) - { - current = child; - continue; - } - - current = await AddFolderAsync(current, name); - } - - return current; - } - - private async Task AddFolderAsync(FolderNode current, string name) - { - var request = new CreateAssetFolderDto - { - FolderName = name, - ParentId = current.Id - }; - - var folder = await session.Assets.PostAssetFolderAsync(session.App, request); - - current = TryAdd(current, folder.Id, name); - current.HasBeenQueried = true; - - return current; - } - - private async Task QueryAsync(string id, bool needsChildren) - { - if (nodes.TryGetValue(id, out var node) && (node.HasBeenQueried || !needsChildren)) - { - return node; - } - - var folders = await session.Assets.GetAssetFoldersAsync(session.App, id); - - var current = rootNode; - - foreach (var folder in folders.Path) - { - current = TryAdd(current, folder.Id, folder.FolderName); - } - - current.HasBeenQueried = true; - - foreach (var child in folders.Items) - { - TryAdd(current, child.Id, child.FolderName); - } - - return current; - } - - private FolderNode TryAdd(FolderNode node, string id, string name) - { - if (node.Children.TryGetValue(name, out var child)) - { - return child; - } - - child = new FolderNode(id, GetPath(node, name)); - - nodes[id] = child; - node.Add(child, name); - - return child; - } - - private static string GetPath(FolderNode node, string name) - { - var path = node.Path; - - if (path.Length > 0) - { - path += '/'; - } - - path += name; - - return path; - } - } -} diff --git a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/ISyncService.cs b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/ISyncService.cs index 929f383b..a905f544 100644 --- a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/ISyncService.cs +++ b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/ISyncService.cs @@ -6,6 +6,7 @@ // ========================================================================== using Squidex.CLI.Commands.Implementation.FileSystem; +using Squidex.ClientLibrary; namespace Squidex.CLI.Commands.Implementation.Sync { @@ -13,7 +14,7 @@ public interface ISyncService { IFileSystem FileSystem { get; } - FolderTree Folders { get; } + AssetFolderTree Folders { get; } T Convert(object value); diff --git a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/Schemas/Extensions.cs b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/Schemas/Extensions.cs index da7408fe..54c7a997 100644 --- a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/Schemas/Extensions.cs +++ b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/Schemas/Extensions.cs @@ -6,6 +6,7 @@ // ========================================================================== using Squidex.CLI.Commands.Implementation.Utils; +using Squidex.ClientLibrary; using Squidex.ClientLibrary.Management; namespace Squidex.CLI.Commands.Implementation.Sync.Schemas @@ -28,7 +29,7 @@ public static CreateSchemaDto ToCreate(this SchemaCreateModel model) return result; } - public static async Task MapFoldersAsync(this SynchronizeSchemaDto schema, FolderTree folders, bool fromId) + public static async Task MapFoldersAsync(this SynchronizeSchemaDto schema, AssetFolderTree folders, bool fromId) { foreach (var field in schema.Fields ?? Enumerable.Empty()) { @@ -36,7 +37,7 @@ public static async Task MapFoldersAsync(this SynchronizeSchemaDto schema, Folde } } - public static async Task MapFoldersAsync(this UpsertSchemaFieldDto field, FolderTree folders, bool fromId) + public static async Task MapFoldersAsync(this UpsertSchemaFieldDto field, AssetFolderTree folders, bool fromId) { await MapFoldersAsync(field.Properties, folders, fromId); @@ -46,7 +47,7 @@ public static async Task MapFoldersAsync(this UpsertSchemaFieldDto field, Folder } } - private static async Task MapFoldersAsync(FieldPropertiesDto properties, FolderTree folders, bool fromId) + private static async Task MapFoldersAsync(FieldPropertiesDto properties, AssetFolderTree folders, bool fromId) { switch (properties) { @@ -59,7 +60,7 @@ private static async Task MapFoldersAsync(FieldPropertiesDto properties, FolderT } } - private static Task MapFoldersAsync(string? folderId, FolderTree folders, bool fromId) + private static Task MapFoldersAsync(string? folderId, AssetFolderTree folders, bool fromId) { if (fromId) { @@ -102,6 +103,9 @@ private static async Task MapReferencesAsync(FieldPropertiesDto properties, Dict case ComponentsFieldPropertiesDto components: components.SchemaIds = await MapReferencesAsync(components.SchemaIds, map); break; + case StringFieldPropertiesDto @string: + @string.SchemaIds = await MapReferencesAsync(@string.SchemaIds, map); + break; } } diff --git a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/SyncService.cs b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/SyncService.cs index 9e7271a4..0c949944 100644 --- a/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/SyncService.cs +++ b/cli/Squidex.CLI/Squidex.CLI.Core/Commands/Implementation/Sync/SyncService.cs @@ -26,7 +26,7 @@ public sealed class SyncService : ISyncService public IFileSystem FileSystem { get; } - public FolderTree Folders { get; } + public AssetFolderTree Folders { get; } internal sealed class CamelCaseExceptDictionaryKeysResolver : CamelCasePropertyNamesContractResolver { @@ -42,7 +42,7 @@ protected override JsonDictionaryContract CreateDictionaryContract(Type objectTy public SyncService(IFileSystem fileSystem, ISession session) { - Folders = new FolderTree(session); + Folders = new AssetFolderTree(session.Assets, session.App); jsonSerializerSettings = new JsonSerializerSettings { diff --git a/cli/Squidex.CLI/Squidex.CLI.Tests/FolderTreeTests.cs b/cli/Squidex.CLI/Squidex.CLI.Tests/FolderTreeTests.cs deleted file mode 100644 index 810a94f0..00000000 --- a/cli/Squidex.CLI/Squidex.CLI.Tests/FolderTreeTests.cs +++ /dev/null @@ -1,276 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Squidex.CLI.Commands.Implementation; -using Squidex.CLI.Commands.Implementation.Sync; -using Squidex.ClientLibrary.Management; -using Xunit; - -namespace Squidex.CLI -{ - public class FolderTreeTests - { - private static readonly string RootId = Guid.Empty.ToString(); - private readonly ISession session = A.Fake(); - private readonly IAssetsClient assets = A.Fake(); - private readonly FolderTree sut; - - public FolderTreeTests() - { - A.CallTo(() => session.Assets) - .Returns(assets); - - A.CallTo(() => session.App) - .Returns("my-app"); - - sut = new FolderTree(session); - } - - [Theory] - [InlineData(null)] - [InlineData("")] - [InlineData(" ")] - [InlineData("/")] - [InlineData(".")] - [InlineData("./")] - [InlineData("\\")] - public async Task Should_provide_null_if_for_root_path(string path) - { - var id = await sut.GetIdAsync(path); - - Assert.Null(id); - } - - [Fact] - public async Task Should_query_for_path_once_for_each_subtree() - { - // * folder1 - // * folder2 - var folder1 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder1" - }; - - var folder2 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder2" - }; - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", folder1.Id, A._, A._)) - .Returns(new AssetFoldersDto - { - Items = new List(), - Path = new List - { - folder1 - } - }); - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", folder2.Id, A._, A._)) - .Returns(new AssetFoldersDto - { - Items = new List(), - Path = new List - { - folder2 - } - }); - - Assert.Equal("folder1", await sut.GetPathAsync(folder1.Id)); - Assert.Equal("folder2", await sut.GetPathAsync(folder2.Id)); - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", A._, A._, A._)) - .MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task Should_not_query_for_path_again_if_child_already_queried() - { - // * folder1 - // * folder2 - var folder1 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder1" - }; - - var folder2 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder2" - }; - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", folder2.Id, A._, A._)) - .Returns(new AssetFoldersDto - { - Items = new List(), - Path = new List - { - folder1, - folder2 - } - }); - - Assert.Equal("folder1/folder2", await sut.GetPathAsync(folder2.Id)); - Assert.Equal("folder1", await sut.GetPathAsync(folder1.Id)); - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", A._, A._, A._)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_not_query_for_path_again_if_parent_already_queried() - { - // * folder1 - // * folder2 - var folder1 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder1" - }; - - var folder2 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder2" - }; - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", folder1.Id, A._, A._)) - .Returns(new AssetFoldersDto - { - Items = new List - { - folder2 - }, - Path = new List - { - folder1 - } - }); - - Assert.Equal("folder1", await sut.GetPathAsync(folder1.Id)); - Assert.Equal("folder1/folder2", await sut.GetPathAsync(folder2.Id)); - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", A._, A._, A._)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_query_for_id_once_for_each_tree_item() - { - // * folder1 - // * folder2 - var folder1 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder1" - }; - - var folder2 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder2" - }; - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", RootId, A._, A._)) - .Returns(new AssetFoldersDto - { - Items = new List - { - folder1, - folder2 - }, - Path = new List() - }); - - Assert.Equal(folder1.Id, await sut.GetIdAsync("folder1")); - Assert.Equal(folder2.Id, await sut.GetIdAsync("folder2")); - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", A._, A._, A._)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_not_query_for_id_again_if_parent_already_queried() - { - // * folder1 - // * folder2 - var folder1 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder1" - }; - - var folder2 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder2" - }; - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", folder1.Id, A._, A._)) - .Returns(new AssetFoldersDto - { - Items = new List - { - folder2 - }, - Path = new List - { - folder1 - } - }); - - Assert.Equal("folder1", await sut.GetPathAsync(folder1.Id)); - Assert.Equal("folder1/folder2", await sut.GetPathAsync(folder2.Id)); - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", A._, A._, A._)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_query_for_id_once_and_create_new_folder() - { - // * folder1 - // * folder2 - var folder1 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder1" - }; - - var folder2 = new AssetFolderDto - { - Id = Guid.NewGuid().ToString(), - FolderName = "folder2" - }; - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", RootId, A._, A._)) - .Returns(new AssetFoldersDto - { - Items = new List - { - folder1 - }, - Path = new List() - }); - - A.CallTo(() => assets.PostAssetFolderAsync("my-app", - A.That.Matches(x => x.FolderName == "folder2" && x.ParentId == RootId), - A._)) - .Returns(folder2); - - Assert.Equal(folder1.Id, await sut.GetIdAsync("folder1")); - Assert.Equal(folder2.Id, await sut.GetIdAsync("folder2")); - - A.CallTo(() => assets.GetAssetFoldersAsync("my-app", A._, A._, A._)) - .MustHaveHappenedOnceExactly(); - } - } -} diff --git a/cli/Squidex.CLI/Squidex.CLI/Commands/App_Assets.cs b/cli/Squidex.CLI/Squidex.CLI/Commands/App_Assets.cs index d7123423..372ab3b2 100644 --- a/cli/Squidex.CLI/Squidex.CLI/Commands/App_Assets.cs +++ b/cli/Squidex.CLI/Squidex.CLI/Commands/App_Assets.cs @@ -13,6 +13,7 @@ using Squidex.CLI.Commands.Implementation.Sync; using Squidex.CLI.Commands.Implementation.Sync.Assets; using Squidex.CLI.Configuration; +using Squidex.ClientLibrary; using Squidex.ClientLibrary.Management; namespace Squidex.CLI.Commands @@ -42,7 +43,7 @@ public async Task Import(ImportArguments arguments) using (var fs = await FileSystems.CreateAsync(arguments.Path, session.WorkingDirectory)) { - var folders = new FolderTree(session); + var folders = new AssetFolderTree(session.Assets, session.App); var assetQuery = new AssetQuery(); @@ -120,7 +121,7 @@ public async Task Export(ImportArguments arguments) using (var fs = await FileSystems.CreateAsync(arguments.Path, session.WorkingDirectory)) { - var folderTree = new FolderTree(session); + var folderTree = new AssetFolderTree(session.Assets, session.App); var folderNames = new HashSet(); var parentId = await folderTree.GetIdAsync(arguments.TargetFolder); diff --git a/cli/Squidex.CLI/Squidex.CLI/Program.cs b/cli/Squidex.CLI/Squidex.CLI/Program.cs index b2678929..db47ce30 100644 --- a/cli/Squidex.CLI/Squidex.CLI/Program.cs +++ b/cli/Squidex.CLI/Squidex.CLI/Program.cs @@ -13,7 +13,7 @@ using Squidex.CLI.Commands.Implementation; using Squidex.CLI.Commands.Implementation.Sync; using Squidex.CLI.Commands.Implementation.Sync.App; -using Squidex.CLI.Commands.Implementation.Sync.AssertFolders; +using Squidex.CLI.Commands.Implementation.Sync.AssetFolders; using Squidex.CLI.Commands.Implementation.Sync.Assets; using Squidex.CLI.Commands.Implementation.Sync.Contents; using Squidex.CLI.Commands.Implementation.Sync.Rules; diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary.Tests/Squidex.ClientLibrary.Tests.csproj b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary.Tests/Squidex.ClientLibrary.Tests.csproj index 06042ea9..979f9e12 100644 --- a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary.Tests/Squidex.ClientLibrary.Tests.csproj +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary.Tests/Squidex.ClientLibrary.Tests.csproj @@ -9,6 +9,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/EnrichedEvents/EnrichedEventEnvelope.cs b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/EnrichedEvents/EnrichedEventEnvelope.cs index 01d17647..234b2b74 100644 --- a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/EnrichedEvents/EnrichedEventEnvelope.cs +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/EnrichedEvents/EnrichedEventEnvelope.cs @@ -61,7 +61,7 @@ public static EnrichedEventEnvelope DeserializeEnvelope(string json, JsonSeriali /// public static EnrichedEventEnvelope FromJson(string json) { - return HttpClientExtensions.FromJsonWithTypes(json); + return json.FromJsonWithTypes(); } } } diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Management/Generated.cs b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Management/Generated.cs index 7a56f1f6..009083c9 100644 --- a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Management/Generated.cs +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Management/Generated.cs @@ -5981,6 +5981,16 @@ public partial interface IRulesClient /// A server side error occurred. System.Threading.Tasks.Task DeleteRuleEventsAsync(string app, string id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Simulate a rule. + /// + /// The name of the app. + /// The rule to simulate. + /// Rule simulated. + /// A server side error occurred. + System.Threading.Tasks.Task SimulatePOSTAsync(string app, CreateRuleDto request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// /// Simulate a rule. @@ -5989,7 +5999,7 @@ public partial interface IRulesClient /// The id of the rule to simulate. /// Rule simulated. /// A server side error occurred. - System.Threading.Tasks.Task SimulateAsync(string app, string id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + System.Threading.Tasks.Task SimulateGETAsync(string app, string id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// @@ -7099,6 +7109,104 @@ private Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() } } + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Simulate a rule. + /// + /// The name of the app. + /// The rule to simulate. + /// Rule simulated. + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task SimulatePOSTAsync(string app, CreateRuleDto request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + { + if (app == null) + throw new System.ArgumentNullException("app"); + + if (request == null) + throw new System.ArgumentNullException("request"); + + var urlBuilder_ = new System.Text.StringBuilder(); + urlBuilder_.Append("api/apps/{app}/rules/simulate"); + urlBuilder_.Replace("{app}", System.Uri.EscapeDataString(ConvertToString(app, System.Globalization.CultureInfo.InvariantCulture))); + + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + var content_ = new System.Net.Http.StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(request, _settings.Value)); + content_.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json"); + request_.Content = content_; + request_.Method = new System.Net.Http.HttpMethod("POST"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = System.Linq.Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value); + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new SquidexManagementException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 404) + { + string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new SquidexManagementException("Rule or app not found.", status_, responseText_, headers_, null); + } + else + if (status_ == 500) + { + var objectResponse_ = await ReadObjectResponseAsync(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new SquidexManagementException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + throw new SquidexManagementException("Operation failed.", status_, objectResponse_.Text, headers_, objectResponse_.Object, null); + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new SquidexManagementException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// /// Simulate a rule. @@ -7107,7 +7215,7 @@ private Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings() /// The id of the rule to simulate. /// Rule simulated. /// A server side error occurred. - public virtual async System.Threading.Tasks.Task SimulateAsync(string app, string id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) + public virtual async System.Threading.Tasks.Task SimulateGETAsync(string app, string id, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { if (app == null) throw new System.ArgumentNullException("app"); @@ -17877,7 +17985,7 @@ public abstract partial class FieldPropertiesDto public string Label { get; set; } /// - /// Hints to describe the schema. + /// Hints to describe the field. /// [Newtonsoft.Json.JsonProperty("hints", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] [System.ComponentModel.DataAnnotations.StringLength(1000)] @@ -18541,12 +18649,24 @@ public partial class StringFieldPropertiesDto : FieldPropertiesDto [Newtonsoft.Json.JsonProperty("allowedValues", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public System.Collections.Generic.List AllowedValues { get; set; } + /// + /// The allowed schema ids that can be embedded. + /// + [Newtonsoft.Json.JsonProperty("schemaIds", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public System.Collections.Generic.List SchemaIds { get; set; } + /// /// Indicates if the field value must be unique. Ignored for nested fields and localized fields. /// [Newtonsoft.Json.JsonProperty("isUnique", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public bool IsUnique { get; set; } + /// + /// Indicates that other content items or references are embedded. + /// + [Newtonsoft.Json.JsonProperty("isEmbeddable", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public bool IsEmbeddable { get; set; } + /// /// Indicates that the inline editor is enabled for this field. /// @@ -19029,6 +19149,12 @@ public partial class BulkUpdateContentsJobDto [Newtonsoft.Json.JsonProperty("schema", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Schema { get; set; } + /// + /// Makes the update as patch. + /// + [Newtonsoft.Json.JsonProperty("patch", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public bool Patch { get; set; } + /// /// True to delete the content permanently. /// @@ -20531,6 +20657,13 @@ public partial class SimulatedRuleEventsDto : Resource [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "13.15.10.0 (NJsonSchema v10.6.10.0 (Newtonsoft.Json v9.0.0.0))")] public partial class SimulatedRuleEventDto { + /// + /// The unique event id. + /// + [Newtonsoft.Json.JsonProperty("eventId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + [System.ComponentModel.DataAnnotations.Required] + public System.Guid EventId { get; set; } + /// /// The name of the event. /// @@ -22466,6 +22599,12 @@ public partial class WorkflowStepDto [Newtonsoft.Json.JsonProperty("color", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string Color { get; set; } + /// + /// True if the content should be validated when moving to this step. + /// + [Newtonsoft.Json.JsonProperty("validate", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] + public bool Validate { get; set; } + /// /// Indicates if updates should not be allowed. /// diff --git a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Squidex.ClientLibrary.csproj b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Squidex.ClientLibrary.csproj index 0ff035b5..0cf4eb78 100644 --- a/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Squidex.ClientLibrary.csproj +++ b/csharp/Squidex.ClientLibrary/Squidex.ClientLibrary/Squidex.ClientLibrary.csproj @@ -5,15 +5,15 @@ MIT ClientLibrary for Squidex Headless CMS true - enable - latest - enable + enable + latest + enable logo-squared.png MIT https://github.com/Squidex/squidex/ Squidex HeadlessCMS netstandard2.0;netcoreapp3.1;net5.0;net6.0 - 8.20.0 + 8.21.0 diff --git a/jscript/react/sample-blog/src/App.js b/jscript/react/sample-blog/src/App.js index a341fd35..9b060403 100644 --- a/jscript/react/sample-blog/src/App.js +++ b/jscript/react/sample-blog/src/App.js @@ -1,18 +1,6 @@ import React from 'react'; -import { - BrowserRouter as Router, - Switch, - Redirect, - Route, - Link - } from 'react-router-dom'; - -import { - PageSite, - PostSite, - PostsSite, - TopNav -} from './components'; +import { BrowserRouter as Router, Switch, Redirect, Route, Link } from 'react-router-dom'; +import { PageSite, PostSite, PostsSite, TopNav } from './components'; function App() { return ( diff --git a/jscript/react/sample-blog/src/components/PageSite.js b/jscript/react/sample-blog/src/components/PageSite.js index e22e1f15..b20a9d3e 100644 --- a/jscript/react/sample-blog/src/components/PageSite.js +++ b/jscript/react/sample-blog/src/components/PageSite.js @@ -1,6 +1,5 @@ import React from 'react'; import { useParams } from 'react-router-dom'; - import { Page } from './Page.js'; import { getPage } from './../service'; @@ -15,7 +14,7 @@ export const PageSite = () => { async function fetchData() { try { const result = await getPage(slug); - + setPage(result); } catch (ex) { setPage(null); @@ -30,6 +29,6 @@ export const PageSite = () => { } else if (page === null) { return
Page not found.
} else { - return
Loading page...
+ return
Loading Page...
} } diff --git a/jscript/react/sample-blog/src/components/PostSite.js b/jscript/react/sample-blog/src/components/PostSite.js index 9969a264..729bbbc2 100644 --- a/jscript/react/sample-blog/src/components/PostSite.js +++ b/jscript/react/sample-blog/src/components/PostSite.js @@ -1,6 +1,5 @@ import React from 'react'; import { useParams } from 'react-router-dom'; - import { Post } from './Post.js'; import { getPost } from './../service'; @@ -13,7 +12,7 @@ export const PostSite = () => { async function fetchData() { try { const result = await getPost(id); - + setPost(result); } catch (ex) { setPost(null); @@ -28,6 +27,6 @@ export const PostSite = () => { } else if (post === null) { return
Post not found.
} else { - return
Loading post...
+ return
Loading Post...
} } diff --git a/jscript/react/sample-blog/src/components/PostsSite.js b/jscript/react/sample-blog/src/components/PostsSite.js index 531b9bf0..3502620d 100644 --- a/jscript/react/sample-blog/src/components/PostsSite.js +++ b/jscript/react/sample-blog/src/components/PostsSite.js @@ -1,5 +1,4 @@ import React from 'react'; - import { Post } from './Post.js'; import { getPosts } from './../service'; @@ -9,7 +8,7 @@ export const PostsSite = () => { React.useEffect(() => { async function fetchData() { const results = await getPosts(); - + setPosts(results.posts); } @@ -17,14 +16,24 @@ export const PostsSite = () => { }, []); if (!posts) { - return
Loading posts...
+ return
Loading Posts...
} - return posts.map(post => ( -
- + return ( + <> + {posts.length === 0 ? ( + <>No Post found + ) : ( +
+ {posts.map(post => ( +
+ -
-
- )); +
+
+ ))} +
+ )} + + ); } diff --git a/jscript/react/sample-blog/src/components/TopNav.js b/jscript/react/sample-blog/src/components/TopNav.js index afc69f19..4ce04132 100644 --- a/jscript/react/sample-blog/src/components/TopNav.js +++ b/jscript/react/sample-blog/src/components/TopNav.js @@ -1,6 +1,5 @@ import React from 'react'; import { NavLink } from 'react-router-dom'; - import { getPages } from './../service'; export const TopNav = () => {