Browse Source

Enable multi-select of roles for prevent updates. (#444)

* Enable multi-select of roles for prevent updates.
pull/445/head
Mittul Madaan 6 years ago
committed by Sebastian Stehle
parent
commit
98a62a248e
  1. 19
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs
  2. 24
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs
  3. 16
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
  4. 29
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs
  5. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs
  6. 19
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs
  7. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs
  8. 22
      backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs
  9. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs
  10. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs
  11. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  12. 29
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs
  13. 10
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs
  14. 13
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs
  15. 25
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs
  16. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs
  17. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs
  18. 67
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs
  19. 6
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs
  20. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs
  21. 38
      frontend/app/features/settings/pages/workflows/workflow-step.component.html
  22. 47
      frontend/app/features/settings/pages/workflows/workflow-step.component.scss
  23. 11
      frontend/app/features/settings/pages/workflows/workflow-step.component.ts
  24. 2
      frontend/app/features/settings/pages/workflows/workflow-transition.component.html
  25. 5
      frontend/app/features/settings/pages/workflows/workflow-transition.component.scss
  26. 6
      frontend/app/features/settings/pages/workflows/workflow-transition.component.ts
  27. 3
      frontend/app/features/settings/pages/workflows/workflow.component.ts
  28. 2
      frontend/app/features/settings/pages/workflows/workflows-page.component.html
  29. 12
      frontend/app/features/settings/pages/workflows/workflows-page.component.ts
  30. 12
      frontend/app/shared/services/workflows.service.ts

19
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Json/JsonWorkflowTransition.cs

@ -1,12 +1,10 @@
// ==========================================================================
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Newtonsoft.Json;
using Squidex.Infrastructure.Reflection;
@ -21,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.Contents.Json
public string Role { get; set; }
[JsonProperty]
public List<string> Roles { get; }
public string[] Roles { get; }
public JsonWorkflowTransition()
{
@ -34,18 +32,11 @@ namespace Squidex.Domain.Apps.Core.Contents.Json
public WorkflowTransition ToTransition()
{
var rolesList = Roles;
var roles = Roles;
if (!string.IsNullOrEmpty(Role))
{
rolesList = new List<string> { Role };
}
ReadOnlyCollection<string>? roles = null;
if (rolesList != null && rolesList.Count > 0)
{
roles = new ReadOnlyCollection<string>(rolesList);
roles = new[] { Role };
}
return new WorkflowTransition(Expression, roles);

24
backend/src/Squidex.Domain.Apps.Core.Model/Contents/NoUpdate.cs

@ -0,0 +1,24 @@
// ==========================================================================
// 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 NoUpdate : WorkflowCondition
{
public static readonly NoUpdate Always = new NoUpdate(null, null);
public NoUpdate(string expression, params string[] roles)
: base(expression, roles)
{
}
public static NoUpdate When(string expression, params string[] roles)
{
return new NoUpdate(expression, roles);
}
}
}

16
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs

@ -55,23 +55,23 @@ namespace Squidex.Domain.Apps.Core.Contents
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Draft] = new WorkflowTransition()
[Status.Draft] = WorkflowTransition.Always
},
StatusColors.Archived, true),
StatusColors.Archived, NoUpdate.Always),
[Status.Draft] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Published] = new WorkflowTransition()
[Status.Archived] = WorkflowTransition.Always,
[Status.Published] = WorkflowTransition.Always
},
StatusColors.Draft),
[Status.Published] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Draft] = new WorkflowTransition()
[Status.Archived] = WorkflowTransition.Always,
[Status.Draft] = WorkflowTransition.Always
},
StatusColors.Published)
}, null, name);
@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Core.Contents
}
else if (TryGetStep(Initial, out var initial))
{
yield return (Initial, initial, WorkflowTransition.Default);
yield return (Initial, initial, WorkflowTransition.Always);
}
}
@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Core.Contents
}
else if (to == Initial)
{
transition = WorkflowTransition.Default;
transition = WorkflowTransition.Always;
return true;
}

29
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowCondition.cs

@ -0,0 +1,29 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.ObjectModel;
using Squidex.Infrastructure.Collections;
namespace Squidex.Domain.Apps.Core.Contents
{
public abstract class WorkflowCondition
{
public string Expression { get; }
public ReadOnlyCollection<string> Roles { get; }
protected WorkflowCondition(string expression, params string[] roles)
{
Expression = expression;
if (roles != null)
{
Roles = ReadOnlyCollection.Create(roles);
}
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs

@ -15,11 +15,11 @@ namespace Squidex.Domain.Apps.Core.Contents
public IReadOnlyDictionary<Status, WorkflowTransition> Transitions { get; }
public string? Color { get; }
public string Color { get; }
public bool NoUpdate { get; }
public NoUpdate NoUpdate { get; }
public WorkflowStep(IReadOnlyDictionary<Status, WorkflowTransition>? transitions = null, string? color = null, bool noUpdate = false)
public WorkflowStep(IReadOnlyDictionary<Status, WorkflowTransition>? transitions = null, string? color = null, NoUpdate? noUpdate = null)
{
Transitions = transitions ?? EmptyTransitions;

19
backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowTransition.cs

@ -5,23 +5,20 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.ObjectModel;
namespace Squidex.Domain.Apps.Core.Contents
{
public sealed class WorkflowTransition
public sealed class WorkflowTransition : WorkflowCondition
{
public static readonly WorkflowTransition Default = new WorkflowTransition();
public string? Expression { get; }
public ReadOnlyCollection<string>? Roles { get; }
public static readonly WorkflowTransition Always = new WorkflowTransition(null, null);
public WorkflowTransition(string? expression = null, ReadOnlyCollection<string>? roles = null)
public WorkflowTransition(string expression, params string[] roles)
: base(expression, roles)
{
Expression = expression;
}
Roles = roles;
public static WorkflowTransition When(string expression, params string[] roles)
{
return new WorkflowTransition(expression, roles);
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs

@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return Task.FromResult(result);
}
public Task<bool> CanUpdateAsync(IContentEntity content)
public Task<bool> CanUpdateAsync(IContentEntity content, ClaimsPrincipal user)
{
var result = content.Status != Status.Archived;

22
backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs

@ -43,23 +43,23 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id);
return workflow.TryGetTransition(content.Status, next, out var transition) && CanUse(transition, content.DataDraft, user);
return workflow.TryGetTransition(content.Status, next, out var transition) && IsTrue(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);
return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && IsTrue(transition, data, user);
}
public async Task<bool> CanUpdateAsync(IContentEntity content)
public async Task<bool> CanUpdateAsync(IContentEntity content, ClaimsPrincipal user)
{
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id);
if (workflow.TryGetStep(content.Status, out var step))
{
return !step.NoUpdate;
return step.NoUpdate == null || !IsTrue(step.NoUpdate, content.DataDraft, user);
}
return true;
@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
foreach (var (to, step, transition) in workflow.GetTransitions(content.Status))
{
if (CanUse(transition, content.DataDraft, user))
if (IsTrue(transition, content.DataDraft, user))
{
result.Add(new StatusInfo(to, GetColor(step)));
}
@ -103,19 +103,21 @@ namespace Squidex.Domain.Apps.Entities.Contents
return result.ToArray();
}
private bool CanUse(WorkflowTransition transition, NamedContentData data, ClaimsPrincipal user)
private bool IsTrue(WorkflowCondition condition, NamedContentData data, ClaimsPrincipal user)
{
if (transition.Roles != null)
if (condition?.Roles != null)
{
if (!user.Claims.Any(x => x.Type == ClaimTypes.Role && transition.Roles.Contains(x.Value)))
if (!user.Claims.Any(x => x.Type == ClaimTypes.Role && condition.Roles.Contains(x.Value)))
{
return false;
}
}
if (!string.IsNullOrWhiteSpace(transition.Expression))
if (!string.IsNullOrWhiteSpace(condition?.Expression))
{
return scriptEngine.Evaluate("data", data, transition.Expression);
var result = false;
result = scriptEngine.Evaluate("data", data, condition.Expression);
return result;
}
return true;

9
backend/src/Squidex.Domain.Apps.Entities/Contents/Guards/GuardContent.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Security.Claims;
using System.Threading.Tasks;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents;
@ -48,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
if (!isProposal)
{
await ValidateCanUpdate(content, contentWorkflow);
await ValidateCanUpdate(content, contentWorkflow, command.User);
}
}
@ -63,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
if (!isProposal)
{
await ValidateCanUpdate(content, contentWorkflow);
await ValidateCanUpdate(content, contentWorkflow, command.User);
}
}
@ -125,9 +126,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guards
}
}
private static async Task ValidateCanUpdate(IContentEntity content, IContentWorkflow contentWorkflow)
private static async Task ValidateCanUpdate(IContentEntity content, IContentWorkflow contentWorkflow, ClaimsPrincipal user)
{
if (!await contentWorkflow.CanUpdateAsync(content))
if (!await contentWorkflow.CanUpdateAsync(content, user))
{
throw new DomainException($"The workflow does not allow updates at status {content.Status}");
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
Task<bool> CanMoveToAsync(IContentEntity content, Status next, ClaimsPrincipal user);
Task<bool> CanUpdateAsync(IContentEntity content);
Task<bool> CanUpdateAsync(IContentEntity content, ClaimsPrincipal user);
Task<StatusInfo> GetInfoAsync(IContentEntity content);

6
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs

@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (ShouldEnrichWithStatuses(context))
{
await EnrichNextsAsync(content, result, context);
await EnrichCanUpdateAsync(content, result);
await EnrichCanUpdateAsync(content, result, context);
}
results.Add(result);
@ -327,9 +327,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return assets.ToLookup(x => x.Id);
}
private async Task EnrichCanUpdateAsync(IContentEntity content, ContentEntity result)
private async Task EnrichCanUpdateAsync(IContentEntity content, ContentEntity result, Context context)
{
result.CanUpdate = await contentWorkflow.CanUpdateAsync(content);
result.CanUpdate = await contentWorkflow.CanUpdateAsync(content, context.User);
}
private async Task EnrichNextsAsync(IContentEntity content, ContentEntity result, Context context)

29
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs

@ -10,6 +10,7 @@ using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure.Reflection;
using NoUpdateType = Squidex.Domain.Apps.Core.Contents.NoUpdate;
namespace Squidex.Areas.Api.Controllers.Apps.Models
{
@ -31,6 +32,16 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary>
public bool NoUpdate { get; set; }
/// <summary>
/// Optional expression that must evaluate to true when you want to prevent updates.
/// </summary>
public string NoUpdateExpression { get; set; }
/// <summary>
/// Optional list of roles to restrict the updates for users with these roles.
/// </summary>
public string[] NoUpdateRoles { get; set; }
public static WorkflowStepDto? FromWorkflowStep(WorkflowStep step)
{
if (step == null)
@ -38,12 +49,21 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
return null;
}
return SimpleMapper.Map(step, new WorkflowStepDto
var response = SimpleMapper.Map(step, new WorkflowStepDto
{
Transitions = step.Transitions.ToDictionary(
y => y.Key,
y => WorkflowTransitionDto.FromWorkflowTransition(y.Value)!)
y => WorkflowTransitionDto.FromWorkflowTransition(y.Value))
});
if (step.NoUpdate != null)
{
response.NoUpdate = true;
response.NoUpdateExpression = step.NoUpdate.Expression;
response.NoUpdateRoles = step.NoUpdate.Roles?.ToArray();
}
return response;
}
public WorkflowStep ToStep()
@ -52,7 +72,10 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
Transitions?.ToDictionary(
y => y.Key,
y => y.Value?.ToTransition()!),
Color, NoUpdate);
Color,
NoUpdate ?
NoUpdateType.When(NoUpdateExpression, NoUpdateRoles) :
null);
}
}
}

10
backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowTransitionDto.cs

@ -5,7 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.ObjectModel;
using System.Linq;
using Squidex.Domain.Apps.Core.Contents;
namespace Squidex.Areas.Api.Controllers.Apps.Models
@ -15,21 +15,21 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// <summary>
/// The optional expression.
/// </summary>
public string? Expression { get; set; }
public string Expression { get; set; }
/// <summary>
/// The optional restricted role.
/// </summary>
public ReadOnlyCollection<string>? Roles { get; set; }
public string[] Roles { get; set; }
public static WorkflowTransitionDto? FromWorkflowTransition(WorkflowTransition transition)
public static WorkflowTransitionDto FromWorkflowTransition(WorkflowTransition transition)
{
if (transition == null)
{
return null;
}
return new WorkflowTransitionDto { Expression = transition.Expression, Roles = transition.Roles };
return new WorkflowTransitionDto { Expression = transition.Expression, Roles = transition.Roles?.ToArray() };
}
public WorkflowTransition ToTransition()

13
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Linq;
using FluentAssertions;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Contents.Json;
@ -24,6 +25,16 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
serialized.Should().BeEquivalentTo(workflow);
}
[Fact]
public void Should_serialize_and_deserialize_no_update_condition()
{
var step = new WorkflowStep(noUpdate: NoUpdate.When("Expression", "Role1", "Role2"));
var serialized = step.SerializeAndDeserialize();
serialized.Should().BeEquivalentTo(step);
}
[Fact]
public void Should_verify_roles_mapping_in_workflow_transition()
{
@ -33,7 +44,7 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
var result = serialized.ToTransition();
Assert.Equal(new string[] { "role_1" }, result.Roles);
Assert.Equal(source.Role, result?.Roles.Single());
}
}
}

25
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowTests.cs

@ -22,8 +22,8 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition("ToArchivedExpr", ReadOnlyCollection.Create("ToArchivedRole" )),
[Status.Published] = new WorkflowTransition("ToPublishedExpr", ReadOnlyCollection.Create("ToPublishedRole" ))
[Status.Archived] = WorkflowTransition.When("ToArchivedExpr", "ToArchivedRole"),
[Status.Published] = WorkflowTransition.When("ToPublishedExpr", "ToPublishedRole")
},
StatusColors.Draft),
[Status.Archived] =
@ -74,8 +74,8 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
var found = workflow.TryGetTransition(Status.Draft, Status.Archived, out var transition);
Assert.True(found);
Assert.Equal("ToArchivedExpr", transition!.Expression);
Assert.Equal(new[] { "ToArchivedRole" }, transition!.Roles);
Assert.Equal("ToArchivedExpr", transition?.Expression);
Assert.Equal("ToArchivedRole", transition?.Roles.Single());
}
[Fact]
@ -84,8 +84,8 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
var found = workflow.TryGetTransition(new Status("Other"), Status.Draft, out var transition);
Assert.True(found);
Assert.Null(transition!.Expression);
Assert.Null(transition!.Roles);
Assert.Null(transition?.Expression);
Assert.Null(transition?.Roles);
}
[Fact]
@ -116,16 +116,15 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
var (status1, step1, transition1) = transitions[0];
Assert.Equal(Status.Archived, status1);
Assert.Equal("ToArchivedExpr", transition1.Expression);
Assert.Equal(new[] { "ToArchivedRole" }, transition1.Roles);
Assert.Equal("ToArchivedExpr", transition1?.Expression);
Assert.Equal("ToArchivedRole", transition1?.Roles.Single());
Assert.Same(workflow.Steps[status1], step1);
var (status2, step2, transition2) = transitions[1];
Assert.Equal(Status.Published, status2);
Assert.Equal("ToPublishedExpr", transition2.Expression);
Assert.Equal(new[] { "ToPublishedRole" }, transition2.Roles);
Assert.Equal("ToPublishedExpr", transition2?.Expression);
Assert.Equal("ToPublishedRole", transition2?.Roles.Single());
Assert.Same(workflow.Steps[status2], step2);
}
@ -139,8 +138,8 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
var (status1, step1, transition1) = transitions[0];
Assert.Equal(Status.Draft, status1);
Assert.Null(transition1.Expression);
Assert.Null(transition1.Roles);
Assert.Null(transition1?.Expression);
Assert.Null(transition1?.Roles);
Assert.Same(workflow.Steps[status1], step1);
}
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppWorkflowTests.cs

@ -150,7 +150,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition()
[Status.Archived] = WorkflowTransition.Always
}),
[Status.Draft] = new WorkflowStep()
}),

6
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs

@ -49,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var content = new ContentEntity { Status = Status.Published };
var result = await sut.CanUpdateAsync(content);
var result = await sut.CanUpdateAsync(content, null!);
Assert.True(result);
}
@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var content = new ContentEntity { Status = Status.Published };
var result = await sut.CanUpdateAsync(content);
var result = await sut.CanUpdateAsync(content, null!);
Assert.True(result);
}
@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var content = new ContentEntity { Status = Status.Archived };
var result = await sut.CanUpdateAsync(content);
var result = await sut.CanUpdateAsync(content, null!);
Assert.False(result);
}

67
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs

@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using FluentAssertions;
@ -37,25 +38,25 @@ namespace Squidex.Domain.Apps.Entities.Contents
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Draft] = new WorkflowTransition()
[Status.Draft] = WorkflowTransition.Always
},
StatusColors.Archived, true),
StatusColors.Archived, NoUpdate.Always),
[Status.Draft] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Published] = new WorkflowTransition("data.field.iv === 2", ReadOnlyCollection.Create("Owner", "Editor"))
[Status.Archived] = WorkflowTransition.Always,
[Status.Published] = WorkflowTransition.When("data.field.iv === 2", "Editor")
},
StatusColors.Draft),
[Status.Published] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Archived] = new WorkflowTransition(),
[Status.Draft] = new WorkflowTransition()
[Status.Archived] = WorkflowTransition.Always,
[Status.Draft] = WorkflowTransition.Always
},
StatusColors.Published)
StatusColors.Published, NoUpdate.When("data.field.iv === 2", "Owner", "Editor"))
});
public DynamicContentWorkflowTests()
@ -70,14 +71,14 @@ namespace Squidex.Domain.Apps.Entities.Contents
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Published] = new WorkflowTransition()
[Status.Published] = WorkflowTransition.Always
},
StatusColors.Draft),
[Status.Published] =
new WorkflowStep(
new Dictionary<Status, WorkflowTransition>
{
[Status.Draft] = new WorkflowTransition()
[Status.Draft] = WorkflowTransition.Always
},
StatusColors.Published)
},
@ -179,7 +180,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var content = CreateContent(Status.Published, 2);
var result = await sut.CanUpdateAsync(content);
var result = await sut.CanUpdateAsync(content, Mocks.FrontendUser("Developer"));
Assert.True(result);
}
@ -189,7 +190,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var content = CreateContent(Status.Published, 2);
var result = await sut.CanUpdateAsync(content);
var result = await sut.CanUpdateAsync(content, Mocks.FrontendUser("Developer"));
Assert.True(result);
}
@ -199,11 +200,51 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var content = CreateContent(Status.Archived, 2);
var result = await sut.CanUpdateAsync(content);
var result = await sut.CanUpdateAsync(content, Mocks.FrontendUser("Developer"));
Assert.False(result);
}
[Fact]
public async Task Should_not_be_able_to_update_published_with_true_expression()
{
var content = CreateContent(Status.Published, 2);
var result = await sut.CanUpdateAsync(content, Mocks.FrontendUser("Owner"));
Assert.False(result);
}
[Fact]
public async Task Should_be_able_to_update_published_with_false_expression()
{
var content = CreateContent(Status.Published, 1);
var result = await sut.CanUpdateAsync(content, Mocks.FrontendUser("Owner"));
Assert.True(result);
}
[Fact]
public async Task Should_not_be_able_to_update_published_with_correct_roles()
{
var content = CreateContent(Status.Published, 2);
var result = await sut.CanUpdateAsync(content, Mocks.FrontendUser("Editor"));
Assert.False(result);
}
[Fact]
public async Task Should_be_able_to_update_published_with_incorrect_roles()
{
var content = CreateContent(Status.Published, 1);
var result = await sut.CanUpdateAsync(content, Mocks.FrontendUser("Owner"));
Assert.True(result);
}
[Fact]
public async Task Should_get_next_statuses_for_draft()
{
@ -276,7 +317,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
new StatusInfo(Status.Draft, StatusColors.Draft)
};
var result = await sut.GetNextsAsync(content, null!);
var result = await sut.GetNextsAsync(content, null);
result.Should().BeEquivalentTo(expected);
}

6
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Guard/GuardContentTests.cs

@ -127,7 +127,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
SetupCanUpdate(true);
var content = CreateContent(Status.Draft, false);
var command = new UpdateContent { Data = new NamedContentData() };
var command = new UpdateContent { Data = new NamedContentData(), User = user };
await GuardContent.CanUpdate(content, contentWorkflow, command, false);
}
@ -165,7 +165,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
SetupCanUpdate(true);
var content = CreateContent(Status.Draft, false);
var command = new PatchContent { Data = new NamedContentData() };
var command = new PatchContent { Data = new NamedContentData(), User = user };
await GuardContent.CanPatch(content, contentWorkflow, command, false);
}
@ -286,7 +286,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Guard
private void SetupCanUpdate(bool canUpdate)
{
A.CallTo(() => contentWorkflow.CanUpdateAsync(A<IContentEntity>.Ignored))
A.CallTo(() => contentWorkflow.CanUpdateAsync(A<IContentEntity>.Ignored, user))
.Returns(canUpdate);
}

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs

@ -149,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var source = new ContentEntity { SchemaId = schemaId };
A.CallTo(() => contentWorkflow.CanUpdateAsync(source))
A.CallTo(() => contentWorkflow.CanUpdateAsync(source, requestContext.User))
.Returns(true);
var result = await sut.EnrichAsync(source, requestContext);
@ -168,7 +168,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.False(result.CanUpdate);
A.CallTo(() => contentWorkflow.CanUpdateAsync(source))
A.CallTo(() => contentWorkflow.CanUpdateAsync(source, requestContext.User))
.MustNotHaveHappened();
}

38
frontend/app/features/settings/pages/workflows/workflow-step.component.html

@ -62,8 +62,10 @@
</div>
</div>
<div class="row transition-prevent-updates no-guttersw">
<div class="col col-step" style="max-width: 12rem">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="preventUpdates_{{step.name}}"
<input class="form-check-input transition-prevent-updates-checkbox" type="checkbox" id="preventUpdates_{{step.name}}"
[disabled]="disabled"
[ngModel]="step.noUpdate"
(ngModelChange)="changeNoUpdate($event)" />
@ -73,4 +75,38 @@
</label>
</div>
</div>
<ng-container *ngIf="step.noUpdate">
<div class="col-auto col-label">
<span class="text-decent">when</span>
</div>
<div class="col col-step-expression">
<input class="form-control" [class.dashed]="!step.noUpdateExpression" spellcheck="false"
[disabled]="disabled"
[ngModelOptions]="onBlur"
[ngModel]="step.noUpdateExpression"
(ngModelChange)="changeNoUpdateExpression($event)"
placeholder="Expression" />
</div>
<div class="col-auto col-label">
<span class="text-decent">for</span>
</div>
<div class="col col-step-role">
<sqx-tag-editor [allowDuplicates]="false"
[dashed]="true"
[disabled]="disabled"
(ngModelChange)="changeNoUpdateRoles($event)"
[ngModel]="step.noUpdateRoles"
[ngModelOptions]="onBlur"
[singleLine]="true"
[suggestions]="roles"
placeholder="Role">
</sqx-tag-editor>
</div>
</ng-container>
</div>
</div>
<div class="col-auto col-div-line">
<hr class="col-line"/>
</div>
</div>

47
frontend/app/features/settings/pages/workflows/workflow-step.component.scss

@ -2,6 +2,12 @@
@import '_mixins';
:host ::ng-deep {
.dashed {
@include placeholder-color($color-theme-secondary);
border-style: dashed;
border-width: 1px;
}
.color {
line-height: 2.8rem;
}
@ -67,3 +73,44 @@
visibility: visible;
}
}
.col-step-role {
width: 56px;
padding-left: 0rem;
padding-right: 4rem;
}
.col-step-expression {
padding-left: 0;
padding-right: 0
}
.transition-prevent-updates {
& {
margin-top: .25rem;
margin-bottom: .5rem;
line-height: 2.5rem;
}
&-to {
padding: .5rem .75rem;
padding-right: 0;
line-height: 1.2rem;
}
}
.col-line {
display: block;
height: 1px;
border: 0;
border-top: 1px solid #e1e4e8;
margin: 1em 0;
}
.col-div-line {
padding-top: 0.5rem
}
.transition-prevent-updates-checkbox {
margin-top: 0.8rem;
}

11
frontend/app/features/settings/pages/workflows/workflow-step.component.ts

@ -8,7 +8,6 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import {
RoleDto,
WorkflowDto,
WorkflowStep,
WorkflowStepValues,
@ -53,7 +52,7 @@ export class WorkflowStepComponent implements OnChanges {
public step: WorkflowStep;
@Input()
public roles: ReadonlyArray<RoleDto>;
public roles: ReadonlyArray<string>;
@Input()
public disabled: boolean;
@ -88,6 +87,14 @@ export class WorkflowStepComponent implements OnChanges {
this.update.emit({ noUpdate });
}
public changeNoUpdateExpression(noUpdateExpression?: string) {
this.update.emit({ noUpdateExpression });
}
public changeNoUpdateRoles(noUpdateRoles?: ReadonlyArray<string>) {
this.update.emit({ noUpdateRoles });
}
public emitMakeInitial() {
this.makeInitial.emit();
}

2
frontend/app/features/settings/pages/workflows/workflow-transition.component.html

@ -31,7 +31,7 @@
[ngModel]="transition.roles"
[ngModelOptions]="onBlur"
[singleLine]="true"
[suggestions]="roleSuggestions"
[suggestions]="roles"
placeholder="Role">
</sqx-tag-editor>
</div>

5
frontend/app/features/settings/pages/workflows/workflow-transition.component.scss

@ -1,11 +1,6 @@
@import '_vars';
@import '_mixins';
.dashed {
border-style: dashed;
border-width: 1px;
}
.select-placeholder {
@include absolute(0, 0, 0, 0);
color: $color-theme-secondary;

6
frontend/app/features/settings/pages/workflows/workflow-transition.component.ts

@ -31,15 +31,11 @@ export class WorkflowTransitionComponent {
public transition: WorkflowTransitionView;
@Input()
public roles: ReadonlyArray<RoleDto>;
public roles: ReadonlyArray<string>;
@Input()
public disabled: boolean;
public get roleSuggestions() {
return this.roles.map(x => x.name);
}
public changeExpression(expression: string) {
this.update.emit({ expression });
}

3
frontend/app/features/settings/pages/workflows/workflow.component.ts

@ -12,7 +12,6 @@ import { Component, Input, OnChanges } from '@angular/core';
import {
ErrorDto,
MathHelper,
RoleDto,
SchemaTagConverter,
WorkflowDto,
WorkflowsState,
@ -34,7 +33,7 @@ export class WorkflowComponent implements OnChanges {
public workflow: WorkflowDto;
@Input()
public roles: ReadonlyArray<RoleDto>;
public roles: ReadonlyArray<string>;
@Input()
public schemasSource: SchemaTagConverter;

2
frontend/app/features/settings/pages/workflows/workflows-page.component.html

@ -26,7 +26,6 @@
</ng-container>
<ng-container *ngIf="schemasSource && workflowsState.workflows | async; let workflows">
<ng-container *ngIf="rolesState.roles | async; let roles">
<div class="table-items-row table-items-row-empty" *ngIf="workflows.length === 0">
No workflows created yet.
</div>
@ -38,7 +37,6 @@
<sqx-workflow-add-form *ngIf="workflowsState.canCreate | async"></sqx-workflow-add-form>
</ng-container>
</ng-container>
</ng-container>
<ng-container sidebar>
<div class="panel-nav">

12
frontend/app/features/settings/pages/workflows/workflows-page.component.ts

@ -8,6 +8,7 @@
import { Component, OnInit } from '@angular/core';
import {
ResourceOwner,
RolesState,
SchemaTagConverter,
WorkflowDto,
@ -19,15 +20,24 @@ import {
styleUrls: ['./workflows-page.component.scss'],
templateUrl: './workflows-page.component.html'
})
export class WorkflowsPageComponent implements OnInit {
export class WorkflowsPageComponent extends ResourceOwner implements OnInit {
public roles: ReadonlyArray<string> = [];
constructor(
public readonly rolesState: RolesState,
public readonly schemasSource: SchemaTagConverter,
public readonly workflowsState: WorkflowsState
) {
super();
}
public ngOnInit() {
this.own(
this.rolesState.roles
.subscribe(roles => {
this.roles = roles.map(x => x.name);
}));
this.rolesState.load();
this.workflowsState.load();

12
frontend/app/shared/services/workflows.service.ts

@ -211,7 +211,7 @@ export class WorkflowDto extends Model<WorkflowDto> {
}
}
export type WorkflowStepValues = { color?: string; isLocked?: boolean; noUpdate?: boolean; };
export type WorkflowStepValues = { color?: string; isLocked?: boolean; noUpdate?: boolean; noUpdateExpression?: string; noUpdateRoles?: ReadonlyArray<string> };
export type WorkflowStep = { name: string } & WorkflowStepValues;
export type WorkflowTransitionValues = { expression?: string; roles?: string[]; };
@ -305,13 +305,13 @@ function parseWorkflow(workflow: any) {
for (let stepName in workflow.steps) {
if (workflow.steps.hasOwnProperty(stepName)) {
const step = workflow.steps[stepName];
const { transitions: srcTransitions, ...step } = workflow.steps[stepName];
steps.push({ name: stepName, color: step.color, noUpdate: step.noUpdate, isLocked: stepName === 'Published' });
steps.push({ name: stepName, isLocked: stepName === 'Published', ...step });
for (let to in step.transitions) {
if (step.transitions.hasOwnProperty(to)) {
const transition = step.transitions[to];
for (let to in srcTransitions) {
if (srcTransitions.hasOwnProperty(to)) {
const transition = srcTransitions[to];
transitions.push({ from: stepName, to, ...transition });
}

Loading…
Cancel
Save