diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs new file mode 100644 index 000000000..ee64fc208 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPattern.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// AppPattern.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class AppPattern + { + private readonly string name; + private readonly string pattern; + private readonly string message; + + public string Name + { + get { return name; } + } + + public string Pattern + { + get { return pattern; } + } + + public string Message + { + get { return message; } + } + + public AppPattern(string name, string pattern, string message = null) + { + Guard.NotNullOrEmpty(name, nameof(name)); + Guard.NotNullOrEmpty(pattern, nameof(pattern)); + + this.name = name; + this.pattern = pattern; + this.message = message; + } + + [Pure] + public AppPattern Update(string name, string pattern, string message) + { + return new AppPattern(name, pattern, message); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs new file mode 100644 index 000000000..459ecc4a6 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/AppPatterns.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// AppPatterns.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== +using System; +using System.Collections.Immutable; +using System.Diagnostics.Contracts; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Core.Apps +{ + public sealed class AppPatterns : DictionaryWrapper + { + public static readonly AppPatterns Empty = new AppPatterns(); + + private AppPatterns() + : base(ImmutableDictionary.Empty) + { + } + + public AppPatterns(ImmutableDictionary inner) + : base(inner) + { + } + + [Pure] + public AppPatterns Add(Guid id, string name, string pattern, string message) + { + var newPattern = new AppPattern(name, pattern, message); + + return new AppPatterns(Inner.Add(id, newPattern)); + } + + [Pure] + public AppPatterns Remove(Guid id) + { + return new AppPatterns(Inner.Remove(id)); + } + + [Pure] + public AppPatterns Update(Guid id, string name, string pattern, string message) + { + Guard.NotNullOrEmpty(name, nameof(name)); + Guard.NotNullOrEmpty(pattern, nameof(pattern)); + + if (!TryGetValue(id, out var appPattern)) + { + return this; + } + + return new AppPatterns(Inner.SetItem(id, appPattern.Update(name, pattern, message))); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs new file mode 100644 index 000000000..2e00fef01 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/AppPatternsConverter.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// AppPatternsConverter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Newtonsoft.Json; +using Squidex.Infrastructure.Json; + +namespace Squidex.Domain.Apps.Core.Apps.Json +{ + public sealed class AppPatternsConverter : JsonClassConverter + { + protected override void WriteValue(JsonWriter writer, AppPatterns value, JsonSerializer serializer) + { + var json = new Dictionary(value.Count); + + foreach (var client in value) + { + json.Add(client.Key, new JsonAppPattern(client.Value)); + } + + serializer.Serialize(writer, json); + } + + protected override AppPatterns ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) + { + var json = serializer.Deserialize>(reader); + + return new AppPatterns(json.ToImmutableDictionary(x => x.Key, x => x.Value.ToPattern())); + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs new file mode 100644 index 000000000..57364a197 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Model/Apps/Json/JsonAppPattern.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// JsonAppPattern.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== +using System; +using Newtonsoft.Json; +using Squidex.Infrastructure.Reflection; + +namespace Squidex.Domain.Apps.Core.Apps.Json +{ + public class JsonAppPattern + { + [JsonProperty] + public string Name { get; set; } + + [JsonProperty] + public string Pattern { get; set; } + + [JsonProperty] + public string Message { get; set; } + + public JsonAppPattern() + { + } + + public JsonAppPattern(AppPattern pattern) + { + SimpleMapper.Map(pattern, this); + } + + public AppPattern ToPattern() + { + return new AppPattern(Name, Pattern, Message); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index 1adb16a06..26cf396fe 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -10,6 +10,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using MongoDB.Bson; using MongoDB.Driver; using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Domain.Apps.Entities.Apps.State; @@ -46,6 +47,15 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return appEntity != null ? Guid.Parse(appEntity["_id"].AsString) : Guid.Empty; } + public async Task> QueryAppIdsAsync() + { + var appEntities = + await Collection.Find(new BsonDocument()).Only(x => x.Id) + .ToListAsync(); + + return appEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList(); + } + public async Task> QueryUserAppIdsAsync(string userId) { var appEntities = diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs index 5a3a7f1d3..72d86aba2 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppCommandMiddleware.cs @@ -138,6 +138,36 @@ namespace Squidex.Domain.Apps.Entities.Apps }); } + protected Task On(AddPattern command, CommandContext context) + { + return handler.UpdateSyncedAsync(context, a => + { + GuardAppPattern.CanAdd(a.State.Patterns, command); + + a.AddPattern(command); + }); + } + + protected Task On(DeletePattern command, CommandContext context) + { + return handler.UpdateSyncedAsync(context, a => + { + GuardAppPattern.CanDelete(a.State.Patterns, command); + + a.DeletePattern(command); + }); + } + + protected async Task On(UpdatePattern command, CommandContext context) + { + await handler.UpdateSyncedAsync(context, a => + { + GuardAppPattern.CanUpdate(a.State.Patterns, command); + + a.UpdatePattern(command); + }); + } + protected Task On(ChangePlan command, CommandContext context) { return handler.UpdateSyncedAsync(context, async a => diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index d11519917..03d8e4881 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs @@ -21,15 +21,29 @@ namespace Squidex.Domain.Apps.Entities.Apps { public sealed class AppDomainObject : DomainObjectBase { + private readonly InitialPatterns initialPatterns; + + public AppDomainObject(InitialPatterns initialPatterns) + { + Guard.NotNull(initialPatterns, nameof(initialPatterns)); + + this.initialPatterns = initialPatterns; + } + public AppDomainObject Create(CreateApp command) { ThrowIfCreated(); var appId = new NamedId(command.AppId, command.Name); - RaiseEvent(SimpleMapper.Map(command, CreateInitalEvent(appId))); - RaiseEvent(SimpleMapper.Map(command, CreateInitialOwner(appId, command))); - RaiseEvent(SimpleMapper.Map(command, CreateInitialLanguage(appId))); + RaiseEvent(CreateInitalEvent(appId, command.Actor, command.Name)); + RaiseEvent(CreateInitialOwner(appId, command.Actor)); + RaiseEvent(CreateInitialLanguage(appId, command.Actor)); + + foreach (var pattern in initialPatterns) + { + RaiseEvent(CreateInitialPattern(appId, command.Actor, pattern.Key, pattern.Value)); + } return this; } @@ -123,6 +137,33 @@ namespace Squidex.Domain.Apps.Entities.Apps return this; } + public AppDomainObject AddPattern(AddPattern command) + { + ThrowIfNotCreated(); + + RaiseEvent(SimpleMapper.Map(command, new AppPatternAdded())); + + return this; + } + + public AppDomainObject DeletePattern(DeletePattern command) + { + ThrowIfNotCreated(); + + RaiseEvent(SimpleMapper.Map(command, new AppPatternDeleted())); + + return this; + } + + public AppDomainObject UpdatePattern(UpdatePattern command) + { + ThrowIfNotCreated(); + + RaiseEvent(SimpleMapper.Map(command, new AppPatternUpdated())); + + return this; + } + private void RaiseEvent(AppEvent @event) { if (@event.AppId == null) @@ -133,19 +174,24 @@ namespace Squidex.Domain.Apps.Entities.Apps RaiseEvent(Envelope.Create(@event)); } - private static AppCreated CreateInitalEvent(NamedId appId) + private static AppCreated CreateInitalEvent(NamedId appId, RefToken actor, string name) + { + return new AppCreated { AppId = appId, Actor = actor, Name = name }; + } + + private static AppPatternAdded CreateInitialPattern(NamedId appId, RefToken actor, Guid id, AppPattern p) { - return new AppCreated { AppId = appId }; + return new AppPatternAdded { AppId = appId, Actor = actor, PatternId = id, Name = p.Name, Pattern = p.Pattern, Message = p.Message }; } - private static AppLanguageAdded CreateInitialLanguage(NamedId id) + private static AppLanguageAdded CreateInitialLanguage(NamedId appId, RefToken actor) { - return new AppLanguageAdded { AppId = id, Language = Language.EN }; + return new AppLanguageAdded { AppId = appId, Actor = actor, Language = Language.EN }; } - private static AppContributorAssigned CreateInitialOwner(NamedId id, SquidexCommand command) + private static AppContributorAssigned CreateInitialOwner(NamedId appId, RefToken actor) { - return new AppContributorAssigned { AppId = id, ContributorId = command.Actor.Identifier, Permission = AppContributorPermission.Owner }; + return new AppContributorAssigned { AppId = appId, Actor = actor, ContributorId = actor.Identifier, Permission = AppContributorPermission.Owner }; } private void ThrowIfNotCreated() diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs new file mode 100644 index 000000000..1c438c376 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/AddPattern.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// AddPattern.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class AddPattern : AppAggregateCommand + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string Message { get; set; } + + public AddPattern() + { + PatternId = Guid.NewGuid(); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs index eb173e4f5..57e1d18db 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/CreateApp.cs @@ -13,10 +13,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Commands { public sealed class CreateApp : SquidexCommand, IAggregateCommand { - public string Name { get; set; } - public Guid AppId { get; set; } + public string Name { get; set; } + Guid IAggregateCommand.AggregateId { get { return AppId; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs new file mode 100644 index 000000000..5fdd10028 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/DeletePattern.cs @@ -0,0 +1,17 @@ +// ========================================================================== +// DeletePattern.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class DeletePattern : AppAggregateCommand + { + public Guid PatternId { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs new file mode 100644 index 000000000..b025aeb22 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Commands/UpdatePattern.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// UpdatePattern.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Domain.Apps.Entities.Apps.Commands +{ + public sealed class UpdatePattern : AppAggregateCommand + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string Message { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPattern.cs b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPattern.cs new file mode 100644 index 000000000..142cb6061 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/Guards/GuardAppPattern.cs @@ -0,0 +1,97 @@ +// ========================================================================== +// GuardAppPattern.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== +using System; +using System.Linq; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Entities.Apps.Guards +{ + public static class GuardAppPattern + { + public static void CanAdd(AppPatterns patterns, AddPattern command) + { + Guard.NotNull(command, nameof(command)); + + Validate.It(() => "Cannot add pattern.", error => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + error(new ValidationError("Pattern name can not be empty.", nameof(command.Name))); + } + + if (patterns.Values.Any(x => x.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) + { + error(new ValidationError("Pattern name is already assigned.", nameof(command.Name))); + } + + if (string.IsNullOrWhiteSpace(command.Pattern)) + { + error(new ValidationError("Pattern can not be empty.", nameof(command.Pattern))); + } + else if (!command.Pattern.IsValidRegex()) + { + error(new ValidationError("Pattern is not a valid regular expression.", nameof(command.Pattern))); + } + + if (patterns.Values.Any(x => x.Pattern == command.Pattern)) + { + error(new ValidationError("Pattern already exists.", nameof(command.Pattern))); + } + }); + } + + public static void CanDelete(AppPatterns patterns, DeletePattern command) + { + Guard.NotNull(command, nameof(command)); + + if (!patterns.ContainsKey(command.PatternId)) + { + throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern)); + } + } + + public static void CanUpdate(AppPatterns patterns, UpdatePattern command) + { + Guard.NotNull(command, nameof(command)); + + if (!patterns.ContainsKey(command.PatternId)) + { + throw new DomainObjectNotFoundException(command.PatternId.ToString(), typeof(AppPattern)); + } + + Validate.It(() => "Cannot update pattern.", error => + { + if (string.IsNullOrWhiteSpace(command.Name)) + { + error(new ValidationError("Pattern name can not be empty.", nameof(command.Name))); + } + + if (patterns.Any(x => x.Key != command.PatternId && x.Value.Name.Equals(command.Name, StringComparison.OrdinalIgnoreCase))) + { + error(new ValidationError("Pattern name is already assigned.", nameof(command.Name))); + } + + if (string.IsNullOrWhiteSpace(command.Pattern)) + { + error(new ValidationError("Pattern can not be empty.", nameof(command.Pattern))); + } + else if (!command.Pattern.IsValidRegex()) + { + error(new ValidationError("Pattern is not a valid regular expression.", nameof(command.Pattern))); + } + + if (patterns.Any(x => x.Key != command.PatternId && x.Value.Pattern == command.Pattern)) + { + error(new ValidationError("Pattern already exists.", nameof(command.Pattern))); + } + }); + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs index 154478f1e..85912cac2 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs @@ -18,6 +18,8 @@ namespace Squidex.Domain.Apps.Entities.Apps AppClients Clients { get; } + AppPatterns Patterns { get; } + AppContributors Contributors { get; } LanguagesConfig LanguagesConfig { get; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs b/src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs new file mode 100644 index 000000000..eed1ca02b --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/InitialPatterns.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// InitialPatterns.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using Squidex.Domain.Apps.Core.Apps; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public sealed class InitialPatterns : Dictionary + { + public InitialPatterns() + { + } + + public InitialPatterns(Dictionary patterns) + : base(patterns) + { + } + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs b/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs index 208ff3d5d..de239006c 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs @@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Repositories { Task FindAppIdByNameAsync(string name); + Task> QueryAppIdsAsync(); + Task> QueryUserAppIdsAsync(string userId); } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs index 35ed457a5..b2d404e83 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/State/AppState.cs @@ -31,6 +31,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.State [JsonProperty] public AppClients Clients { get; set; } = AppClients.Empty; + [JsonProperty] + public AppPatterns Patterns { get; set; } = AppPatterns.Empty; + [JsonProperty] public AppContributors Contributors { get; set; } = AppContributors.Empty; @@ -77,6 +80,21 @@ namespace Squidex.Domain.Apps.Entities.Apps.State Clients = Clients.Revoke(@event.Id); } + protected void On(AppPatternAdded @event) + { + Patterns = Patterns.Add(@event.PatternId, @event.Name, @event.Pattern, @event.Message); + } + + protected void On(AppPatternDeleted @event) + { + Patterns = Patterns.Remove(@event.PatternId); + } + + protected void On(AppPatternUpdated @event) + { + Patterns = Patterns.Update(@event.PatternId, @event.Name, @event.Pattern, @event.Message); + } + protected void On(AppLanguageAdded @event) { LanguagesConfig = LanguagesConfig.Set(new LanguageConfig(@event.Language)); diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs b/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs new file mode 100644 index 000000000..49bc41e10 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Apps/AppPatternAdded.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// AppPatternAdded.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Apps +{ + [EventType(nameof(AppPatternAdded))] + public sealed class AppPatternAdded : AppEvent + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string Message { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs b/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs new file mode 100644 index 000000000..854980f94 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Apps/AppPatternDeleted.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// AppPatternDeleted.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Apps +{ + [EventType(nameof(AppPatternDeleted))] + public sealed class AppPatternDeleted : AppEvent + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + } +} diff --git a/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs b/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs new file mode 100644 index 000000000..b27b5d4e1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Events/Apps/AppPatternUpdated.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// AppPatternUpdated.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Domain.Apps.Events.Apps +{ + [EventType(nameof(AppPatternUpdated))] + public sealed class AppPatternUpdated : AppEvent + { + public Guid PatternId { get; set; } + + public string Name { get; set; } + + public string Pattern { get; set; } + + public string Message { get; set; } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs b/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs index eb0ab586a..f70bf2e71 100644 --- a/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs +++ b/src/Squidex.Infrastructure.MongoDb/Migrations/MongoMigrationStatus.cs @@ -40,7 +40,7 @@ namespace Squidex.Infrastructure.Migrations await Collection.FindOneAndUpdateAsync(x => x.Id == DefaultId, Update .Set(x => x.IsLocked, true) - .Set(x => x.Version, 0), + .SetOnInsert(x => x.Version, 0), UpsertFind); return entity == null || entity.IsLocked == false; diff --git a/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs b/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs new file mode 100644 index 000000000..846353eaf --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Apps/AppPatternsController.cs @@ -0,0 +1,130 @@ +// ========================================================================== +// AppPatternsController.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Squidex.Areas.Api.Controllers.Apps.Models; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Reflection; +using Squidex.Pipeline; + +namespace Squidex.Areas.Api.Controllers.Apps +{ + /// + /// Manages and configures app patterns. + /// + [ApiAuthorize] + [MustBeAppDeveloper] + [ApiExceptionFilter] + [AppApi] + [SwaggerTag(nameof(Apps))] + public sealed class AppPatternsController : ApiController + { + public AppPatternsController(ICommandBus commandBus) + : base(commandBus) + { + } + + /// + /// Get app patterns. + /// + /// The name of the app. + /// + /// 200 => Patterns returned. + /// 404 => App not found. + /// + /// + /// Gets all configured regex patterns for the app with the specified name. + /// + [HttpGet] + [Route("apps/{app}/patterns/")] + [ProducesResponseType(typeof(AppPatternDto[]), 200)] + [ApiCosts(0)] + public IActionResult GetPatterns(string app) + { + var response = + App.Patterns.Select(x => SimpleMapper.Map(x.Value, new AppPatternDto { PatternId = x.Key })) + .OrderBy(x => x.Name).ToList(); + + return Ok(response); + } + + /// + /// Create a new app patterm. + /// + /// The name of the app. + /// Pattern to be added to the app. + /// + /// 201 => Pattern generated. + /// 404 => App not found. + /// + [HttpPost] + [Route("apps/{app}/patterns/")] + [ProducesResponseType(typeof(AppPatternDto), 201)] + [ApiCosts(1)] + public async Task PostPattern(string app, [FromBody] UpdatePatternDto request) + { + var command = SimpleMapper.Map(request, new AddPattern()); + + await CommandBus.PublishAsync(command); + + var response = SimpleMapper.Map(request, new AppPatternDto { PatternId = command.PatternId }); + + return CreatedAtAction(nameof(GetPatterns), new { app }, request); + } + + /// + /// Update an existing app patterm. + /// + /// The name of the app. + /// The id of the pattern to be updated. + /// Pattern to be updated for the app. + /// + /// 204 => Pattern updated. + /// 404 => App not found or pattern not found. + /// + [HttpPut] + [Route("apps/{app}/patterns/{id}/")] + [ProducesResponseType(typeof(AppPatternDto), 201)] + [ApiCosts(1)] + public async Task UpdatePattern(string app, Guid id, [FromBody] UpdatePatternDto request) + { + var command = SimpleMapper.Map(request, new UpdatePattern { PatternId = id }); + + await CommandBus.PublishAsync(command); + + return NoContent(); + } + + /// + /// Revoke an app client + /// + /// The name of the app. + /// The id of the pattern to be deleted. + /// + /// 204 => Pattern removed. + /// 404 => App or pattern not found. + /// + /// + /// Schemas using this pattern will still function using the same Regular Expression + /// + [HttpDelete] + [Route("apps/{app}/patterns/{id}/")] + [ApiCosts(1)] + public async Task DeletePattern(string app, Guid id) + { + await CommandBus.PublishAsync(new DeletePattern { PatternId = id }); + + return NoContent(); + } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs new file mode 100644 index 000000000..ca5b7422e --- /dev/null +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/AppPatternDto.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// AppPatternDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.ComponentModel.DataAnnotations; + +namespace Squidex.Areas.Api.Controllers.Apps.Models +{ + public sealed class AppPatternDto + { + /// + /// Identifier for Pattern + /// + public Guid PatternId { get; set; } + + /// + /// The name of the suggestion. + /// + [Required] + public string Name { get; set; } + + /// + /// The regex pattern. + /// + [Required] + public string Pattern { get; set; } + + /// + /// The regex message. + /// + public string Message { get; set; } + } +} diff --git a/src/Squidex/Areas/Api/Controllers/UI/Models/UIRegexSuggestionDto.cs b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs similarity index 74% rename from src/Squidex/Areas/Api/Controllers/UI/Models/UIRegexSuggestionDto.cs rename to src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs index 363c86758..89506dfcc 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/Models/UIRegexSuggestionDto.cs +++ b/src/Squidex/Areas/Api/Controllers/Apps/Models/UpdatePatternDto.cs @@ -1,5 +1,5 @@ // ========================================================================== -// UIRegexSuggestionDto.cs +// AddPatternDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,9 +8,9 @@ using System.ComponentModel.DataAnnotations; -namespace Squidex.Areas.Api.Controllers.UI.Models +namespace Squidex.Areas.Api.Controllers.Apps.Models { - public sealed class UIRegexSuggestionDto + public class UpdatePatternDto { /// /// The name of the suggestion. @@ -23,5 +23,10 @@ namespace Squidex.Areas.Api.Controllers.UI.Models /// [Required] public string Pattern { get; set; } + + /// + /// The regex message. + /// + public string Message { get; set; } } } diff --git a/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs b/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs index c110d7517..7f6306615 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs @@ -6,19 +6,12 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Squidex.Areas.Api.Controllers.UI.Models { public sealed class UISettingsDto { - /// - /// The regex suggestions. - /// - [Required] - public List RegexSuggestions { get; set; } - /// /// The type of the map control. /// diff --git a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs index 17a3dbc52..441da2aac 100644 --- a/src/Squidex/Areas/Api/Controllers/UI/UIController.cs +++ b/src/Squidex/Areas/Api/Controllers/UI/UIController.cs @@ -6,8 +6,6 @@ // All rights reserved. // ========================================================================== -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using NSwag.Annotations; @@ -44,13 +42,6 @@ namespace Squidex.Areas.Api.Controllers.UI { var dto = new UISettingsDto { - RegexSuggestions = - uiOptions.RegexSuggestions? - .Where(x => - !string.IsNullOrWhiteSpace(x.Key) && - !string.IsNullOrWhiteSpace(x.Value)) - .Select(x => new UIRegexSuggestionDto { Name = x.Key, Pattern = x.Value }).ToList() - ?? new List(), MapType = uiOptions.Map?.Type ?? "OSM", MapKey = uiOptions.Map?.GoogleMaps?.Key }; diff --git a/src/Squidex/Config/Domain/SerializationServices.cs b/src/Squidex/Config/Domain/SerializationServices.cs index bbbd00046..1f98e8801 100644 --- a/src/Squidex/Config/Domain/SerializationServices.cs +++ b/src/Squidex/Config/Domain/SerializationServices.cs @@ -29,7 +29,7 @@ namespace Squidex.Config.Domain { private static readonly TypeNameRegistry TypeNameRegistry = new TypeNameRegistry() - .MapUnmapped(typeof(Migration01).Assembly) + .MapUnmapped(typeof(Migration01_FromCqrs).Assembly) .MapUnmapped(typeof(SquidexCoreModel).Assembly) .MapUnmapped(typeof(SquidexEvents).Assembly) .MapUnmapped(typeof(SquidexInfrastructure).Assembly); @@ -43,6 +43,7 @@ namespace Squidex.Config.Domain settings.ContractResolver = new ConverterContractResolver( new AppClientsConverter(), new AppContributorsConverter(), + new AppPatternsConverter(), new ClaimsPrincipalConverter(), new InstantConverter(), new LanguageConverter(), diff --git a/src/Squidex/Config/Domain/WriteServices.cs b/src/Squidex/Config/Domain/WriteServices.cs index 524c3e121..871cddcd2 100644 --- a/src/Squidex/Config/Domain/WriteServices.cs +++ b/src/Squidex/Config/Domain/WriteServices.cs @@ -6,8 +6,11 @@ // All rights reserved. // ========================================================================== +using System; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Migrate_01; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Assets; @@ -61,7 +64,10 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddTransientAs() + services.AddTransientAs() + .As(); + + services.AddTransientAs() .As(); services.AddTransientAs() @@ -78,6 +84,24 @@ namespace Squidex.Config.Domain services.AddTransientAs() .AsSelf(); + + services.AddSingleton(c => + { + var config = c.GetRequiredService>(); + + var result = new InitialPatterns(); + + foreach (var pattern in config.Value.RegexSuggestions) + { + if (!string.IsNullOrWhiteSpace(pattern.Key) && + !string.IsNullOrWhiteSpace(pattern.Value)) + { + result[Guid.NewGuid()] = new AppPattern(pattern.Key, pattern.Value); + } + } + + return result; + }); } } } diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.html b/src/Squidex/app/features/schemas/pages/schema/field.component.html index 07203613f..322930909 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field.component.html @@ -139,7 +139,7 @@
- +
diff --git a/src/Squidex/app/features/schemas/pages/schema/field.component.ts b/src/Squidex/app/features/schemas/pages/schema/field.component.ts index 0233f931a..b86ec1086 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/field.component.ts @@ -9,6 +9,7 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; import { + AppPatternDto, createProperties, fadeAnimation, FieldDto, @@ -31,6 +32,9 @@ export class FieldComponent implements OnInit { @Input() public schemas: SchemaDto[]; + @Input() + public regexSuggestions: AppPatternDto[] = []; + @Output() public locking = new EventEmitter(); diff --git a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html index 8415c4ac2..30265a7f6 100644 --- a/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/schema-page.component.html @@ -62,6 +62,7 @@
; + public regexSuggestions: AppPatternDto[] = []; + public exportSchemaDialog = new ModalView(); public configureScriptsDialog = new ModalView(); @@ -86,7 +90,8 @@ export class SchemaPageComponent implements OnDestroy, OnInit { constructor(public readonly ctx: AppContext, private readonly formBuilder: FormBuilder, private readonly router: Router, - private readonly schemasService: SchemasService + private readonly schemasService: SchemasService, + private readonly appPatternsService: AppPatternsService ) { } @@ -114,6 +119,11 @@ export class SchemaPageComponent implements OnDestroy, OnInit { } private load() { + this.appPatternsService.getPatterns(this.ctx.appName) + .subscribe(dtos => { + this.regexSuggestions = dtos.patterns; + }); + this.schemasService.getSchemas(this.ctx.appName) .subscribe(dtos => { this.schemas = ImmutableArray.of(dtos); diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html index a81166ef9..b98ffd8ac 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html @@ -33,14 +33,17 @@

Suggestions

-
+
{{suggestion.name}}
{{suggestion.pattern}}
+ + {{patternName}} +
-
+
diff --git a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts index 772da9f1e..4c0b8750a 100644 --- a/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts +++ b/src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts @@ -10,10 +10,9 @@ import { FormControl, FormGroup } from '@angular/forms'; import { Observable, Subscription } from 'rxjs'; import { + AppPatternDto, ModalView, - StringFieldPropertiesDto, - UIRegexSuggestionDto, - UIService + StringFieldPropertiesDto } from 'shared'; @Component({ @@ -23,7 +22,6 @@ import { }) export class StringValidationComponent implements OnDestroy, OnInit { private patternSubscription: Subscription; - private uiSettingsSubscription: Subscription; @Input() public editForm: FormGroup; @@ -31,22 +29,18 @@ export class StringValidationComponent implements OnDestroy, OnInit { @Input() public properties: StringFieldPropertiesDto; + @Input() + public regexSuggestions: AppPatternDto[] = []; + public showDefaultValue: Observable; - public showPatternMessage: Observable; + public showPatternMessage: boolean; public showPatternSuggestions: Observable; - - public regexSuggestions: UIRegexSuggestionDto[] = []; + public patternName: string; public regexSuggestionsModal = new ModalView(false, false); - constructor( - private readonly uiService: UIService - ) { - } - public ngOnDestroy() { this.patternSubscription.unsubscribe(); - this.uiSettingsSubscription.unsubscribe(); } public ngOnInit() { @@ -71,31 +65,41 @@ export class StringValidationComponent implements OnDestroy, OnInit { .map(x => !x); this.showPatternMessage = - this.editForm.controls['pattern'].valueChanges - .startWith('') - .map(x => x && x.trim().length > 0); + this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim().length > 0; this.showPatternSuggestions = this.editForm.controls['pattern'].valueChanges .startWith('') .map(x => !x || x.trim().length === 0); - this.uiSettingsSubscription = - this.uiService.getSettings() - .subscribe(settings => { - this.regexSuggestions = settings.regexSuggestions; - }); - this.patternSubscription = this.editForm.controls['pattern'].valueChanges .subscribe((value: string) => { if (!value || value.length === 0) { this.editForm.controls['patternMessage'].setValue(undefined); } + this.setPatternName(); }); + + this.setPatternName(); } - public setPattern(pattern: string) { - this.editForm.controls['pattern'].setValue(pattern); + public setPattern(pattern: AppPatternDto) { + this.patternName = pattern.name; + this.editForm.controls['pattern'].setValue(pattern.pattern); + this.editForm.controls['patternMessage'].setValue(pattern.message); + this.showPatternMessage = true; + } + + private setPatternName() { + const matchingPattern = this.regexSuggestions.find(x => x.pattern === this.editForm.controls['pattern'].value); + + if (matchingPattern) { + this.patternName = matchingPattern.name; + } else if (this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim() !== '') { + this.patternName = 'Advanced'; + } else { + this.patternName = undefined; + } } } \ No newline at end of file diff --git a/src/Squidex/app/features/settings/declarations.ts b/src/Squidex/app/features/settings/declarations.ts index ee83aef2c..4bbde478f 100644 --- a/src/Squidex/app/features/settings/declarations.ts +++ b/src/Squidex/app/features/settings/declarations.ts @@ -10,6 +10,8 @@ export * from './pages/clients/clients-page.component'; export * from './pages/contributors/contributors-page.component'; export * from './pages/languages/language.component'; export * from './pages/languages/languages-page.component'; +export * from './pages/patterns/pattern.component'; +export * from './pages/patterns/patterns-page.component'; export * from './pages/plans/plans-page.component'; export * from './settings-area.component'; \ No newline at end of file diff --git a/src/Squidex/app/features/settings/module.ts b/src/Squidex/app/features/settings/module.ts index e2be986c4..281a4489f 100644 --- a/src/Squidex/app/features/settings/module.ts +++ b/src/Squidex/app/features/settings/module.ts @@ -22,6 +22,8 @@ import { ContributorsPageComponent, LanguageComponent, LanguagesPageComponent, + PatternComponent, + PatternsPageComponent, PlansPageComponent, SettingsAreaComponent } from './declarations'; @@ -97,6 +99,26 @@ const routes: Routes = [ } } ] + }, + { + path: 'patterns', + component: PatternsPageComponent, + children: [ + { + path: 'history', + component: HistoryComponent, + data: { + channel: 'settings.patterns' + } + }, + { + path: 'help', + component: HelpComponent, + data: { + helpPage: '05-integrated/patterns' + } + } + ] } ] } @@ -115,6 +137,8 @@ const routes: Routes = [ ContributorsPageComponent, LanguageComponent, LanguagesPageComponent, + PatternComponent, + PatternsPageComponent, PlansPageComponent, SettingsAreaComponent ] diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts b/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts index cb23a9962..fdb0081c4 100644 --- a/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts +++ b/src/Squidex/app/features/settings/pages/languages/languages-page.component.ts @@ -82,7 +82,7 @@ export class LanguagesPageComponent implements OnInit { } public updateLanguage(language: AppLanguageDto) { - this.appLanguagesService.updateLanguage(this.ctx.appName, language.iso2Code, language, this.appLanguages.version) + this.appLanguagesService.putLanguage(this.ctx.appName, language.iso2Code, language, this.appLanguages.version) .subscribe(dto => { this.updateLanguages(this.appLanguages.updateLanguage(language, dto.version)); }, error => { diff --git a/src/Squidex/app/features/settings/pages/patterns/pattern.component.html b/src/Squidex/app/features/settings/pages/patterns/pattern.component.html new file mode 100644 index 000000000..7c8a7729e --- /dev/null +++ b/src/Squidex/app/features/settings/pages/patterns/pattern.component.html @@ -0,0 +1,45 @@ +
+
+
+ + + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+
\ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/patterns/pattern.component.scss b/src/Squidex/app/features/settings/pages/patterns/pattern.component.scss new file mode 100644 index 000000000..d5c22dda1 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/patterns/pattern.component.scss @@ -0,0 +1,13 @@ +@import '_vars'; +@import '_mixins'; + +.col-options { + min-width: 7.5rem; + max-width: 7.5rem; +} + +.col-name, +.col-message { + min-width: 10rem; + max-width: 10rem; +} \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/patterns/pattern.component.ts b/src/Squidex/app/features/settings/pages/patterns/pattern.component.ts new file mode 100644 index 000000000..28bf0ca55 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/patterns/pattern.component.ts @@ -0,0 +1,99 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; + +import { + AppPatternDto, + fadeAnimation, + ValidatorsEx, + UpdatePatternDto +} from 'shared'; + +@Component({ + selector: 'sqx-pattern', + styleUrls: ['./pattern.component.scss'], + templateUrl: './pattern.component.html', + animations: [ + fadeAnimation + ] +}) +export class PatternComponent implements OnInit { + @Input() + public isNew = false; + + @Input() + public pattern: AppPatternDto; + + @Output() + public removing = new EventEmitter(); + + @Output() + public updating = new EventEmitter(); + + public editFormSubmitted = false; + public editForm = + this.formBuilder.group({ + name: [ + '', + [ + Validators.required, + Validators.maxLength(100), + ValidatorsEx.pattern('[A-z0-9]+[A-z0-9\- ]*[A-z0-9]', 'Name can only contain letters, numbers, dashes and spaces.') + ] + ], + pattern: [ + '', + [ + Validators.required + ] + ], + message: [ + '', + [ + Validators.maxLength(1000) + ] + ] + }); + + constructor( + private readonly formBuilder: FormBuilder + ) { + } + + public ngOnInit() { + const pattern = this.pattern; + + if (pattern) { + this.editForm.setValue({ name: pattern.name, pattern: pattern.pattern, message: pattern.message || '' }); + } + } + + public cancel() { + this.editFormSubmitted = false; + this.editForm.reset(); + } + + public save() { + this.editFormSubmitted = true; + + if (this.editForm.valid) { + const requestDto = new UpdatePatternDto( + this.editForm.controls['name'].value, + this.editForm.controls['pattern'].value, + this.editForm.controls['message'].value); + + this.updating.emit(requestDto); + + if (!this.pattern) { + this.cancel(); + } + } + } +} + diff --git a/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html b/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html new file mode 100644 index 000000000..433c42aa8 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.html @@ -0,0 +1,41 @@ + + + +
+
+

Patterns

+
+ + + + +
+ +
+
+
+
+ + +
+ + +
+
+ + + +
+
+ + \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.scss b/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.scss new file mode 100644 index 000000000..fbb752506 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.scss @@ -0,0 +1,2 @@ +@import '_vars'; +@import '_mixins'; \ No newline at end of file diff --git a/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts b/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts new file mode 100644 index 000000000..d7e9868c2 --- /dev/null +++ b/src/Squidex/app/features/settings/pages/patterns/patterns-page.component.ts @@ -0,0 +1,87 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { Component, OnInit } from '@angular/core'; + +import { + AppContext, + AppPatternDto, + AppPatternsDto, + AppPatternsService, + HistoryChannelUpdated, + UpdatePatternDto +} from 'shared'; + +@Component({ + selector: 'sqx-patterns-page', + styleUrls: ['./patterns-page.component.scss'], + templateUrl: './patterns-page.component.html', + providers: [ + AppContext + ] +}) +export class PatternsPageComponent implements OnInit { + public appPatterns: AppPatternsDto; + + constructor(public readonly ctx: AppContext, + private readonly appPatternsService: AppPatternsService + ) { + } + + public ngOnInit() { + this.load(); + } + + public load() { + this.appPatternsService.getPatterns(this.ctx.appName).retry(2) + .subscribe(dtos => { + this.updatePatterns(dtos); + }, error => { + this.ctx.notifyError(error); + }); + } + + public addPattern(pattern: AppPatternDto) { + const requestDto = new UpdatePatternDto(pattern.name, pattern.pattern, pattern.message); + + this.appPatternsService.postPattern(this.ctx.appName, requestDto, this.appPatterns.version) + .subscribe(dto => { + this.updatePatterns(this.appPatterns.addPattern(dto.payload, dto.version)); + }, error => { + this.ctx.notifyError(error); + }); + } + + public updatePattern(pattern: AppPatternDto, update: UpdatePatternDto) { + this.appPatternsService.putPattern(this.ctx.appName, pattern.patternId, update, this.appPatterns.version) + .subscribe(dto => { + this.updatePatterns(this.appPatterns.updatePattern(pattern.update(update), dto.version)); + }, error => { + this.ctx.notifyError(error); + }); + } + + public removePattern(pattern: AppPatternDto) { + this.appPatternsService.deletePattern(this.ctx.appName, pattern.patternId, this.appPatterns.version) + .subscribe(dto => { + this.updatePatterns(this.appPatterns.deletePattern(pattern, dto.version)); + }, error => { + this.ctx.notifyError(error); + }); + } + + private updatePatterns(patterns: AppPatternsDto) { + this.appPatterns = + new AppPatternsDto( + patterns.patterns.sort((a, b) => { + return a.name.localeCompare(b.name); + }), + patterns.version); + + this.ctx.bus.emit(new HistoryChannelUpdated()); + } +} \ No newline at end of file diff --git a/src/Squidex/app/features/settings/settings-area.component.html b/src/Squidex/app/features/settings/settings-area.component.html index 7dd011dad..65954946d 100644 --- a/src/Squidex/app/features/settings/settings-area.component.html +++ b/src/Squidex/app/features/settings/settings-area.component.html @@ -32,6 +32,12 @@ +