diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs index d4c0374c8..cc204e759 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/Status.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Infrastructure; using System; namespace Squidex.Domain.Apps.Core.Contents @@ -45,7 +44,7 @@ namespace Squidex.Domain.Apps.Core.Contents public override string ToString() { - return name; + return Name; } public static bool operator ==(Status lhs, Status rhs) diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs new file mode 100644 index 000000000..0e64ea00b --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusColors.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Contents +{ + public static class StatusColors + { + public const string Archived = "#eb3142"; + public const string Draft = "#8091a5"; + public const string Published = "#4bb958"; + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs new file mode 100644 index 000000000..a444badaa --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Contents +{ + public sealed class StatusInfo + { + public Status Status { get; } + + public string Color { get; } + + public StatusInfo(Status status, string color) + { + Status = status; + + Color = color; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 6444ba638..b7ccaf0ed 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs @@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets } } - public async Task> QueryByHashAsync(Guid appId, string hash) + public async Task> QueryByHashAsync(Guid appId, string hash) { using (Profiler.TraceMethod()) { diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index aa24583e6..51e7ca5a9 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs @@ -77,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents if (fullTextIds?.Count == 0) { - return ResultList.Create(0); + return ResultList.CreateFrom(0); } return await contents.QueryAsync(schema, query, fullTextIds, status, inDraft, includeDraft); diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs index 9b79da894..901eb1e68 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -19,15 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Apps public AppHistoryEventsCreator(TypeNameRegistry typeNameRegistry) : base(typeNameRegistry) { - AddEventMessage("AppContributorAssignedEvent", - "assigned {user:[Contributor]} as {[Role]}"); - - AddEventMessage("AppClientUpdatedEvent", - "updated client {[Id]}"); - - AddEventMessage("AppPlanChanged", - "changed plan to {[Plan]}"); - AddEventMessage( "assigned {user:[Contributor]} as {[Role]}"); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index e45e02645..fe8da3714 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -10,7 +10,6 @@ using System.Collections.Generic; using System.Security.Cryptography; using System.Threading.Tasks; using Orleans; -using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; @@ -22,31 +21,31 @@ namespace Squidex.Domain.Apps.Entities.Assets public sealed class AssetCommandMiddleware : GrainCommandMiddleware { private readonly IAssetStore assetStore; + private readonly IAssetEnricher assetEnricher; private readonly IAssetQueryService assetQuery; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IEnumerable> tagGenerators; - private readonly ITagService tagService; public AssetCommandMiddleware( IGrainFactory grainFactory, + IAssetEnricher assetEnricher, IAssetQueryService assetQuery, IAssetStore assetStore, IAssetThumbnailGenerator assetThumbnailGenerator, - IEnumerable> tagGenerators, - ITagService tagService) + IEnumerable> tagGenerators) : base(grainFactory) { + Guard.NotNull(assetEnricher, nameof(assetEnricher)); Guard.NotNull(assetStore, nameof(assetStore)); Guard.NotNull(assetQuery, nameof(assetQuery)); Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); Guard.NotNull(tagGenerators, nameof(tagGenerators)); - Guard.NotNull(tagService, nameof(tagService)); this.assetStore = assetStore; + this.assetEnricher = assetEnricher; this.assetQuery = assetQuery; this.assetThumbnailGenerator = assetThumbnailGenerator; this.tagGenerators = tagGenerators; - this.tagService = tagService; } public override async Task HandleAsync(CommandContext context, Func next) @@ -67,35 +66,30 @@ namespace Squidex.Domain.Apps.Entities.Assets { var existings = await assetQuery.QueryByHashAsync(createAsset.AppId.Id, createAsset.FileHash); - AssetCreatedResult result = null; - foreach (var existing in existings) { if (IsDuplicate(createAsset, existing)) { - var denormalizedTags = await tagService.DenormalizeTagsAsync(createAsset.AppId.Id, TagGroups.Assets, existing.Tags); + var result = new AssetCreatedResult(existing, true); - result = new AssetCreatedResult(existing, true, new HashSet(denormalizedTags.Values)); + context.Complete(result); + await next(); + return; } - - break; } - if (result == null) + foreach (var tagGenerator in tagGenerators) { - foreach (var tagGenerator in tagGenerators) - { - tagGenerator.GenerateTags(createAsset, createAsset.Tags); - } + tagGenerator.GenerateTags(createAsset, createAsset.Tags); + } - var asset = (IAssetEntity)await ExecuteCommandAsync(createAsset); + await HandleCoreAsync(context, next); - result = new AssetCreatedResult(asset, false, createAsset.Tags); + var asset = context.PlainResult as IEnrichedAssetEntity; - await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null); - } + context.Complete(new AssetCreatedResult(asset, false)); - context.Complete(result); + await assetStore.CopyAsync(context.ContextId.ToString(), createAsset.AssetId.ToString(), asset.FileVersion, null); } finally { @@ -112,11 +106,11 @@ namespace Squidex.Domain.Apps.Entities.Assets try { - var result = (AssetResult)await ExecuteAndAdjustTagsAsync(updateAsset); + await HandleCoreAsync(context, next); - context.Complete(result); + var asset = context.PlainResult as IAssetEntity; - await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), result.Asset.FileVersion, null); + await assetStore.CopyAsync(context.ContextId.ToString(), updateAsset.AssetId.ToString(), asset.FileVersion, null); } finally { @@ -126,34 +120,23 @@ namespace Squidex.Domain.Apps.Entities.Assets break; } - case AssetCommand command: - { - var result = await ExecuteAndAdjustTagsAsync(command); - - context.Complete(result); - - break; - } - default: - await base.HandleAsync(context, next); + await HandleCoreAsync(context, next); break; } } - private async Task ExecuteAndAdjustTagsAsync(AssetCommand command) + private async Task HandleCoreAsync(CommandContext context, Func next) { - var result = await ExecuteCommandAsync(command); + await base.HandleAsync(context, next); - if (result is IAssetEntity asset) + if (context.PlainResult is IAssetEntity asset && !(context.PlainResult is IEnrichedAssetEntity)) { - var denormalizedTags = await tagService.DenormalizeTagsAsync(asset.AppId.Id, TagGroups.Assets, asset.Tags); + var enriched = await assetEnricher.EnrichAsync(asset); - return new AssetResult(asset, new HashSet(denormalizedTags.Values)); + context.Complete(enriched); } - - return result; } private static bool IsDuplicate(CreateAsset createAsset, IAssetEntity asset) diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs index 9ccc00763..aa932bf36 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCreatedResult.cs @@ -5,17 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System.Collections.Generic; - namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class AssetCreatedResult : AssetResult + public sealed class AssetCreatedResult { + public IEnrichedAssetEntity Asset { get; } + public bool IsDuplicate { get; } - public AssetCreatedResult(IAssetEntity asset, bool isDuplicate, HashSet tags) - : base(asset, tags) + public AssetCreatedResult(IEnrichedAssetEntity asset, bool isDuplicate) { + Asset = asset; + IsDuplicate = isDuplicate; } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetEnricher.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetEnricher.cs new file mode 100644 index 000000000..3a1b802e8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetEnricher.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public sealed class AssetEnricher : IAssetEnricher + { + private readonly ITagService tagService; + + public AssetEnricher(ITagService tagService) + { + Guard.NotNull(tagService, nameof(tagService)); + + this.tagService = tagService; + } + + public async Task EnrichAsync(IAssetEntity asset) + { + Guard.NotNull(asset, nameof(asset)); + + var enriched = await EnrichAsync(Enumerable.Repeat(asset, 1)); + + return enriched[0]; + } + + public async Task> EnrichAsync(IEnumerable assets) + { + Guard.NotNull(assets, nameof(assets)); + + using (Profiler.TraceMethod()) + { + var results = new List(); + + foreach (var group in assets.GroupBy(x => x.AppId.Id)) + { + var tagsById = await CalculateTags(group); + + foreach (var asset in group) + { + var result = SimpleMapper.Map(asset, new AssetEntity()); + + result.TagNames = new HashSet(); + + if (asset.Tags != null) + { + foreach (var id in asset.Tags) + { + if (tagsById.TryGetValue(id, out var name)) + { + result.TagNames.Add(name); + } + } + } + + results.Add(result); + } + } + + return results; + } + } + + private async Task> CalculateTags(IGrouping group) + { + var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet(); + + return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds); + } + } +} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs similarity index 86% rename from tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs rename to src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs index 2daf028de..150e53b78 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeAssetEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs @@ -1,19 +1,18 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System; using System.Collections.Generic; using NodaTime; -using Squidex.Domain.Apps.Entities.Assets; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Contents.TestData +namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class FakeAssetEntity : IAssetEntity + public sealed class AssetEntity : IEnrichedAssetEntity { public NamedId AppId { get; set; } @@ -31,6 +30,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData public HashSet Tags { get; set; } + public HashSet TagNames { get; set; } + public long Version { get; set; } public string MimeType { get; set; } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs index 833920248..29d6b28e9 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetQueryService.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Options; using Microsoft.OData; @@ -21,9 +20,10 @@ using Squidex.Infrastructure.Queries.OData; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetQueryService : IAssetQueryService + public sealed class AssetQueryService : IAssetQueryService { private readonly ITagService tagService; + private readonly IAssetEnricher assetEnricher; private readonly IAssetRepository assetRepository; private readonly AssetOptions options; @@ -32,76 +32,82 @@ namespace Squidex.Domain.Apps.Entities.Assets get { return options.DefaultPageSizeGraphQl; } } - public AssetQueryService(ITagService tagService, IAssetRepository assetRepository, IOptions options) + public AssetQueryService( + ITagService tagService, + IAssetEnricher assetEnricher, + IAssetRepository assetRepository, + IOptions options) { Guard.NotNull(tagService, nameof(tagService)); - Guard.NotNull(options, nameof(options)); + Guard.NotNull(assetEnricher, nameof(assetEnricher)); Guard.NotNull(assetRepository, nameof(assetRepository)); + Guard.NotNull(options, nameof(options)); + this.tagService = tagService; + this.assetEnricher = assetEnricher; this.assetRepository = assetRepository; this.options = options.Value; - this.tagService = tagService; - } - - public Task FindAssetAsync(QueryContext context, Guid id) - { - Guard.NotNull(context, nameof(context)); - - return FindAssetAsync(context.App.Id, id); } - public async Task FindAssetAsync(Guid appId, Guid id) + public async Task FindAssetAsync( Guid id) { var asset = await assetRepository.FindAssetAsync(id); if (asset != null) { - await DenormalizeTagsAsync(appId, Enumerable.Repeat(asset, 1)); + return await assetEnricher.EnrichAsync(asset); } - return asset; + return null; } - public async Task> QueryByHashAsync(Guid appId, string hash) + public async Task> QueryByHashAsync(Guid appId, string hash) { Guard.NotNull(hash, nameof(hash)); var assets = await assetRepository.QueryByHashAsync(appId, hash); - await DenormalizeTagsAsync(appId, assets); - - return assets; + return await assetEnricher.EnrichAsync(assets); } - public async Task> QueryAsync(QueryContext context, Q query) + public async Task> QueryAsync(QueryContext context, Q query) { Guard.NotNull(context, nameof(context)); Guard.NotNull(query, nameof(query)); IResultList assets; - if (query.Ids != null) + if (query.Ids != null && query.Ids.Count > 0) { - assets = await assetRepository.QueryAsync(context.App.Id, new HashSet(query.Ids)); - assets = Sort(assets, query.Ids); + assets = await QueryByIdsAsync(context, query); } else { - var parsedQuery = ParseQuery(context, query.ODataQuery); - - assets = await assetRepository.QueryAsync(context.App.Id, parsedQuery); + assets = await QueryByQueryAsync(context, query); } - await DenormalizeTagsAsync(context.App.Id, assets); + var enriched = await assetEnricher.EnrichAsync(assets); - return assets; + return ResultList.Create(assets.Total, enriched); } - private static IResultList Sort(IResultList assets, IReadOnlyList ids) + private async Task> QueryByQueryAsync(QueryContext context, Q query) + { + var parsedQuery = ParseQuery(context, query.ODataQuery); + + return await assetRepository.QueryAsync(context.App.Id, parsedQuery); + } + + private async Task> QueryByIdsAsync(QueryContext context, Q query) { - var sorted = ids.Select(id => assets.FirstOrDefault(x => x.Id == id)).Where(x => x != null); + var assets = await assetRepository.QueryAsync(context.App.Id, new HashSet(query.Ids)); - return ResultList.Create(assets.Total, sorted); + return Sort(assets, query.Ids); + } + + private static IResultList Sort(IResultList assets, IReadOnlyList ids) + { + return assets.SortSet(x => x.Id, ids); } private Query ParseQuery(QueryContext context, string query) @@ -140,34 +146,5 @@ namespace Squidex.Domain.Apps.Entities.Assets throw new ValidationException($"Failed to parse query: {ex.Message}", ex); } } - - private async Task DenormalizeTagsAsync(Guid appId, IEnumerable assets) - { - var tags = new HashSet(assets.Where(x => x.Tags != null).SelectMany(x => x.Tags).Distinct()); - - var tagsById = await tagService.DenormalizeTagsAsync(appId, TagGroups.Assets, tags); - - foreach (var asset in assets) - { - if (asset.Tags?.Count > 0) - { - var tagNames = asset.Tags.ToList(); - - asset.Tags.Clear(); - - foreach (var id in tagNames) - { - if (tagsById.TryGetValue(id, out var name)) - { - asset.Tags.Add(name); - } - } - } - else - { - asset.Tags?.Clear(); - } - } - } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs new file mode 100644 index 000000000..1807af316 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetEnricher.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Assets +{ + public interface IAssetEnricher + { + Task EnrichAsync(IAssetEntity asset); + + Task> EnrichAsync(IEnumerable assets); + } +} \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs index 501d690a9..a186e376c 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs @@ -16,10 +16,10 @@ namespace Squidex.Domain.Apps.Entities.Assets { int DefaultPageSizeGraphQl { get; } - Task> QueryByHashAsync(Guid appId, string hash); + Task> QueryByHashAsync(Guid appId, string hash); - Task> QueryAsync(QueryContext contex, Q query); + Task> QueryAsync(QueryContext contex, Q query); - Task FindAssetAsync(QueryContext context, Guid id); + Task FindAssetAsync(Guid id); } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs b/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs similarity index 64% rename from src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs rename to src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs index b43713da5..eab0cde16 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetResult.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/IEnrichedAssetEntity.cs @@ -9,17 +9,8 @@ using System.Collections.Generic; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetResult + public interface IEnrichedAssetEntity : IAssetEntity { - public IAssetEntity Asset { get; } - - public HashSet Tags { get; } - - public AssetResult(IAssetEntity asset, HashSet tags) - { - Asset = asset; - - Tags = tags; - } + HashSet TagNames { get; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs index 12de8c72a..533ce993f 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs @@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories { public interface IAssetRepository { - Task> QueryByHashAsync(Guid appId, string hash); + Task> QueryByHashAsync(Guid appId, string hash); Task> QueryAsync(Guid appId, Query query); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs new file mode 100644 index 000000000..d72e3eee1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentCommandMiddleware.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentCommandMiddleware : GrainCommandMiddleware + { + private readonly IContentEnricher contentEnricher; + + public ContentCommandMiddleware(IGrainFactory grainFactory, IContentEnricher contentEnricher) + : base(grainFactory) + { + Guard.NotNull(contentEnricher, nameof(contentEnricher)); + + this.contentEnricher = contentEnricher; + } + + public override async Task HandleAsync(CommandContext context, Func next) + { + await base.HandleAsync(context, next); + + if (context.PlainResult is IContentEntity content && !(context.PlainResult is IEnrichedContentEntity)) + { + var enriched = await contentEnricher.EnrichAsync(content); + + context.Complete(enriched); + } + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs new file mode 100644 index 000000000..e87aada08 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEnricher.cs @@ -0,0 +1,95 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public sealed class ContentEnricher : IContentEnricher + { + private const string DefaultColor = StatusColors.Draft; + private readonly IContentWorkflow contentWorkflow; + + public ContentEnricher(IContentWorkflow contentWorkflow) + { + this.contentWorkflow = contentWorkflow; + } + + public async Task EnrichAsync(IContentEntity content) + { + Guard.NotNull(content, nameof(content)); + + var enriched = await EnrichAsync(Enumerable.Repeat(content, 1)); + + return enriched[0]; + } + + public async Task> EnrichAsync(IEnumerable contents) + { + Guard.NotNull(contents, nameof(contents)); + + using (Profiler.TraceMethod()) + { + var results = new List(); + + var cache = new Dictionary<(Guid, Status), StatusInfo>(); + + foreach (var content in contents) + { + var result = SimpleMapper.Map(content, new ContentEntity()); + + await ResolveColorAsync(content, result, cache); + await ResolveNextsAsync(content, result); + await ResolveCanUpdateAsync(content, result); + + results.Add(result); + } + + return results; + } + } + + private async Task ResolveCanUpdateAsync(IContentEntity content, ContentEntity result) + { + result.CanUpdate = await contentWorkflow.CanUpdateAsync(content); + } + + private async Task ResolveNextsAsync(IContentEntity content, ContentEntity result) + { + result.Nexts = await contentWorkflow.GetNextsAsync(content); + } + + private async Task ResolveColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache) + { + result.StatusColor = await GetColorAsync(content.SchemaId, content.Status, cache); + } + + private async Task GetColorAsync(NamedId schemaId, Status status, Dictionary<(Guid, Status), StatusInfo> cache) + { + if (!cache.TryGetValue((schemaId.Id, status), out var info)) + { + info = await contentWorkflow.GetInfoAsync(status); + + if (info == null) + { + info = new StatusInfo(status, DefaultColor); + } + + cache[(schemaId.Id, status)] = info; + } + + return info.Color; + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs index 8b1b6aac1..2e9f92115 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentEntity.cs @@ -12,7 +12,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents { - public sealed class ContentEntity : IContentEntity + public sealed class ContentEntity : IEnrichedContentEntity { public Guid Id { get; set; } @@ -38,6 +38,12 @@ namespace Squidex.Domain.Apps.Entities.Contents public Status Status { get; set; } + public StatusInfo[] Nexts { get; set; } + + public string StatusColor { get; set; } + + public bool CanUpdate { get; set; } + public bool IsPending { get; set; } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index 60af67527..9d4ba5eb3 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -83,9 +83,9 @@ namespace Squidex.Domain.Apps.Entities.Contents await ctx.ExecuteScriptAsync(s => s.Change, "Published", c, c.Data); } - var status = await contentWorkflow.GetInitialStatusAsync(ctx.Schema); + var statusInfo = await contentWorkflow.GetInitialStatusAsync(ctx.Schema); - Create(c, status); + Create(c, statusInfo.Status); return Snapshot; }); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs index b67536690..88d8907be 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentQueryService.cs @@ -34,10 +34,12 @@ namespace Squidex.Domain.Apps.Entities.Contents public sealed class ContentQueryService : IContentQueryService { private static readonly Status[] StatusPublishedOnly = { Status.Published }; - private readonly IContentRepository contentRepository; - private readonly IContentVersionLoader contentVersionLoader; + private static readonly IResultList EmptyContents = ResultList.CreateFrom(0); private readonly IAppProvider appProvider; private readonly IAssetUrlGenerator assetUrlGenerator; + private readonly IContentEnricher contentEnricher; + private readonly IContentRepository contentRepository; + private readonly IContentVersionLoader contentVersionLoader; private readonly IScriptEngine scriptEngine; private readonly ContentOptions options; private readonly EdmModelBuilder modelBuilder; @@ -50,6 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public ContentQueryService( IAppProvider appProvider, IAssetUrlGenerator assetUrlGenerator, + IContentEnricher contentEnricher, IContentRepository contentRepository, IContentVersionLoader contentVersionLoader, IScriptEngine scriptEngine, @@ -58,6 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(assetUrlGenerator, nameof(assetUrlGenerator)); + Guard.NotNull(contentEnricher, nameof(contentEnricher)); Guard.NotNull(contentRepository, nameof(contentRepository)); Guard.NotNull(contentVersionLoader, nameof(contentVersionLoader)); Guard.NotNull(modelBuilder, nameof(modelBuilder)); @@ -66,6 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Contents this.appProvider = appProvider; this.assetUrlGenerator = assetUrlGenerator; + this.contentEnricher = contentEnricher; this.contentRepository = contentRepository; this.contentVersionLoader = contentVersionLoader; this.modelBuilder = modelBuilder; @@ -73,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Contents this.scriptEngine = scriptEngine; } - public async Task FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = -1) + public async Task FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = -1) { Guard.NotNull(context, nameof(context)); @@ -83,25 +88,27 @@ namespace Squidex.Domain.Apps.Entities.Contents using (Profiler.TraceMethod()) { - var isVersioned = version > EtagVersion.Empty; - - var status = GetStatus(context); + IContentEntity content; - var content = - isVersioned ? - await FindContentByVersionAsync(id, version) : - await FindContentAsync(context, id, status, schema); + if (version > EtagVersion.Empty) + { + content = await FindByVersionAsync(id, version); + } + else + { + content = await FindCoreAsync(context, id, schema); + } if (content == null || (content.Status != Status.Published && !context.IsFrontendClient) || content.SchemaId.Id != schema.Id) { throw new DomainObjectNotFoundException(id.ToString(), typeof(IContentEntity)); } - return Transform(context, schema, content); + return await TransformAsync(context, schema, content); } } - public async Task> QueryAsync(QueryContext context, string schemaIdOrName, Q query) + public async Task> QueryAsync(QueryContext context, string schemaIdOrName, Q query) { Guard.NotNull(context, nameof(context)); @@ -111,95 +118,88 @@ namespace Squidex.Domain.Apps.Entities.Contents using (Profiler.TraceMethod()) { - var status = GetStatus(context); - IResultList contents; - if (query.Ids?.Count > 0) + if (query.Ids != null && query.Ids.Count > 0) { - contents = await QueryAsync(context, schema, query.Ids.ToHashSet(), status); - contents = SortSet(contents, query.Ids); + contents = await QueryByIdsAsync(context, query, schema); } else { - var parsedQuery = ParseQuery(context, query.ODataQuery, schema); - - contents = await QueryAsync(context, schema, parsedQuery, status); + contents = await QueryByQueryAsync(context, query, schema); } - return Transform(context, schema, contents); + return await TransformAsync(context, schema, contents); } } - public async Task> QueryAsync(QueryContext context, IReadOnlyList ids) + public async Task> QueryAsync(QueryContext context, IReadOnlyList ids) { Guard.NotNull(context, nameof(context)); using (Profiler.TraceMethod()) { - var status = GetStatus(context); - - List result; - - if (ids?.Count > 0) + if (ids == null || ids.Count == 0) { - var contents = await QueryAsync(context, ids, status); + return EmptyContents; + } - var permissions = context.User.Permissions(); + var results = new List(); - contents = contents.Where(x => HasPermission(permissions, x.Schema)).ToList(); + var contents = await QueryCoreAsync(context, ids); - result = contents.Select(x => Transform(context, x.Schema, x.Content)).ToList(); - result = SortList(result, ids).ToList(); - } - else + var permissions = context.User.Permissions(); + + foreach (var group in contents.GroupBy(x => x.Schema.Id)) { - result = new List(); + var schema = group.First().Schema; + + if (HasPermission(permissions, schema)) + { + var enriched = await TransformCoreAsync(context, schema, group.Select(x => x.Content)); + + results.AddRange(enriched); + } } - return result; + return ResultList.Create(results.Count, results.SortList(x => x.Id, ids)); } } - private IResultList Transform(QueryContext context, ISchemaEntity schema, IResultList contents) + private async Task> TransformAsync(QueryContext context, ISchemaEntity schema, IResultList contents) { - var transformed = TransformCore(context, schema, contents); + var transformed = await TransformCoreAsync(context, schema, contents); return ResultList.Create(contents.Total, transformed); } - private IContentEntity Transform(QueryContext context, ISchemaEntity schema, IContentEntity content) + private async Task TransformAsync(QueryContext context, ISchemaEntity schema, IContentEntity content) { - return TransformCore(context, schema, Enumerable.Repeat(content, 1)).FirstOrDefault(); - } + var transformed = await TransformCoreAsync(context, schema, Enumerable.Repeat(content, 1)); - private static IResultList SortSet(IResultList contents, IReadOnlyList ids) - { - return ResultList.Create(contents.Total, SortList(contents, ids)); + return transformed[0]; } - private static IEnumerable SortList(IEnumerable contents, IReadOnlyList ids) - { - return ids.Select(id => contents.FirstOrDefault(x => x.Id == id)).Where(x => x != null); - } - - private IEnumerable TransformCore(QueryContext context, ISchemaEntity schema, IEnumerable contents) + private async Task> TransformCoreAsync(QueryContext context, ISchemaEntity schema, IEnumerable contents) { using (Profiler.TraceMethod()) { + var results = new List(); + var converters = GenerateConverters(context).ToArray(); var scriptText = schema.SchemaDef.Scripts.Query; + var scripting = !string.IsNullOrWhiteSpace(scriptText); - var isScripting = !string.IsNullOrWhiteSpace(scriptText); + var enriched = await contentEnricher.EnrichAsync(contents); - foreach (var content in contents) + foreach (var content in enriched) { var result = SimpleMapper.Map(content, new ContentEntity()); if (result.Data != null) { - if (!context.IsFrontendClient && isScripting) + if (!context.IsFrontendClient && scripting) { var ctx = new ScriptContext { User = context.User, Data = content.Data, ContentId = content.Id }; @@ -218,8 +218,10 @@ namespace Squidex.Domain.Apps.Entities.Contents result.DataDraft = null; } - yield return result; + results.Add(result); } + + return results; } } @@ -344,32 +346,46 @@ namespace Squidex.Domain.Apps.Entities.Contents } } - private Task> QueryAsync(QueryContext context, IReadOnlyList ids, Status[] status) + private async Task> QueryByQueryAsync(QueryContext context, Q query, ISchemaEntity schema) + { + var parsedQuery = ParseQuery(context, query.ODataQuery, schema); + + return await QueryCoreAsync(context, schema, parsedQuery); + } + + private async Task> QueryByIdsAsync(QueryContext context, Q query, ISchemaEntity schema) + { + var contents = await QueryCoreAsync(context, schema, query.Ids.ToHashSet()); + + return contents.SortSet(x => x.Id, query.Ids); + } + + private Task> QueryCoreAsync(QueryContext context, IReadOnlyList ids) { - return contentRepository.QueryAsync(context.App, status, new HashSet(ids), ShouldIncludeDraft(context)); + return contentRepository.QueryAsync(context.App, GetStatus(context), new HashSet(ids), WithDraft(context)); } - private Task> QueryAsync(QueryContext context, ISchemaEntity schema, Query query, Status[] status) + private Task> QueryCoreAsync(QueryContext context, ISchemaEntity schema, Query query) { - return contentRepository.QueryAsync(context.App, schema, status, context.IsFrontendClient, query, ShouldIncludeDraft(context)); + return contentRepository.QueryAsync(context.App, schema, GetStatus(context), context.IsFrontendClient, query, WithDraft(context)); } - private Task> QueryAsync(QueryContext context, ISchemaEntity schema, HashSet ids, Status[] status) + private Task> QueryCoreAsync(QueryContext context, ISchemaEntity schema, HashSet ids) { - return contentRepository.QueryAsync(context.App, schema, status, ids, ShouldIncludeDraft(context)); + return contentRepository.QueryAsync(context.App, schema, GetStatus(context), ids, WithDraft(context)); } - private Task FindContentAsync(QueryContext context, Guid id, Status[] status, ISchemaEntity schema) + private Task FindCoreAsync(QueryContext context, Guid id, ISchemaEntity schema) { - return contentRepository.FindContentAsync(context.App, schema, status, id, ShouldIncludeDraft(context)); + return contentRepository.FindContentAsync(context.App, schema, GetStatus(context), id, WithDraft(context)); } - private Task FindContentByVersionAsync(Guid id, long version) + private Task FindByVersionAsync(Guid id, long version) { return contentVersionLoader.LoadAsync(id, version); } - private static bool ShouldIncludeDraft(QueryContext context) + private static bool WithDraft(QueryContext context) { return context.ApiStatus == StatusForApi.All || context.IsFrontendClient; } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs index a3da96afd..6d0156868 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs @@ -11,42 +11,77 @@ using System.Linq; using System.Threading.Tasks; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Entities.Contents { public sealed class DefaultContentWorkflow : IContentWorkflow { - private static readonly Status[] All = { Status.Archived, Status.Draft, Status.Published }; + private static readonly StatusInfo InfoArchived = new StatusInfo(Status.Archived, StatusColors.Archived); + private static readonly StatusInfo InfoDraft = new StatusInfo(Status.Draft, StatusColors.Draft); + private static readonly StatusInfo InfoPublished = new StatusInfo(Status.Published, StatusColors.Published); - private static readonly Dictionary Flow = new Dictionary + private static readonly StatusInfo[] All = { - [Status.Draft] = new[] { Status.Archived, Status.Published }, - [Status.Archived] = new[] { Status.Draft }, - [Status.Published] = new[] { Status.Draft, Status.Archived } + InfoArchived, + InfoDraft, + InfoPublished }; - public Task GetInitialStatusAsync(ISchemaEntity schema) + private static readonly Dictionary Flow = + new Dictionary + { + [Status.Archived] = (InfoArchived, new[] + { + InfoDraft + }), + [Status.Draft] = (InfoDraft, new[] + { + InfoArchived, + InfoPublished + }), + [Status.Published] = (InfoPublished, new[] + { + InfoDraft, + InfoArchived + }) + }; + + public Task GetInitialStatusAsync(ISchemaEntity schema) { - return Task.FromResult(Status.Draft); + var result = InfoDraft; + + return Task.FromResult(result); } public Task CanMoveToAsync(IContentEntity content, Status next) { - return Task.FromResult(Flow.TryGetValue(content.Status, out var state) && state.Contains(next)); + var result = Flow.TryGetValue(content.Status, out var step) && step.Transitions.Any(x => x.Status == next); + + return Task.FromResult(result); } public Task CanUpdateAsync(IContentEntity content) { - return Task.FromResult(content.Status != Status.Archived); + var result = content.Status != Status.Archived; + + return Task.FromResult(result); } - public Task GetNextsAsync(IContentEntity content) + public Task GetInfoAsync(Status status) { - return Task.FromResult(Flow.TryGetValue(content.Status, out var result) ? result : Array.Empty()); + var result = Flow[status].Info; + + return Task.FromResult(result); + } + + public Task GetNextsAsync(IContentEntity content) + { + var result = Flow.TryGetValue(content.Status, out var step) ? step.Transitions : Array.Empty(); + + return Task.FromResult(result); } - public Task GetAllAsync(ISchemaEntity schema) + public Task GetAllAsync(ISchemaEntity schema) { return Task.FromResult(All); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs index c5dbf6024..db7e2859c 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { public sealed class GraphQLExecutionContext : QueryExecutionContext { - private static readonly List EmptyAssets = new List(); + private static readonly List EmptyAssets = new List(); private static readonly List EmptyContents = new List(); private readonly IDataLoaderContextAccessor dataLoaderContextAccessor; private readonly IDependencyResolver resolver; @@ -53,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL execution.UserContext = this; } - public override Task FindAssetAsync(Guid id) + public override Task FindAssetAsync(Guid id) { var dataLoader = GetAssetsLoader(); @@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return dataLoader.LoadAsync(id); } - public async Task> GetReferencedAssetsAsync(IJsonValue value) + public async Task> GetReferencedAssetsAsync(IJsonValue value) { var ids = ParseIds(value); @@ -95,9 +95,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL return await dataLoader.LoadManyAsync(ids); } - private IDataLoader GetAssetsLoader() + private IDataLoader GetAssetsLoader() { - return dataLoaderContextAccessor.Context.GetOrAddBatchLoader("Assets", + return dataLoaderContextAccessor.Context.GetOrAddBatchLoader("Assets", async batch => { var result = await GetReferencedAssetsAsync(new List(batch)); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs index 8f3207ebf..c8ee5d2f2 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AllTypes.cs @@ -7,7 +7,6 @@ using System; using GraphQL.Types; -using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Utils; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs index ecdcb3620..801f44c81 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs @@ -13,7 +13,7 @@ using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { - public sealed class AssetGraphType : ObjectGraphType + public sealed class AssetGraphType : ObjectGraphType { public AssetGraphType(IGraphModel model) { @@ -167,8 +167,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { Name = "tags", ResolvedType = null, - Resolver = Resolve(x => x.Tags), - Description = "The height of the image in pixels if the asset is an image.", + Resolver = Resolve(x => x.TagNames), + Description = "The asset tags.", Type = AllTypes.NonNullTagsType }); @@ -186,9 +186,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = "An asset"; } - private static IFieldResolver Resolve(Func action) + private static IFieldResolver Resolve(Func action) { - return new FuncFieldResolver(c => action(c.Source)); + return new FuncFieldResolver(c => action(c.Source)); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs index a6d876742..63f571c1a 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/ContentGraphType.cs @@ -13,7 +13,7 @@ using Squidex.Domain.Apps.Entities.Schemas; namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types { - public sealed class ContentGraphType : ObjectGraphType + public sealed class ContentGraphType : ObjectGraphType { public void Initialize(IGraphModel model, ISchemaEntity schema, IComplexGraphType contentDataType) { @@ -78,6 +78,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = $"The the status of the {schemaName} content." }); + AddField(new FieldType + { + Name = "statusColor", + ResolvedType = AllTypes.NonNullString, + Resolver = Resolve(x => x.StatusColor), + Description = $"The color status of the {schemaName} content." + }); + AddField(new FieldType { Name = "url", @@ -108,9 +116,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types Description = $"The structure of a {schemaName} content type."; } - private static IFieldResolver Resolve(Func action) + private static IFieldResolver Resolve(Func action) { - return new FuncFieldResolver(c => action(c.Source)); + return new FuncFieldResolver(c => action(c.Source)); } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs new file mode 100644 index 000000000..1b7334134 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentEnricher.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public interface IContentEnricher + { + Task EnrichAsync(IContentEntity content); + + Task> EnrichAsync(IEnumerable contents); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs index 769422de1..11b64a42f 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentQueryService.cs @@ -17,11 +17,11 @@ namespace Squidex.Domain.Apps.Entities.Contents { int DefaultPageSizeGraphQl { get; } - Task> QueryAsync(QueryContext context, IReadOnlyList ids); + Task> QueryAsync(QueryContext context, IReadOnlyList ids); - Task> QueryAsync(QueryContext context, string schemaIdOrName, Q query); + Task> QueryAsync(QueryContext context, string schemaIdOrName, Q query); - Task FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = EtagVersion.Any); + Task FindContentAsync(QueryContext context, string schemaIdOrName, Guid id, long version = EtagVersion.Any); Task GetSchemaOrThrowAsync(QueryContext context, string schemaIdOrName); } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs index c812f8a4f..8a2f0f571 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs @@ -13,14 +13,16 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentWorkflow { - Task GetInitialStatusAsync(ISchemaEntity schema); + Task GetInitialStatusAsync(ISchemaEntity schema); Task CanMoveToAsync(IContentEntity content, Status next); Task CanUpdateAsync(IContentEntity content); - Task GetNextsAsync(IContentEntity content); + Task GetInfoAsync(Status status); - Task GetAllAsync(ISchemaEntity schema); + Task GetNextsAsync(IContentEntity content); + + Task GetAllAsync(ISchemaEntity schema); } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs b/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs new file mode 100644 index 000000000..45b3506a4 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/IEnrichedContentEntity.cs @@ -0,0 +1,20 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public interface IEnrichedContentEntity : IContentEntity + { + bool CanUpdate { get; } + + string StatusColor { get; } + + StatusInfo[] Nexts { get; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs b/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs index 8b186cbd4..a40896a94 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/QueryExecutionContext.cs @@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Contents public class QueryExecutionContext { private readonly ConcurrentDictionary cachedContents = new ConcurrentDictionary(); - private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cachedAssets = new ConcurrentDictionary(); private readonly IContentQueryService contentQuery; private readonly IAssetQueryService assetQuery; private readonly QueryContext context; @@ -34,13 +34,13 @@ namespace Squidex.Domain.Apps.Entities.Contents this.context = context; } - public virtual async Task FindAssetAsync(Guid id) + public virtual async Task FindAssetAsync(Guid id) { var asset = cachedAssets.GetOrDefault(id); if (asset == null) { - asset = await assetQuery.FindAssetAsync(context, id); + asset = await assetQuery.FindAssetAsync(id); if (asset != null) { @@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return result; } - public virtual async Task> GetReferencedAssetsAsync(ICollection ids) + public virtual async Task> GetReferencedAssetsAsync(ICollection ids) { Guard.NotNull(ids, nameof(ids)); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs b/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs index fe48b89e4..6d0c7799c 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ScheduleJob.cs @@ -16,12 +16,12 @@ namespace Squidex.Domain.Apps.Entities.Contents { public Guid Id { get; } + public Instant DueTime { get; } + public Status Status { get; } public RefToken ScheduledBy { get; } - public Instant DueTime { get; } - public ScheduleJob(Guid id, Status status, RefToken scheduledBy, Instant dueTime) { Id = id; diff --git a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs index 0f0234dc9..e413b9117 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/State/ContentState.cs @@ -50,11 +50,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.State SimpleMapper.Map(@event, this); UpdateData(null, @event.Data, false); - - if (Status == default) - { - Status = Status.Draft; - } } protected void On(ContentChangesPublished @event) @@ -68,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.State { ScheduleJob = null; - Status = @event.Status; + SimpleMapper.Map(@event, this); if (@event.Status == Status.Published) { diff --git a/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs b/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs index 92f18d5a2..7e5c018b3 100644 --- a/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs +++ b/src/Squidex.Domain.Apps.Entities/History/ParsedHistoryEvent.cs @@ -53,14 +53,17 @@ namespace Squidex.Domain.Apps.Entities.History message = new Lazy(() => { - var result = texts[item.Message]; - - foreach (var kvp in item.Parameters) + if (texts.TryGetValue(item.Message, out var result)) { - result = result.Replace("[" + kvp.Key + "]", kvp.Value); + foreach (var kvp in item.Parameters) + { + result = result.Replace("[" + kvp.Key + "]", kvp.Value); + } + + return result; } - return result; + return null; }); } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs index f640cf173..21721c416 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaHistoryEventsCreator.cs @@ -19,12 +19,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas public SchemaHistoryEventsCreator(TypeNameRegistry typeNameRegistry) : base(typeNameRegistry) { - AddEventMessage("SchemaCreatedEvent", - "created schema {[Name]}."); - - AddEventMessage("ScriptsConfiguredEvent", - "configured script of schema {[Name]}."); - AddEventMessage( "reordered fields of schema {[Name]}."); diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs index 317b3b176..0aeb82997 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentCreated.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Contents { - [EventType(nameof(ContentCreated))] + [EventType(nameof(ContentCreated), 2)] public sealed class ContentCreated : ContentEvent { public Status Status { get; set; } diff --git a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs index e7b201ffe..86f782df4 100644 --- a/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs +++ b/src/Squidex.Domain.Apps.Events/Contents/ContentStatusChanged.cs @@ -10,7 +10,7 @@ using Squidex.Infrastructure.EventSourcing; namespace Squidex.Domain.Apps.Events.Contents { - [EventType(nameof(ContentStatusChanged))] + [EventType(nameof(ContentStatusChanged), 2)] public sealed class ContentStatusChanged : ContentEvent { public StatusChange Change { get; set; } diff --git a/src/Squidex.Infrastructure/CollectionExtensions.cs b/src/Squidex.Infrastructure/CollectionExtensions.cs index cfe546a24..a8f44e515 100644 --- a/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -13,6 +13,16 @@ namespace Squidex.Infrastructure { public static class CollectionExtensions { + public static IResultList SortSet(this IResultList input, Func idProvider, IReadOnlyList ids) where T : class + { + return ResultList.Create(input.Total, SortList(input, idProvider, ids)); + } + + public static IEnumerable SortList(this IEnumerable input, Func idProvider, IReadOnlyList ids) where T : class + { + return ids.Select(id => input.FirstOrDefault(x => Equals(idProvider(x), id))).Where(x => x != null); + } + public static void AddRange(this ICollection target, IEnumerable source) { foreach (var value in source) diff --git a/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs b/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs index bbf07c80e..e3f800f4b 100644 --- a/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs +++ b/src/Squidex.Infrastructure/EventSourcing/DefaultEventDataFormatter.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Diagnostics; using Squidex.Infrastructure.Json; namespace Squidex.Infrastructure.EventSourcing @@ -33,6 +34,11 @@ namespace Squidex.Infrastructure.EventSourcing if (payloadObj is IMigrated migratedEvent) { payloadObj = migratedEvent.Migrate(); + + if (ReferenceEquals(migratedEvent, payloadObj)) + { + Debug.WriteLine("Migration should return new event."); + } } var envelope = new Envelope(payloadObj, eventData.Headers); @@ -47,6 +53,11 @@ namespace Squidex.Infrastructure.EventSourcing if (migrate && eventPayload is IMigrated migratedEvent) { eventPayload = migratedEvent.Migrate(); + + if (ReferenceEquals(migratedEvent, eventPayload)) + { + Debug.WriteLine("Migration should return new event."); + } } var payloadType = typeNameRegistry.GetName(eventPayload.GetType()); diff --git a/src/Squidex.Infrastructure/ResultList.cs b/src/Squidex.Infrastructure/ResultList.cs index 957ecc48b..7f835704f 100644 --- a/src/Squidex.Infrastructure/ResultList.cs +++ b/src/Squidex.Infrastructure/ResultList.cs @@ -27,7 +27,7 @@ namespace Squidex.Infrastructure return new Impl(items, total); } - public static IResultList Create(long total, params T[] items) + public static IResultList CreateFrom(long total, params T[] items) { return new Impl(items, total); } diff --git a/src/Squidex.Web/Resource.cs b/src/Squidex.Web/Resource.cs index d3eba847d..31602a124 100644 --- a/src/Squidex.Web/Resource.cs +++ b/src/Squidex.Web/Resource.cs @@ -24,38 +24,38 @@ namespace Squidex.Web AddGetLink("self", href); } - public void AddGetLink(string rel, string href) + public void AddGetLink(string rel, string href, string metadata = null) { - AddLink(rel, "GET", href); + AddLink(rel, "GET", href, metadata); } - public void AddPatchLink(string rel, string href) + public void AddPatchLink(string rel, string href, string metadata = null) { - AddLink(rel, "PATCH", href); + AddLink(rel, "PATCH", href, metadata); } - public void AddPostLink(string rel, string href) + public void AddPostLink(string rel, string href, string metadata = null) { - AddLink(rel, "POST", href); + AddLink(rel, "POST", href, metadata); } - public void AddPutLink(string rel, string href) + public void AddPutLink(string rel, string href, string metadata = null) { - AddLink(rel, "PUT", href); + AddLink(rel, "PUT", href, metadata); } - public void AddDeleteLink(string rel, string href) + public void AddDeleteLink(string rel, string href, string metadata = null) { - AddLink(rel, "DELETE", href); + AddLink(rel, "DELETE", href, metadata); } - public void AddLink(string rel, string method, string href) + public void AddLink(string rel, string method, string href, string metadata = null) { Guard.NotNullOrEmpty(rel, nameof(rel)); Guard.NotNullOrEmpty(href, nameof(href)); Guard.NotNullOrEmpty(method, nameof(method)); - Links[rel] = new ResourceLink { Href = href, Method = method }; + Links[rel] = new ResourceLink { Href = href, Method = method, Metadata = metadata }; } } } diff --git a/src/Squidex.Web/ResourceLink.cs b/src/Squidex.Web/ResourceLink.cs index 964610e7d..2d2b4c0c5 100644 --- a/src/Squidex.Web/ResourceLink.cs +++ b/src/Squidex.Web/ResourceLink.cs @@ -18,5 +18,9 @@ namespace Squidex.Web [Required] [Display(Description = "The link method.")] public string Method { get; set; } + + [Required] + [Display(Description = "Additional data about the link.")] + public string Metadata { get; set; } } } diff --git a/src/Squidex.Web/UrlHelperExtensions.cs b/src/Squidex.Web/UrlHelperExtensions.cs index 27f00a1d9..a4bc9280d 100644 --- a/src/Squidex.Web/UrlHelperExtensions.cs +++ b/src/Squidex.Web/UrlHelperExtensions.cs @@ -38,7 +38,7 @@ namespace Squidex.Web public static string Url(this Controller controller, Func action, object values = null) where T : Controller { - return controller.Url.Url(action, values); + return controller.Url.Url(action, values); } } } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 965056fbe..75b9867a7 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -133,9 +133,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [ApiCosts(1)] public async Task GetAsset(string app, Guid id) { - var context = Context(); - - var asset = await assetQuery.FindAssetAsync(context, id); + var asset = await assetQuery.FindAssetAsync(id); if (asset == null) { @@ -182,7 +180,7 @@ namespace Squidex.Areas.Api.Controllers.Assets var context = await CommandBus.PublishAsync(command); var result = context.Result(); - var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags, result.IsDuplicate); + var response = AssetDto.FromAsset(result.Asset, this, app, result.IsDuplicate); return CreatedAtAction(nameof(GetAsset), new { app, id = response.Id }, response); } @@ -267,8 +265,8 @@ namespace Squidex.Areas.Api.Controllers.Assets { var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - var response = AssetDto.FromAsset(result.Asset, this, app, result.Tags); + var result = context.Result(); + var response = AssetDto.FromAsset(result, this, app); return response; } diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs index ec0e68899..5c996cf0d 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs @@ -118,14 +118,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models [JsonProperty("_meta")] public AssetMetadata Metadata { get; set; } - public static AssetDto FromAsset(IAssetEntity asset, ApiController controller, string app, HashSet tags = null, bool isDuplicate = false) + public static AssetDto FromAsset(IEnrichedAssetEntity asset, ApiController controller, string app, bool isDuplicate = false) { var response = SimpleMapper.Map(asset, new AssetDto { FileType = asset.FileName.FileType() }); - if (tags != null) - { - response.Tags = tags; - } + response.Tags = asset.TagNames; if (isDuplicate) { diff --git a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs index f6ceca82d..efd81147b 100644 --- a/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetsDto.cs @@ -37,7 +37,7 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models return Items.ToSurrogateKeys(); } - public static AssetsDto FromAssets(IResultList assets, ApiController controller, string app) + public static AssetsDto FromAssets(IResultList assets, ApiController controller, string app) { var response = new AssetsDto { diff --git a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 833c67dbe..6885ce6cf 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -16,7 +16,6 @@ using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.GraphQL; -using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Shared; using Squidex.Web; @@ -127,9 +126,8 @@ namespace Squidex.Areas.Api.Controllers.Contents { var context = Context(); var contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(ids).Ids); - var contentsList = ResultList.Create(contents.Count, contents); - var response = await ContentsDto.FromContentsAsync(contentsList, context, this, null, contentWorkflow); + var response = await ContentsDto.FromContentsAsync(contents, context, this, null, contentWorkflow); if (controllerOptions.Value.EnableSurrogateKeys && response.Items.Length <= controllerOptions.Value.MaxItemsForSurrogateKeys) { @@ -201,7 +199,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var context = Context(); var content = await contentQuery.FindContentAsync(context, name, id); - var response = await ContentDto.FromContentAsync(context, content, contentWorkflow, this); + var response = ContentDto.FromContent(context, content, this); if (controllerOptions.Value.EnableSurrogateKeys) { @@ -237,7 +235,7 @@ namespace Squidex.Areas.Api.Controllers.Contents var context = Context(); var content = await contentQuery.FindContentAsync(context, name, id, version); - var response = await ContentDto.FromContentAsync(context, content, contentWorkflow, this); + var response = ContentDto.FromContent(context, content, this); if (controllerOptions.Value.EnableSurrogateKeys) { @@ -447,8 +445,8 @@ namespace Squidex.Areas.Api.Controllers.Contents { var context = await CommandBus.PublishAsync(command); - var result = context.Result(); - var response = await ContentDto.FromContentAsync(null, result, contentWorkflow, this); + var result = context.Result(); + var response = ContentDto.FromContent(null, result, this); return response; } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs index 2a6afaacf..26440c83f 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentDto.cs @@ -7,7 +7,6 @@ using System; using System.ComponentModel.DataAnnotations; -using System.Threading.Tasks; using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.ConvertContent; @@ -71,20 +70,21 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models public Instant LastModified { get; set; } /// - /// The the status of the content. + /// The status of the content. /// public Status Status { get; set; } + /// + /// The color of the status. + /// + public string StatusColor { get; set; } + /// /// The version of the content. /// public long Version { get; set; } - public static ValueTask FromContentAsync( - QueryContext context, - IContentEntity content, - IContentWorkflow contentWorkflow, - ApiController controller) + public static ContentDto FromContent(QueryContext context, IEnrichedContentEntity content, ApiController controller) { var response = SimpleMapper.Map(content, new ContentDto()); @@ -104,14 +104,10 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models response.ScheduleJob = SimpleMapper.Map(content.ScheduleJob, new ScheduleJobDto()); } - return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name, contentWorkflow); + return response.CreateLinksAsync(content, controller, content.AppId.Name, content.SchemaId.Name); } - private async ValueTask CreateLinksAsync(IContentEntity content, - ApiController controller, - string app, - string schema, - IContentWorkflow contentWorkflow) + private ContentDto CreateLinksAsync(IEnrichedContentEntity content, ApiController controller, string app, string schema) { var values = new { app, name = schema, id = Id }; @@ -139,7 +135,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models if (controller.HasPermission(Permissions.AppContentsUpdate, app, schema)) { - if (await contentWorkflow.CanUpdateAsync(content)) + if (content.CanUpdate) { AddPutLink("update", controller.Url(x => nameof(x.PutContent), values)); } @@ -157,13 +153,11 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models AddDeleteLink("delete", controller.Url(x => nameof(x.DeleteContent), values)); } - var nextStatuses = await contentWorkflow.GetNextsAsync(content); - - foreach (var next in nextStatuses) + foreach (var next in content.Nexts) { - if (controller.HasPermission(Helper.StatusPermission(app, schema, next))) + if (controller.HasPermission(Helper.StatusPermission(app, schema, next.Status))) { - AddPutLink($"status/{next}", controller.Url(x => nameof(x.PutContentStatus), values)); + AddPutLink($"status/{next.Status}", controller.Url(x => nameof(x.PutContentStatus), values), next.Color); } } diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs index afcecb7fe..c81ec7b0e 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/ContentsDto.cs @@ -35,7 +35,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models /// The possible statuses. /// [Required] - public Status[] Statuses { get; set; } + public StatusInfoDto[] Statuses { get; set; } public string ToEtag() { @@ -47,20 +47,16 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models return Items.ToSurrogateKeys(); } - public static async Task FromContentsAsync(IResultList contents, QueryContext context, - ApiController controller, - ISchemaEntity schema, - IContentWorkflow contentWorkflow) + public static async Task FromContentsAsync(IResultList contents, + QueryContext context, ApiController controller, ISchemaEntity schema, IContentWorkflow contentWorkflow) { var result = new ContentsDto { Total = contents.Total, - Items = new ContentDto[contents.Count] + Items = contents.Select(x => ContentDto.FromContent(context, x, controller)).ToArray() }; - await Task.WhenAll( - result.AssignContentsAsync(contentWorkflow, contents, context, controller), - result.AssignStatusesAsync(contentWorkflow, schema)); + await result.AssignStatusesAsync(contentWorkflow, schema); return result.CreateLinks(controller, schema.AppId.Name, schema.SchemaDef.Name); } @@ -69,15 +65,7 @@ namespace Squidex.Areas.Api.Controllers.Contents.Models { var allStatuses = await contentWorkflow.GetAllAsync(schema); - Statuses = allStatuses.ToArray(); - } - - private async Task AssignContentsAsync(IContentWorkflow contentWorkflow, IResultList contents, QueryContext context, ApiController controller) - { - for (var i = 0; i < Items.Length; i++) - { - Items[i] = await ContentDto.FromContentAsync(context, contents[i], contentWorkflow, controller); - } + Statuses = allStatuses.Select(StatusInfoDto.FromStatusInfo).ToArray(); } private ContentsDto CreateLinks(ApiController controller, string app, string schema) diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs b/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs new file mode 100644 index 000000000..510ba8a7a --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Contents/Models/StatusInfoDto.cs @@ -0,0 +1,32 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.Contents; + +namespace Squidex.Areas.Api.Controllers.Contents.Models +{ + public sealed class StatusInfoDto + { + /// + /// The name of the status. + /// + [Required] + public Status Status { get; set; } + + /// + /// The color of the status. + /// + [Required] + public string Color { get; set; } + + public static StatusInfoDto FromStatusInfo(StatusInfo statusInfo) + { + return new StatusInfoDto { Status = statusInfo.Status, Color = statusInfo.Color }; + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index cb2b1ecde..4a208143c 100644 --- a/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Squidex.Areas.Api.Controllers.History.Models; @@ -48,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.History { var events = await historyService.QueryByChannelAsync(AppId, channel, 100); - var response = events.ToArray(HistoryEventDto.FromHistoryEvent); + var response = events.Select(HistoryEventDto.FromHistoryEvent).Where(x => x.Message != null).ToArray(); return Ok(response); } diff --git a/src/Squidex/Config/Domain/EntitiesServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs index d6965d669..ce886628c 100644 --- a/src/Squidex/Config/Domain/EntitiesServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -32,7 +32,6 @@ using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Comments; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Edm; using Squidex.Domain.Apps.Entities.Contents.GraphQL; using Squidex.Domain.Apps.Entities.Contents.Text; @@ -97,9 +96,15 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); @@ -222,6 +227,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); @@ -231,9 +239,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs>() .As(); - services.AddSingletonAs>() - .As(); - services.AddSingletonAs>() .As(); diff --git a/src/Squidex/app-config/webpack.config.js b/src/Squidex/app-config/webpack.config.js index eea01650c..8c46363b7 100644 --- a/src/Squidex/app-config/webpack.config.js +++ b/src/Squidex/app-config/webpack.config.js @@ -251,7 +251,9 @@ module.exports = function (env) { waitForLinting: isProduction }) ); + } + if (!isCoverage) { config.plugins.push( new plugins.NgToolsWebpack.AngularCompilerPlugin({ directTemplateLoading: true, diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index 22454ce68..71bd34cf4 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -36,6 +36,7 @@ [class.active]="dropdown.isOpen | async" #optionsButton> - - Status to {{status}} + + Change to {{info.status}} diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html index ea615ec67..600764d90 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html @@ -17,7 +17,7 @@ - {{query.name}} + {{query.name}} diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.scss b/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.scss index 00bb31427..684ee4478 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.scss +++ b/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.scss @@ -3,4 +3,4 @@ .text-muted { pointer-events: none; -} \ No newline at end of file +} diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index a95df25f2..f9f08e2c3 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -207,8 +207,8 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit { const allActions = {}; for (let content of this.contentsState.snapshot.contents.values) { - for (let status of content.statusUpdates) { - allActions[status] = true; + for (let info of content.statusUpdates) { + allActions[info.status] = info.color; } } diff --git a/src/Squidex/app/features/content/shared/content-item.component.html b/src/Squidex/app/features/content/shared/content-item.component.html index 2569c05e1..201ff85f9 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.html +++ b/src/Squidex/app/features/content/shared/content-item.component.html @@ -24,6 +24,7 @@ @@ -55,8 +56,8 @@