mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
630 changed files with 19036 additions and 11745 deletions
@ -0,0 +1,42 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using Newtonsoft.Json; |
||||
|
using Squidex.Infrastructure.Json.Newtonsoft; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Contents.Json |
||||
|
{ |
||||
|
public sealed class StatusConverter : JsonConverter, ISupportedTypes |
||||
|
{ |
||||
|
public IEnumerable<Type> SupportedTypes |
||||
|
{ |
||||
|
get { yield return typeof(Status); } |
||||
|
} |
||||
|
|
||||
|
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) |
||||
|
{ |
||||
|
writer.WriteValue(value.ToString()); |
||||
|
} |
||||
|
|
||||
|
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) |
||||
|
{ |
||||
|
if (reader.TokenType != JsonToken.String) |
||||
|
{ |
||||
|
throw new JsonException($"Expected String, but got {reader.TokenType}."); |
||||
|
} |
||||
|
|
||||
|
return new Status(reader.Value.ToString()); |
||||
|
} |
||||
|
|
||||
|
public override bool CanConvert(Type objectType) |
||||
|
{ |
||||
|
return objectType == typeof(Status); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,32 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Core.Contents |
|
||||
{ |
|
||||
public static class StatusFlow |
|
||||
{ |
|
||||
private static readonly Dictionary<Status, Status[]> Flow = new Dictionary<Status, Status[]> |
|
||||
{ |
|
||||
[Status.Draft] = new[] { Status.Published, Status.Archived }, |
|
||||
[Status.Archived] = new[] { Status.Draft }, |
|
||||
[Status.Published] = new[] { Status.Draft, Status.Archived } |
|
||||
}; |
|
||||
|
|
||||
public static bool Exists(Status status) |
|
||||
{ |
|
||||
return Flow.ContainsKey(status); |
|
||||
} |
|
||||
|
|
||||
public static bool CanChange(Status status, Status toStatus) |
|
||||
{ |
|
||||
return Flow.TryGetValue(status, out var state) && state.Contains(toStatus); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,125 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Contents |
||||
|
{ |
||||
|
public sealed class Workflow : Named |
||||
|
{ |
||||
|
private const string DefaultName = "Unnamed"; |
||||
|
|
||||
|
public static readonly IReadOnlyDictionary<Status, WorkflowStep> EmptySteps = new Dictionary<Status, WorkflowStep>(); |
||||
|
public static readonly IReadOnlyList<Guid> EmptySchemaIds = new List<Guid>(); |
||||
|
public static readonly Workflow Default = CreateDefault(); |
||||
|
public static readonly Workflow Empty = new Workflow(default, EmptySteps); |
||||
|
|
||||
|
public IReadOnlyDictionary<Status, WorkflowStep> Steps { get; } = EmptySteps; |
||||
|
|
||||
|
public IReadOnlyList<Guid> SchemaIds { get; } = EmptySchemaIds; |
||||
|
|
||||
|
public Status Initial { get; } |
||||
|
|
||||
|
public Workflow( |
||||
|
Status initial, |
||||
|
IReadOnlyDictionary<Status, WorkflowStep> steps, |
||||
|
IReadOnlyList<Guid> schemaIds = null, |
||||
|
string name = null) |
||||
|
: base(name ?? DefaultName) |
||||
|
{ |
||||
|
Initial = initial; |
||||
|
|
||||
|
if (steps != null) |
||||
|
{ |
||||
|
Steps = steps; |
||||
|
} |
||||
|
|
||||
|
if (schemaIds != null) |
||||
|
{ |
||||
|
SchemaIds = schemaIds; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static Workflow CreateDefault(string name = null) |
||||
|
{ |
||||
|
return new Workflow( |
||||
|
Status.Draft, 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) |
||||
|
}, null, name); |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
|
} |
||||
|
else if (TryGetStep(Initial, out var initial)) |
||||
|
{ |
||||
|
yield return (Initial, initial, WorkflowTransition.Default); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public bool TryGetTransition(Status from, Status to, out WorkflowTransition transition) |
||||
|
{ |
||||
|
transition = null; |
||||
|
|
||||
|
if (TryGetStep(from, out var step)) |
||||
|
{ |
||||
|
if (step.Transitions.TryGetValue(to, out transition)) |
||||
|
{ |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
else if (to == Initial) |
||||
|
{ |
||||
|
transition = WorkflowTransition.Default; |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
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,84 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 System.Threading.Tasks; |
||||
|
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 Remove(Guid id) |
||||
|
{ |
||||
|
return new Workflows(Without(id)); |
||||
|
} |
||||
|
|
||||
|
[Pure] |
||||
|
public Workflows Add(Guid workflowId, string name) |
||||
|
{ |
||||
|
Guard.NotNullOrEmpty(name, nameof(name)); |
||||
|
|
||||
|
return new Workflows(With(workflowId, Workflow.CreateDefault(name))); |
||||
|
} |
||||
|
|
||||
|
[Pure] |
||||
|
public Workflows Set(Workflow workflow) |
||||
|
{ |
||||
|
Guard.NotNull(workflow, nameof(workflow)); |
||||
|
|
||||
|
return new Workflows(With(Guid.Empty, workflow)); |
||||
|
} |
||||
|
|
||||
|
[Pure] |
||||
|
public Workflows Set(Guid id, Workflow workflow) |
||||
|
{ |
||||
|
Guard.NotNull(workflow, nameof(workflow)); |
||||
|
|
||||
|
return new Workflows(With(id, workflow)); |
||||
|
} |
||||
|
|
||||
|
[Pure] |
||||
|
public Workflows Update(Guid id, Workflow workflow) |
||||
|
{ |
||||
|
Guard.NotNull(workflow, nameof(workflow)); |
||||
|
|
||||
|
if (id == Guid.Empty) |
||||
|
{ |
||||
|
return Set(workflow); |
||||
|
} |
||||
|
|
||||
|
if (!ContainsKey(id)) |
||||
|
{ |
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
return new Workflows(With(id, workflow)); |
||||
|
} |
||||
|
|
||||
|
public Workflow GetFirst() |
||||
|
{ |
||||
|
return Values.FirstOrDefault() ?? Workflow.Default; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,26 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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.Tags |
||||
|
{ |
||||
|
public sealed class TagsSet : Dictionary<string, int> |
||||
|
{ |
||||
|
public long Version { get; set; } |
||||
|
|
||||
|
public TagsSet() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public TagsSet(IDictionary<string, int> tags, long version) |
||||
|
: base(tags) |
||||
|
{ |
||||
|
Version = version; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading; |
||||
|
using MongoDB.Bson.Serialization; |
||||
|
using MongoDB.Bson.Serialization.Serializers; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents |
||||
|
{ |
||||
|
public sealed class StatusSerializer : SerializerBase<Status> |
||||
|
{ |
||||
|
private static volatile int isRegistered; |
||||
|
|
||||
|
public static void Register() |
||||
|
{ |
||||
|
if (Interlocked.Increment(ref isRegistered) == 1) |
||||
|
{ |
||||
|
BsonSerializer.RegisterSerializer(new StatusSerializer()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override Status Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) |
||||
|
{ |
||||
|
var value = context.Reader.ReadString(); |
||||
|
|
||||
|
return new Status(value); |
||||
|
} |
||||
|
|
||||
|
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Status value) |
||||
|
{ |
||||
|
context.Writer.WriteString(value.Name); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.Commands |
||||
|
{ |
||||
|
public sealed class AddWorkflow : AppCommand |
||||
|
{ |
||||
|
public Guid WorkflowId { get; set; } |
||||
|
|
||||
|
public string Name { get; set; } |
||||
|
|
||||
|
public AddWorkflow() |
||||
|
{ |
||||
|
WorkflowId = Guid.NewGuid(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.Commands |
||||
|
{ |
||||
|
public sealed class UpdateWorkflow : AppCommand |
||||
|
{ |
||||
|
public Guid WorkflowId { get; set; } |
||||
|
|
||||
|
public Workflow Workflow { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,109 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
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 CanAdd(AddWorkflow command) |
||||
|
{ |
||||
|
Guard.NotNull(command, nameof(command)); |
||||
|
|
||||
|
Validate.It(() => "Cannot add workflow.", e => |
||||
|
{ |
||||
|
if (string.IsNullOrWhiteSpace(command.Name)) |
||||
|
{ |
||||
|
e(Not.Defined("Name"), nameof(command.Name)); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static void CanUpdate(Workflows workflows, UpdateWorkflow command) |
||||
|
{ |
||||
|
Guard.NotNull(command, nameof(command)); |
||||
|
|
||||
|
GetWorkflowOrThrow(workflows, command.WorkflowId); |
||||
|
|
||||
|
Validate.It(() => "Cannot update 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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public static void CanDelete(Workflows workflows, DeleteWorkflow command) |
||||
|
{ |
||||
|
Guard.NotNull(command, nameof(command)); |
||||
|
|
||||
|
GetWorkflowOrThrow(workflows, command.WorkflowId); |
||||
|
} |
||||
|
|
||||
|
private static Workflow GetWorkflowOrThrow(Workflows workflows, Guid id) |
||||
|
{ |
||||
|
if (!workflows.TryGetValue(id, out var workflow)) |
||||
|
{ |
||||
|
throw new DomainObjectNotFoundException(id.ToString(), "Workflows", typeof(IAppEntity)); |
||||
|
} |
||||
|
|
||||
|
return workflow; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Infrastructure.Assets; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets.Commands |
||||
|
{ |
||||
|
public abstract class UploadAssetCommand : AssetCommand |
||||
|
{ |
||||
|
public AssetFile File { get; set; } |
||||
|
|
||||
|
public ImageInfo ImageInfo { get; set; } |
||||
|
|
||||
|
public string FileHash { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -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,16 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Assets |
||||
|
{ |
||||
|
public interface IEnrichedAssetEntity : IAssetEntity |
||||
|
{ |
||||
|
HashSet<string> TagNames { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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.Command is SquidexCommand command && context.PlainResult is IContentEntity content && NotEnriched(context)) |
||||
|
{ |
||||
|
var enriched = await contentEnricher.EnrichAsync(content, command.User); |
||||
|
|
||||
|
context.Complete(enriched); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static bool NotEnriched(CommandContext context) |
||||
|
{ |
||||
|
return !(context.PlainResult is IEnrichedContentEntity); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,111 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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; |
||||
|
private readonly IContextProvider contextProvider; |
||||
|
|
||||
|
public ContentEnricher(IContentWorkflow contentWorkflow, IContextProvider contextProvider) |
||||
|
{ |
||||
|
Guard.NotNull(contentWorkflow, nameof(contentWorkflow)); |
||||
|
Guard.NotNull(contextProvider, nameof(contextProvider)); |
||||
|
|
||||
|
this.contentWorkflow = contentWorkflow; |
||||
|
this.contextProvider = contextProvider; |
||||
|
} |
||||
|
|
||||
|
public async Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, ClaimsPrincipal user) |
||||
|
{ |
||||
|
Guard.NotNull(content, nameof(content)); |
||||
|
|
||||
|
var enriched = await EnrichAsync(Enumerable.Repeat(content, 1), user); |
||||
|
|
||||
|
return enriched[0]; |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, ClaimsPrincipal user) |
||||
|
{ |
||||
|
Guard.NotNull(contents, nameof(contents)); |
||||
|
Guard.NotNull(user, nameof(user)); |
||||
|
|
||||
|
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); |
||||
|
|
||||
|
if (ShouldEnrichWithStatuses()) |
||||
|
{ |
||||
|
await ResolveNextsAsync(content, result, user); |
||||
|
await ResolveCanUpdateAsync(content, result); |
||||
|
} |
||||
|
|
||||
|
results.Add(result); |
||||
|
} |
||||
|
|
||||
|
return results; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private bool ShouldEnrichWithStatuses() |
||||
|
{ |
||||
|
return contextProvider.Context.IsFrontendClient || contextProvider.Context.IsResolveFlow(); |
||||
|
} |
||||
|
|
||||
|
private async Task ResolveCanUpdateAsync(IContentEntity content, ContentEntity result) |
||||
|
{ |
||||
|
result.CanUpdate = await contentWorkflow.CanUpdateAsync(content); |
||||
|
} |
||||
|
|
||||
|
private async Task ResolveNextsAsync(IContentEntity content, ContentEntity result, ClaimsPrincipal user) |
||||
|
{ |
||||
|
result.Nexts = await contentWorkflow.GetNextsAsync(content, user); |
||||
|
} |
||||
|
|
||||
|
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,139 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 Squidex.Infrastructure; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public static class ContextExtensions |
||||
|
{ |
||||
|
private const string HeaderUnpublished = "X-Unpublished"; |
||||
|
private const string HeaderFlatten = "X-Flatten"; |
||||
|
private const string HeaderLanguages = "X-Languages"; |
||||
|
private const string HeaderResolveFlow = "X-ResolveFlow"; |
||||
|
private const string HeaderResolveAssetUrls = "X-Resolve-Urls"; |
||||
|
private static readonly char[] Separators = { ',', ';' }; |
||||
|
|
||||
|
public static bool IsUnpublished(this Context context) |
||||
|
{ |
||||
|
return context.Headers.ContainsKey(HeaderUnpublished); |
||||
|
} |
||||
|
|
||||
|
public static Context WithUnpublished(this Context context, bool value = true) |
||||
|
{ |
||||
|
if (value) |
||||
|
{ |
||||
|
context.Headers[HeaderUnpublished] = "1"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
context.Headers.Remove(HeaderUnpublished); |
||||
|
} |
||||
|
|
||||
|
return context; |
||||
|
} |
||||
|
|
||||
|
public static bool IsFlatten(this Context context) |
||||
|
{ |
||||
|
return context.Headers.ContainsKey(HeaderFlatten); |
||||
|
} |
||||
|
|
||||
|
public static Context WithFlatten(this Context context, bool value = true) |
||||
|
{ |
||||
|
if (value) |
||||
|
{ |
||||
|
context.Headers[HeaderFlatten] = "1"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
context.Headers.Remove(HeaderFlatten); |
||||
|
} |
||||
|
|
||||
|
return context; |
||||
|
} |
||||
|
|
||||
|
public static bool IsResolveFlow(this Context context) |
||||
|
{ |
||||
|
return context.Headers.ContainsKey(HeaderResolveFlow); |
||||
|
} |
||||
|
|
||||
|
public static Context WithResolveFlow(this Context context, bool value = true) |
||||
|
{ |
||||
|
if (value) |
||||
|
{ |
||||
|
context.Headers[HeaderResolveFlow] = "1"; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
context.Headers.Remove(HeaderResolveFlow); |
||||
|
} |
||||
|
|
||||
|
return context; |
||||
|
} |
||||
|
|
||||
|
public static IEnumerable<string> AssetUrls(this Context context) |
||||
|
{ |
||||
|
if (context.Headers.TryGetValue(HeaderResolveAssetUrls, out var value)) |
||||
|
{ |
||||
|
return value.Split(Separators, StringSplitOptions.RemoveEmptyEntries).Select(x => x.Trim()).ToHashSet(); |
||||
|
} |
||||
|
|
||||
|
return Enumerable.Empty<string>(); |
||||
|
} |
||||
|
|
||||
|
public static Context WithAssetUrlsToResolve(this Context context, IEnumerable<string> fieldNames) |
||||
|
{ |
||||
|
if (fieldNames?.Any() == true) |
||||
|
{ |
||||
|
context.Headers[HeaderResolveAssetUrls] = string.Join(",", fieldNames); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
context.Headers.Remove(HeaderResolveAssetUrls); |
||||
|
} |
||||
|
|
||||
|
return context; |
||||
|
} |
||||
|
|
||||
|
public static IEnumerable<Language> Languages(this Context context) |
||||
|
{ |
||||
|
if (context.Headers.TryGetValue(HeaderResolveAssetUrls, out var value)) |
||||
|
{ |
||||
|
var languages = new HashSet<Language>(); |
||||
|
|
||||
|
foreach (var iso2Code in value.Split(Separators, StringSplitOptions.RemoveEmptyEntries)) |
||||
|
{ |
||||
|
if (Language.TryGetLanguage(iso2Code.Trim(), out var language)) |
||||
|
{ |
||||
|
languages.Add(language); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return languages; |
||||
|
} |
||||
|
|
||||
|
return Enumerable.Empty<Language>(); |
||||
|
} |
||||
|
|
||||
|
public static Context WithLanguages(this Context context, IEnumerable<string> fieldNames) |
||||
|
{ |
||||
|
if (fieldNames?.Any() == true) |
||||
|
{ |
||||
|
context.Headers[HeaderLanguages] = string.Join(",", fieldNames); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
context.Headers.Remove(HeaderLanguages); |
||||
|
} |
||||
|
|
||||
|
return context; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschränkt)
|
||||
|
// 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.Entities.Schemas; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public sealed class DefaultContentWorkflow : IContentWorkflow |
||||
|
{ |
||||
|
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 StatusInfo[] All = |
||||
|
{ |
||||
|
InfoArchived, |
||||
|
InfoDraft, |
||||
|
InfoPublished |
||||
|
}; |
||||
|
|
||||
|
private static readonly Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)> Flow = |
||||
|
new Dictionary<Status, (StatusInfo Info, StatusInfo[] Transitions)> |
||||
|
{ |
||||
|
[Status.Archived] = (InfoArchived, new[] |
||||
|
{ |
||||
|
InfoDraft |
||||
|
}), |
||||
|
[Status.Draft] = (InfoDraft, new[] |
||||
|
{ |
||||
|
InfoArchived, |
||||
|
InfoPublished |
||||
|
}), |
||||
|
[Status.Published] = (InfoPublished, new[] |
||||
|
{ |
||||
|
InfoDraft, |
||||
|
InfoArchived |
||||
|
}) |
||||
|
}; |
||||
|
|
||||
|
public Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema) |
||||
|
{ |
||||
|
var result = InfoDraft; |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
|
||||
|
public Task<bool> CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user) |
||||
|
{ |
||||
|
return TaskHelper.True; |
||||
|
} |
||||
|
|
||||
|
public Task<bool> CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user) |
||||
|
{ |
||||
|
var result = Flow.TryGetValue(content.Status, out var step) && step.Transitions.Any(x => x.Status == next); |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
|
||||
|
public Task<bool> CanUpdateAsync(IContentEntity content) |
||||
|
{ |
||||
|
var result = content.Status != Status.Archived; |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
|
||||
|
public Task<StatusInfo> GetInfoAsync(IContentEntity content) |
||||
|
{ |
||||
|
var result = Flow[content.Status].Info; |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
|
||||
|
public Task<StatusInfo[]> GetNextsAsync(IContentEntity content, ClaimsPrincipal user) |
||||
|
{ |
||||
|
var result = Flow.TryGetValue(content.Status, out var step) ? step.Transitions : Array.Empty<StatusInfo>(); |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
|
||||
|
public Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema) |
||||
|
{ |
||||
|
return Task.FromResult(All); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,57 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public sealed class DefaultWorkflowsValidator : IWorkflowsValidator |
||||
|
{ |
||||
|
private readonly IAppProvider appProvider; |
||||
|
|
||||
|
public DefaultWorkflowsValidator(IAppProvider appProvider) |
||||
|
{ |
||||
|
Guard.NotNull(appProvider, nameof(appProvider)); |
||||
|
|
||||
|
this.appProvider = appProvider; |
||||
|
} |
||||
|
|
||||
|
public async Task<IReadOnlyList<string>> ValidateAsync(Guid appId, Workflows workflows) |
||||
|
{ |
||||
|
Guard.NotNull(workflows, nameof(workflows)); |
||||
|
|
||||
|
var errors = new List<string>(); |
||||
|
|
||||
|
if (workflows.Values.Count(x => x.SchemaIds.Count == 0) > 1) |
||||
|
{ |
||||
|
errors.Add("Multiple workflows cover all schemas."); |
||||
|
} |
||||
|
|
||||
|
var uniqueSchemaIds = workflows.Values.SelectMany(x => x.SchemaIds).Distinct().ToList(); |
||||
|
|
||||
|
foreach (var schemaId in uniqueSchemaIds) |
||||
|
{ |
||||
|
if (workflows.Values.Count(x => x.SchemaIds.Contains(schemaId)) > 1) |
||||
|
{ |
||||
|
var schema = await appProvider.GetSchemaAsync(appId, schemaId); |
||||
|
|
||||
|
if (schema != null) |
||||
|
{ |
||||
|
errors.Add($"The schema `{schema.SchemaDef.Name}` is covered by multiple workflows."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return errors; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,153 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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, schema.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, content.SchemaId.Id); |
||||
|
|
||||
|
return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content.DataDraft, user); |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user) |
||||
|
{ |
||||
|
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); |
||||
|
|
||||
|
return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && CanUse(transition, data, user); |
||||
|
} |
||||
|
|
||||
|
public async Task<bool> CanUpdateAsync(IContentEntity content) |
||||
|
{ |
||||
|
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.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, content.SchemaId.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, schema.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, content.SchemaId.Id); |
||||
|
|
||||
|
foreach (var (to, step, transition) in workflow.GetTransitions(content.Status)) |
||||
|
{ |
||||
|
if (CanUse(transition, content.DataDraft, user)) |
||||
|
{ |
||||
|
result.Add(new StatusInfo(to, GetColor(step))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result.ToArray(); |
||||
|
} |
||||
|
|
||||
|
private bool CanUse(WorkflowTransition transition, NamedContentData data, 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", data, transition.Expression); |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
private async Task<Workflow> GetWorkflowAsync(Guid appId, Guid schemaId) |
||||
|
{ |
||||
|
Workflow result = null; |
||||
|
|
||||
|
var app = await appProvider.GetAppAsync(appId); |
||||
|
|
||||
|
if (app != null) |
||||
|
{ |
||||
|
result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Contains(schemaId)); |
||||
|
|
||||
|
if (result == null) |
||||
|
{ |
||||
|
result = app.Workflows.Values.FirstOrDefault(x => x.SchemaIds.Count == 0); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (result == null) |
||||
|
{ |
||||
|
result = Workflow.Default; |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private static string GetColor(WorkflowStep step) |
||||
|
{ |
||||
|
return step.Color ?? StatusColors.Draft; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public interface IContentEnricher |
||||
|
{ |
||||
|
Task<IEnrichedContentEntity> EnrichAsync(IContentEntity content, ClaimsPrincipal user); |
||||
|
|
||||
|
Task<IReadOnlyList<IEnrichedContentEntity>> EnrichAsync(IEnumerable<IContentEntity> contents, ClaimsPrincipal user); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Security.Claims; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Schemas; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public interface IContentWorkflow |
||||
|
{ |
||||
|
Task<StatusInfo> GetInitialStatusAsync(ISchemaEntity schema); |
||||
|
|
||||
|
Task<bool> CanPublishOnCreateAsync(ISchemaEntity schema, NamedContentData data, ClaimsPrincipal user); |
||||
|
|
||||
|
Task<bool> CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user); |
||||
|
|
||||
|
Task<bool> CanUpdateAsync(IContentEntity content); |
||||
|
|
||||
|
Task<StatusInfo> GetInfoAsync(IContentEntity content); |
||||
|
|
||||
|
Task<StatusInfo[]> GetNextsAsync(IContentEntity content, ClaimsPrincipal user); |
||||
|
|
||||
|
Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema); |
||||
|
} |
||||
|
} |
||||
@ -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,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents |
||||
|
{ |
||||
|
public interface IWorkflowsValidator |
||||
|
{ |
||||
|
Task<IReadOnlyList<string>> ValidateAsync(Guid appId, Workflows workflows); |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue