From 6c4ea7b7d9ca5b3c719ef7dca031106a62dff81a Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 13 Jul 2020 17:21:55 +0200 Subject: [PATCH 01/68] Allow anonymous access for clients. (#544) --- .../OldEvents}/AppClientRenamed.cs | 15 +++- .../Apps/AppClient.cs | 18 ++-- .../Apps/AppClients.cs | 19 +--- .../Apps/AppPattern.cs | 4 +- .../Apps/AppPatterns.cs | 6 +- .../Apps/Json/AppClientsConverter.cs | 10 +-- .../Apps/Json/JsonAppClient.cs | 38 -------- .../Apps/AppDomainObject.cs | 18 ++-- .../Apps/AppHistoryEventsCreator.cs | 16 +--- .../Apps/Commands/UpdateClient.cs | 4 +- .../Apps/Guards/GuardAppClients.cs | 5 -- .../Apps/State/AppState.cs | 5 +- .../Apps/AppClientUpdated.cs | 6 +- .../StringExtensions.cs | 5 ++ .../Squidex.Infrastructure/Validation/Not.cs | 6 -- .../src/Squidex.Web/ApiPermissionAttribute.cs | 2 +- .../src/Squidex.Web/Pipeline/AppResolver.cs | 27 +++++- .../Apps/Models/UpdateClientDto.cs | 7 +- .../Model/Apps/AppClientJsonTests.cs | 8 +- .../Model/Apps/AppClientsTests.cs | 36 +++----- .../Apps/AppDomainObjectTests.cs | 3 +- .../Apps/Guards/GuardAppClientsTests.cs | 11 --- .../Apps/Guards/GuardAppRolesTests.cs | 2 +- .../Pipeline/AppResolverTests.cs | 88 +++++++++++++++---- .../pages/clients/client.component.html | 19 ++++ .../pages/clients/client.component.ts | 4 + .../shared/services/clients.service.spec.ts | 3 +- .../app/shared/services/clients.service.ts | 7 +- 28 files changed, 206 insertions(+), 186 deletions(-) rename backend/src/{Squidex.Domain.Apps.Events/Apps => Migrations/OldEvents}/AppClientRenamed.cs (55%) delete mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs b/backend/src/Migrations/OldEvents/AppClientRenamed.cs similarity index 55% rename from backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs rename to backend/src/Migrations/OldEvents/AppClientRenamed.cs index dcb22ea51..31f1ad9d0 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientRenamed.cs +++ b/backend/src/Migrations/OldEvents/AppClientRenamed.cs @@ -5,15 +5,26 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Migrations; +using Squidex.Infrastructure.Reflection; +using AppClientUpdatedV2 = Squidex.Domain.Apps.Events.Apps.AppClientUpdated; -namespace Squidex.Domain.Apps.Events.Apps +namespace Migrations.OldEvents { [EventType(nameof(AppClientRenamed))] - public sealed class AppClientRenamed : AppEvent + public sealed class AppClientRenamed : AppEvent, IMigrated { public string Id { get; set; } public string Name { get; set; } + + public IEvent Migrate() + { + var result = SimpleMapper.Map(this, new AppClientUpdatedV2()); + + return result; + } } } 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 687d59849..58e607039 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClient.cs @@ -17,7 +17,9 @@ namespace Squidex.Domain.Apps.Core.Apps public string Secret { get; } - public AppClient(string name, string secret, string role) + public bool AllowAnonymous { get; set; } + + public AppClient(string name, string secret, string role, bool allowAnonymous) : base(name) { Guard.NotNullOrEmpty(secret, nameof(secret)); @@ -26,22 +28,14 @@ namespace Squidex.Domain.Apps.Core.Apps Role = role; Secret = secret; - } - - [Pure] - public AppClient Update(string newRole) - { - Guard.NotNullOrEmpty(newRole, nameof(newRole)); - return new AppClient(Name, Secret, newRole); + AllowAnonymous = allowAnonymous; } [Pure] - public AppClient Rename(string newName) + public AppClient Update(string? name, string? role, bool? allowAnonymous) { - Guard.NotNullOrEmpty(newName, nameof(newName)); - - return new AppClient(newName, Secret, Role); + return new AppClient(name.Or(Name), Secret, role.Or(Role), allowAnonymous ?? AllowAnonymous); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs index e8caa2f28..cb6e38679 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppClients.cs @@ -53,11 +53,11 @@ namespace Squidex.Domain.Apps.Core.Apps throw new ArgumentException("Id already exists.", nameof(id)); } - return With(id, new AppClient(id, secret, Role.Editor)); + return With(id, new AppClient(id, secret, Role.Editor, false)); } [Pure] - public AppClients Rename(string id, string newName) + public AppClients Update(string id, string? name = null, string? role = null, bool? allowAnonymous = false) { Guard.NotNullOrEmpty(id, nameof(id)); @@ -66,20 +66,7 @@ namespace Squidex.Domain.Apps.Core.Apps return this; } - return With(id, client.Rename(newName)); - } - - [Pure] - public AppClients Update(string id, string role) - { - Guard.NotNullOrEmpty(id, nameof(id)); - - if (!TryGetValue(id, out var client)) - { - return this; - } - - return With(id, client.Update(role)); + return With(id, client.Update(name, role, allowAnonymous)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs index a4c10d824..7deb3f55b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs @@ -28,9 +28,9 @@ namespace Squidex.Domain.Apps.Core.Apps } [Pure] - public AppPattern Update(string newName, string newPattern, string? newMessage) + public AppPattern Update(string? name, string? pattern, string? message) { - return new AppPattern(newName, newPattern, newMessage); + return new AppPattern(name.Or(Name), pattern.Or(Pattern), message); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs index 294d1ccc0..606765773 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs @@ -8,7 +8,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.Contracts; -using Squidex.Infrastructure; using Squidex.Infrastructure.Collections; namespace Squidex.Domain.Apps.Core.Apps @@ -41,11 +40,8 @@ namespace Squidex.Domain.Apps.Core.Apps } [Pure] - public AppPatterns Update(Guid id, string name, string pattern, string? message = null) + public AppPatterns Update(Guid id, string? name = null, string? pattern = null, string? message = null) { - Guard.NotNullOrEmpty(name, nameof(name)); - Guard.NotNullOrEmpty(pattern, nameof(pattern)); - if (!TryGetValue(id, out var appPattern)) { return this; diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs index a03bba8d5..217705ea3 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppClientsConverter.cs @@ -17,11 +17,11 @@ namespace Squidex.Domain.Apps.Core.Apps.Json { protected override void WriteValue(JsonWriter writer, AppClients value, JsonSerializer serializer) { - var json = new Dictionary(value.Count); + var json = new Dictionary(value.Count); - foreach (var (key, appClient) in value) + foreach (var (key, client) in value) { - json.Add(key, new JsonAppClient(appClient)); + json.Add(key, client); } serializer.Serialize(writer, json); @@ -29,9 +29,9 @@ namespace Squidex.Domain.Apps.Core.Apps.Json protected override AppClients ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) { - var json = serializer.Deserialize>(reader)!; + var json = serializer.Deserialize>(reader)!; - return new AppClients(json.ToDictionary(x => x.Key, x => x.Value.ToClient())); + return new AppClients(json); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs deleted file mode 100644 index a3380c03c..000000000 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppClient.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Newtonsoft.Json; -using Squidex.Infrastructure.Reflection; - -namespace Squidex.Domain.Apps.Core.Apps.Json -{ - public class JsonAppClient - { - [JsonProperty] - public string Name { get; set; } - - [JsonProperty] - public string Secret { get; set; } - - [JsonProperty] - public string Role { get; set; } - - public JsonAppClient() - { - } - - public JsonAppClient(AppClient client) - { - SimpleMapper.Map(client, this); - } - - public AppClient ToClient() - { - return new AppClient(Name, Secret, Role); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index 0da6580e0..f26b10311 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -341,19 +341,6 @@ namespace Squidex.Domain.Apps.Entities.Apps } } - public void UpdateClient(UpdateClient command) - { - if (!string.IsNullOrWhiteSpace(command.Name)) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientRenamed())); - } - - if (command.Role != null) - { - RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated { Role = command.Role })); - } - } - public void ChangePlan(ChangePlan command) { if (string.Equals(appPlansProvider.GetFreePlan()?.Id, command.PlanId)) @@ -371,6 +358,11 @@ namespace Squidex.Domain.Apps.Entities.Apps RaiseEvent(SimpleMapper.Map(command, new AppUpdated())); } + public void UpdateClient(UpdateClient command) + { + RaiseEvent(SimpleMapper.Map(command, new AppClientUpdated())); + } + public void UploadImage(UploadAppImage command) { RaiseEvent(SimpleMapper.Map(command, new AppImageUploaded { Image = new AppImage(command.File.MimeType) })); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs index ed1487a9b..39eaf5c36 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppHistoryEventsCreator.cs @@ -35,9 +35,6 @@ namespace Squidex.Domain.Apps.Entities.Apps AddEventMessage( "updated client {[Id]}"); - AddEventMessage( - "renamed client {[Id]} to {[Name]}"); - AddEventMessage( "changed plan to {[Plan]}"); @@ -85,8 +82,8 @@ namespace Squidex.Domain.Apps.Entities.Apps return CreateContributorsEvent(e, e.ContributorId); case AppClientAttached e: return CreateClientsEvent(e, e.Id); - case AppClientRenamed e: - return CreateClientsEvent(e, e.Id, ClientName(e)); + case AppClientUpdated e: + return CreateClientsEvent(e, e.Id); case AppClientRevoked e: return CreateClientsEvent(e, e.Id); case AppLanguageAdded e: @@ -138,9 +135,9 @@ namespace Squidex.Domain.Apps.Entities.Apps return ForEvent(e, "settings.patterns").Param("PatternId", id).Param("Name", name); } - private HistoryEvent CreateClientsEvent(IEvent e, string id, string? name = null) + private HistoryEvent CreateClientsEvent(IEvent e, string id) { - return ForEvent(e, "settings.clients").Param("Id", id).Param("Name", name); + return ForEvent(e, "settings.clients").Param("Id", id); } private HistoryEvent CreatePlansEvent(IEvent e, string? plan = null) @@ -152,10 +149,5 @@ namespace Squidex.Domain.Apps.Entities.Apps { return Task.FromResult(CreateEvent(@event.Payload)); } - - private static string ClientName(AppClientRenamed e) - { - return !string.IsNullOrWhiteSpace(e.Name) ? e.Name : e.Id; - } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs index 854242be3..474db16b0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdateClient.cs @@ -11,8 +11,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { public string Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } public string? Role { get; set; } + + public bool? AllowAnonymous { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs index a3828ffaf..1bebbfac7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppClients.cs @@ -59,11 +59,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards e(Not.Defined("Client id"), nameof(command.Id)); } - if (string.IsNullOrWhiteSpace(command.Name) && command.Role == null) - { - e(Not.DefinedOr("name", "role"), nameof(command.Name), nameof(command.Role)); - } - if (command.Role != null && !roles.Contains(command.Role)) { e(Not.Valid("role"), nameof(command.Role)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 742608a9b..7c7d15332 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -85,10 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.State return UpdateClients(e, (ev, c) => c.Add(ev.Id, ev.Secret)); case AppClientUpdated e: - return UpdateClients(e, (ev, c) => c.Update(ev.Id, ev.Role)); - - case AppClientRenamed e: - return UpdateClients(e, (ev, c) => c.Rename(ev.Id, ev.Name)); + return UpdateClients(e, (ev, c) => c.Update(ev.Id, ev.Name, ev.Role, ev.AllowAnonymous)); case AppClientRevoked e: return UpdateClients(e, (ev, c) => c.Revoke(ev.Id)); diff --git a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs index e09e4f6cb..519c02e26 100644 --- a/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs +++ b/backend/src/Squidex.Domain.Apps.Events/Apps/AppClientUpdated.cs @@ -14,6 +14,10 @@ namespace Squidex.Domain.Apps.Events.Apps { public string Id { get; set; } - public string Role { get; set; } + public string? Name { get; set; } + + public string? Role { get; set; } + + public bool? AllowAnonymous { get; set; } } } diff --git a/backend/src/Squidex.Infrastructure/StringExtensions.cs b/backend/src/Squidex.Infrastructure/StringExtensions.cs index a3acbab4e..e89ce28fd 100644 --- a/backend/src/Squidex.Infrastructure/StringExtensions.cs +++ b/backend/src/Squidex.Infrastructure/StringExtensions.cs @@ -542,6 +542,11 @@ namespace Squidex.Infrastructure return value != null && PropertyNameRegex.IsMatch(value); } + public static string Or(this string? value, string fallback) + { + return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; + } + public static string WithFallback(this string? value, string fallback) { return !string.IsNullOrWhiteSpace(value) ? value.Trim() : fallback; diff --git a/backend/src/Squidex.Infrastructure/Validation/Not.cs b/backend/src/Squidex.Infrastructure/Validation/Not.cs index f10a78a69..a24767ca0 100644 --- a/backend/src/Squidex.Infrastructure/Validation/Not.cs +++ b/backend/src/Squidex.Infrastructure/Validation/Not.cs @@ -71,12 +71,6 @@ namespace Squidex.Infrastructure.Validation return $"{Upper(property)} is not a valid value."; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string DefinedOr(string property1, string property2) - { - return $"Either {Lower(property1)} or {Lower(property2)} must be defined."; - } - private static string Lower(string property) { if (char.IsUpper(property[0])) diff --git a/backend/src/Squidex.Web/ApiPermissionAttribute.cs b/backend/src/Squidex.Web/ApiPermissionAttribute.cs index edcee1427..c48fd41d8 100644 --- a/backend/src/Squidex.Web/ApiPermissionAttribute.cs +++ b/backend/src/Squidex.Web/ApiPermissionAttribute.cs @@ -15,7 +15,7 @@ using Squidex.Shared; namespace Squidex.Web { - public sealed class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter + public sealed class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter, IAllowAnonymous { private readonly string[] permissionIds; diff --git a/backend/src/Squidex.Web/Pipeline/AppResolver.cs b/backend/src/Squidex.Web/Pipeline/AppResolver.cs index bd6ce35ad..54c8a92ff 100644 --- a/backend/src/Squidex.Web/Pipeline/AppResolver.cs +++ b/backend/src/Squidex.Web/Pipeline/AppResolver.cs @@ -58,6 +58,11 @@ namespace Squidex.Web.Pipeline (role, permissions) = FindByOpenIdClient(app, user); } + if (permissions == null) + { + (role, permissions) = FindAnonymousClient(app); + } + if (permissions != null) { var identity = user.Identities.First(); @@ -77,7 +82,15 @@ namespace Squidex.Web.Pipeline if (!AllowAnonymous(context) && !HasPermission(appName, requestContext)) { - context.Result = new NotFoundResult(); + if (string.IsNullOrWhiteSpace(user.Identity.AuthenticationType)) + { + context.Result = new UnauthorizedResult(); + } + else + { + context.Result = new NotFoundResult(); + } + return; } @@ -136,6 +149,18 @@ namespace Squidex.Web.Pipeline return (null, null); } + private static (string?, PermissionSet?) FindAnonymousClient(IAppEntity app) + { + var client = app.Clients.Values.FirstOrDefault(x => x.AllowAnonymous); + + if (client != null && app.Roles.TryGet(app.Name, client.Role, out var role)) + { + return (client.Role, role.Permissions); + } + + return (null, null); + } + private static (string?, PermissionSet?) FindByOpenIdSubject(IAppEntity app, ClaimsPrincipal user) { var subjectId = user.OpenIdSubject(); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs index 44e949983..9f7cd207f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdateClientDto.cs @@ -17,13 +17,18 @@ namespace Squidex.Areas.Api.Controllers.Apps.Models /// The new display name of the client. /// [StringLength(20)] - public string Name { get; set; } + public string? Name { get; set; } /// /// The role of the client. /// public string? Role { get; set; } + /// + /// True to allow anonymous access without an access token for this client. + /// + public bool? AllowAnonymous { get; set; } + public UpdateClient ToCommand(string clientId) { return SimpleMapper.Map(this, new UpdateClient { Id = clientId }); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs index 5a2ac24a7..ec187aed6 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientJsonTests.cs @@ -23,10 +23,12 @@ namespace Squidex.Domain.Apps.Core.Model.Apps clients = clients.Add("3", "my-secret"); clients = clients.Add("4", "my-secret"); - clients = clients.Update("3", Role.Editor); + clients = clients.Update("3", role: Role.Editor); - clients = clients.Rename("3", "My Client 3"); - clients = clients.Rename("2", "My Client 2"); + clients = clients.Update("3", name: "My Client 3"); + clients = clients.Update("2", name: "My Client 2"); + + clients = clients.Update("1", allowAnonymous: true); clients = clients.Revoke("4"); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs index 1e3ba2b96..252284ebb 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Apps/AppClientsTests.cs @@ -23,15 +23,15 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var clients_1 = clients_0.Add("2", "my-secret"); - clients_1["2"].Should().BeEquivalentTo(new AppClient("2", "my-secret", Role.Editor)); + clients_1["2"].Should().BeEquivalentTo(new AppClient("2", "my-secret", Role.Editor, false)); } [Fact] public void Should_assign_clients_with_permission() { - var clients_1 = clients_0.Add("2", new AppClient("my-name", "my-secret", Role.Reader)); + var clients_1 = clients_0.Add("2", new AppClient("my-name", "my-secret", Role.Reader, false)); - clients_1["2"].Should().BeEquivalentTo(new AppClient("my-name", "my-secret", Role.Reader)); + clients_1["2"].Should().BeEquivalentTo(new AppClient("my-name", "my-secret", Role.Reader, false)); } [Fact] @@ -47,45 +47,37 @@ namespace Squidex.Domain.Apps.Core.Model.Apps { var clients_1 = clients_0.Add("2", "my-secret"); - clients_1.Add("2", new AppClient("my-name", "my-secret", "my-role")); + clients_1.Add("2", new AppClient("my-name", "my-secret", "my-role", false)); } [Fact] - public void Should_rename_client() + public void Should_update_client_with_role() { - var clients_1 = clients_0.Rename("1", "new-name"); + var client_1 = clients_0.Update("1", role: Role.Reader); - clients_1["1"].Should().BeEquivalentTo(new AppClient("new-name", "my-secret", Role.Editor)); + client_1["1"].Should().BeEquivalentTo(new AppClient("1", "my-secret", Role.Reader, false)); } [Fact] - public void Should_return_same_clients_if_client_is_updated_with_the_same_values() + public void Should_update_client_with_name() { - var clients_1 = clients_0.Rename("2", "2"); + var client_1 = clients_0.Update("1", name: "New-Name"); - Assert.Same(clients_0, clients_1); - } - - [Fact] - public void Should_return_same_clients_if_client_to_rename_not_found() - { - var clients_1 = clients_0.Rename("2", "new-name"); - - Assert.Same(clients_0, clients_1); + client_1["1"].Should().BeEquivalentTo(new AppClient("New-Name", "my-secret", Role.Editor, false)); } [Fact] - public void Should_update_client() + public void Should_update_client_with_allow_anonymous() { - var client_1 = clients_0.Update("1", Role.Reader); + var client_1 = clients_0.Update("1", allowAnonymous: true); - client_1["1"].Should().BeEquivalentTo(new AppClient("1", "my-secret", Role.Reader)); + client_1["1"].Should().BeEquivalentTo(new AppClient("1", "my-secret", Role.Editor, true)); } [Fact] public void Should_return_same_clients_if_client_to_update_not_found() { - var clients_1 = clients_0.Update("2", Role.Reader); + var clients_1 = clients_0.Update("2", role: Role.Reader); Assert.Same(clients_0, clients_1); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index e6b0e5794..e9d17f017 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs @@ -406,8 +406,7 @@ namespace Squidex.Domain.Apps.Entities.Apps LastEvents .ShouldHaveSameEvents( - CreateEvent(new AppClientRenamed { Id = clientId, Name = clientNewName }), - CreateEvent(new AppClientUpdated { Id = clientId, Role = Role.Developer }) + CreateEvent(new AppClientUpdated { Id = clientId, Name = clientNewName, Role = Role.Developer }) ); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs index 80bf6162f..64d478b6a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppClientsTests.cs @@ -95,17 +95,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards Assert.Throws(() => GuardAppClients.CanUpdate(clients_0, command, Roles.Empty)); } - [Fact] - public void UpdateClient_should_throw_exception_if_client_has_no_name_and_role() - { - var command = new UpdateClient { Id = "ios" }; - - var clients_1 = clients_0.Add("ios", "secret"); - - ValidationAssert.Throws(() => GuardAppClients.CanUpdate(clients_1, command, roles), - new ValidationError("Either name or role must be defined.", "Name", "Role")); - } - [Fact] public void UpdateClient_should_throw_exception_if_client_has_invalid_role() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs index ed688e8f4..59cb5572e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Guards/GuardAppRolesTests.cs @@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Guards var command = new DeleteRole { Name = roleName }; - ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients.Add("1", new AppClient("client", "1", roleName))), + ValidationAssert.Throws(() => GuardAppRoles.CanDelete(roles_1, command, contributors, clients.Add("1", new AppClient("client", "1", roleName, false))), new ValidationError("Cannot remove a role when a client is assigned.")); } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs index 2b25a85b0..b0c06d516 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -34,16 +34,12 @@ namespace Squidex.Web.Pipeline private readonly ActionContext actionContext; private readonly ActionExecutingContext actionExecutingContext; private readonly ActionExecutionDelegate next; - private readonly ClaimsIdentity userIdentiy = new ClaimsIdentity(); - private readonly ClaimsPrincipal user; private readonly string appName = "my-app"; private readonly AppResolver sut; private bool isNextCalled; public AppResolverTests() { - user = new ClaimsPrincipal(userIdentiy); - actionContext = new ActionContext(httpContext, new RouteData(), new ActionDescriptor { EndpointMetadata = new List() @@ -51,7 +47,6 @@ namespace Squidex.Web.Pipeline actionExecutingContext = new ActionExecutingContext(actionContext, new List(), new Dictionary(), this); actionExecutingContext.HttpContext = httpContext; - actionExecutingContext.HttpContext.User = user; actionExecutingContext.RouteData.Values["app"] = appName; next = () => @@ -67,6 +62,8 @@ namespace Squidex.Web.Pipeline [Fact] public async Task Should_return_not_found_if_app_not_found() { + SetupUser(); + A.CallTo(() => appProvider.GetAppAsync(appName, false)) .Returns(Task.FromResult(null)); @@ -76,13 +73,31 @@ namespace Squidex.Web.Pipeline Assert.False(isNextCalled); } + [Fact] + public async Task Should_return_401_if_user_is_anonymous() + { + var user = SetupUser(null); + + var app = CreateApp(appName); + + A.CallTo(() => appProvider.GetAppAsync(appName, false)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.IsType(actionExecutingContext.Result); + Assert.False(isNextCalled); + } + [Fact] public async Task Should_resolve_app_from_user() { + var user = SetupUser(); + var app = CreateApp(appName, appUser: "user1"); - userIdentiy.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); - userIdentiy.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); + user.AddClaim(new Claim(OpenIdClaims.Subject, "user1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.my-app")); A.CallTo(() => appProvider.GetAppAsync(appName, true)) .Returns(app); @@ -97,9 +112,28 @@ namespace Squidex.Web.Pipeline [Fact] public async Task Should_resolve_app_from_client() { + var user = SetupUser(); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); + var app = CreateApp(appName, appClient: "client1"); - userIdentiy.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); + A.CallTo(() => appProvider.GetAppAsync(appName, true)) + .Returns(app); + + await sut.OnActionExecutionAsync(actionExecutingContext, next); + + Assert.Same(app, httpContext.Context().App); + Assert.True(user.Claims.Count() > 2); + Assert.True(isNextCalled); + } + + [Fact] + public async Task Should_resolve_app_from_anonymous_client() + { + var user = SetupUser(); + + var app = CreateApp(appName, appClient: "client1", allowAnonymous: true); A.CallTo(() => appProvider.GetAppAsync(appName, true)) .Returns(app); @@ -112,12 +146,14 @@ namespace Squidex.Web.Pipeline } [Fact] - public async Task Should_resolve_app_if_anonymous_but_not_permissions() + public async Task Should_resolve_app_if_action_allows_anonymous_but_user_has_no_permissions() { - var app = CreateApp(appName); + var user = SetupUser(); - userIdentiy.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); - userIdentiy.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + + var app = CreateApp(appName); actionContext.ActionDescriptor.EndpointMetadata.Add(new AllowAnonymousAttribute()); @@ -134,10 +170,12 @@ namespace Squidex.Web.Pipeline [Fact] public async Task Should_return_not_found_if_user_has_no_permissions() { - var app = CreateApp(appName); + var user = SetupUser(); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); + user.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); - userIdentiy.AddClaim(new Claim(OpenIdClaims.ClientId, $"{appName}:client1")); - userIdentiy.AddClaim(new Claim(SquidexClaimTypes.Permissions, "squidex.apps.other-app")); + var app = CreateApp(appName); A.CallTo(() => appProvider.GetAppAsync(appName, false)) .Returns(app); @@ -151,9 +189,11 @@ namespace Squidex.Web.Pipeline [Fact] public async Task Should_return_not_found_if_client_is_from_another_app() { - var app = CreateApp(appName, appClient: "client1"); + var user = SetupUser(); + + user.AddClaim(new Claim(OpenIdClaims.ClientId, "other:client1")); - userIdentiy.AddClaim(new Claim(OpenIdClaims.ClientId, "other:client1")); + var app = CreateApp(appName, appClient: "client1"); A.CallTo(() => appProvider.GetAppAsync(appName, false)) .Returns(app); @@ -177,7 +217,17 @@ namespace Squidex.Web.Pipeline .MustNotHaveHappened(); } - private static IAppEntity CreateApp(string name, string? appUser = null, string? appClient = null) + private ClaimsIdentity SetupUser(string? type = "OIDC") + { + var userIdentity = new ClaimsIdentity(type); + var userPrincipal = new ClaimsPrincipal(userIdentity); + + actionExecutingContext.HttpContext.User = userPrincipal; + + return userIdentity; + } + + private static IAppEntity CreateApp(string name, string? appUser = null, string? appClient = null, bool? allowAnonymous = null) { var appEntity = A.Fake(); @@ -195,7 +245,7 @@ namespace Squidex.Web.Pipeline if (appClient != null) { A.CallTo(() => appEntity.Clients) - .Returns(AppClients.Empty.Add(appClient, "secret")); + .Returns(AppClients.Empty.Add(appClient, "secret").Update(appClient, allowAnonymous: allowAnonymous)); } else { diff --git a/frontend/app/features/settings/pages/clients/client.component.html b/frontend/app/features/settings/pages/clients/client.component.html index 5e9869ac5..491cabef6 100644 --- a/frontend/app/features/settings/pages/clients/client.component.html +++ b/frontend/app/features/settings/pages/clients/client.component.html @@ -64,6 +64,25 @@
+
+
+
+ + + +
+ + + Allow access to the API without an access token to all resources that are configured via the role of this client. Do not give more than one client anonymous access. + +
+
+
diff --git a/frontend/app/features/settings/pages/clients/client.component.ts b/frontend/app/features/settings/pages/clients/client.component.ts index 044dba6c6..1b8cb4cfb 100644 --- a/frontend/app/features/settings/pages/clients/client.component.ts +++ b/frontend/app/features/settings/pages/clients/client.component.ts @@ -37,6 +37,10 @@ export class ClientComponent { this.clientsState.update(this.client, { role }); } + public updateAccess(allowAnonymous: boolean) { + this.clientsState.update(this.client, { allowAnonymous }); + } + public rename(name: string) { this.clientsState.update(this.client, { name }); } diff --git a/frontend/app/shared/services/clients.service.spec.ts b/frontend/app/shared/services/clients.service.spec.ts index 6c43c546a..6ef1eac31 100644 --- a/frontend/app/shared/services/clients.service.spec.ts +++ b/frontend/app/shared/services/clients.service.spec.ts @@ -165,6 +165,7 @@ describe('ClientsService', () => { name: `Client ${id}`, role: `Role${id}`, secret: `secret${id}`, + allowAnonymous: true, _links: { update: { method: 'PUT', href: `/clients/id${id}` } } @@ -191,5 +192,5 @@ export function createClient(id: number) { update: { method: 'PUT', href: `/clients/id${id}` } }; - return new ClientDto(links, `id${id}`, `Client ${id}`, `secret${id}`, `Role${id}`); + return new ClientDto(links, `id${id}`, `Client ${id}`, `secret${id}`, `Role${id}`, true); } \ No newline at end of file diff --git a/frontend/app/shared/services/clients.service.ts b/frontend/app/shared/services/clients.service.ts index e90051fdc..2f065386a 100644 --- a/frontend/app/shared/services/clients.service.ts +++ b/frontend/app/shared/services/clients.service.ts @@ -29,7 +29,8 @@ export class ClientDto { public readonly id: string, public readonly name: string, public readonly secret: string, - public readonly role: string + public readonly role: string, + public readonly allowAnonymous: boolean ) { this._links = links; @@ -53,6 +54,7 @@ export interface CreateClientDto { export interface UpdateClientDto { readonly name?: string; readonly role?: string; + readonly allowAnonymous?: boolean; } @Injectable() @@ -144,7 +146,8 @@ function parseClients(response: any): ClientsPayload { item.id, item.name || item.id, item.secret, - item.role)); + item.role, + item.allowAnonymous)); const _links = response._links; From 0fe15cbf10d27ee26d1297b6d63ec425a18b1eeb Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 14 Jul 2020 14:25:21 +0200 Subject: [PATCH 02/68] Error simulation (#545) * Error simulation * Improved tests. * Increase test verbosity * Disable slow test. --- Dockerfile | 2 +- .../src/Squidex.Web/ApiExceptionConverter.cs | 73 +++----- backend/src/Squidex.Web/ErrorDto.cs | 4 +- .../Pipeline/RequestExceptionMiddleware.cs | 53 +++++- .../wwwroot/scripts/editor-json-schema.html | 85 +++++++++ .../Orleans/PubSubTests.cs | 1 + .../RequestExceptionMiddlewareTests.cs | 172 ++++++++++++++++++ 7 files changed, 340 insertions(+), 50 deletions(-) create mode 100644 backend/src/Squidex/wwwroot/scripts/editor-json-schema.html create mode 100644 backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs diff --git a/Dockerfile b/Dockerfile index cbcaac61c..c948abd84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ RUN dotnet restore COPY backend . # Test Backend -RUN dotnet test --no-restore --filter Category!=Dependencies +RUN dotnet test --no-restore --filter Category!=Dependencies -v n # Publish RUN dotnet publish --no-restore src/Squidex/Squidex.csproj --output /build/ --configuration Release -p:version=$SQUIDEX__VERSION diff --git a/backend/src/Squidex.Web/ApiExceptionConverter.cs b/backend/src/Squidex.Web/ApiExceptionConverter.cs index bf496b73e..a9fadd03f 100644 --- a/backend/src/Squidex.Web/ApiExceptionConverter.cs +++ b/backend/src/Squidex.Web/ApiExceptionConverter.cs @@ -34,11 +34,20 @@ namespace Squidex.Web [500] = "https://tools.ietf.org/html/rfc7231#section-6.6.1" }; + public static (ErrorDto Error, bool WellKnown) ToErrorDto(int statusCode, HttpContext? httpContext) + { + var error = new ErrorDto { StatusCode = statusCode }; + + Enrich(httpContext, error); + + return (error, true); + } + public static (ErrorDto Error, bool WellKnown) ToErrorDto(this ProblemDetails problem, HttpContext? httpContext) { Guard.NotNull(problem, nameof(problem)); - var error = new ErrorDto { Message = problem.Title, StatusCode = problem.Status }; + var error = CreateError(problem.Status ?? 500, problem.Title); Enrich(httpContext, error); @@ -60,10 +69,12 @@ namespace Squidex.Web { error.TraceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier; - if (error.StatusCode.HasValue) + if (error.StatusCode == 0) { - error.Type = Links.GetOrDefault(error.StatusCode.Value); + error.StatusCode = 500; } + + error.Type = Links.GetOrDefault(error.StatusCode); } private static (ErrorDto Error, bool WellKnown) CreateError(Exception exception) @@ -71,64 +82,38 @@ namespace Squidex.Web switch (exception) { case ValidationException ex: - return (new ErrorDto - { - StatusCode = 400, - Message = ex.Summary, - Details = ToDetails(ex) - }, true); + return (CreateError(400, ex.Summary, ToDetails(ex)), true); case DomainObjectNotFoundException _: - return (new ErrorDto - { - StatusCode = 404, - Message = null! - }, true); + return (CreateError(404), true); case DomainObjectVersionException _: - return (new ErrorDto - { - StatusCode = 412, - Message = exception.Message - }, true); + return (CreateError(412, exception.Message), true); case DomainForbiddenException _: - return (new ErrorDto - { - StatusCode = 403, - Message = exception.Message - }, true); + return (CreateError(403, exception.Message), true); case DomainException _: - return (new ErrorDto - { - StatusCode = 400, - Message = exception.Message - }, true); + return (CreateError(400, exception.Message), true); case SecurityException _: - return (new ErrorDto - { - StatusCode = 403, - Message = "Forbidden" - }, false); + return (CreateError(403), false); case DecoderFallbackException _: - return (new ErrorDto - { - StatusCode = 400, - Message = exception.Message - }, true); + return (CreateError(400, exception.Message), true); default: - return (new ErrorDto - { - StatusCode = 500, - Message = "Server Error" - }, false); + return (CreateError(500), false); } } + private static ErrorDto CreateError(int status, string? message = null, string[]? details = null) + { + var error = new ErrorDto { StatusCode = status, Message = message, Details = details }; + + return error; + } + private static string[] ToDetails(ValidationException ex) { return ex.Errors.Select(e => diff --git a/backend/src/Squidex.Web/ErrorDto.cs b/backend/src/Squidex.Web/ErrorDto.cs index fe3dfb892..e659674e3 100644 --- a/backend/src/Squidex.Web/ErrorDto.cs +++ b/backend/src/Squidex.Web/ErrorDto.cs @@ -13,7 +13,7 @@ namespace Squidex.Web { [Required] [Display(Description = "Error message.")] - public string Message { get; set; } + public string? Message { get; set; } [Display(Description = "The optional trace id.")] public string? TraceId { get; set; } @@ -25,6 +25,6 @@ namespace Squidex.Web public string[]? Details { get; set; } [Display(Description = "Status code of the http response.")] - public int? StatusCode { get; set; } = 400; + public int StatusCode { get; set; } = 400; } } diff --git a/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs index 2a4da249e..3a4abba81 100644 --- a/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/RequestExceptionMiddleware.cs @@ -7,7 +7,12 @@ using System; using System.Threading.Tasks; +using Grpc.Core; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Routing; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; @@ -15,27 +20,69 @@ namespace Squidex.Web.Pipeline { public sealed class RequestExceptionMiddleware : IMiddleware { + private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor(); + private static readonly RouteData EmptyRouteData = new RouteData(); + private readonly IActionResultExecutor resultWriter; private readonly ISemanticLog log; - public RequestExceptionMiddleware(ISemanticLog log) + public RequestExceptionMiddleware(IActionResultExecutor resultWriter, ISemanticLog log) { + Guard.NotNull(resultWriter, nameof(resultWriter)); Guard.NotNull(log, nameof(log)); + this.resultWriter = resultWriter; + this.log = log; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) { + if (context.Request.Query.TryGetValue("error", out var header) && int.TryParse(header, out var statusCode) && IsErrorStatusCode(statusCode)) + { + var (error, _) = ApiExceptionConverter.ToErrorDto(statusCode, context); + + await WriteErrorAsync(context, error); + return; + } + try { await next(context); } catch (Exception ex) { - log.LogError(ex, w => w.WriteProperty("messag", "An unexpected exception has occurred.")); + log.LogError(ex, w => w.WriteProperty("message", "An unexpected exception has occurred.")); + + if (!context.Response.HasStarted) + { + var (error, _) = ex.ToErrorDto(context); + + await WriteErrorAsync(context, error); + } + } + + if (IsErrorStatusCode(context.Response.StatusCode) && !context.Response.HasStarted) + { + var (error, _) = ApiExceptionConverter.ToErrorDto(context.Response.StatusCode, context); - context.Response.StatusCode = 500; + await WriteErrorAsync(context, error); } } + + private async Task WriteErrorAsync(HttpContext context, ErrorDto error) + { + var actionRouteData = context.GetRouteData() ?? EmptyRouteData; + var actionContext = new ActionContext(context, actionRouteData, EmptyActionDescriptor); + + await resultWriter.ExecuteAsync(actionContext, new ObjectResult(error) + { + StatusCode = error.StatusCode + }); + } + + private static bool IsErrorStatusCode(int statusCode) + { + return statusCode >= 400 && statusCode < 600; + } } } diff --git a/backend/src/Squidex/wwwroot/scripts/editor-json-schema.html b/backend/src/Squidex/wwwroot/scripts/editor-json-schema.html new file mode 100644 index 000000000..281af4e8b --- /dev/null +++ b/backend/src/Squidex/wwwroot/scripts/editor-json-schema.html @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/PubSubTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/PubSubTests.cs index e24013483..fd46f42d0 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/PubSubTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/PubSubTests.cs @@ -14,6 +14,7 @@ using Xunit; namespace Squidex.Infrastructure.Orleans { + [Trait("Category", "Dependencies")] public class PubSubTests { [Fact] diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs new file mode 100644 index 000000000..4500ce80d --- /dev/null +++ b/backend/tests/Squidex.Web.Tests/Pipeline/RequestExceptionMiddlewareTests.cs @@ -0,0 +1,172 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Elasticsearch.Net; +using FakeItEasy; +using GraphQL; +using Grpc.Core.Logging; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Xunit; + +namespace Squidex.Web.Pipeline +{ + public class RequestExceptionMiddlewareTests + { + private readonly ISemanticLog log = A.Fake(); + private readonly IActionResultExecutor resultWriter = A.Fake>(); + private readonly IHttpResponseFeature responseFeature = A.Fake(); + private readonly HttpContext httpContext = new DefaultHttpContext(); + private readonly RequestDelegate next; + private readonly RequestExceptionMiddleware sut; + private bool isNextCalled; + + public RequestExceptionMiddlewareTests() + { + next = new RequestDelegate(context => + { + isNextCalled = true; + + return Task.CompletedTask; + }); + + httpContext.Features.Set(responseFeature); + + sut = new RequestExceptionMiddleware(resultWriter, log); + } + + [Fact] + public async Task Should_write_test_error_if_valid_status_code() + { + httpContext.Request.QueryString = new QueryString("?error=412"); + + await sut.InvokeAsync(httpContext, next); + + Assert.False(isNextCalled); + + A.CallTo(() => resultWriter.ExecuteAsync(A._, + A.That.Matches(x => x.StatusCode == 412 && x.Value is ErrorDto))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_test_error_if_invalid_status_code() + { + httpContext.Request.QueryString = new QueryString("?error=hello"); + + await sut.InvokeAsync(httpContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => resultWriter.ExecuteAsync(A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_not_test_error_if_invalid_error_status_code() + { + httpContext.Request.QueryString = new QueryString("?error=99"); + + await sut.InvokeAsync(httpContext, next); + + Assert.True(isNextCalled); + + A.CallTo(() => resultWriter.ExecuteAsync(A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_handle_exception() + { + var failingNext = new RequestDelegate(context => + { + throw new InvalidOperationException(); + }); + + await sut.InvokeAsync(httpContext, failingNext); + + A.CallTo(() => resultWriter.ExecuteAsync(A._, + A.That.Matches(x => x.StatusCode == 500 && x.Value is ErrorDto))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_log_exception() + { + var ex = new InvalidOperationException(); + + var failingNext = new RequestDelegate(context => + { + throw ex; + }); + + await sut.InvokeAsync(httpContext, failingNext); + + A.CallTo(() => log.Log(SemanticLogLevel.Error, ex, A._)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_handle_exception_if_response_body_written() + { + A.CallTo(() => responseFeature.HasStarted) + .Returns(true); + + var failingNext = new RequestDelegate(context => + { + throw new InvalidOperationException(); + }); + + await sut.InvokeAsync(httpContext, failingNext); + + A.CallTo(() => resultWriter.ExecuteAsync(A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_handle_error_status_code() + { + var failingNext = new RequestDelegate(context => + { + context.Response.StatusCode = 412; + + return Task.CompletedTask; + }); + + await sut.InvokeAsync(httpContext, failingNext); + + A.CallTo(() => resultWriter.ExecuteAsync(A._, + A.That.Matches(x => x.StatusCode == 412 && x.Value is ErrorDto))) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_not_handle_error_status_code_if_response_body_written() + { + A.CallTo(() => responseFeature.HasStarted) + .Returns(true); + + var failingNext = new RequestDelegate(context => + { + context.Response.StatusCode = 412; + + return Task.CompletedTask; + }); + + await sut.InvokeAsync(httpContext, failingNext); + + A.CallTo(() => resultWriter.ExecuteAsync(A._, A._)) + .MustNotHaveHappened(); + } + } +} From 788ec0fccab5ec13c985708b2dc21b72726ece12 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 14 Jul 2020 16:57:21 +0200 Subject: [PATCH 03/68] Options for replicated cache. --- .../Caching/ReplicatedCache.cs | 35 ++++++++++- .../Caching/ReplicatedCacheOptions.cs | 14 +++++ .../Caching/SimplePubSub.cs | 6 +- .../Config/Domain/InfrastructureServices.cs | 2 + backend/src/Squidex/appsettings.json | 13 +++- .../Apps/Indexes/AppsIndexTests.cs | 4 +- .../Schemas/Indexes/SchemasIndexTests.cs | 4 +- .../Caching/ReplicatedCacheTests.cs | 59 ++++++++++++++++--- 8 files changed, 118 insertions(+), 19 deletions(-) create mode 100644 backend/src/Squidex.Infrastructure/Caching/ReplicatedCacheOptions.cs diff --git a/backend/src/Squidex.Infrastructure/Caching/ReplicatedCache.cs b/backend/src/Squidex.Infrastructure/Caching/ReplicatedCache.cs index e9ded4109..81c87e7ba 100644 --- a/backend/src/Squidex.Infrastructure/Caching/ReplicatedCache.cs +++ b/backend/src/Squidex.Infrastructure/Caching/ReplicatedCache.cs @@ -7,6 +7,7 @@ using System; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; namespace Squidex.Infrastructure.Caching { @@ -15,6 +16,7 @@ namespace Squidex.Infrastructure.Caching private readonly Guid instanceId = Guid.NewGuid(); private readonly IMemoryCache memoryCache; private readonly IPubSub pubSub; + private readonly ReplicatedCacheOptions options; public class InvalidateMessage { @@ -23,15 +25,22 @@ namespace Squidex.Infrastructure.Caching public string Key { get; set; } } - public ReplicatedCache(IMemoryCache memoryCache, IPubSub pubSub) + public ReplicatedCache(IMemoryCache memoryCache, IPubSub pubSub, IOptions options) { Guard.NotNull(memoryCache, nameof(memoryCache)); Guard.NotNull(pubSub, nameof(pubSub)); + Guard.NotNull(options, nameof(options)); this.memoryCache = memoryCache; this.pubSub = pubSub; - this.pubSub.Subscribe(OnMessage); + + if (options.Value.Enable) + { + this.pubSub.Subscribe(OnMessage); + } + + this.options = options.Value; } private void OnMessage(object message) @@ -44,6 +53,11 @@ namespace Squidex.Infrastructure.Caching public void Add(string key, object? value, TimeSpan expiration, bool invalidate) { + if (!options.Enable) + { + return; + } + memoryCache.Set(key, value, expiration); if (invalidate) @@ -54,6 +68,11 @@ namespace Squidex.Infrastructure.Caching public void Remove(string key) { + if (!options.Enable) + { + return; + } + memoryCache.Remove(key); Invalidate(key); @@ -61,11 +80,23 @@ namespace Squidex.Infrastructure.Caching public bool TryGetValue(string key, out object? value) { + if (!options.Enable) + { + value = null; + + return false; + } + return memoryCache.TryGetValue(key, out value); } private void Invalidate(string key) { + if (!options.Enable) + { + return; + } + pubSub.Publish(new InvalidateMessage { Key = key, Source = instanceId }); } } diff --git a/backend/src/Squidex.Infrastructure/Caching/ReplicatedCacheOptions.cs b/backend/src/Squidex.Infrastructure/Caching/ReplicatedCacheOptions.cs new file mode 100644 index 000000000..d1d1d9f48 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Caching/ReplicatedCacheOptions.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Caching +{ + public sealed class ReplicatedCacheOptions + { + public bool Enable { get; set; } + } +} diff --git a/backend/src/Squidex.Infrastructure/Caching/SimplePubSub.cs b/backend/src/Squidex.Infrastructure/Caching/SimplePubSub.cs index 7a30c2ddb..21383ee0d 100644 --- a/backend/src/Squidex.Infrastructure/Caching/SimplePubSub.cs +++ b/backend/src/Squidex.Infrastructure/Caching/SimplePubSub.cs @@ -10,11 +10,11 @@ using System.Collections.Generic; namespace Squidex.Infrastructure.Caching { - public sealed class SimplePubSub : IPubSub + public class SimplePubSub : IPubSub { private readonly List> handlers = new List>(); - public void Publish(object message) + public virtual void Publish(object message) { foreach (var handler in handlers) { @@ -22,7 +22,7 @@ namespace Squidex.Infrastructure.Caching } } - public void Subscribe(Action handler) + public virtual void Subscribe(Action handler) { Guard.NotNull(handler, nameof(handler)); diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index debaf394e..d855656a8 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -46,6 +46,8 @@ namespace Squidex.Config.Domain config.GetSection("urls")); services.Configure( config.GetSection("exposedConfiguration")); + services.Configure( + config.GetSection("caching:replicated")); services.AddSingletonAs(_ => SystemClock.Instance) .As(); diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index ccf79cf30..dc5c916d4 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -39,15 +39,22 @@ /* * Restrict the surrogate keys to 17KB. */ - "maxSurrogateKeysSize": 8000 + "maxSurrogateKeysSize": 8000, + + "replicated": { + /* + * Set to true to enable a replicated cache for app, schemas and rules. Increases performance but reduces consistency. + */ + "enable": true + } }, "languages": { /* * Use custom langauges where the key is the language code and the value is the english name. */ - "custom": "" - }, + "custom": "" + }, "rules": { /* diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs index e467d224a..38fb6b7aa 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs @@ -44,7 +44,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes A.CallTo(() => grainFactory.GetGrain(userId, null)) .Returns(indexByUser); - var cache = new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub()); + var cache = + new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(), + Options.Create(new ReplicatedCacheOptions { Enable = true })); sut = new AppsIndex(grainFactory, cache); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs index 834c919ed..434754998 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs @@ -39,7 +39,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes A.CallTo(() => grainFactory.GetGrain(appId.Id, null)) .Returns(index); - var cache = new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub()); + var cache = + new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(), + Options.Create(new ReplicatedCacheOptions { Enable = true })); sut = new SchemasIndex(grainFactory, cache); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Caching/ReplicatedCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Caching/ReplicatedCacheTests.cs index b354bb1a7..58a60a841 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Caching/ReplicatedCacheTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Caching/ReplicatedCacheTests.cs @@ -7,6 +7,7 @@ using System; using System.Threading.Tasks; +using FakeItEasy; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using Xunit; @@ -15,12 +16,13 @@ namespace Squidex.Infrastructure.Caching { public class ReplicatedCacheTests { - private readonly IPubSub pubSub = new SimplePubSub(); + private readonly IPubSub pubSub = A.Fake(options => options.CallsBaseMethods()); + private readonly ReplicatedCacheOptions options = new ReplicatedCacheOptions { Enable = true }; private readonly ReplicatedCache sut; public ReplicatedCacheTests() { - sut = new ReplicatedCache(CreateMemoryCache(), pubSub); + sut = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options)); } [Fact] @@ -36,7 +38,17 @@ namespace Squidex.Infrastructure.Caching } [Fact] - public async Task Should_not_served_when_expired() + public void Should_not_serve_from_cache_disabled() + { + options.Enable = false; + + sut.Add("Key", 1, TimeSpan.FromMilliseconds(100), true); + + AssertCache(sut, "Key", null, false); + } + + [Fact] + public async Task Should_not_serve_from_cache_when_expired() { sut.Add("Key", 1, TimeSpan.FromMilliseconds(1), true); @@ -48,8 +60,8 @@ namespace Squidex.Infrastructure.Caching [Fact] public void Should_not_invalidate_other_instances_when_item_added_and_flag_is_false() { - var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub); - var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub); + var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options)); + var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options)); cache1.Add("Key", 1, TimeSpan.FromMinutes(1), false); cache2.Add("Key", 2, TimeSpan.FromMinutes(1), false); @@ -61,8 +73,8 @@ namespace Squidex.Infrastructure.Caching [Fact] public void Should_invalidate_other_instances_when_item_added_and_flag_is_true() { - var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub); - var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub); + var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options)); + var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options)); cache1.Add("Key", 1, TimeSpan.FromMinutes(1), true); cache2.Add("Key", 2, TimeSpan.FromMinutes(1), true); @@ -74,8 +86,8 @@ namespace Squidex.Infrastructure.Caching [Fact] public void Should_invalidate_other_instances_when_item_removed() { - var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub); - var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub); + var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options)); + var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options)); cache1.Add("Key", 1, TimeSpan.FromMinutes(1), true); cache2.Remove("Key"); @@ -84,6 +96,35 @@ namespace Squidex.Infrastructure.Caching AssertCache(cache2, "Key", null, false); } + [Fact] + public void Should_send_invalidation_message_when_added_and_flag_is_true() + { + sut.Add("Key", 1, TimeSpan.FromMinutes(1), true); + + A.CallTo(() => pubSub.Publish(A._)) + .MustHaveHappened(); + } + + [Fact] + public void Should_not_send_invalidation_message_when_added_flag_is_false() + { + sut.Add("Key", 1, TimeSpan.FromMinutes(1), false); + + A.CallTo(() => pubSub.Publish(A._)) + .MustNotHaveHappened(); + } + + [Fact] + public void Should_not_send_invalidation_message_when_added_but_disabled() + { + options.Enable = false; + + sut.Add("Key", 1, TimeSpan.FromMinutes(1), true); + + A.CallTo(() => pubSub.Publish(A._)) + .MustNotHaveHappened(); + } + private static void AssertCache(IReplicatedCache cache, string key, object? expectedValue, bool expectedFound) { var found = cache.TryGetValue(key, out var value); From 5f0833161a1ee610e342d2eb71f4f23735c071b9 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 19 Jul 2020 11:48:43 +0200 Subject: [PATCH 04/68] Workflow diagram. (#546) --- .../src/Squidex.Web/ApiPermissionAttribute.cs | 2 +- .../ApiPermissionOrAnonymousAttribute.cs | 19 +++ .../Controllers/Apps/AppClientsController.cs | 8 +- .../Apps/AppContributorsController.cs | 6 +- .../Apps/AppLanguagesController.cs | 8 +- .../Controllers/Apps/AppPatternsController.cs | 8 +- .../Controllers/Apps/AppRolesController.cs | 10 +- .../Apps/AppWorkflowsController.cs | 8 +- .../Api/Controllers/Apps/AppsController.cs | 8 +- .../Assets/AssetFoldersController.cs | 10 +- .../Controllers/Assets/AssetsController.cs | 18 +-- .../Controllers/Backups/BackupsController.cs | 6 +- .../Comments/CommentsController.cs | 8 +- .../Contents/ContentsController.cs | 20 +-- .../Controllers/History/HistoryController.cs | 2 +- .../Api/Controllers/Ping/PingController.cs | 2 +- .../Controllers/Plans/AppPlansController.cs | 4 +- .../Api/Controllers/Rules/RulesController.cs | 24 +-- .../Schemas/SchemaFieldsController.cs | 38 ++--- .../Controllers/Schemas/SchemasController.cs | 22 +-- .../Controllers/Search/SearchController.cs | 2 +- .../Statistics/UsagesController.cs | 8 +- .../Translations/TranslationsController.cs | 2 +- frontend/app-config/webpack.config.js | 2 + .../app/features/settings/declarations.ts | 3 +- frontend/app/features/settings/module.ts | 2 + .../workflows/workflow-diagram.component.html | 5 + .../workflows/workflow-diagram.component.scss | 3 + .../workflows/workflow-diagram.component.ts | 151 ++++++++++++++++++ .../pages/workflows/workflow.component.html | 109 +++++++------ .../pages/workflows/workflow.component.ts | 6 + frontend/package-lock.json | 33 ++++ frontend/package.json | 5 + 33 files changed, 401 insertions(+), 161 deletions(-) create mode 100644 backend/src/Squidex.Web/ApiPermissionOrAnonymousAttribute.cs create mode 100644 frontend/app/features/settings/pages/workflows/workflow-diagram.component.html create mode 100644 frontend/app/features/settings/pages/workflows/workflow-diagram.component.scss create mode 100644 frontend/app/features/settings/pages/workflows/workflow-diagram.component.ts diff --git a/backend/src/Squidex.Web/ApiPermissionAttribute.cs b/backend/src/Squidex.Web/ApiPermissionAttribute.cs index c48fd41d8..557958579 100644 --- a/backend/src/Squidex.Web/ApiPermissionAttribute.cs +++ b/backend/src/Squidex.Web/ApiPermissionAttribute.cs @@ -15,7 +15,7 @@ using Squidex.Shared; namespace Squidex.Web { - public sealed class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter, IAllowAnonymous + public class ApiPermissionAttribute : AuthorizeAttribute, IAsyncActionFilter { private readonly string[] permissionIds; diff --git a/backend/src/Squidex.Web/ApiPermissionOrAnonymousAttribute.cs b/backend/src/Squidex.Web/ApiPermissionOrAnonymousAttribute.cs new file mode 100644 index 000000000..9175d9aa9 --- /dev/null +++ b/backend/src/Squidex.Web/ApiPermissionOrAnonymousAttribute.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Authorization; + +namespace Squidex.Web +{ + public class ApiPermissionOrAnonymousAttribute : ApiPermissionAttribute, IAllowAnonymous + { + public ApiPermissionOrAnonymousAttribute(params string[] ids) + : base(ids) + { + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index d3efa2fcc..17070ecf8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -42,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpGet] [Route("apps/{app}/clients/")] [ProducesResponseType(typeof(ClientsDto), 200)] - [ApiPermission(Permissions.AppClientsRead)] + [ApiPermissionOrAnonymous(Permissions.AppClientsRead)] [ApiCosts(0)] public IActionResult GetClients(string app) { @@ -73,7 +73,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPost] [Route("apps/{app}/clients/")] [ProducesResponseType(typeof(ClientsDto), 201)] - [ApiPermission(Permissions.AppClientsCreate)] + [ApiPermissionOrAnonymous(Permissions.AppClientsCreate)] [ApiCosts(1)] public async Task PostClient(string app, [FromBody] CreateClientDto request) { @@ -101,7 +101,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPut] [Route("apps/{app}/clients/{id}/")] [ProducesResponseType(typeof(ClientsDto), 200)] - [ApiPermission(Permissions.AppClientsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppClientsUpdate)] [ApiCosts(1)] public async Task PutClient(string app, string id, [FromBody] UpdateClientDto request) { @@ -127,7 +127,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpDelete] [Route("apps/{app}/clients/{id}/")] [ProducesResponseType(typeof(ClientsDto), 200)] - [ApiPermission(Permissions.AppClientsDelete)] + [ApiPermissionOrAnonymous(Permissions.AppClientsDelete)] [ApiCosts(1)] public async Task DeleteClient(string app, string id) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index f9699bee7..18a330b68 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpGet] [Route("apps/{app}/contributors/")] [ProducesResponseType(typeof(ContributorsDto), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public IActionResult GetContributors(string app) { @@ -75,7 +75,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPost] [Route("apps/{app}/contributors/")] [ProducesResponseType(typeof(ContributorsDto), 201)] - [ApiPermission(Permissions.AppContributorsAssign)] + [ApiPermissionOrAnonymous(Permissions.AppContributorsAssign)] [ApiCosts(1)] public async Task PostContributor(string app, [FromBody] AssignContributorDto request) { @@ -98,7 +98,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpDelete] [Route("apps/{app}/contributors/{id}/")] [ProducesResponseType(typeof(ContributorsDto), 200)] - [ApiPermission(Permissions.AppContributorsRevoke)] + [ApiPermissionOrAnonymous(Permissions.AppContributorsRevoke)] [ApiCosts(1)] public async Task DeleteContributor(string app, string id) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index 2ce73190e..4595058b8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -42,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpGet] [Route("apps/{app}/languages/")] [ProducesResponseType(typeof(AppLanguagesDto), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public IActionResult GetLanguages(string app) { @@ -69,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPost] [Route("apps/{app}/languages/")] [ProducesResponseType(typeof(AppLanguagesDto), 201)] - [ApiPermission(Permissions.AppLanguagesCreate)] + [ApiPermissionOrAnonymous(Permissions.AppLanguagesCreate)] [ApiCosts(1)] public async Task PostLanguage(string app, [FromBody] AddLanguageDto request) { @@ -94,7 +94,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPut] [Route("apps/{app}/languages/{language}/")] [ProducesResponseType(typeof(AppLanguagesDto), 200)] - [ApiPermission(Permissions.AppLanguagesUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppLanguagesUpdate)] [ApiCosts(1)] public async Task PutLanguage(string app, string language, [FromBody] UpdateLanguageDto request) { @@ -118,7 +118,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpDelete] [Route("apps/{app}/languages/{language}/")] [ProducesResponseType(typeof(AppLanguagesDto), 200)] - [ApiPermission(Permissions.AppLanguagesDelete)] + [ApiPermissionOrAnonymous(Permissions.AppLanguagesDelete)] [ApiCosts(1)] public async Task DeleteLanguage(string app, string language) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs index da84d6945..94ee344d3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs @@ -43,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpGet] [Route("apps/{app}/patterns/")] [ProducesResponseType(typeof(PatternsDto), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public IActionResult GetPatterns(string app) { @@ -70,7 +70,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPost] [Route("apps/{app}/patterns/")] [ProducesResponseType(typeof(PatternsDto), 201)] - [ApiPermission(Permissions.AppPatternsCreate)] + [ApiPermissionOrAnonymous(Permissions.AppPatternsCreate)] [ApiCosts(1)] public async Task PostPattern(string app, [FromBody] UpdatePatternDto request) { @@ -95,7 +95,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPut] [Route("apps/{app}/patterns/{id}/")] [ProducesResponseType(typeof(PatternsDto), 200)] - [ApiPermission(Permissions.AppPatternsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppPatternsUpdate)] [ApiCosts(1)] public async Task PutPattern(string app, Guid id, [FromBody] UpdatePatternDto request) { @@ -121,7 +121,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpDelete] [Route("apps/{app}/patterns/{id}/")] [ProducesResponseType(typeof(PatternsDto), 200)] - [ApiPermission(Permissions.AppPatternsDelete)] + [ApiPermissionOrAnonymous(Permissions.AppPatternsDelete)] [ApiCosts(1)] public async Task DeletePattern(string app, Guid id) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs index 62eafb071..53292cf69 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs @@ -43,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpGet] [Route("apps/{app}/roles/")] [ProducesResponseType(typeof(RolesDto), 200)] - [ApiPermission(Permissions.AppRolesRead)] + [ApiPermissionOrAnonymous(Permissions.AppRolesRead)] [ApiCosts(0)] public IActionResult GetRoles(string app) { @@ -68,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpGet] [Route("apps/{app}/roles/permissions")] [ProducesResponseType(typeof(string[]), 200)] - [ApiPermission(Permissions.AppRolesRead)] + [ApiPermissionOrAnonymous(Permissions.AppRolesRead)] [ApiCosts(0)] public IActionResult GetPermissions(string app) { @@ -95,7 +95,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPost] [Route("apps/{app}/roles/")] [ProducesResponseType(typeof(RolesDto), 201)] - [ApiPermission(Permissions.AppRolesCreate)] + [ApiPermissionOrAnonymous(Permissions.AppRolesCreate)] [ApiCosts(1)] public async Task PostRole(string app, [FromBody] AddRoleDto request) { @@ -120,7 +120,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPut] [Route("apps/{app}/roles/{roleName}/")] [ProducesResponseType(typeof(RolesDto), 200)] - [ApiPermission(Permissions.AppRolesUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppRolesUpdate)] [ApiCosts(1)] public async Task PutRole(string app, string roleName, [FromBody] UpdateRoleDto request) { @@ -144,7 +144,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpDelete] [Route("apps/{app}/roles/{roleName}/")] [ProducesResponseType(typeof(RolesDto), 200)] - [ApiPermission(Permissions.AppRolesDelete)] + [ApiPermissionOrAnonymous(Permissions.AppRolesDelete)] [ApiCosts(1)] public async Task DeleteRole(string app, string roleName) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs index 5a5099d4e..6dc5308b8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs @@ -44,7 +44,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpGet] [Route("apps/{app}/workflows/")] [ProducesResponseType(typeof(WorkflowsDto), 200)] - [ApiPermission(Permissions.AppWorkflowsRead)] + [ApiPermissionOrAnonymous(Permissions.AppWorkflowsRead)] [ApiCosts(0)] public IActionResult GetWorkflows(string app) { @@ -71,7 +71,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPost] [Route("apps/{app}/workflows/")] [ProducesResponseType(typeof(WorkflowsDto), 200)] - [ApiPermission(Permissions.AppWorkflowsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppWorkflowsUpdate)] [ApiCosts(1)] public async Task PostWorkflow(string app, [FromBody] AddWorkflowDto request) { @@ -96,7 +96,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPut] [Route("apps/{app}/workflows/{id}")] [ProducesResponseType(typeof(WorkflowsDto), 200)] - [ApiPermission(Permissions.AppWorkflowsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppWorkflowsUpdate)] [ApiCosts(1)] public async Task PutWorkflow(string app, Guid id, [FromBody] UpdateWorkflowDto request) { @@ -119,7 +119,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpDelete] [Route("apps/{app}/workflows/{id}")] [ProducesResponseType(typeof(WorkflowsDto), 200)] - [ApiPermission(Permissions.AppWorkflowsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppWorkflowsUpdate)] [ApiCosts(1)] public async Task DeleteWorkflow(string app, Guid id) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index 32392e1df..45afd435c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -152,7 +152,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPut] [Route("apps/{app}/")] [ProducesResponseType(typeof(AppDto), 200)] - [ApiPermission(Permissions.AppUpdateGeneral)] + [ApiPermissionOrAnonymous(Permissions.AppUpdateGeneral)] [ApiCosts(0)] public async Task UpdateApp(string app, [FromBody] UpdateAppDto request) { @@ -173,7 +173,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpPost] [Route("apps/{app}/image")] [ProducesResponseType(typeof(AppDto), 200)] - [ApiPermission(Permissions.AppUpdateImage)] + [ApiPermissionOrAnonymous(Permissions.AppUpdateImage)] [ApiCosts(0)] public async Task UploadImage(string app, IFormFile file) { @@ -264,7 +264,7 @@ namespace Squidex.Areas.Api.Controllers.Apps [HttpDelete] [Route("apps/{app}/image")] [ProducesResponseType(typeof(AppDto), 200)] - [ApiPermission(Permissions.AppUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppUpdate)] [ApiCosts(0)] public async Task DeleteImage(string app) { @@ -283,7 +283,7 @@ namespace Squidex.Areas.Api.Controllers.Apps /// [HttpDelete] [Route("apps/{app}/")] - [ApiPermission(Permissions.AppDelete)] + [ApiPermissionOrAnonymous(Permissions.AppDelete)] [ApiCosts(0)] public async Task DeleteApp(string app) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs index ba46c2964..585f402a9 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs @@ -48,7 +48,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [HttpGet] [Route("apps/{app}/assets/folders", Order = -1)] [ProducesResponseType(typeof(AssetsDto), 200)] - [ApiPermission(Permissions.AppAssetsRead)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAssetFolders(string app, [FromQuery] Guid parentId) { @@ -79,7 +79,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [Route("apps/{app}/assets/folders", Order = -1)] [ProducesResponseType(typeof(AssetDto), 201)] [AssetRequestSizeLimit] - [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] public async Task PostAssetFolder(string app, [FromBody] CreateAssetFolderDto request) { @@ -105,7 +105,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [Route("apps/{app}/assets/folders/{id}/", Order = -1)] [ProducesResponseType(typeof(AssetDto), 200)] [AssetRequestSizeLimit] - [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] public async Task PutAssetFolder(string app, Guid id, [FromBody] RenameAssetFolderDto request) { @@ -130,7 +130,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [Route("apps/{app}/assets/folders/{id}/parent", Order = -1)] [ProducesResponseType(typeof(AssetDto), 200)] [AssetRequestSizeLimit] - [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] public async Task PutAssetFolderParent(string app, Guid id, [FromBody] MoveAssetItemDto request) { @@ -152,7 +152,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpDelete] [Route("apps/{app}/assets/folders/{id}/", Order = -1)] - [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] public async Task DeleteAssetFolder(string app, Guid id) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index a6312f470..6f86b3d40 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -64,7 +64,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [HttpGet] [Route("apps/{app}/assets/tags")] [ProducesResponseType(typeof(Dictionary), 200)] - [ApiPermission(Permissions.AppAssetsRead)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetTags(string app) { @@ -92,7 +92,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [HttpGet] [Route("apps/{app}/assets/")] [ProducesResponseType(typeof(AssetsDto), 200)] - [ApiPermission(Permissions.AppAssetsRead)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAssets(string app, [FromQuery] Guid? parentId, [FromQuery] string? ids = null, [FromQuery] string? q = null) { @@ -121,7 +121,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [HttpPost] [Route("apps/{app}/assets/query")] [ProducesResponseType(typeof(AssetsDto), 200)] - [ApiPermission(Permissions.AppAssetsRead)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAssetsPost(string app, [FromBody] QueryDto query) { @@ -147,7 +147,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [HttpGet] [Route("apps/{app}/assets/{id}/")] [ProducesResponseType(typeof(AssetDto), 200)] - [ApiPermission(Permissions.AppAssetsRead)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsRead)] [ApiCosts(1)] public async Task GetAsset(string app, Guid id) { @@ -184,7 +184,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [Route("apps/{app}/assets/")] [ProducesResponseType(typeof(AssetDto), 201)] [AssetRequestSizeLimit] - [ApiPermission(Permissions.AppAssetsCreate)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsCreate)] [ApiCosts(1)] public async Task PostAsset(string app, [FromQuery] Guid parentId, IFormFile file) { @@ -214,7 +214,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [HttpPut] [Route("apps/{app}/assets/{id}/content/")] [ProducesResponseType(typeof(AssetDto), 200)] - [ApiPermission(Permissions.AppAssetsUpload)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsUpload)] [ApiCosts(1)] public async Task PutAssetContent(string app, Guid id, IFormFile file) { @@ -242,7 +242,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [Route("apps/{app}/assets/{id}/")] [ProducesResponseType(typeof(AssetDto), 200)] [AssetRequestSizeLimit] - [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] public async Task PutAsset(string app, Guid id, [FromBody] AnnotateAssetDto request) { @@ -267,7 +267,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [Route("apps/{app}/assets/{id}/parent")] [ProducesResponseType(typeof(AssetDto), 200)] [AssetRequestSizeLimit] - [ApiPermission(Permissions.AppAssetsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsUpdate)] [ApiCosts(1)] public async Task PutAssetParent(string app, Guid id, [FromBody] MoveAssetItemDto request) { @@ -289,7 +289,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// [HttpDelete] [Route("apps/{app}/assets/{id}/")] - [ApiPermission(Permissions.AppAssetsDelete)] + [ApiPermissionOrAnonymous(Permissions.AppAssetsDelete)] [ApiCosts(1)] public async Task DeleteAsset(string app, Guid id) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs index d73e03c19..faf4faf5b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs @@ -44,7 +44,7 @@ namespace Squidex.Areas.Api.Controllers.Backups [HttpGet] [Route("apps/{app}/backups/")] [ProducesResponseType(typeof(BackupJobsDto), 200)] - [ApiPermission(Permissions.AppBackupsRead)] + [ApiPermissionOrAnonymous(Permissions.AppBackupsRead)] [ApiCosts(0)] public async Task GetBackups(string app) { @@ -66,7 +66,7 @@ namespace Squidex.Areas.Api.Controllers.Backups [HttpPost] [Route("apps/{app}/backups/")] [ProducesResponseType(typeof(List), 200)] - [ApiPermission(Permissions.AppBackupsCreate)] + [ApiPermissionOrAnonymous(Permissions.AppBackupsCreate)] [ApiCosts(0)] public IActionResult PostBackup(string app) { @@ -87,7 +87,7 @@ namespace Squidex.Areas.Api.Controllers.Backups [HttpDelete] [Route("apps/{app}/backups/{id}")] [ProducesResponseType(typeof(List), 200)] - [ApiPermission(Permissions.AppBackupsDelete)] + [ApiPermissionOrAnonymous(Permissions.AppBackupsDelete)] [ApiCosts(0)] public async Task DeleteBackup(string app, Guid id) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs index 792f038a5..b64d9e867 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs @@ -49,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Comments [HttpGet] [Route("apps/{app}/comments/{commentsId}")] [ProducesResponseType(typeof(CommentsDto), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public async Task GetComments(string app, string commentsId, [FromQuery] long version = EtagVersion.Any) { @@ -79,7 +79,7 @@ namespace Squidex.Areas.Api.Controllers.Comments [HttpPost] [Route("apps/{app}/comments/{commentsId}")] [ProducesResponseType(typeof(CommentDto), 201)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public async Task PostComment(string app, string commentsId, [FromBody] UpsertCommentDto request) { @@ -106,7 +106,7 @@ namespace Squidex.Areas.Api.Controllers.Comments /// [HttpPut] [Route("apps/{app}/comments/{commentsId}/{commentId}")] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public async Task PutComment(string app, string commentsId, Guid commentId, [FromBody] UpsertCommentDto request) { @@ -127,7 +127,7 @@ namespace Squidex.Areas.Api.Controllers.Comments /// [HttpDelete] [Route("apps/{app}/comments/{commentsId}/{commentId}")] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public async Task DeleteComment(string app, string commentsId, Guid commentId) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 8d222b9d6..32daf8efb 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -268,7 +268,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpGet] [Route("content/{app}/{name}/{id}/{version}/")] - [ApiPermission(Permissions.AppContentsRead)] + [ApiPermissionOrAnonymous(Permissions.AppContentsRead)] [ApiCosts(1)] public async Task GetContentVersion(string app, string name, Guid id, int version) { @@ -297,7 +297,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPost] [Route("content/{app}/{name}/")] [ProducesResponseType(typeof(ContentsDto), 201)] - [ApiPermission(Permissions.AppContentsCreate)] + [ApiPermissionOrAnonymous(Permissions.AppContentsCreate)] [ApiCosts(1)] public async Task PostContent(string app, string name, [FromBody] NamedContentData request, [FromQuery] bool publish = false) { @@ -325,7 +325,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPost] [Route("content/{app}/{name}/import")] [ProducesResponseType(typeof(BulkResultDto[]), 200)] - [ApiPermission(Permissions.AppContentsCreate)] + [ApiPermissionOrAnonymous(Permissions.AppContentsCreate)] [ApiCosts(5)] public async Task PostContents(string app, string name, [FromBody] ImportContentsDto request) { @@ -356,7 +356,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPost] [Route("content/{app}/{name}/bulk")] [ProducesResponseType(typeof(BulkResultDto[]), 200)] - [ApiPermission(Permissions.AppContents)] + [ApiPermissionOrAnonymous(Permissions.AppContents)] [ApiCosts(5)] public async Task BulkContents(string app, string name, [FromBody] BulkUpdateDto request) { @@ -388,7 +388,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/")] [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PutContent(string app, string name, Guid id, [FromBody] NamedContentData request) { @@ -417,7 +417,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPatch] [Route("content/{app}/{name}/{id}/")] [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PatchContent(string app, string name, Guid id, [FromBody] NamedContentData request) { @@ -446,7 +446,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPut] [Route("content/{app}/{name}/{id}/status/")] [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppContentsUpdate)] [ApiCosts(1)] public async Task PutContentStatus(string app, string name, Guid id, ChangeStatusDto request) { @@ -473,7 +473,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpPost] [Route("content/{app}/{name}/{id}/draft/")] [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsVersionCreate)] + [ApiPermissionOrAnonymous(Permissions.AppContentsVersionCreate)] [ApiCosts(1)] public async Task CreateDraft(string app, string name, Guid id) { @@ -500,7 +500,7 @@ namespace Squidex.Areas.Api.Controllers.Contents [HttpDelete] [Route("content/{app}/{name}/{id}/draft/")] [ProducesResponseType(typeof(ContentsDto), 200)] - [ApiPermission(Permissions.AppContentsDelete)] + [ApiPermissionOrAnonymous(Permissions.AppContentsDelete)] [ApiCosts(1)] public async Task DeleteVersion(string app, string name, Guid id) { @@ -526,7 +526,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// [HttpDelete] [Route("content/{app}/{name}/{id}/")] - [ApiPermission(Permissions.AppContentsDelete)] + [ApiPermissionOrAnonymous(Permissions.AppContentsDelete)] [ApiCosts(1)] public async Task DeleteContent(string app, string name, Guid id) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs index dda051f83..a12a3f265 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/History/HistoryController.cs @@ -42,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.History [HttpGet] [Route("apps/{app}/history/")] [ProducesResponseType(typeof(HistoryEventDto), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0.1)] public async Task GetHistory(string app, string channel) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs index 6771038f2..fa83c3818 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Ping/PingController.cs @@ -68,7 +68,7 @@ namespace Squidex.Areas.Api.Controllers.Ping /// [HttpGet] [Route("ping/{app}/")] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public IActionResult GetAppPing(string app) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index 2194f56c7..b58a0de1d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -45,7 +45,7 @@ namespace Squidex.Areas.Api.Controllers.Plans [HttpGet] [Route("apps/{app}/plans/")] [ProducesResponseType(typeof(AppPlansDto), 200)] - [ApiPermission(Permissions.AppPlansRead)] + [ApiPermissionOrAnonymous(Permissions.AppPlansRead)] [ApiCosts(0)] public IActionResult GetPlans(string app) { @@ -74,7 +74,7 @@ namespace Squidex.Areas.Api.Controllers.Plans [HttpPut] [Route("apps/{app}/plan/")] [ProducesResponseType(typeof(PlanChangedDto), 200)] - [ApiPermission(Permissions.AppPlansChange)] + [ApiPermissionOrAnonymous(Permissions.AppPlansChange)] [ApiCosts(0)] public async Task PutPlan(string app, [FromBody] ChangePlanDto request) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index d34c674d0..e8b6f72f6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -85,7 +85,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [HttpGet] [Route("apps/{app}/rules/")] [ProducesResponseType(typeof(RulesDto), 200)] - [ApiPermission(Permissions.AppRulesRead)] + [ApiPermissionOrAnonymous(Permissions.AppRulesRead)] [ApiCosts(1)] public async Task GetRules(string app) { @@ -114,7 +114,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [HttpPost] [Route("apps/{app}/rules/")] [ProducesResponseType(typeof(RuleDto), 201)] - [ApiPermission(Permissions.AppRulesCreate)] + [ApiPermissionOrAnonymous(Permissions.AppRulesCreate)] [ApiCosts(1)] public async Task PostRule(string app, [FromBody] CreateRuleDto request) { @@ -135,7 +135,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [HttpDelete] [Route("apps/{app}/rules/run")] [ProducesResponseType(204)] - [ApiPermission(Permissions.AppRulesEvents)] + [ApiPermissionOrAnonymous(Permissions.AppRulesEvents)] [ApiCosts(1)] public async Task DeleteRuleRun(string app) { @@ -158,7 +158,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [HttpPut] [Route("apps/{app}/rules/{id}/")] [ProducesResponseType(typeof(RuleDto), 200)] - [ApiPermission(Permissions.AppRulesUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppRulesUpdate)] [ApiCosts(1)] public async Task PutRule(string app, Guid id, [FromBody] UpdateRuleDto request) { @@ -181,7 +181,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [HttpPut] [Route("apps/{app}/rules/{id}/enable/")] [ProducesResponseType(typeof(RuleDto), 200)] - [ApiPermission(Permissions.AppRulesDisable)] + [ApiPermissionOrAnonymous(Permissions.AppRulesDisable)] [ApiCosts(1)] public async Task EnableRule(string app, Guid id) { @@ -204,7 +204,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [HttpPut] [Route("apps/{app}/rules/{id}/disable/")] [ProducesResponseType(typeof(RuleDto), 200)] - [ApiPermission(Permissions.AppRulesDisable)] + [ApiPermissionOrAnonymous(Permissions.AppRulesDisable)] [ApiCosts(1)] public async Task DisableRule(string app, Guid id) { @@ -226,7 +226,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpPut] [Route("apps/{app}/rules/{id}/trigger/")] - [ApiPermission(Permissions.AppRulesEvents)] + [ApiPermissionOrAnonymous(Permissions.AppRulesEvents)] [ApiCosts(1)] public async Task TriggerRule(string app, Guid id) { @@ -248,7 +248,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [HttpPut] [Route("apps/{app}/rules/{id}/run")] [ProducesResponseType(204)] - [ApiPermission(Permissions.AppRulesEvents)] + [ApiPermissionOrAnonymous(Permissions.AppRulesEvents)] [ApiCosts(1)] public async Task PutRuleRun(string app, Guid id) { @@ -268,7 +268,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpDelete] [Route("apps/{app}/rules/{id}/")] - [ApiPermission(Permissions.AppRulesDelete)] + [ApiPermissionOrAnonymous(Permissions.AppRulesDelete)] [ApiCosts(1)] public async Task DeleteRule(string app, Guid id) { @@ -291,7 +291,7 @@ namespace Squidex.Areas.Api.Controllers.Rules [HttpGet] [Route("apps/{app}/rules/events/")] [ProducesResponseType(typeof(RuleEventsDto), 200)] - [ApiPermission(Permissions.AppRulesRead)] + [ApiPermissionOrAnonymous(Permissions.AppRulesRead)] [ApiCosts(0)] public async Task GetEvents(string app, [FromQuery] Guid? ruleId = null, [FromQuery] int skip = 0, [FromQuery] int take = 20) { @@ -313,7 +313,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpPut] [Route("apps/{app}/rules/events/{id}/")] - [ApiPermission(Permissions.AppRulesEvents)] + [ApiPermissionOrAnonymous(Permissions.AppRulesEvents)] [ApiCosts(0)] public async Task PutEvent(string app, Guid id) { @@ -340,7 +340,7 @@ namespace Squidex.Areas.Api.Controllers.Rules /// [HttpDelete] [Route("apps/{app}/rules/events/{id}/")] - [ApiPermission(Permissions.AppRulesEvents)] + [ApiPermissionOrAnonymous(Permissions.AppRulesEvents)] [ApiCosts(0)] public async Task DeleteEvent(string app, Guid id) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index 458701778..b9ad5c1b5 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -42,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPost] [Route("apps/{app}/schemas/{name}/fields/")] [ProducesResponseType(typeof(SchemaDetailsDto), 201)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PostField(string app, string name, [FromBody] AddFieldDto request) { @@ -69,7 +69,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPost] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/")] [ProducesResponseType(typeof(SchemaDetailsDto), 201)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PostNestedField(string app, string name, long parentId, [FromBody] AddFieldDto request) { @@ -94,7 +94,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/ui/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutSchemaUIFields(string app, string name, [FromBody] ConfigureUIFieldsDto request) { @@ -119,7 +119,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/ordering/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutSchemaFieldOrdering(string app, string name, [FromBody] ReorderFieldsDto request) { @@ -145,7 +145,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/ordering/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutNestedFieldOrdering(string app, string name, long parentId, [FromBody] ReorderFieldsDto request) { @@ -171,7 +171,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutField(string app, string name, long id, [FromBody] UpdateFieldDto request) { @@ -198,7 +198,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutNestedField(string app, string name, long parentId, long id, [FromBody] UpdateFieldDto request) { @@ -225,7 +225,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/lock/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task LockField(string app, string name, long id) { @@ -253,7 +253,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/lock/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task LockNestedField(string app, string name, long parentId, long id) { @@ -280,7 +280,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/hide/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task HideField(string app, string name, long id) { @@ -308,7 +308,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/hide/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task HideNestedField(string app, string name, long parentId, long id) { @@ -335,7 +335,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/show/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task ShowField(string app, string name, long id) { @@ -363,7 +363,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/show/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task ShowNestedField(string app, string name, long parentId, long id) { @@ -390,7 +390,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/enable/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task EnableField(string app, string name, long id) { @@ -418,7 +418,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/enable/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task EnableNestedField(string app, string name, long parentId, long id) { @@ -445,7 +445,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{id:long}/disable/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task DisableField(string app, string name, long id) { @@ -473,7 +473,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/disable/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task DisableNestedField(string app, string name, long parentId, long id) { @@ -498,7 +498,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpDelete] [Route("apps/{app}/schemas/{name}/fields/{id:long}/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task DeleteField(string app, string name, long id) { @@ -524,7 +524,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpDelete] [Route("apps/{app}/schemas/{name}/fields/{parentId:long}/nested/{id:long}/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task DeleteNestedField(string app, string name, long parentId, long id) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 68a678d88..b331b1bfe 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -44,7 +44,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpGet] [Route("apps/{app}/schemas/")] [ProducesResponseType(typeof(SchemasDto), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public async Task GetSchemas(string app) { @@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpGet] [Route("apps/{app}/schemas/{name}/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public async Task GetSchema(string app, string name) { @@ -115,7 +115,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPost] [Route("apps/{app}/schemas/")] [ProducesResponseType(typeof(SchemaDetailsDto), 201)] - [ApiPermission(Permissions.AppSchemasCreate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasCreate)] [ApiCosts(1)] public async Task PostSchema(string app, [FromBody] CreateSchemaDto request) { @@ -140,7 +140,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutSchema(string app, string name, [FromBody] UpdateSchemaDto request) { @@ -165,7 +165,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/sync")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutSchemaSync(string app, string name, [FromBody] SynchronizeSchemaDto request) { @@ -189,7 +189,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/category")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutCategory(string app, string name, [FromBody] ChangeCategoryDto request) { @@ -213,7 +213,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/preview-urls")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasUpdate)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] [ApiCosts(1)] public async Task PutPreviewUrls(string app, string name, [FromBody] ConfigurePreviewUrlsDto request) { @@ -238,7 +238,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/scripts/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasScripts)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasScripts)] [ApiCosts(1)] public async Task PutScripts(string app, string name, [FromBody] SchemaScriptsDto request) { @@ -261,7 +261,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/publish/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasPublish)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasPublish)] [ApiCosts(1)] public async Task PublishSchema(string app, string name) { @@ -284,7 +284,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas [HttpPut] [Route("apps/{app}/schemas/{name}/unpublish/")] [ProducesResponseType(typeof(SchemaDetailsDto), 200)] - [ApiPermission(Permissions.AppSchemasPublish)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasPublish)] [ApiCosts(1)] public async Task UnpublishSchema(string app, string name) { @@ -306,7 +306,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// [HttpDelete] [Route("apps/{app}/schemas/{name}/")] - [ApiPermission(Permissions.AppSchemasDelete)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasDelete)] [ApiCosts(1)] public async Task DeleteSchema(string app, string name) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs b/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs index b2c38aa77..c4351a411 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Search/SearchController.cs @@ -42,7 +42,7 @@ namespace Squidex.Areas.Api.Controllers.Search [HttpGet] [Route("apps/{app}/search/")] [ProducesResponseType(typeof(SearchResultDto[]), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public async Task GetSchemas(string app, [FromQuery] string? query = null) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index 8f42efb86..13b744ccd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -66,7 +66,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics [HttpGet] [Route("apps/{app}/usages/log/")] [ProducesResponseType(typeof(LogDownloadDto), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public IActionResult GetLog(string app) { @@ -93,7 +93,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics [HttpGet] [Route("apps/{app}/usages/calls/{fromDate}/{toDate}/")] [ProducesResponseType(typeof(CallsUsageDtoDto), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public async Task GetUsages(string app, DateTime fromDate, DateTime toDate) { @@ -122,7 +122,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics [HttpGet] [Route("apps/{app}/usages/storage/today/")] [ProducesResponseType(typeof(CurrentStorageDto), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public async Task GetCurrentStorageSize(string app) { @@ -149,7 +149,7 @@ namespace Squidex.Areas.Api.Controllers.Statistics [HttpGet] [Route("apps/{app}/usages/storage/{fromDate}/{toDate}/")] [ProducesResponseType(typeof(StorageUsagePerDateDto[]), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public async Task GetStorageSizes(string app, DateTime fromDate, DateTime toDate) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs index 5fc5cd8e4..6aa691b33 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Translations/TranslationsController.cs @@ -40,7 +40,7 @@ namespace Squidex.Areas.Api.Controllers.Translations [HttpPost] [Route("apps/{app}/translations/")] [ProducesResponseType(typeof(TranslationDto), 200)] - [ApiPermission(Permissions.AppCommon)] + [ApiPermissionOrAnonymous(Permissions.AppCommon)] [ApiCosts(0)] public async Task GetLanguages(string app, [FromBody] TranslateDto request) { diff --git a/frontend/app-config/webpack.config.js b/frontend/app-config/webpack.config.js index e2e13750f..609ef0da8 100644 --- a/frontend/app-config/webpack.config.js +++ b/frontend/app-config/webpack.config.js @@ -227,6 +227,8 @@ module.exports = function (env) { { from: './node_modules/font-awesome/css/font-awesome.min.css', to: 'dependencies/font-awesome/css/font-awesome.min.css' }, { from: './node_modules/font-awesome/fonts', to: 'dependencies/font-awesome/fonts' }, + + { from: './node_modules/vis-network/standalone/umd/vis-network.min.js', to: 'dependencies/vis-network.min.js' }, ], }), ], diff --git a/frontend/app/features/settings/declarations.ts b/frontend/app/features/settings/declarations.ts index 039727896..fc9ad5074 100644 --- a/frontend/app/features/settings/declarations.ts +++ b/frontend/app/features/settings/declarations.ts @@ -27,8 +27,9 @@ export * from './pages/roles/role-add-form.component'; export * from './pages/roles/role.component'; export * from './pages/roles/roles-page.component'; export * from './pages/workflows/workflow-add-form.component'; +export * from './pages/workflows/workflow-diagram.component'; export * from './pages/workflows/workflow-step.component'; export * from './pages/workflows/workflow-transition.component'; -export * from './pages/workflows/workflow.component'; export * from './pages/workflows/workflows-page.component'; +export * from './pages/workflows/workflow.component'; export * from './settings-area.component'; \ No newline at end of file diff --git a/frontend/app/features/settings/module.ts b/frontend/app/features/settings/module.ts index 4e6709dec..9e7eed837 100644 --- a/frontend/app/features/settings/module.ts +++ b/frontend/app/features/settings/module.ts @@ -11,6 +11,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HelpComponent, HistoryComponent, SqxFrameworkModule, SqxSharedModule } from '@app/shared'; import { BackupComponent, BackupsPageComponent, ClientAddFormComponent, ClientComponent, ClientConnectFormComponent, ClientsPageComponent, ContributorAddFormComponent, ContributorComponent, ContributorsPageComponent, ImportContributorsDialogComponent, LanguageAddFormComponent, LanguageComponent, LanguagesPageComponent, MorePageComponent, PatternComponent, PatternsPageComponent, PlanComponent, PlansPageComponent, RoleAddFormComponent, RoleComponent, RolesPageComponent, SettingsAreaComponent, WorkflowAddFormComponent, WorkflowComponent, WorkflowsPageComponent, WorkflowStepComponent, WorkflowTransitionComponent } from './declarations'; +import { WorkflowDiagramComponent } from './pages/workflows/workflow-diagram.component'; const routes: Routes = [ { @@ -195,6 +196,7 @@ const routes: Routes = [ SettingsAreaComponent, WorkflowAddFormComponent, WorkflowComponent, + WorkflowDiagramComponent, WorkflowsPageComponent, WorkflowStepComponent, WorkflowTransitionComponent diff --git a/frontend/app/features/settings/pages/workflows/workflow-diagram.component.html b/frontend/app/features/settings/pages/workflows/workflow-diagram.component.html new file mode 100644 index 000000000..341a2d4f9 --- /dev/null +++ b/frontend/app/features/settings/pages/workflows/workflow-diagram.component.html @@ -0,0 +1,5 @@ + + +
+
+
\ No newline at end of file diff --git a/frontend/app/features/settings/pages/workflows/workflow-diagram.component.scss b/frontend/app/features/settings/pages/workflows/workflow-diagram.component.scss new file mode 100644 index 000000000..51c331517 --- /dev/null +++ b/frontend/app/features/settings/pages/workflows/workflow-diagram.component.scss @@ -0,0 +1,3 @@ +div { + height: 500px; +} \ No newline at end of file diff --git a/frontend/app/features/settings/pages/workflows/workflow-diagram.component.ts b/frontend/app/features/settings/pages/workflows/workflow-diagram.component.ts new file mode 100644 index 000000000..b6bbd63b2 --- /dev/null +++ b/frontend/app/features/settings/pages/workflows/workflow-diagram.component.ts @@ -0,0 +1,151 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { AfterViewInit, Component, ElementRef, Input, OnChanges, OnDestroy, ViewChild } from '@angular/core'; +import { ResourceLoaderService, WorkflowDto } from '@app/shared'; + +declare var vis: any; + +@Component({ + selector: 'sqx-workflow-diagram', + styleUrls: ['./workflow-diagram.component.scss'], + templateUrl: './workflow-diagram.component.html' +}) +export class WorkflowDiagramComponent implements AfterViewInit, OnDestroy, OnChanges { + private network: any; + + @ViewChild('chartContainer', { static: false }) + public chartContainer: ElementRef; + + @Input() + public workflow: WorkflowDto; + + public isLoaded = false; + + constructor( + private readonly resourceLoader: ResourceLoaderService + ) { + } + + public ngOnDestroy() { + this.network?.destroy(); + } + + public ngOnChanges() { + this.updateNetwork(); + } + + public ngAfterViewInit() { + this.updateNetwork(); + } + + private updateNetwork() { + if (this.chartContainer?.nativeElement && this.workflow) { + this.resourceLoader.loadLocalScript('dependencies/vis-network.min.js') + .then(() => { + this.network?.destroy(); + + const nodes = new vis.DataSet(); + + for (const step of this.workflow.steps) { + let label = `${step.name}`; + + if (step.noUpdate) { + label += `\nPrevent updates`; + + if (step.noUpdateExpression) { + label += `\nwhen (${step.noUpdateExpression})`; + } + + if (step.noUpdateRoles && step.noUpdateRoles.length > 0) { + label += `\nfor ${step.noUpdateRoles.join(', ')}`; + } + } + + if (step.name === 'Published') { + label += `\nAvailable in the API`; + } + + const node: any = { id: step.name, label, color: step.color }; + + nodes.add(node); + } + + if (this.workflow.initial) { + nodes.add({ id: 0, color: '#000', label: 'Start', shape: 'dot', size: 3 }); + } + + const edges = new vis.DataSet(); + + for (const transition of this.workflow.transitions) { + let label = ''; + + if (transition.expression) { + label += `\nwhen (${transition.expression})`; + } + + if (transition.roles && transition.roles.length > 0) { + label += `\nfor ${transition.roles.join(', ')}`; + } + + const edge: any = { ...transition, label }; + + edges.add(edge); + } + + if (this.workflow.initial) { + edges.add({ from: 0, to: this.workflow.initial }); + } + + this.network = new vis.Network(this.chartContainer.nativeElement, { edges, nodes }, GRAPH_OPTIONS); + this.network.stabilize(); + this.network.fit(); + + this.isLoaded = true; + }); + } + } +} + +const GRAPH_OPTIONS = { + nodes: { + borderWidth: 2, + font: { + multi: true, align: 'left', + ital: { + size: 16 + }, + bold: { + size: 20 + }, + size: 16 + }, + shape: 'dot', + shadow: true + }, + edges: { + arrows: 'to', + font: { + multi: true, + ital: { + size: 16 + }, + size: 16 + }, + color: 'gray' + }, + layout: { + randomSeed: 2 + }, + physics: { + enabled: false, + repulsion: { + nodeDistance: 300 + }, + solver: 'repulsion' + } +}; \ No newline at end of file diff --git a/frontend/app/features/settings/pages/workflows/workflow.component.html b/frontend/app/features/settings/pages/workflows/workflow.component.html index 6c2a71048..7dcdc8dd7 100644 --- a/frontend/app/features/settings/pages/workflows/workflow.component.html +++ b/frontend/app/features/settings/pages/workflows/workflow.component.html @@ -32,6 +32,15 @@
+ +
@@ -39,57 +48,61 @@
- - -
- - -
- - - - Optional name for the workflow. - + + + +
+ + +
+ + + + Optional name for the workflow. + +
-
- -
- - -
- - - - - Restrict this workflow to specific schemas or keep it empty for all schemas. - + +
+ + +
+ + + + + Restrict this workflow to specific schemas or keep it empty for all schemas. + +
-
- - - + + + + + + - + + +
diff --git a/frontend/app/features/settings/pages/workflows/workflow.component.ts b/frontend/app/features/settings/pages/workflows/workflow.component.ts index f14dd157e..53a030edb 100644 --- a/frontend/app/features/settings/pages/workflows/workflow.component.ts +++ b/frontend/app/features/settings/pages/workflows/workflow.component.ts @@ -32,6 +32,8 @@ export class WorkflowComponent implements OnChanges { public isEditing = false; public isEditable = false; + public selectedTab = 0; + constructor( private readonly workflowsState: WorkflowsState ) { @@ -111,6 +113,10 @@ export class WorkflowComponent implements OnChanges { this.workflow = this.workflow.removeStep(step.name); } + public selectTab(tab: number) { + this.selectedTab = tab; + } + public trackByStep(index: number, step: WorkflowStep) { return step.name; } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 024061231..602cbacb4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -414,6 +414,14 @@ "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", "dev": true }, + "@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "requires": { + "@types/hammerjs": "^2.0.36" + } + }, "@ngtools/webpack": { "version": "9.0.6", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-9.0.6.tgz", @@ -534,6 +542,11 @@ "@types/node": "*" } }, + "@types/hammerjs": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.36.tgz", + "integrity": "sha512-7TUK/k2/QGpEAv/BCwSHlYu3NXZhQ9ZwBYpzr9tjlPIL2C5BeGhH3DmVavRx3ZNyELX5TLC91JTz/cen6AAtIQ==" + }, "@types/jasmine": { "version": "3.5.9", "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.5.9.tgz", @@ -7366,6 +7379,11 @@ } } }, + "keycharm": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.3.1.tgz", + "integrity": "sha512-zn47Ti4FJT9zdF+YBBLWJsfKF/fYQHkrYlBeB5Ez5e2PjW7SoIxr43yehAne2HruulIoid4NKZZxO0dHBygCtQ==" + }, "killable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", @@ -13402,6 +13420,21 @@ "extsprintf": "^1.2.0" } }, + "vis-data": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-6.6.1.tgz", + "integrity": "sha512-xmujDB2Dzf8T04rGFJ9OP4OA6zRVrz8R9hb0CVKryBrZRCljCga9JjSfgctA8S7wdZu7otDtUIwX4ZOgfV/57w==" + }, + "vis-network": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-7.10.0.tgz", + "integrity": "sha512-SYQ3y+cGqcFpPfaQbpGBXsaQjjbQq5xjt5dVztCD6x5ETQHlAYaFiYiWnVo0qoVI9lspfwoCfp4wOhW+rvId6Q==" + }, + "vis-util": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-4.3.2.tgz", + "integrity": "sha512-FIS75hhrzbX1qJwFVwVVm1q2/TEktJWjgWsV0T3E9AYC4PWyQCBKk2LgsSLi+O8NBi7gTe9D4K75MqdPTHrRnA==" + }, "vm-browserify": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0947f5b34..72d4e8a13 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,6 +26,7 @@ "@angular/platform-browser-dynamic": "9.0.6", "@angular/platform-server": "9.0.6", "@angular/router": "9.0.6", + "@egjs/hammerjs": "^2.0.17", "ace-builds": "^1.4.11", "angular-gridster2": "^9.2.0", "angular-mentions": "1.1.4", @@ -39,6 +40,7 @@ "graphiql": "0.17.5", "graphql": "14.6.0", "image-focus": "1.1.0", + "keycharm": "^0.3.1", "marked": "0.8.0", "mersenne-twister": "1.1.0", "mousetrap": "1.6.5", @@ -53,6 +55,9 @@ "slugify": "1.4.0", "tinymce": "^5.2.0", "tslib": "1.11.1", + "vis-data": "^6.6.1", + "vis-network": "^7.10.0", + "vis-util": "^4.3.2", "zone.js": "0.10.3" }, "devDependencies": { From 2298a4d9d8bd5b35f8f76c63201ea7b450c4424d Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 21 Jul 2020 10:37:54 +0200 Subject: [PATCH 05/68] Form model (#547) * Conditional field rules: Require, Hide, Disable --- Dockerfile | 2 +- .../Schemas/FieldNames.cs | 2 +- .../Schemas/FieldRule.cs | 43 + .../Schemas/FieldRuleAction.cs | 16 + .../Schemas/FieldRules.cs | 30 + .../Schemas/Json/JsonSchemaModel.cs | 8 + .../Schemas/Schema.cs | 28 + .../SchemaSynchronizer.cs | 5 + .../Text/Lucene/Storage/AssetIndexStorage.cs | 4 +- .../Schemas/Commands/ConfigureFieldRules.cs | 30 + .../Schemas/Commands/FieldRuleCommand.cs | 25 + .../Schemas/Commands/UpsertCommand.cs | 9 + .../Schemas/Guards/GuardSchema.cs | 37 + .../Schemas/SchemaDomainObject.cs | 15 + .../Schemas/State/SchemaState.cs | 7 + .../Squidex.Domain.Apps.Entities.csproj | 1 + .../Schemas/SchemaFieldRulesConfigured.cs | 18 + .../Squidex.Infrastructure.csproj | 1 + .../Validation/CustomValidators.cs | 19 + .../Schemas/Models/ConfigureFieldRulesDto.cs | 29 + .../Schemas/Models/FieldRuleDto.cs | 44 + .../Schemas/Models/SchemaDetailsDto.cs | 8 +- .../Controllers/Schemas/Models/SchemaDto.cs | 1 + .../Controllers/Schemas/SchemasController.cs | 28 +- .../Model/Schemas/SchemaTests.cs | 12 + .../SchemaSynchronizerTests.cs | 18 + .../Schemas/Guards/GuardSchemaTests.cs | 45 + .../Schemas/SchemaDomainObjectTests.cs | 27 + frontend/app-config/karma-test-shim.js | 2 +- .../event-consumers-page.component.ts | 2 +- .../pages/users/user-page.component.html | 10 +- .../pages/users/users-page.component.ts | 2 +- .../apps/pages/apps-page.component.ts | 2 +- .../apps/pages/news-dialog.component.ts | 2 +- .../assets/pages/asset-tags.component.ts | 2 +- .../pages/assets-filters-page.component.ts | 2 +- frontend/app/features/content/declarations.ts | 1 - frontend/app/features/content/module.ts | 3 +- .../content/content-field.component.html | 30 +- .../pages/content/content-field.component.ts | 55 +- .../content/content-history-page.component.ts | 2 +- .../pages/content/content-page.component.html | 6 +- .../pages/content/content-page.component.ts | 11 +- .../content/content-section.component.html | 40 +- .../content/content-section.component.ts | 21 +- .../pages/contents/contents-page.component.ts | 2 +- .../pages/schemas/schemas-page.component.ts | 2 +- .../shared/forms/array-editor.component.html | 23 +- .../shared/forms/array-editor.component.ts | 47 +- .../shared/forms/array-item.component.html | 7 +- .../shared/forms/array-item.component.ts | 27 +- .../shared/forms/array-section.component.html | 35 +- .../shared/forms/array-section.component.ts | 21 +- .../shared/forms/assets-editor.component.ts | 2 +- .../shared/forms/field-editor.component.html | 10 +- .../shared/forms/field-editor.component.ts | 17 +- .../forms/stock-photo-editor.component.ts | 2 +- .../content/shared/group-fields.pipe.ts | 45 - .../references/content-creator.component.html | 6 +- .../references/content-creator.component.ts | 2 +- .../references/content-selector.component.ts | 2 +- .../references/references-editor.component.ts | 2 +- .../events/rule-events-page.component.ts | 2 +- .../actions/generic-action.component.html | 2 +- .../rules/actions/generic-action.component.ts | 3 - .../pages/rules/rule-wizard.component.html | 18 +- .../rules/pages/rules/rules-page.component.ts | 2 +- .../asset-changed-trigger.component.html | 2 +- .../asset-changed-trigger.component.ts | 3 - .../triggers/comment-trigger.component.html | 2 +- .../triggers/comment-trigger.component.ts | 3 - .../content-changed-trigger.component.ts | 5 +- .../schema-changed-trigger.component.html | 2 +- .../schema-changed-trigger.component.ts | 3 - .../triggers/usage-trigger.component.html | 4 +- .../rules/triggers/usage-trigger.component.ts | 3 - frontend/app/features/schemas/declarations.ts | 1 + frontend/app/features/schemas/module.ts | 3 +- .../common/schema-edit-form.component.html | 8 +- .../common/schema-edit-form.component.ts | 12 +- .../export/schema-export-form.component.html | 2 +- .../schema/fields/field-wizard.component.html | 9 +- .../pages/schema/fields/field.component.html | 12 +- .../pages/schema/fields/field.component.scss | 3 +- .../pages/schema/fields/field.component.ts | 4 +- .../forms/field-form-common.component.html | 6 +- .../forms/field-form-common.component.ts | 5 +- .../fields/forms/field-form-ui.component.html | 18 +- .../fields/forms/field-form-ui.component.ts | 2 +- .../field-form-validation.component.html | 20 +- .../forms/field-form-validation.component.ts | 2 +- .../fields/forms/field-form.component.html | 7 +- .../fields/forms/field-form.component.ts | 11 +- .../schema/fields/schema-fields.component.ts | 4 +- .../types/array-validation.component.html | 2 +- .../types/array-validation.component.ts | 6 +- .../fields/types/assets-ui.component.html | 2 +- .../fields/types/assets-ui.component.ts | 6 +- .../types/assets-validation.component.html | 2 +- .../types/assets-validation.component.ts | 28 +- .../fields/types/boolean-ui.component.html | 6 +- .../fields/types/boolean-ui.component.ts | 6 +- .../types/boolean-validation.component.html | 2 +- .../types/boolean-validation.component.ts | 8 +- .../fields/types/date-time-ui.component.html | 6 +- .../fields/types/date-time-ui.component.ts | 4 +- .../types/date-time-validation.component.html | 2 +- .../types/date-time-validation.component.ts | 14 +- .../types/geolocation-ui.component.html | 4 +- .../fields/types/geolocation-ui.component.ts | 4 +- .../geolocation-validation.component.html | 2 +- .../types/geolocation-validation.component.ts | 2 +- .../fields/types/json-ui.component.html | 2 +- .../schema/fields/types/json-ui.component.ts | 2 +- .../types/json-validation.component.html | 2 +- .../fields/types/json-validation.component.ts | 2 +- .../fields/types/number-ui.component.html | 10 +- .../fields/types/number-ui.component.ts | 16 +- .../types/number-validation.component.html | 2 +- .../types/number-validation.component.ts | 12 +- .../fields/types/references-ui.component.html | 10 +- .../fields/types/references-ui.component.ts | 6 +- .../references-validation.component.html | 2 +- .../types/references-validation.component.ts | 10 +- .../fields/types/string-ui.component.html | 22 +- .../fields/types/string-ui.component.ts | 16 +- .../types/string-validation.component.html | 2 +- .../types/string-validation.component.ts | 34 +- .../fields/types/tags-ui.component.html | 8 +- .../schema/fields/types/tags-ui.component.ts | 6 +- .../types/tags-validation.component.html | 2 +- .../fields/types/tags-validation.component.ts | 6 +- .../schema-preview-urls-form.component.html | 32 +- .../schema-preview-urls-form.component.scss | 11 +- .../schema-preview-urls-form.component.ts | 20 +- .../schema-field-rules-form.component.html | 80 ++ .../schema-field-rules-form.component.scss | 25 + .../schema-field-rules-form.component.ts | 76 ++ .../pages/schema/schema-page.component.html | 3 +- .../pages/schemas/schema-form.component.html | 2 +- .../pages/schemas/schemas-page.component.ts | 2 +- .../pages/backups/backups-page.component.ts | 2 +- .../clients/client-add-form.component.html | 2 +- .../pages/clients/client.component.ts | 2 +- .../pages/clients/clients-page.component.ts | 2 +- .../contributors-page.component.ts | 2 +- .../pages/languages/language.component.ts | 2 +- .../languages/languages-page.component.ts | 2 +- .../pages/patterns/pattern.component.html | 6 +- .../pages/patterns/patterns-page.component.ts | 2 +- .../pages/plans/plans-page.component.ts | 2 +- .../pages/roles/role-add-form.component.html | 2 +- .../settings/pages/roles/role.component.html | 2 +- .../pages/roles/roles-page.component.ts | 2 +- .../workflow-add-form.component.html | 2 +- .../workflows/workflow-step.component.ts | 2 +- .../workflow-transition.component.ts | 2 +- .../pages/workflows/workflow.component.ts | 2 +- .../workflows/workflows-page.component.ts | 2 +- .../angular/forms/control-errors.component.ts | 14 +- .../framework/angular/forms/forms-helper.ts | 4 + .../angular/forms/string-form-control.ts | 2 +- frontend/app/framework/state.ts | 2 + .../shared/components/app-form.component.html | 2 +- .../assets/asset-dialog.component.html | 10 +- .../assets/asset-folder-dialog.component.html | 2 +- .../assets/asset-history.component.ts | 2 +- .../assets/asset-uploader.component.ts | 2 +- .../assets/assets-list.component.ts | 2 +- .../components/comments/comments.component.ts | 2 +- .../forms/language-selector.component.ts | 2 +- .../history/history-list.component.ts | 2 +- .../components/schema-category.component.ts | 2 +- .../components/search/query-list.component.ts | 2 +- .../search/search-form.component.html | 4 +- frontend/app/shared/internal.ts | 1 + .../shared/services/schemas.service.spec.ts | 36 +- .../app/shared/services/schemas.service.ts | 22 + frontend/app/shared/services/schemas.types.ts | 2 +- .../shared/state/contents.forms-helpers.ts | 101 +++ .../app/shared/state/contents.forms.spec.ts | 171 +++- frontend/app/shared/state/contents.forms.ts | 813 +++++++----------- .../shared/state/contents.forms.visitors.ts | 468 ++++++++++ frontend/app/shared/state/languages.forms.ts | 6 +- frontend/app/shared/state/schemas.forms.ts | 64 +- frontend/app/shared/state/schemas.state.ts | 10 +- .../pages/internal/apps-menu.component.ts | 2 +- 187 files changed, 2324 insertions(+), 1137 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRule.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRuleAction.cs create mode 100644 backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRules.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldRuleCommand.cs create mode 100644 backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldRulesConfigured.cs create mode 100644 backend/src/Squidex.Infrastructure/Validation/CustomValidators.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs create mode 100644 backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldRuleDto.cs delete mode 100644 frontend/app/features/content/shared/group-fields.pipe.ts create mode 100644 frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html create mode 100644 frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.scss create mode 100644 frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.ts create mode 100644 frontend/app/shared/state/contents.forms-helpers.ts create mode 100644 frontend/app/shared/state/contents.forms.visitors.ts diff --git a/Dockerfile b/Dockerfile index c948abd84..cbcaac61c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ RUN dotnet restore COPY backend . # Test Backend -RUN dotnet test --no-restore --filter Category!=Dependencies -v n +RUN dotnet test --no-restore --filter Category!=Dependencies # Publish RUN dotnet publish --no-restore src/Squidex/Squidex.csproj --output /build/ --configuration Release -p:version=$SQUIDEX__VERSION diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs index 1df458c5e..4bab0e67f 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldNames.cs @@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Core.Schemas { private static readonly List EmptyNames = new List(); - public static readonly FieldNames Empty = new FieldNames(new List()); + public static readonly FieldNames Empty = new FieldNames(EmptyNames); public FieldNames(params string[] fields) : base(fields?.ToList() ?? EmptyNames) diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRule.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRule.cs new file mode 100644 index 000000000..52e08eb43 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRule.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + [Equals(DoNotAddEqualityOperators = true)] + public sealed class FieldRule + { + public FieldRuleAction Action { get; } + + public string Field { get; } + + public string? Condition { get; } + + public FieldRule(FieldRuleAction action, string field, string? condition) + { + Guard.Enum(action, nameof(action)); + Guard.NotNullOrEmpty(field, nameof(field)); + + Action = action; + + Field = field; + + Condition = condition; + } + + public static FieldRule Disable(string field, string? condition = null) + { + return new FieldRule(FieldRuleAction.Disable, field, condition); + } + + public static FieldRule Hide(string field, string? condition = null) + { + return new FieldRule(FieldRuleAction.Hide, field, condition); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRuleAction.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRuleAction.cs new file mode 100644 index 000000000..8018ced32 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRuleAction.cs @@ -0,0 +1,16 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public enum FieldRuleAction + { + Disable, + Hide, + Require + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRules.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRules.cs new file mode 100644 index 000000000..b7bae685d --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/FieldRules.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Squidex.Domain.Apps.Core.Schemas +{ + public sealed class FieldRules : ReadOnlyCollection + { + private static readonly List EmptyRules = new List(); + + public static readonly FieldRules Empty = new FieldRules(EmptyRules); + + public FieldRules(params FieldRule[] fields) + : base(fields?.ToList() ?? EmptyRules) + { + } + + public FieldRules(IList list) + : base(list ?? EmptyRules) + { + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs index 70137af90..feddfd84e 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Json/JsonSchemaModel.cs @@ -42,6 +42,9 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json [JsonProperty] public FieldNames? FieldsInReferences { get; set; } + [JsonProperty] + public FieldRules? FieldRules { get; set; } + [JsonProperty] public JsonFieldModel[] Fields { get; set; } @@ -118,6 +121,11 @@ namespace Squidex.Domain.Apps.Core.Schemas.Json schema = schema.SetFieldsInReferences(FieldsInReferences); } + if (FieldRules?.Count > 0) + { + schema = schema.SetFieldRules(FieldRules); + } + if (PreviewUrls?.Count > 0) { schema = schema.SetPreviewUrls(PreviewUrls); diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs index 54cd45cc6..a87032c59 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/Schema.cs @@ -21,6 +21,7 @@ namespace Squidex.Domain.Apps.Core.Schemas private string category; private FieldNames fieldsInLists = FieldNames.Empty; private FieldNames fieldsInReferences = FieldNames.Empty; + private FieldRules fieldRules = FieldRules.Empty; private FieldCollection fields = FieldCollection.Empty; private IReadOnlyDictionary previewUrls = EmptyPreviewUrls; private SchemaScripts scripts = SchemaScripts.Empty; @@ -72,6 +73,11 @@ namespace Squidex.Domain.Apps.Core.Schemas get { return fields; } } + public FieldRules FieldRules + { + get { return fieldRules; } + } + public FieldNames FieldsInLists { get { return fieldsInLists; } @@ -192,6 +198,28 @@ namespace Squidex.Domain.Apps.Core.Schemas return SetFieldsInReferences(new FieldNames(names)); } + [Pure] + public Schema SetFieldRules(FieldRules rules) + { + rules ??= FieldRules.Empty; + + if (fieldRules.SetEquals(rules)) + { + return this; + } + + return Clone(clone => + { + clone.fieldRules = rules; + }); + } + + [Pure] + public Schema SetFieldRules(params FieldRule[] rules) + { + return SetFieldRules(new FieldRules(rules)); + } + [Pure] public Schema Publish() { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs index 37cd8387f..bc9d138c4 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs @@ -80,6 +80,11 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization { yield return E(new SchemaUIFieldsConfigured { FieldsInReferences = target.FieldsInReferences }); } + + if (!source.FieldRules.SetEquals(target.FieldRules)) + { + yield return E(new SchemaFieldRulesConfigured { FieldRules = target.FieldRules }); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/Storage/AssetIndexStorage.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/Storage/AssetIndexStorage.cs index 24703e09f..2d6fde2c2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/Storage/AssetIndexStorage.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Lucene/Storage/AssetIndexStorage.cs @@ -29,9 +29,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text.Lucene.Storage this.assetStore = assetStore; } - public async Task CreateDirectoryAsync(Guid schemaId) + public async Task CreateDirectoryAsync(Guid ownerId) { - var directoryInfo = new DirectoryInfo(Path.Combine(Path.GetTempPath(), "LocalIndices", schemaId.ToString())); + var directoryInfo = new DirectoryInfo(Path.Combine(Path.GetTempPath(), "LocalIndices", ownerId.ToString())); if (directoryInfo.Exists) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs new file mode 100644 index 000000000..a20df4232 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/ConfigureFieldRules.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class ConfigureFieldRules : SchemaCommand + { + public List? FieldRules { get; set; } + + public FieldRules ToFieldRules() + { + if (FieldRules?.Count > 0) + { + return new FieldRules(FieldRules.Select(x => x.ToFieldRule()).ToList()); + } + else + { + return Core.Schemas.FieldRules.Empty; + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldRuleCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldRuleCommand.cs new file mode 100644 index 000000000..61deac01f --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/FieldRuleCommand.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; + +namespace Squidex.Domain.Apps.Entities.Schemas.Commands +{ + public sealed class FieldRuleCommand + { + public FieldRuleAction Action { get; set; } + + public string Field { get; set; } + + public string? Condition { get; set; } + + public FieldRule ToFieldRule() + { + return new FieldRule(Action, Field, Condition); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs index 1dfcc95d2..00489c0d5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Commands/UpsertCommand.cs @@ -6,8 +6,10 @@ // ========================================================================== using System.Collections.Generic; +using System.Linq; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.Schemas; +using FieldRules = System.Collections.Generic.List; using SchemaFields = System.Collections.Generic.List; namespace Squidex.Domain.Apps.Entities.Schemas.Commands @@ -24,6 +26,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands public FieldNames? FieldsInLists { get; set; } + public FieldRules? FieldRules { get; set; } + public SchemaScripts? Scripts { get; set; } public SchemaProperties Properties { get; set; } @@ -59,6 +63,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Commands schema = schema.SetFieldsInReferences(FieldsInReferences); } + if (FieldRules != null) + { + schema = schema.SetFieldRules(FieldRules.Select(x => x.ToFieldRule()).ToArray()); + } + if (!string.IsNullOrWhiteSpace(Category)) { schema = schema.ChangeCategory(Category); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs index 5ac8e78b5..f90c38201 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs @@ -99,6 +99,16 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards }); } + public static void CanConfigureFieldRules(ConfigureFieldRules command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot configure field rules.", e => + { + ValidateFieldRules(command.FieldRules, nameof(command.FieldRules), e); + }); + } + public static void CanPublish(PublishSchema command) { Guard.NotNull(command, nameof(command)); @@ -155,6 +165,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards ValidateFieldNames(command, command.FieldsInLists, nameof(command.FieldsInLists), e, IsMetaField); ValidateFieldNames(command, command.FieldsInReferences, nameof(command.FieldsInReferences), e, IsNotAllowed); + + ValidateFieldRules(command.FieldRules, nameof(command.FieldRules), e); } private static void ValidateRootField(UpsertSchemaField field, string prefix, AddValidation e) @@ -290,6 +302,31 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards } } + private static void ValidateFieldRules(List? fieldRules, string path, AddValidation e) + { + if (fieldRules != null) + { + var ruleIndex = 0; + var rulePrefix = string.Empty; + + foreach (var fieldRule in fieldRules) + { + ruleIndex++; + rulePrefix = $"{path}[{ruleIndex}]"; + + if (string.IsNullOrWhiteSpace(fieldRule.Field)) + { + e(Not.Defined(nameof(fieldRule.Field)), $"{rulePrefix}.{nameof(fieldRule.Field)}"); + } + + if (!fieldRule.Action.IsEnumValue()) + { + e(Not.Valid(nameof(fieldRule.Action)), $"{rulePrefix}.{nameof(fieldRule.Action)}"); + } + } + } + } + private static void ValidateFieldNames(UpsertCommand command, FieldNames? fields, string path, AddValidation e, Func isAllowed) { if (fields != null) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs index 24d9659f9..efd84de50 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs @@ -188,6 +188,16 @@ namespace Squidex.Domain.Apps.Entities.Schemas return Snapshot; }); + case ConfigureFieldRules configureFieldRules: + return UpdateReturn(configureFieldRules, c => + { + GuardSchema.CanConfigureFieldRules(c); + + ConfigureFieldRules(c); + + return Snapshot; + }); + case ConfigureScripts configureScripts: return UpdateReturn(configureScripts, c => { @@ -325,6 +335,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas RaiseEvent(command, new SchemaScriptsConfigured()); } + public void ConfigureFieldRules(ConfigureFieldRules command) + { + RaiseEvent(command, new SchemaFieldRulesConfigured { FieldRules = command.ToFieldRules() }); + } + public void ChangeCategory(ChangeCategory command) { RaiseEvent(command, new SchemaCategoryChanged()); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs index 06e141d75..65879c991 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/State/SchemaState.cs @@ -101,6 +101,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.State break; } + case SchemaFieldRulesConfigured e: + { + SchemaDef = SchemaDef.SetFieldRules(e.FieldRules); + + break; + } + case SchemaPublished _: { SchemaDef = SchemaDef.Publish(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index e8dc8cf01..a2a8a8d80 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -20,6 +20,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldRulesConfigured.cs b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldRulesConfigured.cs new file mode 100644 index 000000000..177d291d4 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Events/Schemas/SchemaFieldRulesConfigured.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Schemas +{ + [EventType(nameof(SchemaFieldRulesConfigured))] + public sealed class SchemaFieldRulesConfigured : SchemaEvent + { + public FieldRules FieldRules { get; set; } + } +} diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index 6cd1437b6..9cb38209d 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -11,6 +11,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/backend/src/Squidex.Infrastructure/Validation/CustomValidators.cs b/backend/src/Squidex.Infrastructure/Validation/CustomValidators.cs new file mode 100644 index 000000000..0416133e8 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Validation/CustomValidators.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +namespace FluentValidation +{ + public static class CustomValidators + { + public static IRuleBuilderOptions Slug(this IRuleBuilder ruleBuilder) + { + return ruleBuilder.Must(x => x.IsSlug()).WithMessage("{PropertyName} must be a valid slug."); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs new file mode 100644 index 000000000..3619877f7 --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/ConfigureFieldRulesDto.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using Squidex.Domain.Apps.Entities.Schemas.Commands; + +namespace Squidex.Areas.Api.Controllers.Schemas.Models +{ + public sealed class ConfigureFieldRulesDto + { + /// + /// The field rules to configure. + /// + public List? FieldRules { get; set; } + + public ConfigureFieldRules ToCommand() + { + return new ConfigureFieldRules + { + FieldRules = FieldRules?.Select(x => x.ToCommand()).ToList() + }; + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldRuleDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldRuleDto.cs new file mode 100644 index 000000000..f1dcb191e --- /dev/null +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/FieldRuleDto.cs @@ -0,0 +1,44 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.ComponentModel.DataAnnotations; +using Squidex.Domain.Apps.Core.Schemas; +using Squidex.Domain.Apps.Entities.Schemas.Commands; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Areas.Api.Controllers.Schemas.Models +{ + public sealed class FieldRuleDto + { + /// + /// The action to perform when the condition is met. + /// + [Required] + public FieldRuleAction Action { get; set; } + + /// + /// The field to update. + /// + [Required] + public string Field { get; set; } + + /// + /// The condition. + /// + public string? Condition { get; set; } + + public static FieldRuleDto FromFieldRule(FieldRule fieldRule) + { + return SimpleMapper.Map(fieldRule, new FieldRuleDto()); + } + + public FieldRuleCommand ToCommand() + { + return SimpleMapper.Map(this, new FieldRuleCommand()); + } + } +} diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs index dc9439fa3..2643a14fa 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDetailsDto.cs @@ -42,6 +42,11 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models [Required] public List FieldsInReferences { get; set; } + /// + /// The field rules. + /// + public List FieldRules { get; set; } + /// /// The list of fields. /// @@ -58,9 +63,10 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models SimpleMapper.Map(schema.SchemaDef.Properties, result.Properties); result.FieldsInLists = schema.SchemaDef.FieldsInLists.ToList(); - result.FieldsInReferences = schema.SchemaDef.FieldsInReferences.ToList(); + result.FieldRules = schema.SchemaDef.FieldRules.Select(FieldRuleDto.FromFieldRule).ToList(); + if (schema.SchemaDef.PreviewUrls.Count > 0) { result.PreviewUrls = new Dictionary(schema.SchemaDef.PreviewUrls); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs index 5dd4cd33c..a833c76bf 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaDto.cs @@ -133,6 +133,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models AddPutLink("update", resources.Url(x => nameof(x.PutSchema), values)); AddPutLink("update/sync", resources.Url(x => nameof(x.PutSchemaSync), values)); AddPutLink("update/urls", resources.Url(x => nameof(x.PutPreviewUrls), values)); + AddPutLink("update/rules", resources.Url(x => nameof(x.PutRules), values)); AddPutLink("update/category", resources.Url(x => nameof(x.PutCategory), values)); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index b331b1bfe..640d087b0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -208,6 +208,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The preview urls for the schema. /// /// 200 => Schema updated. + /// 400 => Schema urls are not valid. /// 404 => Schema or app not found. /// [HttpPut] @@ -232,7 +233,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas /// The schema scripts object that needs to updated. /// /// 200 => Schema updated. - /// 400 => Schema properties are not valid. + /// 400 => Schema scripts are not valid. /// 404 => Schema or app not found. /// [HttpPut] @@ -249,6 +250,31 @@ namespace Squidex.Areas.Api.Controllers.Schemas return Ok(response); } + /// + /// Update the rules. + /// + /// The name of the app. + /// The name of the schema. + /// The schema rules object that needs to updated. + /// + /// 200 => Schema updated. + /// 400 => Schema rules are not valid. + /// 404 => Schema or app not found. + /// + [HttpPut] + [Route("apps/{app}/schemas/{name}/rules/")] + [ProducesResponseType(typeof(SchemaDetailsDto), 200)] + [ApiPermissionOrAnonymous(Permissions.AppSchemasUpdate)] + [ApiCosts(1)] + public async Task PutRules(string app, string name, [FromBody] ConfigureFieldRulesDto request) + { + var command = request.ToCommand(); + + var response = await InvokeCommandAsync(app, command); + + return Ok(response); + } + /// /// Publish a schema. /// diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs index 274b8559e..cf1ddb0d6 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Model/Schemas/SchemaTests.cs @@ -397,6 +397,17 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas Assert.NotSame(schema_1, schema_2); } + [Fact] + public void Should_set_field_rules() + { + var schema_1 = schema_0.SetFieldRules(FieldRule.Hide("2")); + var schema_2 = schema_1.SetFieldRules(FieldRule.Hide("2")); + + Assert.NotEmpty(schema_1.FieldRules); + Assert.NotEmpty(schema_2.FieldRules); + Assert.Same(schema_1, schema_2); + } + [Fact] public void Should_set_scripts() { @@ -447,6 +458,7 @@ namespace Squidex.Domain.Apps.Core.Model.Schemas var schemaSource = TestUtils.MixedSchema(true) .ChangeCategory("Category") + .SetFieldRules(FieldRule.Hide("2")) .SetFieldsInLists("field2") .SetFieldsInReferences("field1") .SetPreviewUrls(new Dictionary diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs index ed8ab0d7f..3e57ade43 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/EventSynchronization/SchemaSynchronizerTests.cs @@ -174,6 +174,24 @@ namespace Squidex.Domain.Apps.Core.Operations.EventSynchronization ); } + [Fact] + public void Should_create_events_if_field_rules_changed_changed() + { + var sourceSchema = + new Schema("source") + .SetFieldRules(FieldRule.Hide("2")); + + var targetSchema = + new Schema("target") + .SetFieldRules(FieldRule.Hide("1")); + + var events = sourceSchema.Synchronize(targetSchema, idGenerator); + + events.ShouldHaveSameEvents( + new SchemaFieldRulesConfigured { FieldRules = new FieldRules(FieldRule.Hide("1")) } + ); + } + [Fact] public void Should_create_events_if_nested_field_deleted() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs index 1b077b24f..c3a9e4663 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Guards/GuardSchemaTests.cs @@ -569,6 +569,51 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards GuardSchema.CanConfigureUIFields(command, schema_0); } + [Fact] + public void CanConfigureFieldRules_should_throw_exception_if_field_rules_are_invalid() + { + var command = new ConfigureFieldRules + { + FieldRules = new List + { + new FieldRuleCommand { Field = "field", Action = (FieldRuleAction)5 }, + new FieldRuleCommand(), + } + }; + + ValidationAssert.Throws(() => GuardSchema.CanConfigureFieldRules(command), + new ValidationError("Action is not a valid value.", + "FieldRules[1].Action"), + new ValidationError("Field is required.", + "FieldRules[2].Field")); + } + + [Fact] + public void CanConfigureFieldRules_should_not_throw_exception_if_field_rules_are_valid() + { + var command = new ConfigureFieldRules + { + FieldRules = new List + { + new FieldRuleCommand { Field = "field1", Action = FieldRuleAction.Disable, Condition = "a == b" }, + new FieldRuleCommand { Field = "field2" } + } + }; + + GuardSchema.CanConfigureFieldRules(command); + } + + [Fact] + public void CanConfigureFieldRules_should_not_throw_exception_if_field_rules_are_null() + { + var command = new ConfigureFieldRules + { + FieldRules = null + }; + + GuardSchema.CanConfigureFieldRules(command); + } + [Fact] public void CanPublish_should_not_throw_exception() { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs index e9e08d053..d9950b497 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaDomainObjectTests.cs @@ -148,12 +148,39 @@ namespace Squidex.Domain.Apps.Entities.Schemas result.ShouldBeEquivalent(sut.Snapshot); + Assert.Equal("", sut.Snapshot.SchemaDef.Scripts.Query); + LastEvents .ShouldHaveSameEvents( CreateEvent(new SchemaScriptsConfigured { Scripts = command.Scripts }) ); } + [Fact] + public async Task ConfigureFieldRules_should_create_events_and_update_schema_field_rules() + { + var command = new ConfigureFieldRules + { + FieldRules = new List + { + new FieldRuleCommand { Field = "field1" } + } + }; + + await ExecuteCreateAsync(); + + var result = await PublishIdempotentAsync(command); + + result.ShouldBeEquivalent(sut.Snapshot); + + Assert.NotEmpty(sut.Snapshot.SchemaDef.FieldRules); + + LastEvents + .ShouldHaveSameEvents( + CreateEvent(new SchemaFieldRulesConfigured { FieldRules = new FieldRules(FieldRule.Disable("field1")) }) + ); + } + [Fact] public async Task ConfigureUIFields_should_create_events_for_list_fields_and_update_schema() { diff --git a/frontend/app-config/karma-test-shim.js b/frontend/app-config/karma-test-shim.js index a9627af2f..e0c8bbea6 100644 --- a/frontend/app-config/karma-test-shim.js +++ b/frontend/app-config/karma-test-shim.js @@ -14,6 +14,6 @@ testing.getTestBed().initTestEnvironment( ); // Then we find all the tests. -const context = require.context('./../app', true, /\.spec\.ts$/); +const context = require.context('./../app', true, /contents\.forms\.spec\.ts$/); // And load the modules. context.keys().map(context); \ No newline at end of file diff --git a/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index 3c871f8ce..db581d1f1 100644 --- a/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/frontend/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -36,7 +36,7 @@ export class EventConsumersPageComponent extends ResourceOwner implements OnInit this.eventConsumersState.load(true, false); } - public trackByEventConsumer(index: number, es: EventConsumerDto) { + public trackByEventConsumer(_index: number, es: EventConsumerDto) { return es.name; } diff --git a/frontend/app/features/administration/pages/users/user-page.component.html b/frontend/app/features/administration/pages/users/user-page.component.html index afaa0a55b..36a6441b9 100644 --- a/frontend/app/features/administration/pages/users/user-page.component.html +++ b/frontend/app/features/administration/pages/users/user-page.component.html @@ -43,14 +43,14 @@
- +
- +
@@ -59,7 +59,7 @@
- +
@@ -67,7 +67,7 @@
- +
@@ -76,7 +76,7 @@
- +
diff --git a/frontend/app/features/administration/pages/users/users-page.component.ts b/frontend/app/features/administration/pages/users/users-page.component.ts index be99a19cb..e45532772 100644 --- a/frontend/app/features/administration/pages/users/users-page.component.ts +++ b/frontend/app/features/administration/pages/users/users-page.component.ts @@ -44,7 +44,7 @@ export class UsersPageComponent extends ResourceOwner implements OnInit { this.usersState.search(this.usersFilter.value); } - public trackByUser(index: number, user: UserDto) { + public trackByUser(_ndex: number, user: UserDto) { return user.id; } } \ No newline at end of file diff --git a/frontend/app/features/apps/pages/apps-page.component.ts b/frontend/app/features/apps/pages/apps-page.component.ts index 4b889156a..4396e14cb 100644 --- a/frontend/app/features/apps/pages/apps-page.component.ts +++ b/frontend/app/features/apps/pages/apps-page.component.ts @@ -70,7 +70,7 @@ export class AppsPageComponent implements OnInit { this.addAppDialog.show(); } - public trackByApp(index: number, app: AppDto) { + public trackByApp(_index: number, app: AppDto) { return app.id; } } \ No newline at end of file diff --git a/frontend/app/features/apps/pages/news-dialog.component.ts b/frontend/app/features/apps/pages/news-dialog.component.ts index 001477bdd..bbb796bad 100644 --- a/frontend/app/features/apps/pages/news-dialog.component.ts +++ b/frontend/app/features/apps/pages/news-dialog.component.ts @@ -20,7 +20,7 @@ export class NewsDialogComponent { @Input() public features: ReadonlyArray; - public trackByFeature(index: number, feature: FeatureDto) { + public trackByFeature(_index: number, feature: FeatureDto) { return feature; } } \ No newline at end of file diff --git a/frontend/app/features/assets/pages/asset-tags.component.ts b/frontend/app/features/assets/pages/asset-tags.component.ts index c2868e822..76756985d 100644 --- a/frontend/app/features/assets/pages/asset-tags.component.ts +++ b/frontend/app/features/assets/pages/asset-tags.component.ts @@ -35,7 +35,7 @@ export class AssetTagsComponent { return this.tagsSelected[tag.name] === true; } - public trackByTag(index: number, tag: Tag) { + public trackByTag(_index: number, tag: Tag) { return tag.name; } } \ No newline at end of file diff --git a/frontend/app/features/assets/pages/assets-filters-page.component.ts b/frontend/app/features/assets/pages/assets-filters-page.component.ts index c1e067904..bcd9c5f05 100644 --- a/frontend/app/features/assets/pages/assets-filters-page.component.ts +++ b/frontend/app/features/assets/pages/assets-filters-page.component.ts @@ -38,7 +38,7 @@ export class AssetsFiltersPageComponent { this.assetsState.resetTags(); } - public trackByTag(index: number, tag: { name: string }) { + public trackByTag(_index: number, tag: { name: string }) { return tag.name; } } \ No newline at end of file diff --git a/frontend/app/features/content/declarations.ts b/frontend/app/features/content/declarations.ts index 1b1fee9df..2245c5082 100644 --- a/frontend/app/features/content/declarations.ts +++ b/frontend/app/features/content/declarations.ts @@ -24,7 +24,6 @@ export * from './shared/forms/array-section.component'; export * from './shared/forms/assets-editor.component'; export * from './shared/forms/field-editor.component'; export * from './shared/forms/stock-photo-editor.component'; -export * from './shared/group-fields.pipe'; export * from './shared/list/content-list-cell.directive'; export * from './shared/list/content-list-field.component'; export * from './shared/list/content-list-header.component'; diff --git a/frontend/app/features/content/module.ts b/frontend/app/features/content/module.ts index b94c13a98..a16823683 100644 --- a/frontend/app/features/content/module.ts +++ b/frontend/app/features/content/module.ts @@ -10,7 +10,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule, UnsetContentGuard } from '@app/shared'; -import { ArrayEditorComponent, ArrayItemComponent, ArraySectionComponent, AssetsEditorComponent, CommentsPageComponent, ContentComponent, ContentCreatorComponent, ContentEventComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentPageComponent, ContentSectionComponent, ContentSelectorComponent, ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, GroupFieldsPipe, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, StockPhotoEditorComponent } from './declarations'; +import { ArrayEditorComponent, ArrayItemComponent, ArraySectionComponent, AssetsEditorComponent, CommentsPageComponent, ContentComponent, ContentCreatorComponent, ContentEventComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentPageComponent, ContentSectionComponent, ContentSelectorComponent, ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, StockPhotoEditorComponent } from './declarations'; const routes: Routes = [ { @@ -101,7 +101,6 @@ const routes: Routes = [ DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, - GroupFieldsPipe, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, diff --git a/frontend/app/features/content/pages/content/content-field.component.html b/frontend/app/features/content/pages/content/content-field.component.html index 75482a695..df7ae8cd6 100644 --- a/frontend/app/features/content/pages/content/content-field.component.html +++ b/frontend/app/features/content/pages/content/content-field.component.html @@ -1,13 +1,13 @@ -
-
-
+
+
+
-
+
@@ -32,10 +31,9 @@ @@ -43,15 +41,15 @@
-
+
-
+
@@ -74,8 +73,7 @@ diff --git a/frontend/app/features/content/pages/content/content-field.component.ts b/frontend/app/features/content/pages/content/content-field.component.ts index 62cf5dcd8..4b466415d 100644 --- a/frontend/app/features/content/pages/content/content-field.component.ts +++ b/frontend/app/features/content/pages/content/content-field.component.ts @@ -6,8 +6,7 @@ */ import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; -import { FormGroup } from '@angular/forms'; -import { AppLanguageDto, AppsState, EditContentForm, fieldInvariant, invalid$, LocalStoreService, RootFieldDto, SchemaDto, StringFieldPropertiesDto, TranslationsService, Types, value$ } from '@app/shared'; +import { AppLanguageDto, AppsState, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, StringFieldPropertiesDto, TranslationsService, Types, value$ } from '@app/shared'; import { Observable } from 'rxjs'; import { combineLatest } from 'rxjs/operators'; @@ -24,16 +23,16 @@ export class ContentFieldComponent implements OnChanges { public form: EditContentForm; @Input() - public formContext: any; + public formCompare?: EditContentForm; @Input() - public field: RootFieldDto; + public formContext: any; @Input() - public fieldForm: FormGroup; + public formModel: FieldForm; @Input() - public fieldFormCompare?: FormGroup; + public formModelCompare?: FieldForm; @Input() public schema: SchemaDto; @@ -54,11 +53,11 @@ export class ContentFieldComponent implements OnChanges { return false; } - if (!this.field.isLocalizable) { + if (!this.formModel.field.isLocalizable) { return false; } - const properties = this.field.properties; + const properties = this.formModel.field.properties; return Types.is(properties, StringFieldPropertiesDto) && (properties.editor === 'Input' || properties.editor === 'TextArea'); } @@ -73,14 +72,14 @@ export class ContentFieldComponent implements OnChanges { public ngOnChanges(changes: SimpleChanges) { this.showAllControls = this.localStore.getBoolean(this.configKey()); - if (changes['fieldForm'] && this.fieldForm) { - this.isInvalid = invalid$(this.fieldForm); + if (changes['formModel'] && this.formModel) { + this.isInvalid = invalid$(this.formModel.form); } - if ((changes['fieldForm'] || changes['fieldFormCompare']) && this.fieldFormCompare) { + if ((changes['formModel'] || changes['formModelCompare']) && this.formModelCompare) { this.isDifferent = - value$(this.fieldForm).pipe( - combineLatest(value$(this.fieldFormCompare), + value$(this.formModel.form).pipe( + combineLatest(value$(this.formModelCompare!.form), (lhs, rhs) => !Types.equals(lhs, rhs, true))); } } @@ -92,11 +91,11 @@ export class ContentFieldComponent implements OnChanges { } public copy() { - if (this.fieldFormCompare && this.fieldFormCompare) { + if (this.formModel && this.formModelCompare) { if (this.showAllControls) { - this.fieldForm.setValue(this.fieldFormCompare.value); + this.formModel.copyAllFrom(this.formModelCompare); } else { - this.getControl()!.setValue(this.getControlCompare()!.value); + this.formModel.copyFrom(this.formModelCompare, this.language.iso2Code); } } } @@ -106,7 +105,7 @@ export class ContentFieldComponent implements OnChanges { if (master) { const masterCode = master.iso2Code; - const masterValue = this.fieldForm.get(masterCode)!.value; + const masterValue = this.formModel.get(masterCode)!.form.value; if (masterValue) { if (this.showAllControls) { @@ -123,10 +122,10 @@ export class ContentFieldComponent implements OnChanges { } private translateValue(text: string, sourceLanguage: string, targetLanguage: string) { - const control = this.fieldForm.get(targetLanguage); + const control = this.formModel.get(targetLanguage); if (control) { - const value = control.value; + const value = control.form.value; if (!value) { const request = { text, sourceLanguage, targetLanguage }; @@ -134,38 +133,30 @@ export class ContentFieldComponent implements OnChanges { this.translations.translate(this.appsState.appName, request) .subscribe(result => { if (result.text) { - control.setValue(result.text); + control.form.setValue(result.text); } }); } } } - private findControl(form?: FormGroup) { - if (this.field.isLocalizable) { - return form?.controls[this.language.iso2Code]; - } else { - return form?.controls[fieldInvariant]; - } - } - public prefix(language: AppLanguageDto) { return `(${language.iso2Code})`; } public getControl() { - return this.findControl(this.fieldForm); + return this.formModel.get(this.language.iso2Code); } public getControlCompare() { - return this.findControl(this.fieldFormCompare); + return this.formModelCompare?.get(this.language.iso2Code); } - public trackByLanguage(index: number, language: AppLanguageDto) { + public trackByLanguage(_index: number, language: AppLanguageDto) { return language.iso2Code; } private configKey() { - return `squidex.schemas.${this.schema?.id}.fields.${this.field?.fieldId}.show-all`; + return `squidex.schemas.${this.schema?.id}.fields.${this.formModel.field.fieldId}.show-all`; } } \ No newline at end of file diff --git a/frontend/app/features/content/pages/content/content-history-page.component.ts b/frontend/app/features/content/pages/content/content-history-page.component.ts index 7c22761d8..be0559ae1 100644 --- a/frontend/app/features/content/pages/content/content-history-page.component.ts +++ b/frontend/app/features/content/pages/content/content-history-page.component.ts @@ -98,7 +98,7 @@ export class ContentHistoryPageComponent extends ResourceOwner implements OnInit this.contentPage.loadVersion(event.version, true); } - public trackByEvent(index: number, event: HistoryEventDto) { + public trackByEvent(_index: number, event: HistoryEventDto) { return event.eventId; } } \ No newline at end of file diff --git a/frontend/app/features/content/pages/content/content-page.component.html b/frontend/app/features/content/pages/content/content-page.component.html index 5ccb728a8..5cb520d36 100644 --- a/frontend/app/features/content/pages/content/content-page.component.html +++ b/frontend/app/features/content/pages/content/content-page.component.html @@ -81,14 +81,14 @@
- + [schema]="schema">
diff --git a/frontend/app/features/content/pages/content/content-page.component.ts b/frontend/app/features/content/pages/content/content-page.component.ts index f79dbe732..141308f4b 100644 --- a/frontend/app/features/content/pages/content/content-page.component.ts +++ b/frontend/app/features/content/pages/content/content-page.component.ts @@ -9,10 +9,9 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { ApiUrlConfig, AppLanguageDto, AuthService, AutoSaveKey, AutoSaveService, CanComponentDeactivate, ContentDto, ContentsState, DialogService, EditContentForm, fadeAnimation, LanguagesState, ModalModel, ResourceOwner, RootFieldDto, SchemaDetailsDto, SchemasState, TempService, Version } from '@app/shared'; +import { ApiUrlConfig, AppLanguageDto, AuthService, AutoSaveKey, AutoSaveService, CanComponentDeactivate, ContentDto, ContentsState, DialogService, EditContentForm, fadeAnimation, FieldForm, FieldSection, LanguagesState, ModalModel, ResourceOwner, RootFieldDto, SchemaDetailsDto, SchemasState, TempService, valueAll$, Version } from '@app/shared'; import { Observable, of } from 'rxjs'; import { debounceTime, filter, onErrorResumeNext, tap } from 'rxjs/operators'; -import { FieldSection } from '../../shared/group-fields.pipe'; @Component({ selector: 'sqx-content-page', @@ -70,7 +69,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD .subscribe(schema => { this.schema = schema; - this.contentForm = new EditContentForm(this.languages, this.schema); + this.contentForm = new EditContentForm(this.languages, this.schema, this.formContext.user); })); this.own( @@ -111,7 +110,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD })); this.own( - this.contentForm.form.valueChanges.pipe( + valueAll$(this.contentForm.form).pipe( filter(_ => !this.isLoadingContent), filter(_ => this.contentForm.form.enabled), debounceTime(2000) @@ -220,7 +219,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD this.contentsState.loadVersion(content, version) .subscribe(dto => { if (compare) { - this.contentFormCompare = new EditContentForm(this.languages, this.schema); + this.contentFormCompare = new EditContentForm(this.languages, this.schema, this.formContext.user); this.contentFormCompare.load(dto.payload); this.contentFormCompare.setEnabled(false); @@ -250,7 +249,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD } } - public trackBySection(index: number, section: FieldSection) { + public trackBySection(_index: number, section: FieldSection) { return section.separator?.fieldId; } } diff --git a/frontend/app/features/content/pages/content/content-section.component.html b/frontend/app/features/content/pages/content/content-section.component.html index cb6409a25..b62a82228 100644 --- a/frontend/app/features/content/pages/content/content-section.component.html +++ b/frontend/app/features/content/pages/content/content-section.component.html @@ -1,28 +1,30 @@ -
-
-
- -
-
-

{{separator!.displayName}}

+ +
+
+
+ +
+
+

{{separator!.displayName}}

- - {{separator!.properties.hints}} - + + {{separator!.properties.hints}} + +
-
+ -
- + diff --git a/frontend/app/features/content/pages/content/content-section.component.ts b/frontend/app/features/content/pages/content/content-section.component.ts index c181d0fed..6af052635 100644 --- a/frontend/app/features/content/pages/content/content-section.component.ts +++ b/frontend/app/features/content/pages/content/content-section.component.ts @@ -6,8 +6,7 @@ */ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; -import { AppLanguageDto, EditContentForm, LocalStoreService, RootFieldDto, SchemaDto } from '@app/shared'; -import { FieldSection } from './../../shared/group-fields.pipe'; +import { AppLanguageDto, EditContentForm, FieldForm, FieldSection, LocalStoreService, RootFieldDto, SchemaDto } from '@app/shared'; @Component({ selector: 'sqx-content-section', @@ -29,10 +28,10 @@ export class ContentSectionComponent implements OnChanges { public formContext: any; @Input() - public schema: SchemaDto; + public formSection: FieldSection; @Input() - public section: FieldSection; + public schema: SchemaDto; @Input() public language: AppLanguageDto; @@ -57,19 +56,15 @@ export class ContentSectionComponent implements OnChanges { this.localStore.setBoolean(this.configKey(), this.isCollapsed); } - public getFieldForm(field: RootFieldDto) { - return this.form.form.get(field.name)!; - } - - public getFieldFormCompare(field: RootFieldDto) { - return this.formCompare?.form.get(field.name)!; + public getFieldFormCompare(formState: FieldForm) { + return this.formCompare?.get(formState.field.name); } - public trackByField(index: number, field: RootFieldDto) { - return field.fieldId; + public trackByField(_index: number, formState: FieldForm) { + return formState.field.fieldId; } private configKey(): string { - return `squidex.schemas.${this.schema?.id}.fields.${this.section?.separator?.fieldId}.closed`; + return `squidex.schemas.${this.schema?.id}.fields.${this.formSection?.separator?.fieldId}.closed`; } } \ No newline at end of file diff --git a/frontend/app/features/content/pages/contents/contents-page.component.ts b/frontend/app/features/content/pages/contents/contents-page.component.ts index a6d8a4818..f69c41cef 100644 --- a/frontend/app/features/content/pages/contents/contents-page.component.ts +++ b/frontend/app/features/content/pages/contents/contents-page.component.ts @@ -181,7 +181,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit { this.updateSelectionSummary(); } - public trackByContent(content: ContentDto): string { + public trackByContent(_index: number, content: ContentDto): string { return content.id; } diff --git a/frontend/app/features/content/pages/schemas/schemas-page.component.ts b/frontend/app/features/content/pages/schemas/schemas-page.component.ts index 356d9c897..85728cfb6 100644 --- a/frontend/app/features/content/pages/schemas/schemas-page.component.ts +++ b/frontend/app/features/content/pages/schemas/schemas-page.component.ts @@ -40,7 +40,7 @@ export class SchemasPageComponent implements OnInit { this.localStore.setBoolean('content.schemas.collapsed', this.isCollapsed); } - public trackByCategory(index: number, category: SchemaCategory) { + public trackByCategory(_index: number, category: SchemaCategory) { return category.name; } } \ No newline at end of file diff --git a/frontend/app/features/content/shared/forms/array-editor.component.html b/frontend/app/features/content/shared/forms/array-editor.component.html index a4e88db22..0b9a3d9bc 100644 --- a/frontend/app/features/content/shared/forms/array-editor.component.html +++ b/frontend/app/features/content/shared/forms/array-editor.component.html @@ -1,26 +1,23 @@ -
-
+ (clone)="itemAdd(itemForm)" (move)="move(itemForm, $event)" (remove)="itemRemove(i)">
@@ -28,12 +25,12 @@
-
-
+
diff --git a/frontend/app/features/content/shared/forms/array-editor.component.ts b/frontend/app/features/content/shared/forms/array-editor.component.ts index 7102d6333..3b7949393 100644 --- a/frontend/app/features/content/shared/forms/array-editor.component.ts +++ b/frontend/app/features/content/shared/forms/array-editor.component.ts @@ -7,8 +7,7 @@ import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { ChangeDetectionStrategy, Component, Input, QueryList, ViewChildren } from '@angular/core'; -import { AbstractControl, FormArray, FormGroup } from '@angular/forms'; -import { AppLanguageDto, EditContentForm, RootFieldDto, sorted } from '@app/shared'; +import { AppLanguageDto, EditContentForm, FieldArrayForm, FieldArrayItemForm, sorted } from '@app/shared'; import { ArrayItemComponent } from './array-item.component'; @Component({ @@ -25,7 +24,7 @@ export class ArrayEditorComponent { public formContext: any; @Input() - public field: RootFieldDto; + public formModel: FieldArrayForm; @Input() public language: AppLanguageDto; @@ -33,22 +32,31 @@ export class ArrayEditorComponent { @Input() public languages: ReadonlyArray; - @Input() - public arrayControl: FormArray; - @ViewChildren(ArrayItemComponent) public children: QueryList; + public get field() { + return this.formModel.field; + } + public itemRemove(index: number) { - this.form.arrayItemRemove(this.field, this.language, index); + this.formModel.removeItemAt(index); } - public itemAdd(value?: FormGroup) { - this.form.arrayItemInsert(this.field, this.language, value); + public itemAdd(value?: FieldArrayItemForm) { + this.formModel.addItem(value); } - public sort(event: CdkDragDrop>) { - this.sortInternal(sorted(event)); + public sort(event: CdkDragDrop>) { + this.formModel.sort(sorted(event)); + + this.reset(); + } + + public move(index: number, item: FieldArrayItemForm) { + this.formModel.move(index, item); + + this.reset(); } public collapseAll() { @@ -68,21 +76,4 @@ export class ArrayEditorComponent { child.reset(); }); } - - public move(control: AbstractControl, index: number) { - const controls = [...this.arrayControl.controls]; - - controls.splice(controls.indexOf(control), 1); - controls.splice(index, 0, control); - - this.sortInternal(controls); - } - - private sortInternal(controls: ReadonlyArray) { - for (let i = 0; i < controls.length; i++) { - this.arrayControl.setControl(i, controls[i]); - } - - this.reset(); - } } \ No newline at end of file diff --git a/frontend/app/features/content/shared/forms/array-item.component.html b/frontend/app/features/content/shared/forms/array-item.component.html index 19432add3..0cc9211fd 100644 --- a/frontend/app/features/content/shared/forms/array-item.component.html +++ b/frontend/app/features/content/shared/forms/array-item.component.html @@ -43,14 +43,13 @@
-
+
+ [languages]="languages">
diff --git a/frontend/app/features/content/shared/forms/array-item.component.ts b/frontend/app/features/content/shared/forms/array-item.component.ts index bb484ecfb..6b64cb489 100644 --- a/frontend/app/features/content/shared/forms/array-item.component.ts +++ b/frontend/app/features/content/shared/forms/array-item.component.ts @@ -6,11 +6,9 @@ */ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, Output, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; -import { FormGroup } from '@angular/forms'; -import { AppLanguageDto, EditContentForm, FieldFormatter, invalid$, NestedFieldDto, RootFieldDto, value$ } from '@app/shared'; +import { AppLanguageDto, EditContentForm, FieldArrayItemForm, FieldArrayItemValueForm, FieldFormatter, FieldSection, invalid$, NestedFieldDto, value$ } from '@app/shared'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { FieldSection } from './../group-fields.pipe'; import { ArraySectionComponent } from './array-section.component'; import { FieldEditorComponent } from './field-editor.component'; @@ -37,7 +35,7 @@ export class ArrayItemComponent implements OnChanges { public formContext: any; @Input() - public field: RootFieldDto; + public formModel: FieldArrayItemForm; @Input() public isFirst = false; @@ -51,9 +49,6 @@ export class ArrayItemComponent implements OnChanges { @Input() public index: number; - @Input() - public itemForm: FormGroup; - @Input() public language: AppLanguageDto; @@ -74,23 +69,21 @@ export class ArrayItemComponent implements OnChanges { } public ngOnChanges(changes: SimpleChanges) { - if (changes['itemForm']) { - this.isInvalid = invalid$(this.itemForm); - } + if (changes['formModel']) { + this.isInvalid = invalid$(this.formModel.form); - if (changes['itemForm'] || changes['field']) { - this.title = value$(this.itemForm).pipe(map(x => this.getTitle(x))); + this.title = value$(this.formModel.form).pipe(map(x => this.getTitle(x))); } } private getTitle(value: any) { const values: string[] = []; - for (const field of this.field.nested) { - const control = this.itemForm.get(field.name); + for (const field of this.formModel.field.nested) { + const fieldValue = value[field.name]; - if (control) { - const formatted = FieldFormatter.format(field, control.value); + if (fieldValue) { + const formatted = FieldFormatter.format(field, fieldValue); if (formatted) { values.push(formatted); @@ -135,7 +128,7 @@ export class ArrayItemComponent implements OnChanges { }); } - public trackBySection(index: number, section: FieldSection) { + public trackBySection(_index: number, section: FieldSection) { return section.separator?.fieldId; } } \ No newline at end of file diff --git a/frontend/app/features/content/shared/forms/array-section.component.html b/frontend/app/features/content/shared/forms/array-section.component.html index 6aea12189..68bd477ed 100644 --- a/frontend/app/features/content/shared/forms/array-section.component.html +++ b/frontend/app/features/content/shared/forms/array-section.component.html @@ -1,18 +1,19 @@ -
-

{{separator!.displayName}}

+ +
+

{{separator!.displayName}}

- - {{separator!.properties.hints}} - -
- -
- - -
\ No newline at end of file + + {{separator!.properties.hints}} + +
+ +
+ + +
+ \ No newline at end of file diff --git a/frontend/app/features/content/shared/forms/array-section.component.ts b/frontend/app/features/content/shared/forms/array-section.component.ts index 8120477fd..e910bf239 100644 --- a/frontend/app/features/content/shared/forms/array-section.component.ts +++ b/frontend/app/features/content/shared/forms/array-section.component.ts @@ -6,9 +6,7 @@ */ import { ChangeDetectionStrategy, Component, Input, QueryList, ViewChildren } from '@angular/core'; -import { FormGroup } from '@angular/forms'; -import { AppLanguageDto, EditContentForm, NestedFieldDto } from '@app/shared'; -import { FieldSection } from './../group-fields.pipe'; +import { AppLanguageDto, EditContentForm, FieldArrayItemForm, FieldSection, NestedFieldDto } from '@app/shared'; import { FieldEditorComponent } from './field-editor.component'; @Component({ @@ -18,9 +16,6 @@ import { FieldEditorComponent } from './field-editor.component'; changeDetection: ChangeDetectionStrategy.OnPush }) export class ArraySectionComponent { - @Input() - public itemForm: FormGroup; - @Input() public form: EditContentForm; @@ -28,28 +23,24 @@ export class ArraySectionComponent { public formContext: any; @Input() - public language: AppLanguageDto; + public formSection: FieldSection; @Input() - public languages: ReadonlyArray; + public language: AppLanguageDto; @Input() - public section: FieldSection; + public languages: ReadonlyArray; @ViewChildren(FieldEditorComponent) public editors: QueryList; - public getControl(field: NestedFieldDto) { - return this.itemForm.get(field.name)!; - } - public reset() { this.editors.forEach(editor => { editor.reset(); }); } - public trackByField(index: number, field: NestedFieldDto) { - return field.fieldId; + public trackByField(_index: number, field: FieldArrayItemForm) { + return field.field.fieldId; } } \ No newline at end of file diff --git a/frontend/app/features/content/shared/forms/assets-editor.component.ts b/frontend/app/features/content/shared/forms/assets-editor.component.ts index abeba63b0..b01dd4344 100644 --- a/frontend/app/features/content/shared/forms/assets-editor.component.ts +++ b/frontend/app/features/content/shared/forms/assets-editor.component.ts @@ -172,7 +172,7 @@ export class AssetsEditorComponent extends StatefulControlComponent +
Disabled - +
@@ -19,11 +19,9 @@ - diff --git a/frontend/app/features/content/shared/forms/field-editor.component.ts b/frontend/app/features/content/shared/forms/field-editor.component.ts index 30e816e38..3b7b294fa 100644 --- a/frontend/app/features/content/shared/forms/field-editor.component.ts +++ b/frontend/app/features/content/shared/forms/field-editor.component.ts @@ -7,7 +7,7 @@ import { Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core'; import { AbstractControl, FormArray, FormControl } from '@angular/forms'; -import { AppLanguageDto, EditContentForm, FieldDto, MathHelper, RootFieldDto, Types } from '@app/shared'; +import { AbstractContentForm, AppLanguageDto, EditContentForm, FieldDto, MathHelper, RootFieldDto, Types } from '@app/shared'; @Component({ selector: 'sqx-field-editor', @@ -22,10 +22,7 @@ export class FieldEditorComponent implements OnChanges { public formContext: any; @Input() - public field: FieldDto; - - @Input() - public control: AbstractControl; + public formModel: AbstractContentForm; @Input() public language: AppLanguageDto; @@ -39,16 +36,20 @@ export class FieldEditorComponent implements OnChanges { @ViewChild('editor', { static: false }) public editor: ElementRef; + public get field() { + return this.formModel.field; + } + public get arrayControl() { - return this.control as FormArray; + return this.formModel.form as FormArray; } public get editorControl() { - return this.control as FormControl; + return this.formModel.form as FormControl; } public get rootField() { - return this.field as RootFieldDto; + return this.formModel.field as RootFieldDto; } public uniqueId = MathHelper.guid(); diff --git a/frontend/app/features/content/shared/forms/stock-photo-editor.component.ts b/frontend/app/features/content/shared/forms/stock-photo-editor.component.ts index a7d8a5a5e..688b0973f 100644 --- a/frontend/app/features/content/shared/forms/stock-photo-editor.component.ts +++ b/frontend/app/features/content/shared/forms/stock-photo-editor.component.ts @@ -112,7 +112,7 @@ export class StockPhotoEditorComponent extends StatefulControlComponent { - separator?: T; - - fields: ReadonlyArray; -} - -@Pipe({ - name: 'sqxGroupFields', - pure: true -}) -export class GroupFieldsPipe implements PipeTransform { - public transform(fields: ReadonlyArray) { - const sections: FieldSection[] = []; - - let currentSeparator: T | undefined = undefined; - let currentFields: T[] = []; - - for (const field of fields) { - if (field.properties.isContentField) { - currentFields.push(field); - } else { - sections.push({ separator: currentSeparator, fields: currentFields }); - - currentFields = []; - currentSeparator = field; - } - } - - if (currentFields.length > 0) { - sections.push({ separator: currentSeparator, fields: currentFields }); - } - - return sections; - } -} \ No newline at end of file diff --git a/frontend/app/features/content/shared/references/content-creator.component.html b/frontend/app/features/content/shared/references/content-creator.component.html index a3983b5a6..91bb9a529 100644 --- a/frontend/app/features/content/shared/references/content-creator.component.html +++ b/frontend/app/features/content/shared/references/content-creator.component.html @@ -40,13 +40,13 @@
- + [schema]="schema">
diff --git a/frontend/app/features/content/shared/references/content-creator.component.ts b/frontend/app/features/content/shared/references/content-creator.component.ts index 1817f8f3e..e73361a1f 100644 --- a/frontend/app/features/content/shared/references/content-creator.component.ts +++ b/frontend/app/features/content/shared/references/content-creator.component.ts @@ -68,7 +68,7 @@ export class ContentCreatorComponent extends ResourceOwner implements OnInit { this.schema = schema; this.contentsState.schema = schema; - this.contentForm = new EditContentForm(this.languages, this.schema); + this.contentForm = new EditContentForm(this.languages, this.schema, this.contentFormContext.user); this.changeDetector.markForCheck(); } diff --git a/frontend/app/features/content/shared/references/content-selector.component.ts b/frontend/app/features/content/shared/references/content-selector.component.ts index cb62e60b1..5543e38bb 100644 --- a/frontend/app/features/content/shared/references/content-selector.component.ts +++ b/frontend/app/features/content/shared/references/content-selector.component.ts @@ -153,7 +153,7 @@ export class ContentSelectorComponent extends ResourceOwner implements OnInit { } } - public trackByContent(index: number, content: ContentDto): string { + public trackByContent(_index: number, content: ContentDto): string { return content.id; } } \ No newline at end of file diff --git a/frontend/app/features/content/shared/references/references-editor.component.ts b/frontend/app/features/content/shared/references/references-editor.component.ts index 7bf2d0b32..89dca7275 100644 --- a/frontend/app/features/content/shared/references/references-editor.component.ts +++ b/frontend/app/features/content/shared/references/references-editor.component.ts @@ -131,7 +131,7 @@ export class ReferencesEditorComponent extends StatefulControlComponent ({ ...s, isCompact })); } - public trackByContent(index: number, content: ContentDto) { + public trackByContent(_index: number, content: ContentDto) { return content.id; } } \ No newline at end of file diff --git a/frontend/app/features/rules/pages/events/rule-events-page.component.ts b/frontend/app/features/rules/pages/events/rule-events-page.component.ts index 82f52bb1b..b03d78e5e 100644 --- a/frontend/app/features/rules/pages/events/rule-events-page.component.ts +++ b/frontend/app/features/rules/pages/events/rule-events-page.component.ts @@ -45,7 +45,7 @@ export class RuleEventsPageComponent implements OnInit { this.selectedEventId = this.selectedEventId !== id ? id : null; } - public trackByRuleEvent(index: number, ruleEvent: RuleEventDto) { + public trackByRuleEvent(_index: number, ruleEvent: RuleEventDto) { return ruleEvent.id; } } \ No newline at end of file diff --git a/frontend/app/features/rules/pages/rules/actions/generic-action.component.html b/frontend/app/features/rules/pages/rules/actions/generic-action.component.html index 0e57104cf..038c5d184 100644 --- a/frontend/app/features/rules/pages/rules/actions/generic-action.component.html +++ b/frontend/app/features/rules/pages/rules/actions/generic-action.component.html @@ -5,7 +5,7 @@
- + diff --git a/frontend/app/features/rules/pages/rules/actions/generic-action.component.ts b/frontend/app/features/rules/pages/rules/actions/generic-action.component.ts index ec8fc4aa7..72e112951 100644 --- a/frontend/app/features/rules/pages/rules/actions/generic-action.component.ts +++ b/frontend/app/features/rules/pages/rules/actions/generic-action.component.ts @@ -24,9 +24,6 @@ export class GenericActionComponent implements OnInit { @Input() public actionForm: FormGroup; - @Input() - public actionFormSubmitted = false; - public ngOnInit() { for (const property of this.definition.properties) { const validators = []; diff --git a/frontend/app/features/rules/pages/rules/rule-wizard.component.html b/frontend/app/features/rules/pages/rules/rule-wizard.component.html index 70390653c..85b6d84ad 100644 --- a/frontend/app/features/rules/pages/rules/rule-wizard.component.html +++ b/frontend/app/features/rules/pages/rules/rule-wizard.component.html @@ -59,37 +59,32 @@ + [triggerForm]="triggerForm.form"> + [triggerForm]="triggerForm.form"> + [triggerForm]="triggerForm.form"> + [triggerForm]="triggerForm.form"> + [triggerForm]="triggerForm.form"> @@ -117,8 +112,7 @@ + [actionForm]="actionForm.form"> diff --git a/frontend/app/features/rules/pages/rules/rules-page.component.ts b/frontend/app/features/rules/pages/rules/rules-page.component.ts index d563a4d1c..498c8781b 100644 --- a/frontend/app/features/rules/pages/rules/rules-page.component.ts +++ b/frontend/app/features/rules/pages/rules/rules-page.component.ts @@ -81,7 +81,7 @@ export class RulesPageComponent implements OnInit { this.addRuleDialog.show(); } - public trackByRule(index: number, rule: RuleDto) { + public trackByRule(_index: number, rule: RuleDto) { return rule.id; } } diff --git a/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html index 1ae2b45f6..ef8cac61f 100644 --- a/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html +++ b/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.html @@ -2,7 +2,7 @@
- +
diff --git a/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts index 2c8138bb3..31959bd99 100644 --- a/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts +++ b/frontend/app/features/rules/pages/rules/triggers/asset-changed-trigger.component.ts @@ -20,9 +20,6 @@ export class AssetChangedTriggerComponent implements OnInit { @Input() public triggerForm: FormGroup; - @Input() - public triggerFormSubmitted = false; - public ngOnInit() { this.triggerForm.setControl('condition', new FormControl(this.trigger.condition || '')); diff --git a/frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.html index b042da2db..7cbf53e6d 100644 --- a/frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.html +++ b/frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.html @@ -2,7 +2,7 @@
- +
diff --git a/frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.ts index 75d1a8dc9..5a832722d 100644 --- a/frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.ts +++ b/frontend/app/features/rules/pages/rules/triggers/comment-trigger.component.ts @@ -20,9 +20,6 @@ export class CommentTriggerComponent implements OnInit { @Input() public triggerForm: FormGroup; - @Input() - public triggerFormSubmitted = false; - public ngOnInit() { this.triggerForm.setControl('condition', new FormControl(this.trigger.condition || '')); diff --git a/frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts index 589c1595b..c21e0af5b 100644 --- a/frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts +++ b/frontend/app/features/rules/pages/rules/triggers/content-changed-trigger.component.ts @@ -30,9 +30,6 @@ export class ContentChangedTriggerComponent implements OnInit { @Input() public triggerForm: FormGroup; - @Input() - public triggerFormSubmitted = false; - public triggerSchemas: ReadonlyArray; public schemaToAdd: SchemaDto; @@ -99,7 +96,7 @@ export class ContentChangedTriggerComponent implements OnInit { this.schemaToAdd = this.schemasToAdd[0]; } - public trackBySchema(index: number, schema: SchemaDto) { + public trackBySchema(_index: number, schema: SchemaDto) { return schema.id; } } \ No newline at end of file diff --git a/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html index ff6b78cdf..666ae22f9 100644 --- a/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html +++ b/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.html @@ -2,7 +2,7 @@
- +
diff --git a/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts index 4096a8410..f087db798 100644 --- a/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts +++ b/frontend/app/features/rules/pages/rules/triggers/schema-changed-trigger.component.ts @@ -20,9 +20,6 @@ export class SchemaChangedTriggerComponent implements OnInit { @Input() public triggerForm: FormGroup; - @Input() - public triggerFormSubmitted = false; - public ngOnInit() { this.triggerForm.setControl('condition', new FormControl(this.trigger.condition || '')); diff --git a/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.html b/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.html index 5a0575e7d..e27b580de 100644 --- a/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.html +++ b/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.html @@ -2,7 +2,7 @@
- + @@ -14,7 +14,7 @@
- + diff --git a/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.ts b/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.ts index b1ae1dc6b..6e4536f4c 100644 --- a/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.ts +++ b/frontend/app/features/rules/pages/rules/triggers/usage-trigger.component.ts @@ -21,9 +21,6 @@ export class UsageTriggerComponent implements OnInit { @Input() public triggerForm: FormGroup; - @Input() - public triggerFormSubmitted = false; - public ngOnInit() { this.triggerForm.setControl('limit', new FormControl(this.trigger.limit || 20000, [ diff --git a/frontend/app/features/schemas/declarations.ts b/frontend/app/features/schemas/declarations.ts index 69818c416..df2295476 100644 --- a/frontend/app/features/schemas/declarations.ts +++ b/frontend/app/features/schemas/declarations.ts @@ -34,6 +34,7 @@ export * from './pages/schema/fields/types/string-validation.component'; export * from './pages/schema/fields/types/tags-ui.component'; export * from './pages/schema/fields/types/tags-validation.component'; export * from './pages/schema/preview/schema-preview-urls-form.component'; +export * from './pages/schema/rules/schema-field-rules-form.component'; export * from './pages/schema/schema-page.component'; export * from './pages/schema/scripts/schema-scripts-form.component'; export * from './pages/schema/ui/field-list.component'; diff --git a/frontend/app/features/schemas/module.ts b/frontend/app/features/schemas/module.ts index 105e8876d..8fe96046f 100644 --- a/frontend/app/features/schemas/module.ts +++ b/frontend/app/features/schemas/module.ts @@ -10,7 +10,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HelpComponent, SchemaMustExistGuard, SqxFrameworkModule, SqxSharedModule } from '@app/shared'; -import { ArrayValidationComponent, AssetsUIComponent, AssetsValidationComponent, BooleanUIComponent, BooleanValidationComponent, DateTimeUIComponent, DateTimeValidationComponent, FieldComponent, FieldFormCommonComponent, FieldFormComponent, FieldFormUIComponent, FieldFormValidationComponent, FieldListComponent, FieldWizardComponent, GeolocationUIComponent, GeolocationValidationComponent, JsonUIComponent, JsonValidationComponent, NumberUIComponent, NumberValidationComponent, ReferencesUIComponent, ReferencesValidationComponent, SchemaEditFormComponent, SchemaExportFormComponent, SchemaFieldsComponent, SchemaFormComponent, SchemaPageComponent, SchemaPreviewUrlsFormComponent, SchemaScriptsFormComponent, SchemasPageComponent, SchemaUIFormComponent, StringUIComponent, StringValidationComponent, TagsUIComponent, TagsValidationComponent } from './declarations'; +import { ArrayValidationComponent, AssetsUIComponent, AssetsValidationComponent, BooleanUIComponent, BooleanValidationComponent, DateTimeUIComponent, DateTimeValidationComponent, FieldComponent, FieldFormCommonComponent, FieldFormComponent, FieldFormUIComponent, FieldFormValidationComponent, FieldListComponent, FieldWizardComponent, GeolocationUIComponent, GeolocationValidationComponent, JsonUIComponent, JsonValidationComponent, NumberUIComponent, NumberValidationComponent, ReferencesUIComponent, ReferencesValidationComponent, SchemaEditFormComponent, SchemaExportFormComponent, SchemaFieldRulesFormComponent, SchemaFieldsComponent, SchemaFormComponent, SchemaPageComponent, SchemaPreviewUrlsFormComponent, SchemaScriptsFormComponent, SchemasPageComponent, SchemaUIFormComponent, StringUIComponent, StringValidationComponent, TagsUIComponent, TagsValidationComponent } from './declarations'; const routes: Routes = [ { @@ -69,6 +69,7 @@ const routes: Routes = [ ReferencesValidationComponent, SchemaEditFormComponent, SchemaExportFormComponent, + SchemaFieldRulesFormComponent, SchemaFieldsComponent, SchemaFormComponent, SchemaPageComponent, diff --git a/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html b/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html index 41b80f503..dfedcb8d1 100644 --- a/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html +++ b/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.html @@ -1,5 +1,5 @@ -
+
Common
@@ -13,7 +13,7 @@
- + @@ -23,7 +23,7 @@
- +
@@ -31,7 +31,7 @@
- + diff --git a/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.ts b/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.ts index 6f7b556fe..a7b575985 100644 --- a/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.ts +++ b/frontend/app/features/schemas/pages/schema/common/schema-edit-form.component.ts @@ -20,7 +20,7 @@ export class SchemaEditFormComponent implements OnChanges { @Input() public schema: SchemaDetailsDto; - public editForm = new EditSchemaForm(this.formBuilder); + public fieldForm = new EditSchemaForm(this.formBuilder); public isEditable = false; @@ -33,8 +33,8 @@ export class SchemaEditFormComponent implements OnChanges { public ngOnChanges() { this.isEditable = this.schema.canUpdate; - this.editForm.load(this.schema.properties); - this.editForm.setEnabled(this.isEditable); + this.fieldForm.load(this.schema.properties); + this.fieldForm.setEnabled(this.isEditable); } public saveSchema() { @@ -42,14 +42,14 @@ export class SchemaEditFormComponent implements OnChanges { return; } - const value = this.editForm.submit(); + const value = this.fieldForm.submit(); if (value) { this.schemasState.update(this.schema, value) .subscribe(() => { - this.editForm.submitCompleted({ noReset: true }); + this.fieldForm.submitCompleted({ noReset: true }); }, error => { - this.editForm.submitFailed(error); + this.fieldForm.submitFailed(error); }); } } diff --git a/frontend/app/features/schemas/pages/schema/export/schema-export-form.component.html b/frontend/app/features/schemas/pages/schema/export/schema-export-form.component.html index 0284bd357..5e0434a08 100644 --- a/frontend/app/features/schemas/pages/schema/export/schema-export-form.component.html +++ b/frontend/app/features/schemas/pages/schema/export/schema-export-form.component.html @@ -20,7 +20,7 @@
- +
diff --git a/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html b/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html index 9169f1582..03a70bec4 100644 --- a/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/field-wizard.component.html @@ -37,7 +37,7 @@
- + @@ -66,10 +66,9 @@ + [field]="field" + [fieldForm]="editForm.form" + [patterns]="patternsState.patterns | async"> diff --git a/frontend/app/features/schemas/pages/schema/fields/field.component.html b/frontend/app/features/schemas/pages/schema/fields/field.component.html index 1854edbc9..6a3c02d9b 100644 --- a/frontend/app/features/schemas/pages/schema/fields/field.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/field.component.html @@ -80,14 +80,12 @@
- + [patterns]="patterns" + [fieldForm]="editForm.form" + [field]="field" + [isEditable]="isEditable">
diff --git a/frontend/app/features/schemas/pages/schema/fields/field.component.scss b/frontend/app/features/schemas/pages/schema/fields/field.component.scss index f97ee0cc8..d3ab1fbe5 100644 --- a/frontend/app/features/schemas/pages/schema/fields/field.component.scss +++ b/frontend/app/features/schemas/pages/schema/fields/field.component.scss @@ -37,8 +37,7 @@ $padding: 1rem; .nested-fields { background: $color-theme-secondary; - border: 1px solid $color-border; - border-top-width: 0; + border: 0; padding: $padding; padding-left: 2 * $padding; position: relative; diff --git a/frontend/app/features/schemas/pages/schema/fields/field.component.ts b/frontend/app/features/schemas/pages/schema/fields/field.component.ts index 964cddc6c..0a804b260 100644 --- a/frontend/app/features/schemas/pages/schema/fields/field.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/field.component.ts @@ -33,7 +33,7 @@ export class FieldComponent implements OnChanges { public dropdown = new ModalModel(); - public trackByFieldFn: (index: number, field: NestedFieldDto) => any; + public trackByFieldFn: (_index: number, field: NestedFieldDto) => any; public isEditing = false; public isEditable = false; @@ -112,7 +112,7 @@ export class FieldComponent implements OnChanges { } } - public trackByField(index: number, field: NestedFieldDto) { + public trackByField(_index: number, field: NestedFieldDto) { return field.fieldId + this.schema.id; } } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html index b6114b6d9..02c16ad8f 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.html @@ -1,4 +1,4 @@ -
+
@@ -15,7 +15,7 @@
- + @@ -29,7 +29,7 @@
- + diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.ts b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.ts index bb7b649ad..a225924fb 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-common.component.ts @@ -18,10 +18,7 @@ export class FieldFormCommonComponent { public readonly standalone = { standalone: true }; @Input() - public editForm: FormGroup; - - @Input() - public editFormSubmitted = false; + public fieldForm: FormGroup; @Input() public field: FieldDto; diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html index bef156f55..6ee23ff9d 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html @@ -1,29 +1,29 @@ - + - + - + - + - + - + - + - + - + \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.ts b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.ts index 45044b6ea..bfecc0da5 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.ts @@ -16,7 +16,7 @@ import { FieldDto } from '@app/shared'; }) export class FieldFormUIComponent { @Input() - public editForm: FormGroup; + public fieldForm: FormGroup; @Input() public field: FieldDto; diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html index 4a7b0f9dc..9341d0650 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.html @@ -1,32 +1,32 @@ - + - + - + - + - + - + - + - + - + - + \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts index 073be28ed..60f722ac8 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-validation.component.ts @@ -16,7 +16,7 @@ import { FieldDto, PatternDto } from '@app/shared'; }) export class FieldFormValidationComponent { @Input() - public editForm: FormGroup; + public fieldForm: FormGroup; @Input() public field: FieldDto; diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.html b/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.html index 274beedc0..44070098a 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.html @@ -13,18 +13,19 @@
+
- +
- +
- +
\ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.ts b/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.ts index f9e109f01..16ab3e60a 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form.component.ts @@ -22,17 +22,14 @@ export class FieldFormComponent implements AfterViewInit { public isEditable: boolean; @Input() - public editForm: FormGroup; + public fieldForm: FormGroup; @Input() - public editFormSubmitted: boolean; + public field: FieldDto; @Input() public patterns: ReadonlyArray; - @Input() - public field: FieldDto; - @Output() public cancel = new EventEmitter(); @@ -40,9 +37,9 @@ export class FieldFormComponent implements AfterViewInit { public ngAfterViewInit() { if (!this.isEditable) { - this.editForm.disable(); + this.fieldForm.disable(); } else { - this.editForm.enable(); + this.fieldForm.enable(); } } diff --git a/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.ts b/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.ts index 3866ada01..7cd45c635 100644 --- a/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/schema-fields.component.ts @@ -22,7 +22,7 @@ export class SchemaFieldsComponent implements OnInit { public addFieldDialog = new DialogModel(); - public trackByFieldFn: (index: number, field: FieldDto) => any; + public trackByFieldFn: (_index: number, field: FieldDto) => any; constructor( public readonly schemasState: SchemasState, @@ -39,7 +39,7 @@ export class SchemaFieldsComponent implements OnInit { this.schemasState.orderFields(this.schema, sorted(event)).subscribe(); } - public trackByField(index: number, field: FieldDto) { + public trackByField(_index: number, field: FieldDto) { return field.fieldId + this.schema.id; } } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html index 852ce60e8..26fd16182 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.ts index 714e04a67..a99ddaa44 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/array-validation.component.ts @@ -16,7 +16,7 @@ import { ArrayFieldPropertiesDto, FieldDto, SchemaTagSource } from '@app/shared' }) export class ArrayValidationComponent implements OnInit { @Input() - public editForm: FormGroup; + public fieldForm: FormGroup; @Input() public field: FieldDto; @@ -30,10 +30,10 @@ export class ArrayValidationComponent implements OnInit { } public ngOnInit() { - this.editForm.setControl('maxItems', + this.fieldForm.setControl('maxItems', new FormControl(this.properties.maxItems)); - this.editForm.setControl('minItems', + this.fieldForm.setControl('minItems', new FormControl(this.properties.minItems)); } } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html index 905a62088..05ae4ff8c 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.html @@ -1,5 +1,5 @@ -
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.ts index 61ddbe296..cb763b5a0 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/assets-ui.component.ts @@ -16,7 +16,7 @@ import { AssetsFieldPropertiesDto, FieldDto } from '@app/shared'; }) export class AssetsUIComponent implements OnInit { @Input() - public editForm: FormGroup; + public fieldForm: FormGroup; @Input() public field: FieldDto; @@ -25,10 +25,10 @@ export class AssetsUIComponent implements OnInit { public properties: AssetsFieldPropertiesDto; public ngOnInit() { - this.editForm.setControl('previewMode', + this.fieldForm.setControl('previewMode', new FormControl(this.properties.previewMode)); - this.editForm.setControl('resolveFirst', + this.fieldForm.setControl('resolveFirst', new FormControl(this.properties.resolveFirst)); } } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html b/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html index 7f620efbc..fffc9d8b4 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.html @@ -1,4 +1,4 @@ -
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts b/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts index d5889a051..d771f8f70 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts +++ b/frontend/app/features/schemas/pages/schema/fields/types/assets-validation.component.ts @@ -16,7 +16,7 @@ import { AssetsFieldPropertiesDto, FieldDto } from '@app/shared'; }) export class AssetsValidationComponent implements OnInit { @Input() - public editForm: FormGroup; + public fieldForm: FormGroup; @Input() public field: FieldDto; @@ -25,43 +25,43 @@ export class AssetsValidationComponent implements OnInit { public properties: AssetsFieldPropertiesDto; public ngOnInit() { - this.editForm.setControl('minItems', + this.fieldForm.setControl('minItems', new FormControl(this.properties.minItems)); - this.editForm.setControl('maxItems', + this.fieldForm.setControl('maxItems', new FormControl(this.properties.maxItems)); - this.editForm.setControl('minSize', + this.fieldForm.setControl('minSize', new FormControl(this.properties.minSize)); - this.editForm.setControl('maxSize', + this.fieldForm.setControl('maxSize', new FormControl(this.properties.maxSize)); - this.editForm.setControl('allowedExtensions', + this.fieldForm.setControl('allowedExtensions', new FormControl(this.properties.allowedExtensions)); - this.editForm.setControl('mustBeImage', + this.fieldForm.setControl('mustBeImage', new FormControl(this.properties.mustBeImage)); - this.editForm.setControl('minWidth', + this.fieldForm.setControl('minWidth', new FormControl(this.properties.minWidth)); - this.editForm.setControl('maxWidth', + this.fieldForm.setControl('maxWidth', new FormControl(this.properties.maxWidth)); - this.editForm.setControl('minHeight', + this.fieldForm.setControl('minHeight', new FormControl(this.properties.minHeight)); - this.editForm.setControl('maxHeight', + this.fieldForm.setControl('maxHeight', new FormControl(this.properties.maxHeight)); - this.editForm.setControl('aspectWidth', + this.fieldForm.setControl('aspectWidth', new FormControl(this.properties.aspectWidth)); - this.editForm.setControl('aspectHeight', + this.fieldForm.setControl('aspectHeight', new FormControl(this.properties.aspectHeight)); - this.editForm.setControl('allowDuplicates', + this.fieldForm.setControl('allowDuplicates', new FormControl(this.properties.allowDuplicates)); } } \ No newline at end of file diff --git a/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html index 95534937d..d9a5ddd1b 100644 --- a/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/types/boolean-ui.component.html @@ -1,4 +1,4 @@ -
+
@@ -25,14 +25,14 @@
-