mirror of https://github.com/Squidex/squidex.git
171 changed files with 4039 additions and 1730 deletions
@ -0,0 +1,37 @@ |
|||
// ==========================================================================
|
|||
// 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 Newtonsoft.Json; |
|||
using Squidex.Infrastructure.Json.Newtonsoft; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents.Json |
|||
{ |
|||
public sealed class WorkflowConverter : JsonClassConverter<Workflows> |
|||
{ |
|||
protected override void WriteValue(JsonWriter writer, Workflows value, JsonSerializer serializer) |
|||
{ |
|||
var json = new Dictionary<Guid, Workflow>(value.Count); |
|||
|
|||
foreach (var workflow in value) |
|||
{ |
|||
json.Add(workflow.Key, workflow.Value); |
|||
} |
|||
|
|||
serializer.Serialize(writer, json); |
|||
} |
|||
|
|||
protected override Workflows ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) |
|||
{ |
|||
var json = serializer.Deserialize<Dictionary<Guid, Workflow>>(reader); |
|||
|
|||
return new Workflows(json.ToArray()); |
|||
} |
|||
} |
|||
} |
|||
@ -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"; |
|||
} |
|||
} |
|||
@ -0,0 +1,36 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Globalization; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents |
|||
{ |
|||
public sealed class StatusConverter : TypeConverter |
|||
{ |
|||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|||
{ |
|||
return sourceType == typeof(string); |
|||
} |
|||
|
|||
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) |
|||
{ |
|||
return destinationType == typeof(string); |
|||
} |
|||
|
|||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|||
{ |
|||
return new Status(value?.ToString()); |
|||
} |
|||
|
|||
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) |
|||
{ |
|||
return value.ToString(); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,88 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents |
|||
{ |
|||
public sealed class Workflow |
|||
{ |
|||
private static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>(); |
|||
|
|||
public static readonly Workflow Default = new Workflow( |
|||
new Dictionary<Status, WorkflowStep> |
|||
{ |
|||
[Status.Archived] = |
|||
new WorkflowStep( |
|||
new Dictionary<Status, WorkflowTransition> |
|||
{ |
|||
[Status.Draft] = new WorkflowTransition() |
|||
}, |
|||
StatusColors.Archived, true), |
|||
[Status.Draft] = |
|||
new WorkflowStep( |
|||
new Dictionary<Status, WorkflowTransition> |
|||
{ |
|||
[Status.Archived] = new WorkflowTransition(), |
|||
[Status.Published] = new WorkflowTransition() |
|||
}, |
|||
StatusColors.Draft), |
|||
[Status.Published] = |
|||
new WorkflowStep( |
|||
new Dictionary<Status, WorkflowTransition> |
|||
{ |
|||
[Status.Archived] = new WorkflowTransition(), |
|||
[Status.Draft] = new WorkflowTransition() |
|||
}, |
|||
StatusColors.Published) |
|||
}, Status.Draft); |
|||
|
|||
public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; } |
|||
|
|||
public Status Initial { get; } |
|||
|
|||
public Workflow(IReadOnlyDictionary<Status, WorkflowStep> steps, Status initial) |
|||
{ |
|||
Steps = steps ?? EmptySteps; |
|||
|
|||
Initial = initial; |
|||
} |
|||
|
|||
public IEnumerable<(Status Status, WorkflowStep Step, WorkflowTransition Transition)> GetTransitions(Status status) |
|||
{ |
|||
if (TryGetStep(status, out var step)) |
|||
{ |
|||
foreach (var transition in step.Transitions) |
|||
{ |
|||
yield return (transition.Key, Steps[transition.Key], transition.Value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool TryGetTransition(Status from, Status to, out WorkflowTransition transition) |
|||
{ |
|||
if (TryGetStep(from, out var step) && step.Transitions.TryGetValue(to, out transition)) |
|||
{ |
|||
return true; |
|||
} |
|||
|
|||
transition = null; |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public bool TryGetStep(Status status, out WorkflowStep step) |
|||
{ |
|||
return Steps.TryGetValue(status, out step); |
|||
} |
|||
|
|||
public (Status Key, WorkflowStep) GetInitialStep() |
|||
{ |
|||
return (Initial, Steps[Initial]); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,31 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents |
|||
{ |
|||
public sealed class WorkflowStep |
|||
{ |
|||
private static readonly IReadOnlyDictionary<Status, WorkflowTransition> EmptyTransitions = new Dictionary<Status, WorkflowTransition>(); |
|||
|
|||
public IReadOnlyDictionary<Status, WorkflowTransition> Transitions { get; } |
|||
|
|||
public string Color { get; } |
|||
|
|||
public bool NoUpdate { get; } |
|||
|
|||
public WorkflowStep(IReadOnlyDictionary<Status, WorkflowTransition> transitions = null, string color = null, bool noUpdate = false) |
|||
{ |
|||
Transitions = transitions ?? EmptyTransitions; |
|||
|
|||
Color = color; |
|||
|
|||
NoUpdate = noUpdate; |
|||
} |
|||
} |
|||
} |
|||
@ -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 WorkflowTransition |
|||
{ |
|||
public string Expression { get; } |
|||
|
|||
public string Role { get; } |
|||
|
|||
public WorkflowTransition(string expression = null, string role = null) |
|||
{ |
|||
Expression = expression; |
|||
|
|||
Role = role; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,43 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.Contracts; |
|||
using System.Linq; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Collections; |
|||
|
|||
namespace Squidex.Domain.Apps.Core.Contents |
|||
{ |
|||
public sealed class Workflows : ArrayDictionary<Guid, Workflow> |
|||
{ |
|||
public static readonly Workflows Empty = new Workflows(); |
|||
|
|||
private Workflows() |
|||
{ |
|||
} |
|||
|
|||
public Workflows(KeyValuePair<Guid, Workflow>[] items) |
|||
: base(items) |
|||
{ |
|||
} |
|||
|
|||
[Pure] |
|||
public Workflows Set(Workflow workflow) |
|||
{ |
|||
Guard.NotNull(workflow, nameof(workflow)); |
|||
|
|||
return new Workflows(With(Guid.Empty, workflow)); |
|||
} |
|||
|
|||
public Workflow GetFirst() |
|||
{ |
|||
return Values.FirstOrDefault() ?? Workflow.Default; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// ==========================================================================
|
|||
// 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.Apps.Commands |
|||
{ |
|||
public sealed class ConfigureWorkflow : AppCommand |
|||
{ |
|||
public Workflow Workflow { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,76 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Entities.Apps.Commands; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Apps.Guards |
|||
{ |
|||
public static class GuardAppWorkflows |
|||
{ |
|||
public static void CanConfigure(ConfigureWorkflow command) |
|||
{ |
|||
Guard.NotNull(command, nameof(command)); |
|||
|
|||
Validate.It(() => "Cannot configure workflow.", e => |
|||
{ |
|||
if (command.Workflow == null) |
|||
{ |
|||
e(Not.Defined("Workflow"), nameof(command.Workflow)); |
|||
return; |
|||
} |
|||
|
|||
var workflow = command.Workflow; |
|||
|
|||
if (!workflow.Steps.ContainsKey(workflow.Initial)) |
|||
{ |
|||
e(Not.Defined("Initial step"), $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); |
|||
} |
|||
|
|||
if (workflow.Initial == Status.Published) |
|||
{ |
|||
e("Initial step cannot be published step.", $"{nameof(command.Workflow)}.{nameof(workflow.Initial)}"); |
|||
} |
|||
|
|||
var stepsPrefix = $"{nameof(command.Workflow)}.{nameof(workflow.Steps)}"; |
|||
|
|||
if (!workflow.Steps.ContainsKey(Status.Published)) |
|||
{ |
|||
e("Workflow must have a published step.", stepsPrefix); |
|||
} |
|||
|
|||
foreach (var step in workflow.Steps) |
|||
{ |
|||
var stepPrefix = $"{stepsPrefix}.{step.Key}"; |
|||
|
|||
if (step.Value == null) |
|||
{ |
|||
e(Not.Defined("Step"), stepPrefix); |
|||
} |
|||
else |
|||
{ |
|||
foreach (var transition in step.Value.Transitions) |
|||
{ |
|||
var transitionPrefix = $"{stepPrefix}.{nameof(step.Value.Transitions)}.{transition.Key}"; |
|||
|
|||
if (!workflow.Steps.ContainsKey(transition.Key)) |
|||
{ |
|||
e("Transition has an invalid target.", transitionPrefix); |
|||
} |
|||
|
|||
if (transition.Value == null) |
|||
{ |
|||
e(Not.Defined("Transition"), transitionPrefix); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset) |
|||
{ |
|||
Guard.NotNull(asset, nameof(asset)); |
|||
|
|||
var enriched = await EnrichAsync(Enumerable.Repeat(asset, 1)); |
|||
|
|||
return enriched[0]; |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IEnrichedAssetEntity>> EnrichAsync(IEnumerable<IAssetEntity> assets) |
|||
{ |
|||
Guard.NotNull(assets, nameof(assets)); |
|||
|
|||
using (Profiler.TraceMethod<AssetEnricher>()) |
|||
{ |
|||
var results = new List<IEnrichedAssetEntity>(); |
|||
|
|||
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<string>(); |
|||
|
|||
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<Dictionary<string, string>> CalculateTags(IGrouping<System.Guid, IAssetEntity> group) |
|||
{ |
|||
var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet(); |
|||
|
|||
return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds); |
|||
} |
|||
} |
|||
} |
|||
@ -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<IEnrichedAssetEntity> EnrichAsync(IAssetEntity asset); |
|||
|
|||
Task<IReadOnlyList<IEnrichedAssetEntity>> EnrichAsync(IEnumerable<IAssetEntity> assets); |
|||
} |
|||
} |
|||
@ -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<ContentCommand, IContentGrain> |
|||
{ |
|||
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<Task> 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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
// ==========================================================================
|
|||
// 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.Security.Claims; |
|||
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<IEnrichedContentEntity> EnrichAsync(IContentEntity content) |
|||
{ |
|||
Guard.NotNull(content, nameof(content)); |
|||
|
|||
var enriched = await EnrichAsync(Enumerable.Repeat(content, 1)); |
|||
|
|||
return enriched[0]; |
|||
} |
|||
|
|||
public async Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents) |
|||
{ |
|||
Guard.NotNull(contents, nameof(contents)); |
|||
|
|||
using (Profiler.TraceMethod<ContentEnricher>()) |
|||
{ |
|||
var results = new List<ContentEntity>(); |
|||
|
|||
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, ClaimsPrincipal.Current); |
|||
} |
|||
|
|||
private async Task ResolveColorAsync(IContentEntity content, ContentEntity result, Dictionary<(Guid, Status), StatusInfo> cache) |
|||
{ |
|||
result.StatusColor = await GetColorAsync(content, cache); |
|||
} |
|||
|
|||
private async Task<string> GetColorAsync(IContentEntity content, Dictionary<(Guid, Status), StatusInfo> cache) |
|||
{ |
|||
if (!cache.TryGetValue((content.SchemaId.Id, content.Status), out var info)) |
|||
{ |
|||
info = await contentWorkflow.GetInfoAsync(content); |
|||
|
|||
if (info == null) |
|||
{ |
|||
info = new StatusInfo(content.Status, DefaultColor); |
|||
} |
|||
|
|||
cache[(content.SchemaId.Id, content.Status)] = info; |
|||
} |
|||
|
|||
return info.Color; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,129 @@ |
|||
// ==========================================================================
|
|||
// 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.Security.Claims; |
|||
using System.Threading.Tasks; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Core.Scripting; |
|||
using Squidex.Domain.Apps.Entities.Schemas; |
|||
using Squidex.Infrastructure; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public sealed class DynamicContentWorkflow : IContentWorkflow |
|||
{ |
|||
private readonly IScriptEngine scriptEngine; |
|||
private readonly IAppProvider appProvider; |
|||
|
|||
public DynamicContentWorkflow(IScriptEngine scriptEngine, IAppProvider appProvider) |
|||
{ |
|||
Guard.NotNull(scriptEngine, nameof(scriptEngine)); |
|||
Guard.NotNull(appProvider, nameof(appProvider)); |
|||
|
|||
this.scriptEngine = scriptEngine; |
|||
|
|||
this.appProvider = appProvider; |
|||
} |
|||
|
|||
public async Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema) |
|||
{ |
|||
var workflow = await GetWorkflowAsync(schema.AppId.Id); |
|||
|
|||
return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); |
|||
} |
|||
|
|||
public async Task<bool> CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user) |
|||
{ |
|||
var workflow = await GetWorkflowAsync(content.AppId.Id); |
|||
|
|||
return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content, user); |
|||
} |
|||
|
|||
public async Task<bool> CanUpdateAsync(IContentEntity content) |
|||
{ |
|||
var workflow = await GetWorkflowAsync(content.AppId.Id); |
|||
|
|||
if (workflow.TryGetStep(content.Status, out var step)) |
|||
{ |
|||
return !step.NoUpdate; |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public async Task<StatusInfo> GetInfoAsync(IContentEntity content) |
|||
{ |
|||
var workflow = await GetWorkflowAsync(content.AppId.Id); |
|||
|
|||
if (workflow.TryGetStep(content.Status, out var step)) |
|||
{ |
|||
return new StatusInfo(content.Status, GetColor(step)); |
|||
} |
|||
|
|||
return new StatusInfo(content.Status, StatusColors.Draft); |
|||
} |
|||
|
|||
public async Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema) |
|||
{ |
|||
var workflow = await GetWorkflowAsync(schema.AppId.Id); |
|||
|
|||
var (status, step) = workflow.GetInitialStep(); |
|||
|
|||
return new StatusInfo(status, GetColor(step)); |
|||
} |
|||
|
|||
public async Task<StatusInfo[]> GetNextsAsync(IContentEntity content, ClaimsPrincipal user) |
|||
{ |
|||
var result = new List<StatusInfo>(); |
|||
|
|||
var workflow = await GetWorkflowAsync(content.AppId.Id); |
|||
|
|||
foreach (var (to, step, transition) in workflow.GetTransitions(content.Status)) |
|||
{ |
|||
if (CanUse(transition, content, user)) |
|||
{ |
|||
result.Add(new StatusInfo(to, GetColor(step))); |
|||
} |
|||
} |
|||
|
|||
return result.ToArray(); |
|||
} |
|||
|
|||
private bool CanUse(WorkflowTransition transition, IContentEntity content, ClaimsPrincipal user) |
|||
{ |
|||
if (!string.IsNullOrWhiteSpace(transition.Role)) |
|||
{ |
|||
if (!user.Claims.Any(x => x.Type == ClaimTypes.Role && x.Value == transition.Role)) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
if (!string.IsNullOrWhiteSpace(transition.Expression)) |
|||
{ |
|||
return scriptEngine.Evaluate("data", content.DataDraft, transition.Expression); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private async Task<Workflow> GetWorkflowAsync(Guid appId) |
|||
{ |
|||
var app = await appProvider.GetAppAsync(appId); |
|||
|
|||
return app?.Workflows.GetFirst(); |
|||
} |
|||
|
|||
private static string GetColor(WorkflowStep step) |
|||
{ |
|||
return step.Color ?? StatusColors.Draft; |
|||
} |
|||
} |
|||
} |
|||
@ -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<IEnrichedContentEntity> EnrichAsync(IContentEntity content); |
|||
|
|||
Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents); |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure.EventSourcing; |
|||
|
|||
namespace Squidex.Domain.Apps.Events.Apps |
|||
{ |
|||
[EventType(nameof(AppWorkflowConfigured))] |
|||
public sealed class AppWorkflowConfigured : AppEvent |
|||
{ |
|||
public Workflow Workflow { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Tasks; |
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Microsoft.Net.Http.Headers; |
|||
using Squidex.Areas.Api.Controllers.Apps.Models; |
|||
using Squidex.Domain.Apps.Entities.Apps; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Squidex.Shared; |
|||
using Squidex.Web; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Apps |
|||
{ |
|||
/// <summary>
|
|||
/// Manages and configures apps.
|
|||
/// </summary>
|
|||
[ApiExplorerSettings(GroupName = nameof(Apps))] |
|||
public sealed class AppWorkflowsController : ApiController |
|||
{ |
|||
public AppWorkflowsController(ICommandBus commandBus) |
|||
: base(commandBus) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get app workflow.
|
|||
/// </summary>
|
|||
/// <param name="app">The name of the app.</param>
|
|||
/// <returns>
|
|||
/// 200 => App workflows returned.
|
|||
/// 404 => App not found.
|
|||
/// </returns>
|
|||
[HttpGet] |
|||
[Route("apps/{app}/workflow/")] |
|||
[ProducesResponseType(typeof(WorkflowResponseDto), 200)] |
|||
[ApiPermission(Permissions.AppWorkflowsRead)] |
|||
[ApiCosts(0)] |
|||
public IActionResult GetWorkflow(string app) |
|||
{ |
|||
var response = WorkflowResponseDto.FromApp(App, this); |
|||
|
|||
Response.Headers[HeaderNames.ETag] = App.Version.ToString(); |
|||
|
|||
return Ok(response); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Configure workflow of the app.
|
|||
/// </summary>
|
|||
/// <param name="app">The name of the app.</param>
|
|||
/// <param name="request">The new workflow.</param>
|
|||
/// <returns>
|
|||
/// 200 => Workflow configured.
|
|||
/// 400 => Workflow is not valid.
|
|||
/// 404 => App not found.
|
|||
/// </returns>
|
|||
[HttpPut] |
|||
[Route("apps/{app}/workflow/")] |
|||
[ProducesResponseType(typeof(WorkflowResponseDto), 200)] |
|||
[ApiPermission(Permissions.AppWorkflowsUpdate)] |
|||
[ApiCosts(1)] |
|||
public async Task<IActionResult> PutWorkflow(string app, [FromBody] UpsertWorkflowDto request) |
|||
{ |
|||
var command = request.ToCommand(); |
|||
|
|||
var response = await InvokeCommandAsync(command); |
|||
|
|||
return Ok(response); |
|||
} |
|||
|
|||
private async Task<WorkflowResponseDto> InvokeCommandAsync(ICommand command) |
|||
{ |
|||
var context = await CommandBus.PublishAsync(command); |
|||
|
|||
var result = context.Result<IAppEntity>(); |
|||
var response = WorkflowResponseDto.FromApp(result, this); |
|||
|
|||
return response; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Linq; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Domain.Apps.Entities.Apps.Commands; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Apps.Models |
|||
{ |
|||
public sealed class UpsertWorkflowDto |
|||
{ |
|||
/// <summary>
|
|||
/// The workflow steps.
|
|||
/// </summary>
|
|||
[Required] |
|||
public Dictionary<Status, WorkflowStepDto> Steps { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The initial step.
|
|||
/// </summary>
|
|||
public Status Initial { get; set; } |
|||
|
|||
public ConfigureWorkflow ToCommand() |
|||
{ |
|||
var workflow = new Workflow( |
|||
Steps?.ToDictionary( |
|||
x => x.Key, |
|||
x => new WorkflowStep( |
|||
x.Value?.Transitions.ToDictionary( |
|||
y => x.Key, |
|||
y => new WorkflowTransition(y.Value.Expression, y.Value.Role)), |
|||
x.Value.Color, |
|||
x.Value.NoUpdate)), |
|||
Initial); |
|||
|
|||
return new ConfigureWorkflow { Workflow = workflow }; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using System.Linq; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
using Squidex.Infrastructure.Reflection; |
|||
using Squidex.Shared; |
|||
using Squidex.Web; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Apps.Models |
|||
{ |
|||
public sealed class WorkflowDto : Resource |
|||
{ |
|||
/// <summary>
|
|||
/// The workflow steps.
|
|||
/// </summary>
|
|||
[Required] |
|||
public Dictionary<Status, WorkflowStepDto> Steps { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The initial step.
|
|||
/// </summary>
|
|||
public Status Initial { get; set; } |
|||
|
|||
public static WorkflowDto FromWorkflow(Workflow workflow, ApiController controller, string app) |
|||
{ |
|||
var result = new WorkflowDto |
|||
{ |
|||
Steps = workflow.Steps.ToDictionary( |
|||
x => x.Key, |
|||
x => SimpleMapper.Map(x.Value, new WorkflowStepDto |
|||
{ |
|||
Transitions = x.Value.Transitions.ToDictionary( |
|||
y => y.Key, |
|||
y => new WorkflowTransitionDto { Expression = y.Value.Expression, Role = y.Value.Role }) |
|||
})), |
|||
Initial = workflow.Initial |
|||
}; |
|||
|
|||
return result.CreateLinks(controller, app); |
|||
} |
|||
|
|||
private WorkflowDto CreateLinks(ApiController controller, string app) |
|||
{ |
|||
var values = new { app }; |
|||
|
|||
if (controller.HasPermission(Permissions.AppWorkflowsUpdate, app)) |
|||
{ |
|||
AddPutLink("update", controller.Url<AppWorkflowsController>(x => nameof(x.PutWorkflow), values)); |
|||
} |
|||
|
|||
return this; |
|||
} |
|||
} |
|||
} |
|||
@ -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.Entities.Apps; |
|||
using Squidex.Web; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Apps.Models |
|||
{ |
|||
public sealed class WorkflowResponseDto : Resource |
|||
{ |
|||
/// <summary>
|
|||
/// The workflow.
|
|||
/// </summary>
|
|||
[Required] |
|||
public WorkflowDto Workflow { get; set; } |
|||
|
|||
public static WorkflowResponseDto FromApp(IAppEntity app, ApiController controller) |
|||
{ |
|||
var result = new WorkflowResponseDto |
|||
{ |
|||
Workflow = WorkflowDto.FromWorkflow(app.Workflows.GetFirst(), controller, app.Name) |
|||
}; |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Generic; |
|||
using System.ComponentModel.DataAnnotations; |
|||
using Squidex.Domain.Apps.Core.Contents; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Apps.Models |
|||
{ |
|||
public sealed class WorkflowStepDto |
|||
{ |
|||
/// <summary>
|
|||
/// The transitions.
|
|||
/// </summary>
|
|||
[Required] |
|||
public Dictionary<Status, WorkflowTransitionDto> Transitions { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The optional color.
|
|||
/// </summary>
|
|||
public string Color { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Indicates if updates should not be allowed.
|
|||
/// </summary>
|
|||
public bool NoUpdate { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Apps.Models |
|||
{ |
|||
public sealed class WorkflowTransitionDto |
|||
{ |
|||
/// <summary>
|
|||
/// The optional expression.
|
|||
/// </summary>
|
|||
public string Expression { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The optional restricted role.
|
|||
/// </summary>
|
|||
public string Role { get; set; } |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// The name of the status.
|
|||
/// </summary>
|
|||
[Required] |
|||
public Status Status { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// The color of the status.
|
|||
/// </summary>
|
|||
[Required] |
|||
public string Color { get; set; } |
|||
|
|||
public static StatusInfoDto FromStatusInfo(StatusInfo statusInfo) |
|||
{ |
|||
return new StatusInfoDto { Status = statusInfo.Status, Color = statusInfo.Color }; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue