mirror of https://github.com/Squidex/squidex.git
19 changed files with 583 additions and 11 deletions
@ -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,71 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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)}"); |
||||
|
} |
||||
|
|
||||
|
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,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)) ?? WorkflowStep.EmptyTransitions, |
||||
|
x.Value.Color, |
||||
|
x.Value.NoUpdate)) ?? Workflow.EmptySteps, |
||||
|
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,134 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Squidex.Domain.Apps.Entities.Apps.Commands; |
||||
|
using Squidex.Domain.Apps.Entities.TestHelpers; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Apps.Guards |
||||
|
{ |
||||
|
public class GuardAppWorkflowTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void CanConfigure_should_throw_exception_if_workflow_is_not_defined() |
||||
|
{ |
||||
|
var command = new ConfigureWorkflow(); |
||||
|
|
||||
|
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), |
||||
|
new ValidationError("Workflow is required.", "Workflow")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanConfigure_should_throw_exception_if_workflow_has_no_initial_step() |
||||
|
{ |
||||
|
var command = new ConfigureWorkflow |
||||
|
{ |
||||
|
Workflow = new Workflow( |
||||
|
new Dictionary<Status, WorkflowStep> |
||||
|
{ |
||||
|
[Status.Published] = new WorkflowStep(WorkflowStep.EmptyTransitions) |
||||
|
}, |
||||
|
default) |
||||
|
}; |
||||
|
|
||||
|
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), |
||||
|
new ValidationError("Initial step is required.", "Workflow.Initial")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanConfigure_should_throw_exception_if_workflow_does_not_have_published_state() |
||||
|
{ |
||||
|
var command = new ConfigureWorkflow |
||||
|
{ |
||||
|
Workflow = new Workflow( |
||||
|
new Dictionary<Status, WorkflowStep> |
||||
|
{ |
||||
|
[Status.Draft] = new WorkflowStep(WorkflowStep.EmptyTransitions) |
||||
|
}, |
||||
|
Status.Draft) |
||||
|
}; |
||||
|
|
||||
|
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), |
||||
|
new ValidationError("Workflow must have a published step.", "Workflow.Steps")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanConfigure_should_throw_exception_if_workflow_step_is_not_defined() |
||||
|
{ |
||||
|
var command = new ConfigureWorkflow |
||||
|
{ |
||||
|
Workflow = new Workflow( |
||||
|
new Dictionary<Status, WorkflowStep> |
||||
|
{ |
||||
|
[Status.Published] = null |
||||
|
}, |
||||
|
Status.Published) |
||||
|
}; |
||||
|
|
||||
|
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), |
||||
|
new ValidationError("Step is required.", "Workflow.Steps.Published")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanConfigure_should_throw_exception_if_workflow_transition_is_invalid() |
||||
|
{ |
||||
|
var command = new ConfigureWorkflow |
||||
|
{ |
||||
|
Workflow = new Workflow( |
||||
|
new Dictionary<Status, WorkflowStep> |
||||
|
{ |
||||
|
[Status.Published] = |
||||
|
new WorkflowStep( |
||||
|
new Dictionary<Status, WorkflowTransition> |
||||
|
{ |
||||
|
[Status.Draft] = new WorkflowTransition() |
||||
|
}) |
||||
|
}, |
||||
|
Status.Published) |
||||
|
}; |
||||
|
|
||||
|
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), |
||||
|
new ValidationError("Transition has an invalid target.", "Workflow.Steps.Published.Transitions.Draft")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanConfigure_should_throw_exception_if_workflow_transition_is_not_defined() |
||||
|
{ |
||||
|
var command = new ConfigureWorkflow |
||||
|
{ |
||||
|
Workflow = new Workflow( |
||||
|
new Dictionary<Status, WorkflowStep> |
||||
|
{ |
||||
|
[Status.Draft] = |
||||
|
new WorkflowStep( |
||||
|
WorkflowStep.EmptyTransitions), |
||||
|
[Status.Published] = |
||||
|
new WorkflowStep( |
||||
|
new Dictionary<Status, WorkflowTransition> |
||||
|
{ |
||||
|
[Status.Draft] = null |
||||
|
}) |
||||
|
}, |
||||
|
Status.Published) |
||||
|
}; |
||||
|
|
||||
|
ValidationAssert.Throws(() => GuardAppWorkflows.CanConfigure(command), |
||||
|
new ValidationError("Transition is required.", "Workflow.Steps.Published.Transitions.Draft")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void CanConfigure_should_not_throw_exception_if_workflow_is_valid() |
||||
|
{ |
||||
|
var command = new ConfigureWorkflow { Workflow = Workflow.Default }; |
||||
|
|
||||
|
GuardAppWorkflows.CanConfigure(command); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue