Browse Source

Define an option per workflow when a content should be published. (#877)

* Define an option per workflow when a content should be published.

* Fix tests
pull/878/head
Sebastian Stehle 4 years ago
committed by GitHub
parent
commit
16fdfdbf82
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      backend/i18n/frontend_en.json
  2. 2
      backend/i18n/frontend_it.json
  3. 2
      backend/i18n/frontend_nl.json
  4. 2
      backend/i18n/frontend_zh.json
  5. 2
      backend/i18n/source/frontend_en.json
  6. 7
      backend/src/Migrations/OldEvents/AppPatternAdded.cs
  7. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs
  8. 24
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs
  9. 3
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs
  10. 1
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Editor.cs
  11. 18
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs
  12. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Pattern.cs
  13. 33
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs
  14. 2
      backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs
  15. 3
      backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs
  16. 7
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs
  17. 3
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs
  18. 33
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs
  19. 22
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs
  20. 23
      backend/src/Squidex.Domain.Apps.Core.Model/Named.cs
  21. 39
      backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs
  22. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs
  23. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs
  24. 30
      backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs
  25. 18
      backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs
  26. 1
      backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs
  27. 91
      backend/src/Squidex.Infrastructure/Guard.cs
  28. 8
      backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs
  29. 6
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs
  30. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs
  31. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs
  32. 38
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs
  33. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs
  34. 46
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs
  35. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs
  36. 4
      frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html
  37. 20
      frontend/src/app/features/settings/pages/workflows/workflow-step.component.html
  38. 4
      frontend/src/app/features/settings/pages/workflows/workflow-step.component.scss
  39. 4
      frontend/src/app/features/settings/pages/workflows/workflow-step.component.ts
  40. 2
      frontend/src/app/shared/services/workflows.service.ts

2
backend/i18n/frontend_en.json

@ -947,6 +947,7 @@
"schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.", "schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.",
"schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.", "schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.",
"schemas.validateOnPublish": "Validate when publishing", "schemas.validateOnPublish": "Validate when publishing",
"schemas.validateOnPublishHint": "Use custom workflows when you need more control for which statuses a content should be validated.",
"search.addFilter": "Add Filter", "search.addFilter": "Add Filter",
"search.addGroup": "Add Group", "search.addGroup": "Add Group",
"search.addSorting": "Add Sorting", "search.addSorting": "Add Sorting",
@ -1065,6 +1066,7 @@
"workflows.tabEdit": "Editing", "workflows.tabEdit": "Editing",
"workflows.tabVisualize": "Visualize", "workflows.tabVisualize": "Visualize",
"workflows.updateFailed": "Failed to update Workflow. Please reload.", "workflows.updateFailed": "Failed to update Workflow. Please reload.",
"workflows.validate": "Validate content when changed to this status.",
"workflows.workflowNameHint": "Optional name for the workflow.", "workflows.workflowNameHint": "Optional name for the workflow.",
"workflows.workflowNamePlaceholder": "Enter workflow name" "workflows.workflowNamePlaceholder": "Enter workflow name"
} }

2
backend/i18n/frontend_it.json

@ -947,6 +947,7 @@
"schemas.updateScriptsFailed": "Non è stato possibile aggiornare gli script dello schema. Per favore ricarica.", "schemas.updateScriptsFailed": "Non è stato possibile aggiornare gli script dello schema. Per favore ricarica.",
"schemas.updateUIFieldsFailed": "Non è stato possibile aggiornare i campi della UI. Per favore ricarica.", "schemas.updateUIFieldsFailed": "Non è stato possibile aggiornare i campi della UI. Per favore ricarica.",
"schemas.validateOnPublish": "Valida quando pubblichi", "schemas.validateOnPublish": "Valida quando pubblichi",
"schemas.validateOnPublishHint": "Use custom workflows when you need more control for which statuses a content should be validated.",
"search.addFilter": "Aggiungi un Filtro", "search.addFilter": "Aggiungi un Filtro",
"search.addGroup": "Aggiungi un Gruppo", "search.addGroup": "Aggiungi un Gruppo",
"search.addSorting": "Aggiungi ordinamento", "search.addSorting": "Aggiungi ordinamento",
@ -1065,6 +1066,7 @@
"workflows.tabEdit": "Modifica", "workflows.tabEdit": "Modifica",
"workflows.tabVisualize": "Visualizza", "workflows.tabVisualize": "Visualizza",
"workflows.updateFailed": "Non è stato possibile aggiornare il Workflow. Per favore ricarica.", "workflows.updateFailed": "Non è stato possibile aggiornare il Workflow. Per favore ricarica.",
"workflows.validate": "Validate content when changed to this status.",
"workflows.workflowNameHint": "Nome facoltativo per il workflow.", "workflows.workflowNameHint": "Nome facoltativo per il workflow.",
"workflows.workflowNamePlaceholder": "Inserisci il nome del workflow" "workflows.workflowNamePlaceholder": "Inserisci il nome del workflow"
} }

2
backend/i18n/frontend_nl.json

@ -947,6 +947,7 @@
"schemas.updateScriptsFailed": "Updaten van schemascripts is mislukt. Laad opnieuw.", "schemas.updateScriptsFailed": "Updaten van schemascripts is mislukt. Laad opnieuw.",
"schemas.updateUIFieldsFailed": "Bijwerken van UI-velden is mislukt. Laad opnieuw.", "schemas.updateUIFieldsFailed": "Bijwerken van UI-velden is mislukt. Laad opnieuw.",
"schemas.validateOnPublish": "Valideren bij het publiceren", "schemas.validateOnPublish": "Valideren bij het publiceren",
"schemas.validateOnPublishHint": "Use custom workflows when you need more control for which statuses a content should be validated.",
"search.addFilter": "Filter toevoegen", "search.addFilter": "Filter toevoegen",
"search.addGroup": "Groep toevoegen", "search.addGroup": "Groep toevoegen",
"search.addSorting": "Sortering toevoegen", "search.addSorting": "Sortering toevoegen",
@ -1065,6 +1066,7 @@
"workflows.tabEdit": "Bewerken", "workflows.tabEdit": "Bewerken",
"workflows.tabVisualize": "Visualiseren", "workflows.tabVisualize": "Visualiseren",
"workflows.updateFailed": "Update van workflow is mislukt. Laad opnieuw.", "workflows.updateFailed": "Update van workflow is mislukt. Laad opnieuw.",
"workflows.validate": "Validate content when changed to this status.",
"workflows.workflowNameHint": "Optionele naam voor de workflow.", "workflows.workflowNameHint": "Optionele naam voor de workflow.",
"workflows.workflowNamePlaceholder": "Voer de werkstroomnaam in" "workflows.workflowNamePlaceholder": "Voer de werkstroomnaam in"
} }

2
backend/i18n/frontend_zh.json

@ -947,6 +947,7 @@
"schemas.updateScriptsFailed": "更新Schemas脚本失败。请重新加载。", "schemas.updateScriptsFailed": "更新Schemas脚本失败。请重新加载。",
"schemas.updateUIFieldsFailed": "无法更新 UI 字段。请重新加载。", "schemas.updateUIFieldsFailed": "无法更新 UI 字段。请重新加载。",
"schemas.validateOnPublish": "发布时验证", "schemas.validateOnPublish": "发布时验证",
"schemas.validateOnPublishHint": "Use custom workflows when you need more control for which statuses a content should be validated.",
"search.addFilter": "添加过滤器", "search.addFilter": "添加过滤器",
"search.addGroup": "添加组", "search.addGroup": "添加组",
"search.addSorting": "添加排序", "search.addSorting": "添加排序",
@ -1065,6 +1066,7 @@
"workflows.tabEdit": "编辑", "workflows.tabEdit": "编辑",
"workflows.tabVisualize": "可视化", "workflows.tabVisualize": "可视化",
"workflows.updateFailed": "无法更新工作流。请重新加载。", "workflows.updateFailed": "无法更新工作流。请重新加载。",
"workflows.validate": "Validate content when changed to this status.",
"workflows.workflowNameHint": "工作流的可选名称。", "workflows.workflowNameHint": "工作流的可选名称。",
"workflows.workflowNamePlaceholder": "输入工作流名称" "workflows.workflowNamePlaceholder": "输入工作流名称"
} }

2
backend/i18n/source/frontend_en.json

@ -947,6 +947,7 @@
"schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.", "schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.",
"schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.", "schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.",
"schemas.validateOnPublish": "Validate when publishing", "schemas.validateOnPublish": "Validate when publishing",
"schemas.validateOnPublishHint": "Use custom workflows when you need more control for which statuses a content should be validated.",
"search.addFilter": "Add Filter", "search.addFilter": "Add Filter",
"search.addGroup": "Add Group", "search.addGroup": "Add Group",
"search.addSorting": "Add Sorting", "search.addSorting": "Add Sorting",
@ -1065,6 +1066,7 @@
"workflows.tabEdit": "Editing", "workflows.tabEdit": "Editing",
"workflows.tabVisualize": "Visualize", "workflows.tabVisualize": "Visualize",
"workflows.updateFailed": "Failed to update Workflow. Please reload.", "workflows.updateFailed": "Failed to update Workflow. Please reload.",
"workflows.validate": "Validate content when changed to this status.",
"workflows.workflowNameHint": "Optional name for the workflow.", "workflows.workflowNameHint": "Optional name for the workflow.",
"workflows.workflowNamePlaceholder": "Enter workflow name" "workflows.workflowNamePlaceholder": "Enter workflow name"
} }

7
backend/src/Migrations/OldEvents/AppPatternAdded.cs

@ -31,16 +31,15 @@ namespace Migrations.OldEvents
public IEvent Migrate(AppDomainObject.State state) public IEvent Migrate(AppDomainObject.State state)
{ {
var newSettings = new AppSettings var newSettings = state.Settings with
{ {
Patterns = ReadonlyList.ToReadonlyList(new List<Pattern>(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern)) Patterns = new List<Pattern>(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern))
{ {
new Pattern(Name, Pattern) new Pattern(Name, Pattern)
{ {
Message = Message Message = Message
} }
}), }.ToReadonlyList()
Editors = state.Settings.Editors
}; };
var newEvent = new AppSettingsUpdated var newEvent = new AppSettingsUpdated

6
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs

@ -7,10 +7,16 @@
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable SA1313 // Parameter names should begin with lower-case letter
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
public sealed record AppClient(string Name, string Secret) public sealed record AppClient(string Name, string Secret)
{ {
public string Name { get; init; } = Guard.NotNullOrEmpty(Name);
public string Secret { get; } = Guard.NotNullOrEmpty(Secret);
public string Role { get; init; } = "Editor"; public string Role { get; init; } = "Editor";
public long ApiCallsLimit { get; init; } public long ApiCallsLimit { get; init; }

24
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs

@ -7,28 +7,14 @@
using Squidex.Infrastructure; using Squidex.Infrastructure;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
public sealed record AppImage public sealed record AppImage(string MimeType, string? Etag = null)
{ {
public string MimeType { get; } public string MimeType { get; } = Guard.NotNullOrEmpty(MimeType);
public string Etag { get; }
public AppImage(string mimeType, string? etag = null)
{
Guard.NotNullOrEmpty(mimeType);
MimeType = mimeType;
if (string.IsNullOrWhiteSpace(etag)) public string Etag { get; } = Etag ?? RandomHash.Simple();
{
Etag = RandomHash.Simple();
}
else
{
Etag = etag;
}
}
} }
} }

3
backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs

@ -13,5 +13,8 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
public sealed record AppPlan(RefToken Owner, string PlanId) public sealed record AppPlan(RefToken Owner, string PlanId)
{ {
public RefToken Owner { get; } = Guard.NotNull(Owner);
public string PlanId { get; } = Guard.NotNullOrEmpty(PlanId);
} }
} }

1
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Editor.cs

@ -7,6 +7,7 @@
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
public sealed record Editor(string Name, string Url) public sealed record Editor(string Name, string Url)

18
backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs

@ -8,25 +8,15 @@
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
public sealed record LanguageConfig public sealed record LanguageConfig(bool IsOptional = false, ReadonlyList<Language>? Fallbacks = null)
{ {
public static readonly LanguageConfig Default = new LanguageConfig(); public static readonly LanguageConfig Default = new LanguageConfig();
public bool IsOptional { get; } public ReadonlyList<Language> Fallbacks { get; } = Fallbacks ?? ReadonlyList.Empty<Language>();
public ReadonlyList<Language> Fallbacks { get; } = ReadonlyList.Empty<Language>();
public LanguageConfig(bool isOptional = false, ReadonlyList<Language>? fallbacks = null)
{
IsOptional = isOptional;
if (fallbacks != null)
{
Fallbacks = fallbacks;
}
}
internal LanguageConfig Cleanup(string self, IReadOnlyDictionary<string, LanguageConfig> allowed) internal LanguageConfig Cleanup(string self, IReadOnlyDictionary<string, LanguageConfig> allowed)
{ {

2
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Pattern.cs

@ -13,4 +13,4 @@ namespace Squidex.Domain.Apps.Core.Apps
{ {
public string? Message { get; init; } public string? Message { get; init; }
} }
} }

33
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs

@ -10,9 +10,11 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Core.Apps namespace Squidex.Domain.Apps.Core.Apps
{ {
public sealed record Role : Named public sealed record Role(string Name, PermissionSet? Permissions = null, JsonObject? Properties = null)
{ {
private static readonly HashSet<string> ExtraPermissions = new HashSet<string> private static readonly HashSet<string> ExtraPermissions = new HashSet<string>
{ {
@ -33,38 +35,25 @@ namespace Squidex.Domain.Apps.Core.Apps
public const string Owner = "Owner"; public const string Owner = "Owner";
public const string Reader = "Reader"; public const string Reader = "Reader";
public PermissionSet Permissions { get; } public string Name { get; } = Guard.NotNullOrEmpty(Name);
public PermissionSet Permissions { get; } = Permissions ?? PermissionSet.Empty;
public JsonObject Properties { get; } public JsonObject Properties { get; } = Properties ?? new JsonObject();
public bool IsDefault public bool IsDefault
{ {
get => Roles.IsDefault(this); get => Roles.IsDefault(this);
} }
public Role(string name, PermissionSet permissions, JsonObject properties) public static Role WithPermissions(string name, params string[] permissions)
: base(name)
{
Guard.NotNull(permissions);
Guard.NotNull(properties);
Permissions = permissions;
Properties = properties;
}
public static Role WithPermissions(string role, params string[] permissions)
{
return new Role(role, new PermissionSet(permissions), JsonValue.Object());
}
public static Role WithProperties(string role, JsonObject properties)
{ {
return new Role(role, PermissionSet.Empty, properties); return new Role(name, new PermissionSet(permissions), JsonValue.Object());
} }
public static Role Create(string role) public static Role WithProperties(string name, JsonObject properties)
{ {
return new Role(role, PermissionSet.Empty, JsonValue.Object()); return new Role(name, PermissionSet.Empty, properties);
} }
[Pure] [Pure]

2
backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs

@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Core.Apps
return this; return this;
} }
var newRole = Role.Create(name); var newRole = new Role(name);
if (!inner.TryAdd(name, newRole, out var updated)) if (!inner.TryAdd(name, newRole, out var updated))
{ {

3
backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs

@ -14,5 +14,8 @@ namespace Squidex.Domain.Apps.Core.Comments
{ {
public sealed record Comment(DomainId Id, Instant Time, RefToken User, string Text, Uri? Url = null) public sealed record Comment(DomainId Id, Instant Time, RefToken User, string Text, Uri? Url = null)
{ {
public RefToken User { get; } = Guard.NotNull(User);
public string Text { get; } = Guard.NotNullOrEmpty(Text);
} }
} }

7
backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs

@ -7,6 +7,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable SA1313 // Parameter names should begin with lower-case letter
@ -17,6 +18,12 @@ namespace Squidex.Domain.Apps.Core.Contents
{ {
public const string Discriminator = "schemaId"; public const string Discriminator = "schemaId";
public string Type { get; } = Guard.NotNullOrEmpty(Type);
public JsonObject Data { get; } = Guard.NotNull(Data);
public Schema Schema { get; } = Guard.NotNull(Schema);
public static bool IsValid(IJsonValue? value, [MaybeNullWhen(false)] out string discriminator) public static bool IsValid(IJsonValue? value, [MaybeNullWhen(false)] out string discriminator)
{ {
discriminator = null!; discriminator = null!;

3
backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs

@ -7,9 +7,12 @@
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter #pragma warning disable SA1313 // Parameter names should begin with lower-case letter
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
public sealed record StatusInfo(Status Status, string Color) public sealed record StatusInfo(Status Status, string Color)
{ {
public string Color { get; } = Guard.NotNullOrEmpty(Color);
} }
} }

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

@ -9,43 +9,22 @@ using System.Diagnostics.CodeAnalysis;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
public sealed record Workflow public sealed record Workflow(Status Initial, ReadonlyDictionary<Status, WorkflowStep>? Steps = null, ReadonlyList<DomainId>? SchemaIds = null, string? Name = null)
{ {
private const string DefaultName = "Unnamed"; private const string DefaultName = "Unnamed";
public static readonly Workflow Default = CreateDefault(); public static readonly Workflow Default = CreateDefault();
public static readonly Workflow Empty = new Workflow(default, null); public static readonly Workflow Empty = new Workflow(default, null);
public Status Initial { get; } public string Name { get; } = Name.Or(DefaultName);
public ReadonlyDictionary<Status, WorkflowStep> Steps { get; } = ReadonlyDictionary.Empty<Status, WorkflowStep>();
public ReadonlyList<DomainId> SchemaIds { get; } = ReadonlyList.Empty<DomainId>();
public string Name { get; }
public Workflow(
Status initial,
ReadonlyDictionary<Status, WorkflowStep>? steps = null,
ReadonlyList<DomainId>? schemaIds = null,
string? name = null)
{
Initial = initial;
if (steps != null)
{
Steps = steps;
}
if (schemaIds != null) public ReadonlyDictionary<Status, WorkflowStep> Steps { get; } = Steps ?? ReadonlyDictionary.Empty<Status, WorkflowStep>();
{
SchemaIds = schemaIds;
}
Name = name.Or(DefaultName); public ReadonlyList<DomainId> SchemaIds { get; } = SchemaIds ?? ReadonlyList.Empty<DomainId>();
}
public static Workflow CreateDefault(string? name = null) public static Workflow CreateDefault(string? name = null)
{ {

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

@ -7,26 +7,12 @@
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Core.Contents namespace Squidex.Domain.Apps.Core.Contents
{ {
public sealed record WorkflowStep public sealed record WorkflowStep(ReadonlyDictionary<Status, WorkflowTransition>? Transitions = null, string? Color = null, NoUpdate? NoUpdate = null, bool Validate = false)
{ {
public ReadonlyDictionary<Status, WorkflowTransition> Transitions { get; } = ReadonlyDictionary.Empty<Status, WorkflowTransition>(); public ReadonlyDictionary<Status, WorkflowTransition> Transitions { get; } = Transitions ?? ReadonlyDictionary.Empty<Status, WorkflowTransition>();
public string? Color { get; }
public NoUpdate? NoUpdate { get; }
public WorkflowStep(ReadonlyDictionary<Status, WorkflowTransition>? transitions = null, string? color = null, NoUpdate? noUpdate = null)
{
Color = color;
if (transitions != null)
{
Transitions = transitions;
}
NoUpdate = noUpdate;
}
} }
} }

23
backend/src/Squidex.Domain.Apps.Core.Model/Named.cs

@ -1,23 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core
{
public abstract record Named
{
public string Name { get; }
protected Named(string name)
{
Guard.NotNullOrEmpty(name);
Name = name;
}
}
}

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

@ -43,54 +43,61 @@ namespace Squidex.Domain.Apps.Entities.Contents
}) })
}; };
public Task<Status> GetInitialStatusAsync(ISchemaEntity schema) public ValueTask<Status> GetInitialStatusAsync(ISchemaEntity schema)
{ {
return Task.FromResult(Status.Draft); return ValueTask.FromResult(Status.Draft);
} }
public Task<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user) public ValueTask<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user)
{ {
return Task.FromResult(true); return ValueTask.FromResult(true);
} }
public Task<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) public ValueTask<bool> ShouldValidateAsync(ISchemaEntity schema, Status status)
{
var result = status == Status.Published && schema.SchemaDef.Properties.ValidateOnPublish;
return ValueTask.FromResult(result);
}
public ValueTask<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user)
{ {
var result = Flow.TryGetValue(status, out var step) && step.Transitions.Any(x => x.Status == next); var result = Flow.TryGetValue(status, out var step) && step.Transitions.Any(x => x.Status == next);
return Task.FromResult(result); return ValueTask.FromResult(result);
} }
public Task<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) public ValueTask<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user)
{ {
var result = Flow.TryGetValue(status, out var step) && step.Transitions.Any(x => x.Status == next); var result = Flow.TryGetValue(status, out var step) && step.Transitions.Any(x => x.Status == next);
return Task.FromResult(result); return ValueTask.FromResult(result);
} }
public Task<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user) public ValueTask<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user)
{ {
var result = status != Status.Archived; var result = status != Status.Archived;
return Task.FromResult(result); return ValueTask.FromResult(result);
} }
public Task<StatusInfo?> GetInfoAsync(IContentEntity content, Status status) public ValueTask<StatusInfo?> GetInfoAsync(IContentEntity content, Status status)
{ {
var result = Flow.GetValueOrDefault(status).Info; var result = Flow.GetValueOrDefault(status).Info;
return Task.FromResult<StatusInfo?>(result); return ValueTask.FromResult<StatusInfo?>(result);
} }
public Task<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user) public ValueTask<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user)
{ {
var result = Flow.TryGetValue(status, out var step) ? step.Transitions : Array.Empty<StatusInfo>(); var result = Flow.TryGetValue(status, out var step) ? step.Transitions : Array.Empty<StatusInfo>();
return Task.FromResult(result); return ValueTask.FromResult(result);
} }
public Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema) public ValueTask<StatusInfo[]> GetAllAsync(ISchemaEntity schema)
{ {
return Task.FromResult(All); return ValueTask.FromResult(All);
} }
} }
} }

2
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs

@ -297,7 +297,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject
await operation.CheckReferrersAsync(); await operation.CheckReferrersAsync();
} }
if (!c.DoNotValidate && c.Status == Status.Published && operation.SchemaDef.Properties.ValidateOnPublish) if (!c.DoNotValidate && await operation.ShouldValidateAsync(c.Status))
{ {
await operation.ValidateContentAndInputAsync(Snapshot.Data, c.OptimizeValidation, true); await operation.ValidateContentAndInputAsync(Snapshot.Data, c.OptimizeValidation, true);
} }

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

@ -14,13 +14,20 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
public static class WorkflowExtensions public static class WorkflowExtensions
{ {
public static Task<Status> GetInitialStatusAsync(this ContentOperation operation) public static ValueTask<Status> GetInitialStatusAsync(this ContentOperation operation)
{ {
var workflow = GetWorkflow(operation); var workflow = GetWorkflow(operation);
return workflow.GetInitialStatusAsync(operation.Schema); return workflow.GetInitialStatusAsync(operation.Schema);
} }
public static ValueTask<bool> ShouldValidateAsync(this ContentOperation operation, Status status)
{
var workflow = GetWorkflow(operation);
return workflow.ShouldValidateAsync(operation.Schema, status);
}
public static async Task CheckTransitionAsync(this ContentOperation operation, Status status) public static async Task CheckTransitionAsync(this ContentOperation operation, Status status)
{ {
if (operation.SchemaDef.Type != SchemaType.Singleton) if (operation.SchemaDef.Type != SchemaType.Singleton)

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

@ -25,35 +25,35 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.appProvider = appProvider; this.appProvider = appProvider;
} }
public async Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema) public async ValueTask<StatusInfo[]> GetAllAsync(ISchemaEntity schema)
{ {
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray();
} }
public async Task<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user) public async ValueTask<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user)
{ {
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && IsTrue(transition, null, user); return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && IsTrue(transition, null, user);
} }
public async Task<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) public async ValueTask<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user)
{ {
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, data, user); return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, data, user);
} }
public async Task<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) public async ValueTask<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user)
{ {
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id);
return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, content.Data, user); return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, content.Data, user);
} }
public async Task<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user) public async ValueTask<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user)
{ {
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id);
@ -65,7 +65,19 @@ namespace Squidex.Domain.Apps.Entities.Contents
return true; return true;
} }
public async Task<StatusInfo?> GetInfoAsync(IContentEntity content, Status status) public async ValueTask<bool> ShouldValidateAsync(ISchemaEntity schema, Status status)
{
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
if (workflow.TryGetStep(status, out var step) && step.Validate)
{
return true;
}
return status == Status.Published && schema.SchemaDef.Properties.ValidateOnPublish;
}
public async ValueTask<StatusInfo?> GetInfoAsync(IContentEntity content, Status status)
{ {
var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id);
@ -77,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return null; return null;
} }
public async Task<Status> GetInitialStatusAsync(ISchemaEntity schema) public async ValueTask<Status> GetInitialStatusAsync(ISchemaEntity schema)
{ {
var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id);
@ -86,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return status; return status;
} }
public async Task<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user) public async ValueTask<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user)
{ {
var result = new List<StatusInfo>(); var result = new List<StatusInfo>();
@ -126,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
return true; return true;
} }
private async Task<Workflow> GetWorkflowAsync(DomainId appId, DomainId schemaId) private async ValueTask<Workflow> GetWorkflowAsync(DomainId appId, DomainId schemaId)
{ {
Workflow? result = null; Workflow? result = null;

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

@ -13,20 +13,22 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
public interface IContentWorkflow public interface IContentWorkflow
{ {
Task<Status> GetInitialStatusAsync(ISchemaEntity schema); ValueTask<Status> GetInitialStatusAsync(ISchemaEntity schema);
Task<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user); ValueTask<bool> CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user);
Task<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user); ValueTask<bool> CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user);
Task<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user); ValueTask<bool> CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user);
Task<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user); ValueTask<bool> CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user);
Task<StatusInfo?> GetInfoAsync(IContentEntity content, Status status); ValueTask<bool> ShouldValidateAsync(ISchemaEntity schema, Status status);
Task<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user); ValueTask<StatusInfo?> GetInfoAsync(IContentEntity content, Status status);
Task<StatusInfo[]> GetAllAsync(ISchemaEntity schema); ValueTask<StatusInfo[]> GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user);
ValueTask<StatusInfo[]> GetAllAsync(ISchemaEntity schema);
} }
} }

1
backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Rules.Runner namespace Squidex.Domain.Apps.Entities.Rules.Runner
{ {

91
backend/src/Squidex.Infrastructure/Guard.cs

@ -16,29 +16,33 @@ namespace Squidex.Infrastructure
{ {
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidNumber(float target, public static float ValidNumber(float target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
if (float.IsNaN(target) || float.IsPositiveInfinity(target) || float.IsNegativeInfinity(target)) if (float.IsNaN(target) || float.IsPositiveInfinity(target) || float.IsNegativeInfinity(target))
{ {
throw new ArgumentException("Value must be a valid number.", parameterName); throw new ArgumentException("Value must be a valid number.", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidNumber(double target, public static double ValidNumber(double target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
if (double.IsNaN(target) || double.IsPositiveInfinity(target) || double.IsNegativeInfinity(target)) if (double.IsNaN(target) || double.IsPositiveInfinity(target) || double.IsNegativeInfinity(target))
{ {
throw new ArgumentException("Value must be a valid number.", parameterName); throw new ArgumentException("Value must be a valid number.", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidSlug(string? target, public static string ValidSlug(string? target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
NotNullOrEmpty(target, parameterName); NotNullOrEmpty(target, parameterName);
@ -47,11 +51,13 @@ namespace Squidex.Infrastructure
{ {
throw new ArgumentException("Target is not a valid slug.", parameterName); throw new ArgumentException("Target is not a valid slug.", parameterName);
} }
return target!;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidPropertyName(string? target, public static string ValidPropertyName(string? target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
NotNullOrEmpty(target, parameterName); NotNullOrEmpty(target, parameterName);
@ -60,99 +66,117 @@ namespace Squidex.Infrastructure
{ {
throw new ArgumentException("Target is not a valid property name.", parameterName); throw new ArgumentException("Target is not a valid property name.", parameterName);
} }
return target!;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void HasType<T>(object? target, public static object? HasType<T>(object? target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
if (target != null && target.GetType() != typeof(T)) if (target != null && target.GetType() != typeof(T))
{ {
throw new ArgumentException($"The parameter must be of type {typeof(T)}", parameterName); throw new ArgumentException($"The parameter must be of type {typeof(T)}", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void HasType(object? target, Type? expectedType, public static object? HasType(object? target, Type? expectedType,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
if (target != null && expectedType != null && target.GetType() != expectedType) if (target != null && expectedType != null && target.GetType() != expectedType)
{ {
throw new ArgumentException($"The parameter must be of type {expectedType}", parameterName); throw new ArgumentException($"The parameter must be of type {expectedType}", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Between<TValue>(TValue target, TValue lower, TValue upper, public static TValue Between<TValue>(TValue target, TValue lower, TValue upper,
[CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable
{ {
if (!target.IsBetween(lower, upper)) if (!target.IsBetween(lower, upper))
{ {
throw new ArgumentException($"Value must be between {lower} and {upper}", parameterName); throw new ArgumentException($"Value must be between {lower} and {upper}", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Enum<TEnum>(TEnum target, public static TEnum Enum<TEnum>(TEnum target,
[CallerArgumentExpression("target")] string? parameterName = null) where TEnum : struct [CallerArgumentExpression("target")] string? parameterName = null) where TEnum : struct
{ {
if (!target.IsEnumValue()) if (!target.IsEnumValue())
{ {
throw new ArgumentException($"Value must be a valid enum type {typeof(TEnum)}", parameterName); throw new ArgumentException($"Value must be a valid enum type {typeof(TEnum)}", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void GreaterThan<TValue>(TValue target, TValue lower, public static TValue GreaterThan<TValue>(TValue target, TValue lower,
[CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable
{ {
if (target.CompareTo(lower) <= 0) if (target.CompareTo(lower) <= 0)
{ {
throw new ArgumentException($"Value must be greater than {lower}", parameterName); throw new ArgumentException($"Value must be greater than {lower}", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void GreaterEquals<TValue>(TValue target, TValue lower, public static TValue GreaterEquals<TValue>(TValue target, TValue lower,
[CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable
{ {
if (target.CompareTo(lower) < 0) if (target.CompareTo(lower) < 0)
{ {
throw new ArgumentException($"Value must be greater or equal to {lower}", parameterName); throw new ArgumentException($"Value must be greater or equal to {lower}", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LessThan<TValue>(TValue target, TValue upper, public static TValue LessThan<TValue>(TValue target, TValue upper,
[CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable
{ {
if (target.CompareTo(upper) >= 0) if (target.CompareTo(upper) >= 0)
{ {
throw new ArgumentException($"Value must be less than {upper}", parameterName); throw new ArgumentException($"Value must be less than {upper}", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void LessEquals<TValue>(TValue target, TValue upper, public static TValue LessEquals<TValue>(TValue target, TValue upper,
[CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable
{ {
if (target.CompareTo(upper) > 0) if (target.CompareTo(upper) > 0)
{ {
throw new ArgumentException($"Value must be less or equal to {upper}", parameterName); throw new ArgumentException($"Value must be less or equal to {upper}", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotEmpty<TType>(IReadOnlyCollection<TType>? target, public static IReadOnlyCollection<TType> NotEmpty<TType>(IReadOnlyCollection<TType>? target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
NotNull(target, parameterName); NotNull(target, parameterName);
@ -161,55 +185,78 @@ namespace Squidex.Infrastructure
{ {
throw new ArgumentException("Collection does not contain an item.", parameterName); throw new ArgumentException("Collection does not contain an item.", parameterName);
} }
return target!;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotEmpty(Guid target, public static Guid NotEmpty(Guid target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
if (target == Guid.Empty) if (target == Guid.Empty)
{ {
throw new ArgumentException("Value cannot be empty.", parameterName); throw new ArgumentException("Value cannot be empty.", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotEmpty(DomainId target, public static DomainId NotEmpty(DomainId target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
if (target == DomainId.Empty) if (target == DomainId.Empty)
{ {
throw new ArgumentException("Value cannot be empty.", parameterName); throw new ArgumentException("Value cannot be empty.", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotNull(object? target, public static TValue NotNull<TValue>(TValue? target,
[CallerArgumentExpression("target")] string? parameterName = null) where TValue : class
{
if (target == null)
{
throw new ArgumentNullException(parameterName);
}
return target;
}
[DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static object? NotNull(object? target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
if (target == null) if (target == null)
{ {
throw new ArgumentNullException(parameterName); throw new ArgumentNullException(parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotDefault<T>(T target, public static TValue NotDefault<TValue>(TValue target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
if (Equals(target, default(T)!)) if (Equals(target, default(TValue)!))
{ {
throw new ArgumentException("Value cannot be an the default value.", parameterName); throw new ArgumentException("Value cannot be an the default value.", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void NotNullOrEmpty(string? target, public static string NotNullOrEmpty(string? target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
NotNull(target, parameterName); NotNull(target, parameterName);
@ -218,11 +265,13 @@ namespace Squidex.Infrastructure
{ {
throw new ArgumentException("String parameter cannot be null or empty and cannot contain only blanks.", parameterName); throw new ArgumentException("String parameter cannot be null or empty and cannot contain only blanks.", parameterName);
} }
return target;
} }
[DebuggerStepThrough] [DebuggerStepThrough]
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void ValidFileName(string? target, public static string ValidFileName(string? target,
[CallerArgumentExpression("target")] string? parameterName = null) [CallerArgumentExpression("target")] string? parameterName = null)
{ {
NotNullOrEmpty(target, parameterName); NotNullOrEmpty(target, parameterName);
@ -231,6 +280,8 @@ namespace Squidex.Infrastructure
{ {
throw new ArgumentException("Value contains an invalid character.", parameterName); throw new ArgumentException("Value contains an invalid character.", parameterName);
} }
return target!;
} }
} }
} }

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

@ -26,6 +26,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
/// </summary> /// </summary>
public string? Color { get; set; } public string? Color { get; set; }
/// <summary>
/// True if the content should be validated when moving to this step.
/// </summary>
public bool Validate { get; set; }
/// <summary> /// <summary>
/// Indicates if updates should not be allowed. /// Indicates if updates should not be allowed.
/// </summary> /// </summary>
@ -69,7 +74,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models
Color, Color,
NoUpdate ? NoUpdate ?
NoUpdateType.When(NoUpdateExpression, NoUpdateRoles) : NoUpdateType.When(NoUpdateExpression, NoUpdateRoles) :
null); null,
Validate);
} }
} }
} }

6
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs

@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact] [Fact]
public void Should_be_default_role() public void Should_be_default_role()
{ {
var role = Role.Create("Owner"); var role = new Role("Owner");
Assert.True(role.IsDefault); Assert.True(role.IsDefault);
} }
@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact] [Fact]
public void Should_not_be_default_role() public void Should_not_be_default_role()
{ {
var role = Role.Create("Custom"); var role = new Role("Custom");
Assert.False(role.IsDefault); Assert.False(role.IsDefault);
} }
@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
[Fact] [Fact]
public void Should_not_add_common_permission() public void Should_not_add_common_permission()
{ {
var role = Role.Create("Name"); var role = new Role("Name");
var result = role.ForApp("my-app").Permissions.ToIds(); var result = role.ForApp("my-app").Permissions.ToIds();

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs

@ -39,7 +39,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps
{ {
var roles_1 = roles_0.Add(role); var roles_1 = roles_0.Add(role);
Assert.Equal(Role.Create(role), roles_1[role]); Assert.Equal(new Role(role), roles_1[role]);
} }
[Fact] [Fact]

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

@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Model.Contents
[Fact] [Fact]
public void Should_serialize_and_deserialize_no_update_condition() public void Should_serialize_and_deserialize_no_update_condition()
{ {
var step = new WorkflowStep(noUpdate: NoUpdate.When("Expression", "Role1", "Role2")); var step = new WorkflowStep(NoUpdate: NoUpdate.When("Expression", "Role1", "Role2"));
var serialized = step.SerializeAndDeserialize(); var serialized = step.SerializeAndDeserialize();

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

@ -7,6 +7,10 @@
using FluentAssertions; using FluentAssertions;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
@ -158,5 +162,39 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.Should().BeEquivalentTo(expected); result.Should().BeEquivalentTo(expected);
} }
[Fact]
public async Task Should_not_validate_when_not_publishing()
{
var result = await sut.ShouldValidateAsync(null!, Status.Draft);
Assert.False(result);
}
[Fact]
public async Task Should_not_validate_when_publishing_but_not_enabled()
{
var result = await sut.ShouldValidateAsync(CreateSchema(false), Status.Published);
Assert.False(result);
}
[Fact]
public async Task Should_validate_when_publishing_and_enabled()
{
var result = await sut.ShouldValidateAsync(CreateSchema(true), Status.Published);
Assert.True(result);
}
private static ISchemaEntity CreateSchema(bool validateOnPublish)
{
var schema = new Schema("my-schema", new SchemaProperties
{
ValidateOnPublish = validateOnPublish
});
return Mocks.Schema(NamedId.Of(DomainId.NewGuid(), "my-app"), NamedId.Of(DomainId.NewGuid(), schema.Name), schema);
}
} }
} }

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs

@ -20,6 +20,8 @@ using Squidex.Infrastructure.Validation;
using Squidex.Shared; using Squidex.Shared;
using Xunit; using Xunit;
#pragma warning disable CA2012 // Use ValueTasks correctly
namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
{ {
public class GuardContentTests : IClassFixture<TranslationsFixture> public class GuardContentTests : IClassFixture<TranslationsFixture>
@ -240,7 +242,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards
var operation = Operation(CreateContent(Status.Draft), normalSchema); var operation = Operation(CreateContent(Status.Draft), normalSchema);
A.CallTo(() => contentWorkflow.GetInfoAsync((ContentEntity)operation.Snapshot, Status.Archived)) A.CallTo(() => contentWorkflow.GetInfoAsync((ContentEntity)operation.Snapshot, Status.Archived))
.Returns(Task.FromResult<StatusInfo?>(null)); .Returns(ValueTask.FromResult<StatusInfo?>(null));
await Assert.ThrowsAsync<ValidationException>(() => operation.CheckStatusAsync(Status.Archived)); await Assert.ThrowsAsync<ValidationException>(() => operation.CheckStatusAsync(Status.Archived));
} }

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

@ -10,8 +10,10 @@ using FluentAssertions;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections; using Squidex.Infrastructure.Collections;
@ -38,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
[Status.Draft] = WorkflowTransition.Always [Status.Draft] = WorkflowTransition.Always
}.ToReadonlyDictionary(), }.ToReadonlyDictionary(),
StatusColors.Archived, NoUpdate.Always), StatusColors.Archived, NoUpdate.Always, Validate: true),
[Status.Draft] = [Status.Draft] =
new WorkflowStep( new WorkflowStep(
new Dictionary<Status, WorkflowTransition> new Dictionary<Status, WorkflowTransition>
@ -387,6 +389,48 @@ namespace Squidex.Domain.Apps.Entities.Contents
result.Should().BeEquivalentTo(expected); result.Should().BeEquivalentTo(expected);
} }
[Fact]
public async Task Should_not_validate_when_not_publishing()
{
var result = await sut.ShouldValidateAsync(Mocks.Schema(appId, schemaId), Status.Draft);
Assert.False(result);
}
[Fact]
public async Task Should_not_validate_when_publishing_but_not_enabled()
{
var result = await sut.ShouldValidateAsync(CreateSchema(false), Status.Published);
Assert.False(result);
}
[Fact]
public async Task Should_validate_when_publishing_and_enabled()
{
var result = await sut.ShouldValidateAsync(CreateSchema(true), Status.Published);
Assert.True(result);
}
[Fact]
public async Task Should_validate_when_enabled_in_step()
{
var result = await sut.ShouldValidateAsync(Mocks.Schema(appId, schemaId), Status.Archived);
Assert.True(result);
}
private ISchemaEntity CreateSchema(bool validateOnPublish)
{
var schema = new Schema("my-schema", new SchemaProperties
{
ValidateOnPublish = validateOnPublish
});
return Mocks.Schema(appId, simpleSchemaId, schema);
}
private ContentEntity CreateContent(Status status, int value, bool simple = false) private ContentEntity CreateContent(Status status, int value, bool simple = false)
{ {
var content = new ContentEntity { AppId = appId, Status = status }; var content = new ContentEntity { AppId = appId, Status = status };

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

@ -12,6 +12,8 @@ using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Xunit; using Xunit;
#pragma warning disable CA2012 // Use ValueTasks correctly
namespace Squidex.Domain.Apps.Entities.Contents.Queries namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public class EnrichWithWorkflowsTests public class EnrichWithWorkflowsTests
@ -119,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
var content = new ContentEntity { SchemaId = schemaId }; var content = new ContentEntity { SchemaId = schemaId };
A.CallTo(() => workflow.GetInfoAsync(content, content.Status)) A.CallTo(() => workflow.GetInfoAsync(content, content.Status))
.Returns(Task.FromResult<StatusInfo?>(null!)); .Returns(ValueTask.FromResult<StatusInfo?>(null!));
var ctx = requestContext.Clone(b => b.WithResolveFlow(false)); var ctx = requestContext.Clone(b => b.WithResolveFlow(false));

4
frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html

@ -77,6 +77,10 @@
{{ 'schemas.validateOnPublish' | sqxTranslate }} {{ 'schemas.validateOnPublish' | sqxTranslate }}
</label> </label>
</div> </div>
<sqx-form-alert>
{{ 'schemas.validateOnPublishHint' | sqxTranslate }}
</sqx-form-alert>
</div> </div>
</div> </div>
<div class="card-footer"> <div class="card-footer">

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

@ -61,11 +61,11 @@
</div> </div>
</div> </div>
<div class="row transition-prevent-updates g-0 align-items-center"> <div class="row step-prevent-updates g-0 align-items-center">
<div class="col col-arrow"></div> <div class="col col-arrow"></div>
<div class="col col-step text-right"> <div class="col col-step text-right">
<div class="form-check"> <div class="form-check">
<input class="form-check-input transition-prevent-updates-checkbox" type="checkbox" id="preventUpdates_{{step.name}}" <input class="form-check-input step-prevent-updates-checkbox" type="checkbox" id="preventUpdates_{{step.name}}"
[disabled]="!!disabled" [disabled]="!!disabled"
[ngModel]="step.noUpdate" [ngModel]="step.noUpdate"
(ngModelChange)="changeNoUpdate($event)"> (ngModelChange)="changeNoUpdate($event)">
@ -105,5 +105,21 @@
<div class="col col-button"></div> <div class="col col-button"></div>
</ng-container> </ng-container>
</div> </div>
<div class="row step-validate g-0 mt-2 mb-4 align-items-center">
<div class="col col-arrow"></div>
<div class="col">
<div class="form-check">
<input class="form-check-input step-validate-checkbox" type="checkbox" id="validate_{{step.name}}"
[disabled]="!!disabled"
[ngModel]="step.validate"
(ngModelChange)="changeValidate($event)">
<label class="form-check-label" for="validate_{{step.name}}">
{{ 'workflows.validate' | sqxTranslate }}
</label>
</div>
</div>
</div>
</div> </div>
</div> </div>

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

@ -84,9 +84,7 @@
} }
} }
.transition-prevent-updates { .step-prevent-updates {
margin-bottom: 1rem;
margin-top: .25rem;
min-height: 2.5rem; min-height: 2.5rem;
&-to { &-to {

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

@ -75,6 +75,10 @@ export class WorkflowStepComponent implements OnChanges {
this.update.emit({ color }); this.update.emit({ color });
} }
public changeValidate(validate: boolean) {
this.update.emit({ validate });
}
public changeNoUpdate(noUpdate: boolean) { public changeNoUpdate(noUpdate: boolean) {
this.update.emit({ noUpdate }); this.update.emit({ noUpdate });
} }

2
frontend/src/app/shared/services/workflows.service.ts

@ -187,7 +187,7 @@ export class WorkflowDto extends Model<WorkflowDto> {
} }
export type WorkflowStepValues = export type WorkflowStepValues =
Readonly<{ color?: string; isLocked?: boolean; noUpdate?: boolean; noUpdateExpression?: string; noUpdateRoles?: ReadonlyArray<string> }>; Readonly<{ color?: string; isLocked?: boolean; validate?: boolean; noUpdate?: boolean; noUpdateExpression?: string; noUpdateRoles?: ReadonlyArray<string> }>;
export type WorkflowStep = export type WorkflowStep =
Readonly<{ name: string } & WorkflowStepValues>; Readonly<{ name: string } & WorkflowStepValues>;

Loading…
Cancel
Save