diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index c1ffde414..eb52109d1 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -947,6 +947,7 @@ "schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.", "schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.", "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.addGroup": "Add Group", "search.addSorting": "Add Sorting", @@ -1065,6 +1066,7 @@ "workflows.tabEdit": "Editing", "workflows.tabVisualize": "Visualize", "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.workflowNamePlaceholder": "Enter workflow name" } \ No newline at end of file diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 6e8cd5d83..561378312 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -947,6 +947,7 @@ "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.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.addGroup": "Aggiungi un Gruppo", "search.addSorting": "Aggiungi ordinamento", @@ -1065,6 +1066,7 @@ "workflows.tabEdit": "Modifica", "workflows.tabVisualize": "Visualizza", "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.workflowNamePlaceholder": "Inserisci il nome del workflow" } \ No newline at end of file diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 25dd3d87e..cf67f11f8 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -947,6 +947,7 @@ "schemas.updateScriptsFailed": "Updaten van schemascripts is mislukt. Laad opnieuw.", "schemas.updateUIFieldsFailed": "Bijwerken van UI-velden is mislukt. Laad opnieuw.", "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.addGroup": "Groep toevoegen", "search.addSorting": "Sortering toevoegen", @@ -1065,6 +1066,7 @@ "workflows.tabEdit": "Bewerken", "workflows.tabVisualize": "Visualiseren", "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.workflowNamePlaceholder": "Voer de werkstroomnaam in" } \ No newline at end of file diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index d344c6d98..0143e0c20 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -947,6 +947,7 @@ "schemas.updateScriptsFailed": "更新Schemas脚本失败。请重新加载。", "schemas.updateUIFieldsFailed": "无法更新 UI 字段。请重新加载。", "schemas.validateOnPublish": "发布时验证", + "schemas.validateOnPublishHint": "Use custom workflows when you need more control for which statuses a content should be validated.", "search.addFilter": "添加过滤器", "search.addGroup": "添加组", "search.addSorting": "添加排序", @@ -1065,6 +1066,7 @@ "workflows.tabEdit": "编辑", "workflows.tabVisualize": "可视化", "workflows.updateFailed": "无法更新工作流。请重新加载。", + "workflows.validate": "Validate content when changed to this status.", "workflows.workflowNameHint": "工作流的可选名称。", "workflows.workflowNamePlaceholder": "输入工作流名称" } \ No newline at end of file diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index c1ffde414..eb52109d1 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -947,6 +947,7 @@ "schemas.updateScriptsFailed": "Failed to update schema scripts. Please reload.", "schemas.updateUIFieldsFailed": "Failed to update UI fields. Please reload.", "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.addGroup": "Add Group", "search.addSorting": "Add Sorting", @@ -1065,6 +1066,7 @@ "workflows.tabEdit": "Editing", "workflows.tabVisualize": "Visualize", "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.workflowNamePlaceholder": "Enter workflow name" } \ No newline at end of file diff --git a/backend/src/Migrations/OldEvents/AppPatternAdded.cs b/backend/src/Migrations/OldEvents/AppPatternAdded.cs index 81fcdb4b9..cdd19d986 100644 --- a/backend/src/Migrations/OldEvents/AppPatternAdded.cs +++ b/backend/src/Migrations/OldEvents/AppPatternAdded.cs @@ -31,16 +31,15 @@ namespace Migrations.OldEvents public IEvent Migrate(AppDomainObject.State state) { - var newSettings = new AppSettings + var newSettings = state.Settings with { - Patterns = ReadonlyList.ToReadonlyList(new List(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern)) + Patterns = new List(state.Settings.Patterns.Where(x => x.Name != Name || x.Regex != Pattern)) { new Pattern(Name, Pattern) { Message = Message } - }), - Editors = state.Settings.Editors + }.ToReadonlyList() }; var newEvent = new AppSettingsUpdated diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs index 62af8b670..5ffc163be 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs +++ b/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 +using Squidex.Infrastructure; + namespace Squidex.Domain.Apps.Core.Apps { 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 long ApiCallsLimit { get; init; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs index c203ca02f..40156590a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppImage.cs @@ -7,28 +7,14 @@ using Squidex.Infrastructure; +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + 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 Etag { get; } - - public AppImage(string mimeType, string? etag = null) - { - Guard.NotNullOrEmpty(mimeType); - - MimeType = mimeType; + public string MimeType { get; } = Guard.NotNullOrEmpty(MimeType); - if (string.IsNullOrWhiteSpace(etag)) - { - Etag = RandomHash.Simple(); - } - else - { - Etag = etag; - } - } + public string Etag { get; } = Etag ?? RandomHash.Simple(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs index 3eac34c68..efbddd2f8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPlan.cs +++ b/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 RefToken Owner { get; } = Guard.NotNull(Owner); + + public string PlanId { get; } = Guard.NotNullOrEmpty(PlanId); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Editor.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Editor.cs index 5ec9ab71a..731b5ac30 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Editor.cs +++ b/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 + namespace Squidex.Domain.Apps.Core.Apps { public sealed record Editor(string Name, string Url) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs index 950ed29ee..afea555cf 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/LanguageConfig.cs @@ -8,25 +8,15 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + namespace Squidex.Domain.Apps.Core.Apps { - public sealed record LanguageConfig + public sealed record LanguageConfig(bool IsOptional = false, ReadonlyList? Fallbacks = null) { public static readonly LanguageConfig Default = new LanguageConfig(); - public bool IsOptional { get; } - - public ReadonlyList Fallbacks { get; } = ReadonlyList.Empty(); - - public LanguageConfig(bool isOptional = false, ReadonlyList? fallbacks = null) - { - IsOptional = isOptional; - - if (fallbacks != null) - { - Fallbacks = fallbacks; - } - } + public ReadonlyList Fallbacks { get; } = Fallbacks ?? ReadonlyList.Empty(); internal LanguageConfig Cleanup(string self, IReadOnlyDictionary allowed) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Pattern.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Pattern.cs index c4df66333..47c041ce4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Pattern.cs +++ b/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; } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs index 8befb86c9..c23083106 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Role.cs +++ b/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.Security; +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + 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 ExtraPermissions = new HashSet { @@ -33,38 +35,25 @@ namespace Squidex.Domain.Apps.Core.Apps public const string Owner = "Owner"; 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 { get => Roles.IsDefault(this); } - public Role(string name, PermissionSet permissions, JsonObject properties) - : 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) + public static Role WithPermissions(string name, params string[] permissions) { - 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] diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs index 9db7bedf1..d65577ec5 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Roles.cs @@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - var newRole = Role.Create(name); + var newRole = new Role(name); if (!inner.TryAdd(name, newRole, out var updated)) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs index a16c25c6d..f22d355e0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Comments/Comment.cs +++ b/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 RefToken User { get; } = Guard.NotNull(User); + + public string Text { get; } = Guard.NotNullOrEmpty(Text); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs index b0e543e8a..67a23a16b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Component.cs @@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis; using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; #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 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) { discriminator = null!; diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs index 6c7e79d08..7568d3295 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/StatusInfo.cs +++ b/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 +using Squidex.Infrastructure; + namespace Squidex.Domain.Apps.Core.Contents { public sealed record StatusInfo(Status Status, string Color) { + public string Color { get; } = Guard.NotNullOrEmpty(Color); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs index 1f59023ef..502c5a855 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/Workflow.cs @@ -9,43 +9,22 @@ using System.Diagnostics.CodeAnalysis; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + namespace Squidex.Domain.Apps.Core.Contents { - public sealed record Workflow + public sealed record Workflow(Status Initial, ReadonlyDictionary? Steps = null, ReadonlyList? SchemaIds = null, string? Name = null) { private const string DefaultName = "Unnamed"; public static readonly Workflow Default = CreateDefault(); public static readonly Workflow Empty = new Workflow(default, null); - public Status Initial { get; } - - public ReadonlyDictionary Steps { get; } = ReadonlyDictionary.Empty(); - - public ReadonlyList SchemaIds { get; } = ReadonlyList.Empty(); - - public string Name { get; } - - public Workflow( - Status initial, - ReadonlyDictionary? steps = null, - ReadonlyList? schemaIds = null, - string? name = null) - { - Initial = initial; - - if (steps != null) - { - Steps = steps; - } + public string Name { get; } = Name.Or(DefaultName); - if (schemaIds != null) - { - SchemaIds = schemaIds; - } + public ReadonlyDictionary Steps { get; } = Steps ?? ReadonlyDictionary.Empty(); - Name = name.Or(DefaultName); - } + public ReadonlyList SchemaIds { get; } = SchemaIds ?? ReadonlyList.Empty(); public static Workflow CreateDefault(string? name = null) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs index 7d0855fa0..6e798b4eb 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/WorkflowStep.cs @@ -7,26 +7,12 @@ using Squidex.Infrastructure.Collections; +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + namespace Squidex.Domain.Apps.Core.Contents { - public sealed record WorkflowStep + public sealed record WorkflowStep(ReadonlyDictionary? Transitions = null, string? Color = null, NoUpdate? NoUpdate = null, bool Validate = false) { - public ReadonlyDictionary Transitions { get; } = ReadonlyDictionary.Empty(); - - public string? Color { get; } - - public NoUpdate? NoUpdate { get; } - - public WorkflowStep(ReadonlyDictionary? transitions = null, string? color = null, NoUpdate? noUpdate = null) - { - Color = color; - - if (transitions != null) - { - Transitions = transitions; - } - - NoUpdate = noUpdate; - } + public ReadonlyDictionary Transitions { get; } = Transitions ?? ReadonlyDictionary.Empty(); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Named.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Named.cs deleted file mode 100644 index 6c8168b3c..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Named.cs +++ /dev/null @@ -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; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs index 35def042b..5eeaa4307 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DefaultContentWorkflow.cs @@ -43,54 +43,61 @@ namespace Squidex.Domain.Apps.Entities.Contents }) }; - public Task GetInitialStatusAsync(ISchemaEntity schema) + public ValueTask GetInitialStatusAsync(ISchemaEntity schema) { - return Task.FromResult(Status.Draft); + return ValueTask.FromResult(Status.Draft); } - public Task CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user) + public ValueTask CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user) { - return Task.FromResult(true); + return ValueTask.FromResult(true); } - public Task CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) + public ValueTask ShouldValidateAsync(ISchemaEntity schema, Status status) + { + var result = status == Status.Published && schema.SchemaDef.Properties.ValidateOnPublish; + + return ValueTask.FromResult(result); + } + + public ValueTask 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); - return Task.FromResult(result); + return ValueTask.FromResult(result); } - public Task CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) + public ValueTask CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) { 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 CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user) + public ValueTask CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user) { var result = status != Status.Archived; - return Task.FromResult(result); + return ValueTask.FromResult(result); } - public Task GetInfoAsync(IContentEntity content, Status status) + public ValueTask GetInfoAsync(IContentEntity content, Status status) { var result = Flow.GetValueOrDefault(status).Info; - return Task.FromResult(result); + return ValueTask.FromResult(result); } - public Task GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user) + public ValueTask GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user) { var result = Flow.TryGetValue(status, out var step) ? step.Transitions : Array.Empty(); - return Task.FromResult(result); + return ValueTask.FromResult(result); } - public Task GetAllAsync(ISchemaEntity schema) + public ValueTask GetAllAsync(ISchemaEntity schema) { - return Task.FromResult(All); + return ValueTask.FromResult(All); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs index cb69bee00..0f3565b81 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs @@ -297,7 +297,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject 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); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs index 9443e4b74..c253b59c5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/WorkflowExtensions.cs +++ b/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 Task GetInitialStatusAsync(this ContentOperation operation) + public static ValueTask GetInitialStatusAsync(this ContentOperation operation) { var workflow = GetWorkflow(operation); return workflow.GetInitialStatusAsync(operation.Schema); } + public static ValueTask 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) { if (operation.SchemaDef.Type != SchemaType.Singleton) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs index 7b36c74c0..744cf3687 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DynamicContentWorkflow.cs @@ -25,35 +25,35 @@ namespace Squidex.Domain.Apps.Entities.Contents this.appProvider = appProvider; } - public async Task GetAllAsync(ISchemaEntity schema) + public async ValueTask GetAllAsync(ISchemaEntity schema) { var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); return workflow.Steps.Select(x => new StatusInfo(x.Key, GetColor(x.Value))).ToArray(); } - public async Task CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user) + public async ValueTask CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user) { var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); return workflow.TryGetTransition(workflow.Initial, Status.Published, out var transition) && IsTrue(transition, null, user); } - public async Task CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) + public async ValueTask CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user) { var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, data, user); } - public async Task CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) + public async ValueTask CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user) { var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); return workflow.TryGetTransition(status, next, out var transition) && IsTrue(transition, content.Data, user); } - public async Task CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user) + public async ValueTask CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user) { var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); @@ -65,7 +65,19 @@ namespace Squidex.Domain.Apps.Entities.Contents return true; } - public async Task GetInfoAsync(IContentEntity content, Status status) + public async ValueTask 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 GetInfoAsync(IContentEntity content, Status status) { var workflow = await GetWorkflowAsync(content.AppId.Id, content.SchemaId.Id); @@ -77,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return null; } - public async Task GetInitialStatusAsync(ISchemaEntity schema) + public async ValueTask GetInitialStatusAsync(ISchemaEntity schema) { var workflow = await GetWorkflowAsync(schema.AppId.Id, schema.Id); @@ -86,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return status; } - public async Task GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user) + public async ValueTask GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user) { var result = new List(); @@ -126,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents return true; } - private async Task GetWorkflowAsync(DomainId appId, DomainId schemaId) + private async ValueTask GetWorkflowAsync(DomainId appId, DomainId schemaId) { Workflow? result = null; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs index 11821a337..ebcc62bc4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentWorkflow.cs @@ -13,20 +13,22 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentWorkflow { - Task GetInitialStatusAsync(ISchemaEntity schema); + ValueTask GetInitialStatusAsync(ISchemaEntity schema); - Task CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user); + ValueTask CanMoveToAsync(ISchemaEntity schema, Status status, Status next, ContentData data, ClaimsPrincipal? user); - Task CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user); + ValueTask CanMoveToAsync(IContentEntity content, Status status, Status next, ClaimsPrincipal? user); - Task CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user); + ValueTask CanUpdateAsync(IContentEntity content, Status status, ClaimsPrincipal? user); - Task CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user); + ValueTask CanPublishInitialAsync(ISchemaEntity schema, ClaimsPrincipal? user); - Task GetInfoAsync(IContentEntity content, Status status); + ValueTask ShouldValidateAsync(ISchemaEntity schema, Status status); - Task GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user); + ValueTask GetInfoAsync(IContentEntity content, Status status); - Task GetAllAsync(ISchemaEntity schema); + ValueTask GetNextAsync(IContentEntity content, Status status, ClaimsPrincipal? user); + + ValueTask GetAllAsync(ISchemaEntity schema); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs index 023c2f7ca..31ac6b9ba 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/SimulatedRuleEvent.cs @@ -6,7 +6,6 @@ // ========================================================================== using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Rules.Runner { diff --git a/backend/src/Squidex.Infrastructure/Guard.cs b/backend/src/Squidex.Infrastructure/Guard.cs index 72bd375e0..1f250d936 100644 --- a/backend/src/Squidex.Infrastructure/Guard.cs +++ b/backend/src/Squidex.Infrastructure/Guard.cs @@ -16,29 +16,33 @@ namespace Squidex.Infrastructure { [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidNumber(float target, + public static float ValidNumber(float target, [CallerArgumentExpression("target")] string? parameterName = null) { if (float.IsNaN(target) || float.IsPositiveInfinity(target) || float.IsNegativeInfinity(target)) { throw new ArgumentException("Value must be a valid number.", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidNumber(double target, + public static double ValidNumber(double target, [CallerArgumentExpression("target")] string? parameterName = null) { if (double.IsNaN(target) || double.IsPositiveInfinity(target) || double.IsNegativeInfinity(target)) { throw new ArgumentException("Value must be a valid number.", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidSlug(string? target, + public static string ValidSlug(string? target, [CallerArgumentExpression("target")] string? parameterName = null) { NotNullOrEmpty(target, parameterName); @@ -47,11 +51,13 @@ namespace Squidex.Infrastructure { throw new ArgumentException("Target is not a valid slug.", parameterName); } + + return target!; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidPropertyName(string? target, + public static string ValidPropertyName(string? target, [CallerArgumentExpression("target")] string? parameterName = null) { NotNullOrEmpty(target, parameterName); @@ -60,99 +66,117 @@ namespace Squidex.Infrastructure { throw new ArgumentException("Target is not a valid property name.", parameterName); } + + return target!; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void HasType(object? target, + public static object? HasType(object? target, [CallerArgumentExpression("target")] string? parameterName = null) { if (target != null && target.GetType() != typeof(T)) { throw new ArgumentException($"The parameter must be of type {typeof(T)}", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void HasType(object? target, Type? expectedType, + public static object? HasType(object? target, Type? expectedType, [CallerArgumentExpression("target")] string? parameterName = null) { if (target != null && expectedType != null && target.GetType() != expectedType) { throw new ArgumentException($"The parameter must be of type {expectedType}", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Between(TValue target, TValue lower, TValue upper, + public static TValue Between(TValue target, TValue lower, TValue upper, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable { if (!target.IsBetween(lower, upper)) { throw new ArgumentException($"Value must be between {lower} and {upper}", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Enum(TEnum target, + public static TEnum Enum(TEnum target, [CallerArgumentExpression("target")] string? parameterName = null) where TEnum : struct { if (!target.IsEnumValue()) { throw new ArgumentException($"Value must be a valid enum type {typeof(TEnum)}", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GreaterThan(TValue target, TValue lower, + public static TValue GreaterThan(TValue target, TValue lower, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable { if (target.CompareTo(lower) <= 0) { throw new ArgumentException($"Value must be greater than {lower}", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void GreaterEquals(TValue target, TValue lower, + public static TValue GreaterEquals(TValue target, TValue lower, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable { if (target.CompareTo(lower) < 0) { throw new ArgumentException($"Value must be greater or equal to {lower}", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LessThan(TValue target, TValue upper, + public static TValue LessThan(TValue target, TValue upper, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable { if (target.CompareTo(upper) >= 0) { throw new ArgumentException($"Value must be less than {upper}", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LessEquals(TValue target, TValue upper, + public static TValue LessEquals(TValue target, TValue upper, [CallerArgumentExpression("target")] string? parameterName = null) where TValue : IComparable { if (target.CompareTo(upper) > 0) { throw new ArgumentException($"Value must be less or equal to {upper}", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotEmpty(IReadOnlyCollection? target, + public static IReadOnlyCollection NotEmpty(IReadOnlyCollection? target, [CallerArgumentExpression("target")] string? parameterName = null) { NotNull(target, parameterName); @@ -161,55 +185,78 @@ namespace Squidex.Infrastructure { throw new ArgumentException("Collection does not contain an item.", parameterName); } + + return target!; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotEmpty(Guid target, + public static Guid NotEmpty(Guid target, [CallerArgumentExpression("target")] string? parameterName = null) { if (target == Guid.Empty) { throw new ArgumentException("Value cannot be empty.", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotEmpty(DomainId target, + public static DomainId NotEmpty(DomainId target, [CallerArgumentExpression("target")] string? parameterName = null) { if (target == DomainId.Empty) { throw new ArgumentException("Value cannot be empty.", parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotNull(object? target, + public static TValue NotNull(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) { if (target == null) { throw new ArgumentNullException(parameterName); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotDefault(T target, + public static TValue NotDefault(TValue target, [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); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void NotNullOrEmpty(string? target, + public static string NotNullOrEmpty(string? target, [CallerArgumentExpression("target")] string? parameterName = null) { 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); } + + return target; } [DebuggerStepThrough] [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ValidFileName(string? target, + public static string ValidFileName(string? target, [CallerArgumentExpression("target")] string? parameterName = null) { NotNullOrEmpty(target, parameterName); @@ -231,6 +280,8 @@ namespace Squidex.Infrastructure { throw new ArgumentException("Value contains an invalid character.", parameterName); } + + return target!; } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs index dd6cd3ef1..c5990d677 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/WorkflowStepDto.cs @@ -26,6 +26,11 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// public string? Color { get; set; } + /// + /// True if the content should be validated when moving to this step. + /// + public bool Validate { get; set; } + /// /// Indicates if updates should not be allowed. /// @@ -69,7 +74,8 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models Color, NoUpdate ? NoUpdateType.When(NoUpdateExpression, NoUpdateRoles) : - null); + null, + Validate); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs index 0eec80497..72b71f0e0 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RoleTests.cs @@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_be_default_role() { - var role = Role.Create("Owner"); + var role = new Role("Owner"); Assert.True(role.IsDefault); } @@ -23,7 +23,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] public void Should_not_be_default_role() { - var role = Role.Create("Custom"); + var role = new Role("Custom"); Assert.False(role.IsDefault); } @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.Model.Apps [Fact] 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(); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs index 8a5d2f203..0fd89d392 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/RolesTests.cs +++ b/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); - Assert.Equal(Role.Create(role), roles_1[role]); + Assert.Equal(new Role(role), roles_1[role]); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs index b4225a673..f7d42a8d7 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/WorkflowJsonTests.cs @@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Core.Model.Contents [Fact] 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(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs index ca09d6fc5..006b91940 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultContentWorkflowTests.cs @@ -7,6 +7,10 @@ using FluentAssertions; 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; namespace Squidex.Domain.Apps.Entities.Contents @@ -158,5 +162,39 @@ namespace Squidex.Domain.Apps.Entities.Contents 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); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs index 8532499d2..33b085295 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/Guards/GuardContentTests.cs @@ -20,6 +20,8 @@ using Squidex.Infrastructure.Validation; using Squidex.Shared; using Xunit; +#pragma warning disable CA2012 // Use ValueTasks correctly + namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards { public class GuardContentTests : IClassFixture @@ -240,7 +242,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject.Guards var operation = Operation(CreateContent(Status.Draft), normalSchema); A.CallTo(() => contentWorkflow.GetInfoAsync((ContentEntity)operation.Snapshot, Status.Archived)) - .Returns(Task.FromResult(null)); + .Returns(ValueTask.FromResult(null)); await Assert.ThrowsAsync(() => operation.CheckStatusAsync(Status.Archived)); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs index 4c61f40bb..d2a850bc6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DynamicContentWorkflowTests.cs @@ -10,8 +10,10 @@ using FluentAssertions; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; @@ -38,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { [Status.Draft] = WorkflowTransition.Always }.ToReadonlyDictionary(), - StatusColors.Archived, NoUpdate.Always), + StatusColors.Archived, NoUpdate.Always, Validate: true), [Status.Draft] = new WorkflowStep( new Dictionary @@ -387,6 +389,48 @@ namespace Squidex.Domain.Apps.Entities.Contents 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) { var content = new ContentEntity { AppId = appId, Status = status }; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs index 39becf152..87dfbd67c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/EnrichWithWorkflowsTests.cs +++ b/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 Xunit; +#pragma warning disable CA2012 // Use ValueTasks correctly + namespace Squidex.Domain.Apps.Entities.Contents.Queries { public class EnrichWithWorkflowsTests @@ -119,7 +121,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var content = new ContentEntity { SchemaId = schemaId }; A.CallTo(() => workflow.GetInfoAsync(content, content.Status)) - .Returns(Task.FromResult(null!)); + .Returns(ValueTask.FromResult(null!)); var ctx = requestContext.Clone(b => b.WithResolveFlow(false)); diff --git a/frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html b/frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html index 05c0a1b93..e6cd3f8e9 100644 --- a/frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html +++ b/frontend/src/app/features/schemas/pages/schema/common/schema-edit-form.component.html @@ -77,6 +77,10 @@ {{ 'schemas.validateOnPublish' | sqxTranslate }} + + + {{ 'schemas.validateOnPublishHint' | sqxTranslate }} + -
+
- @@ -105,5 +105,21 @@
+ +
+
+
+
+ + + +
+
+
\ No newline at end of file diff --git a/frontend/src/app/features/settings/pages/workflows/workflow-step.component.scss b/frontend/src/app/features/settings/pages/workflows/workflow-step.component.scss index bd852c699..f8bbba526 100644 --- a/frontend/src/app/features/settings/pages/workflows/workflow-step.component.scss +++ b/frontend/src/app/features/settings/pages/workflows/workflow-step.component.scss @@ -84,9 +84,7 @@ } } -.transition-prevent-updates { - margin-bottom: 1rem; - margin-top: .25rem; +.step-prevent-updates { min-height: 2.5rem; &-to { diff --git a/frontend/src/app/features/settings/pages/workflows/workflow-step.component.ts b/frontend/src/app/features/settings/pages/workflows/workflow-step.component.ts index 9a3f8b0aa..15d83ce67 100644 --- a/frontend/src/app/features/settings/pages/workflows/workflow-step.component.ts +++ b/frontend/src/app/features/settings/pages/workflows/workflow-step.component.ts @@ -75,6 +75,10 @@ export class WorkflowStepComponent implements OnChanges { this.update.emit({ color }); } + public changeValidate(validate: boolean) { + this.update.emit({ validate }); + } + public changeNoUpdate(noUpdate: boolean) { this.update.emit({ noUpdate }); } diff --git a/frontend/src/app/shared/services/workflows.service.ts b/frontend/src/app/shared/services/workflows.service.ts index 33a5b5b4f..35da27322 100644 --- a/frontend/src/app/shared/services/workflows.service.ts +++ b/frontend/src/app/shared/services/workflows.service.ts @@ -187,7 +187,7 @@ export class WorkflowDto extends Model { } export type WorkflowStepValues = - Readonly<{ color?: string; isLocked?: boolean; noUpdate?: boolean; noUpdateExpression?: string; noUpdateRoles?: ReadonlyArray }>; + Readonly<{ color?: string; isLocked?: boolean; validate?: boolean; noUpdate?: boolean; noUpdateExpression?: string; noUpdateRoles?: ReadonlyArray }>; export type WorkflowStep = Readonly<{ name: string } & WorkflowStepValues>;