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