diff --git a/extensions/Squidex.Extensions/Squidex.Extensions.csproj b/extensions/Squidex.Extensions/Squidex.Extensions.csproj index 4f8e9fcb0..0ad8a8925 100644 --- a/extensions/Squidex.Extensions/Squidex.Extensions.csproj +++ b/extensions/Squidex.Extensions/Squidex.Extensions.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs index c58263cb1..99d62bd4e 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs +++ b/src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs @@ -37,12 +37,12 @@ namespace Squidex.Domain.Apps.Core.Schemas } public ArrayField(long id, string name, Partitioning partitioning, ArrayFieldProperties properties = null, IFieldSettings settings = null) - : base(id, name, partitioning, properties) + : base(id, name, partitioning, properties, settings) { } public ArrayField(long id, string name, Partitioning partitioning, NestedField[] fields, ArrayFieldProperties properties = null, IFieldSettings settings = null) - : this(id, name, partitioning, properties) + : this(id, name, partitioning, properties, settings) { Guard.NotNull(fields, nameof(fields)); diff --git a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs b/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs index cebdfbf99..dc889f26b 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs @@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization { foreach (var sourceField in source.Ordered) { - if (!target.ByName.TryGetValue(sourceField.Name, out var targetField)) + if (!target.ByName.TryGetValue(sourceField.Name, out _)) { var id = sourceField.NamedId(); @@ -184,7 +184,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField) { - var fields = (sourceField as IArrayField)?.FieldCollection ?? FieldCollection.Empty; + var fields = ((IArrayField)sourceField)?.FieldCollection ?? FieldCollection.Empty; var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, id, options); diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index a1f5e3b7b..835e3125a 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules return null; } - if (!(@event.Payload is AppEvent appEvent)) + if (!(@event.Payload is AppEvent)) { return null; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs index d5f8d47df..c369497ac 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs @@ -33,9 +33,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger) { - var typed = @event as TEnrichedEvent; - - if (typed != null) + if (@event is TEnrichedEvent typed) { return Trigger(typed, (TTrigger)trigger); } @@ -45,9 +43,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId) { - var typed = @event as TEvent; - - if (typed != null) + if (@event is TEvent typed) { return Trigger(typed, (TTrigger)trigger, ruleId); } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs index 33e5fd891..4720ddfce 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper switch (value) { - case JsonNull n: + case JsonNull _: return JsValue.Null; case JsonScalar s: return new JsString(s.Value); diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs new file mode 100644 index 000000000..5959acc30 --- /dev/null +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs @@ -0,0 +1,57 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Security.Claims; +using Jint; +using Jint.Native; +using Jint.Runtime.Interop; +using NodaTime; +using Squidex.Domain.Apps.Core.Contents; +using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Core.Scripting +{ + public sealed class DefaultConverter : IObjectConverter + { + public static readonly DefaultConverter Instance = new DefaultConverter(); + + private DefaultConverter() + { + } + + public bool TryConvert(Engine engine, object value, out JsValue result) + { + result = null; + + if (value is Enum) + { + result = value.ToString(); + return true; + } + + switch (value) + { + case IUser user: + result = JintUser.Create(engine, user); + return true; + case ClaimsPrincipal principal: + result = JintUser.Create(engine, principal); + return true; + case Instant instant: + result = JsValue.FromObject(engine, instant.ToDateTimeUtc()); + return true; + case NamedContentData content: + result = new ContentDataObject(engine, content); + return true; + } + + return false; + } + } +} diff --git a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs index 3b3aa4b5b..8512ce30b 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs @@ -8,18 +8,15 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.Security.Claims; using Jint; using Jint.Native; using Jint.Native.Date; using Jint.Native.Object; using Jint.Runtime; using Jint.Runtime.Interop; -using NodaTime; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; using Squidex.Infrastructure; -using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Core.Scripting { @@ -27,40 +24,6 @@ namespace Squidex.Domain.Apps.Core.Scripting { public TimeSpan Timeout { get; set; } = TimeSpan.FromMilliseconds(200); - public sealed class Converter : IObjectConverter - { - public Engine Engine { get; set; } - - public bool TryConvert(object value, out JsValue result) - { - result = null; - - if (value is Enum) - { - result = value.ToString(); - return true; - } - - switch (value) - { - case IUser user: - result = JintUser.Create(Engine, user); - return true; - case ClaimsPrincipal principal: - result = JintUser.Create(Engine, principal); - return true; - case Instant instant: - result = JsValue.FromObject(Engine, instant.ToDateTimeUtc()); - return true; - case NamedContentData content: - result = new ContentDataObject(Engine, content); - return true; - } - - return false; - } - } - public void Execute(ScriptContext context, string script) { Guard.NotNull(context, nameof(context)); @@ -198,8 +161,6 @@ namespace Squidex.Domain.Apps.Core.Scripting private Engine CreateScriptEngine(IReferenceResolver resolver = null, Dictionary> customFormatters = null) { - var converter = new Converter(); - var engine = new Engine(options => { if (resolver != null) @@ -207,7 +168,7 @@ namespace Squidex.Domain.Apps.Core.Scripting options.SetReferencesResolver(resolver); } - options.TimeoutInterval(Timeout).Strict().AddObjectConverter(converter); + options.TimeoutInterval(Timeout).Strict().AddObjectConverter(DefaultConverter.Instance); }); if (customFormatters != null) @@ -218,8 +179,6 @@ namespace Squidex.Domain.Apps.Core.Scripting } } - converter.Engine = engine; - engine.SetValue("slugify", new ClrFunctionInstance(engine, "slugify", Slugify)); engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate)); engine.SetValue("formatDate", new ClrFunctionInstance(engine, "formatDate", FormatDate)); diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index a2f8491f7..f22143521 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs index 330014cb8..bf73299e7 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs @@ -29,8 +29,8 @@ namespace Squidex.Domain.Apps.Entities.Apps private readonly IUserResolver userResolver; private readonly IAppsByNameIndex appsByNameIndex; private readonly HashSet contributors = new HashSet(); + private readonly Dictionary userMapping = new Dictionary(); private Dictionary usersWithEmail = new Dictionary(); - private Dictionary userMapping = new Dictionary(); private bool isReserved; private string appName; diff --git a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs index f990ae781..c5da46700 100644 --- a/src/Squidex.Domain.Apps.Entities/EntityMapper.cs +++ b/src/Squidex.Domain.Apps.Entities/EntityMapper.cs @@ -6,7 +6,6 @@ // ========================================================================== using System; -using NodaTime; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure.EventSourcing; @@ -46,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities private static void SetCreated(IEntity entity, EnvelopeHeaders headers) { - if (entity is IUpdateableEntity updateable && updateable.Created == default(Instant)) + if (entity is IUpdateableEntity updateable && updateable.Created == default) { updateable.Created = headers.Timestamp(); } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs index a0cb3b57b..ba7773c68 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs @@ -79,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { var target = kvp.Value; - var (from, to) = GetDateRange(today, target.NumDays); + var (from, _) = GetDateRange(today, target.NumDays); if (!target.Triggered.HasValue || target.Triggered < from) { @@ -107,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking await WriteStateAsync(); } - private (DateTime, DateTime) GetDateRange(DateTime today, int? numDays) + private static (DateTime, DateTime) GetDateRange(DateTime today, int? numDays) { if (numDays.HasValue) { diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs index 70ec38ddc..167971a34 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs @@ -220,9 +220,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas var schemaSource = Snapshot.SchemaDef; var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton); - var @events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options); + var events = schemaSource.Synchronize(schemaTarget, serializer, () => Snapshot.SchemaFieldsTotal + 1, options); - foreach (var @event in @events) + foreach (var @event in events) { RaiseEvent(SimpleMapper.Map(command, (SchemaEvent)@event)); } diff --git a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index d4c7a0b8c..3f0a356b1 100644 --- a/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.States return (existing.Doc, existing.Version); } - return (default(T), EtagVersion.NotFound); + return (default, EtagVersion.NotFound); } } diff --git a/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs b/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs index 60a9f5679..58bfb6125 100644 --- a/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs +++ b/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs @@ -82,7 +82,7 @@ namespace Squidex.Infrastructure.EventSourcing { Task.Delay(ReconnectWaitMs, timerCts.Token).ContinueWith(t => { - dispatcher.DispatchAsync(() => Subscribe()); + dispatcher.DispatchAsync(Subscribe); }).Forget(); } else @@ -104,7 +104,7 @@ namespace Squidex.Infrastructure.EventSourcing public async Task StopAsync() { - await dispatcher.DispatchAsync(() => Unsubscribe()); + await dispatcher.DispatchAsync(Unsubscribe); await dispatcher.StopAndWaitAsync(); timerCts.Cancel(); diff --git a/src/Squidex.Infrastructure/Log/LockingLogStore.cs b/src/Squidex.Infrastructure/Log/LockingLogStore.cs index e6b931ebf..bafce0bd2 100644 --- a/src/Squidex.Infrastructure/Log/LockingLogStore.cs +++ b/src/Squidex.Infrastructure/Log/LockingLogStore.cs @@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.Log break; } - await Task.Delay(2000); + await Task.Delay(2000, cts.Token); } if (!cts.IsCancellationRequested) diff --git a/src/Squidex.Infrastructure/States/IStore.cs b/src/Squidex.Infrastructure/States/IStore.cs index e94d94558..390e555de 100644 --- a/src/Squidex.Infrastructure/States/IStore.cs +++ b/src/Squidex.Infrastructure/States/IStore.cs @@ -12,7 +12,7 @@ namespace Squidex.Infrastructure.States { public delegate void HandleEvent(Envelope @event); - public delegate void HandleSnapshot(T state); + public delegate void HandleSnapshot(T state); public interface IStore { diff --git a/src/Squidex/AppServices.cs b/src/Squidex/AppServices.cs deleted file mode 100644 index c880e9e1f..000000000 --- a/src/Squidex/AppServices.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Squidex.Areas.Api.Config.Swagger; -using Squidex.Areas.Api.Controllers.Contents; -using Squidex.Areas.IdentityServer.Config; -using Squidex.Config; -using Squidex.Config.Authentication; -using Squidex.Config.Domain; -using Squidex.Config.Web; -using Squidex.Domain.Apps.Entities.Assets; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Extensions.Actions.Twitter; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Diagnostics; -using Squidex.Pipeline; -using Squidex.Pipeline.Robots; - -namespace Squidex -{ - public static class AppServices - { - public static void AddAppServices(this IServiceCollection services, IConfiguration config) - { - services.AddHttpClient(); - services.AddLogging(); - services.AddMemoryCache(); - services.AddOptions(); - - services.AddMyAssetServices(config); - services.AddMyAuthentication(config); - services.AddMyEntitiesServices(config); - services.AddMyEventPublishersServices(config); - services.AddMyEventStoreServices(config); - services.AddMyIdentityServer(); - services.AddMyInfrastructureServices(); - services.AddMyLoggingServices(config); - services.AddMyMigrationServices(); - services.AddMyMvc(); - services.AddMyRuleServices(); - services.AddMySerializers(); - services.AddMyStoreServices(config); - services.AddMySwaggerSettings(); - services.AddMySubscriptionServices(config); - - services.Configure( - config.GetSection("contents")); - services.Configure( - config.GetSection("assets")); - services.Configure( - config.GetSection("mode")); - services.Configure( - config.GetSection("twitter")); - services.Configure( - config.GetSection("robots")); - services.Configure( - config.GetSection("healthz:gc")); - services.Configure( - config.GetSection("etags")); - - services.Configure( - config.GetSection("contentsController")); - services.Configure( - config.GetSection("urls")); - services.Configure( - config.GetSection("identity")); - services.Configure( - config.GetSection("ui")); - services.Configure( - config.GetSection("usage")); - } - } -} diff --git a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs index 703e77ce3..6f5812945 100644 --- a/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs +++ b/src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs @@ -14,7 +14,6 @@ using Squidex.Config; using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core.GenerateJsonSchema; using Squidex.Domain.Apps.Core.Schemas; -using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; using Squidex.Pipeline.Swagger; using Squidex.Shared; diff --git a/src/Squidex/Config/Domain/EventStoreServices.cs b/src/Squidex/Config/Domain/EventStoreServices.cs index e203383e1..defcbc2c7 100644 --- a/src/Squidex/Config/Domain/EventStoreServices.cs +++ b/src/Squidex/Config/Domain/EventStoreServices.cs @@ -37,7 +37,6 @@ namespace Squidex.Config.Domain return new MongoEventStore(mongDatabase, c.GetRequiredService()); }) - .As() .As(); }, ["GetEventStore"] = () => diff --git a/src/Squidex/Config/Domain/LoggingServices.cs b/src/Squidex/Config/Domain/LoggingServices.cs index 36af59330..5e18bd491 100644 --- a/src/Squidex/Config/Domain/LoggingServices.cs +++ b/src/Squidex/Config/Domain/LoggingServices.cs @@ -12,15 +12,10 @@ using Squidex.Domain.Apps.Entities.Apps; using Squidex.Infrastructure.Log; using Squidex.Pipeline; -#pragma warning disable RECS0092 // Convert field to readonly - namespace Squidex.Config.Domain { public static class LoggingServices { - private static ILogChannel console = new ConsoleLogChannel(); - private static ILogChannel file; - public static void AddMyLoggingServices(this IServiceCollection services, IConfiguration config) { if (config.GetValue("logging:human")) @@ -38,18 +33,13 @@ namespace Squidex.Config.Domain if (!string.IsNullOrWhiteSpace(loggingFile)) { - services.AddSingletonAs(file ?? (file = new FileChannel(loggingFile))) + services.AddSingletonAs(new FileChannel(loggingFile)) .As(); } var useColors = config.GetValue("logging:colors"); - if (console == null) - { - console = new ConsoleLogChannel(useColors); - } - - services.AddSingletonAs(console) + services.AddSingletonAs(new ConsoleLogChannel(useColors)) .As(); services.AddSingletonAs(c => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid())) diff --git a/src/Squidex/Config/Domain/SystemExtensions.cs b/src/Squidex/Config/Domain/SystemExtensions.cs deleted file mode 100644 index c11bd99fd..000000000 --- a/src/Squidex/Config/Domain/SystemExtensions.cs +++ /dev/null @@ -1,87 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Migrations; - -namespace Squidex.Config.Domain -{ - public static class SystemExtensions - { - public sealed class InitializeHostedService : IHostedService - { - private readonly IEnumerable targets; - private readonly IApplicationLifetime lifetime; - private readonly ISemanticLog log; - - public InitializeHostedService(IEnumerable targets, IApplicationLifetime lifetime, ISemanticLog log) - { - this.targets = targets; - this.lifetime = lifetime; - this.log = log; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - try - { - foreach (var target in targets) - { - await target.InitializeAsync(cancellationToken); - - log.LogInformation(w => w.WriteProperty("initializedSystem", target.GetType().Name)); - } - } - catch - { - lifetime.StopApplication(); - throw; - } - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - } - - public sealed class MigratorHostedService : IHostedService - { - private readonly IApplicationLifetime lifetime; - private readonly Migrator migrator; - - public MigratorHostedService(IApplicationLifetime lifetime, Migrator migrator) - { - this.lifetime = lifetime; - this.migrator = migrator; - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - try - { - await migrator.MigrateAsync(); - } - catch - { - lifetime.StopApplication(); - throw; - } - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - } - } -} diff --git a/src/Squidex/Config/Orleans/Extensions.cs b/src/Squidex/Config/Orleans/Extensions.cs index 7f9a14a8e..4ecbd022f 100644 --- a/src/Squidex/Config/Orleans/Extensions.cs +++ b/src/Squidex/Config/Orleans/Extensions.cs @@ -5,9 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Orleans; using Orleans.ApplicationParts; using Orleans.Configuration; +using Orleans.Hosting; +using OrleansDashboard; +using OrleansDashboard.Client; +using OrleansDashboard.Metrics; using Squidex.Domain.Apps.Entities; using Squidex.Infrastructure; @@ -26,5 +31,27 @@ namespace Squidex.Config.Orleans options.ClusterId = Constants.OrleansClusterId; options.ServiceId = Constants.OrleansClusterId; } + + public static ISiloHostBuilder UseDashboardEx(this ISiloHostBuilder builder, Action configurator = null) + { + builder.AddStartupTask(); + + builder.ConfigureApplicationParts(appParts => + appParts + .AddFrameworkPart(typeof(Dashboard).Assembly) + .AddFrameworkPart(typeof(DashboardClient).Assembly)); + + builder.ConfigureServices(services => + { + services.AddDashboard(options => + { + options.HostSelf = false; + }); + }); + + builder.AddIncomingGrainCallFilter(); + + return builder; + } } } diff --git a/src/Squidex/Config/Orleans/OrleansServices.cs b/src/Squidex/Config/Orleans/OrleansServices.cs index cb32bced3..352611909 100644 --- a/src/Squidex/Config/Orleans/OrleansServices.cs +++ b/src/Squidex/Config/Orleans/OrleansServices.cs @@ -1,34 +1,113 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; +using System.Net; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Orleans; +using Orleans.Configuration; +using Orleans.Hosting; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; +using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.Orleans; namespace Squidex.Config.Orleans { public static class OrleansServices { - public static void AddOrleansSilo(this IServiceCollection services) + public static IServiceProvider AddAndBuildOrleans(this IServiceCollection services, IConfiguration config) { - services.AddSingletonAs() - .As() - .AsSelf(); + services.Configure(options => + { + options.Configure(); + }); + + services.Configure(options => + { + options.FastKillOnProcessExit = false; + }); services.AddServicesForSelfHostedDashboard(null, options => { options.HideTrace = true; }); - services.AddSingletonAs(c => c.GetRequiredService()) - .As(); + services.AddHostedService(); + + var hostBuilder = new SiloHostBuilder() + .UseDashboardEx() + .EnableDirectClient() + .AddIncomingGrainCallFilter() + .AddStartupTask>() + .AddStartupTask>() + .AddStartupTask>() + .AddStartupTask>() + .ConfigureApplicationParts(builder => + { + builder.AddMyParts(); + }); + + config.ConfigureByOption("orleans:clustering", new Options + { + ["MongoDB"] = () => + { + hostBuilder.ConfigureEndpoints(Dns.GetHostName(), 11111, 40000, listenOnAnyHostAddress: true); + + var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); + + hostBuilder.UseMongoDBClustering(options => + { + options.ConnectionString = mongoConfiguration; + options.CollectionPrefix = "Orleans_"; + options.DatabaseName = mongoDatabaseName; + }); + }, + ["Development"] = () => + { + hostBuilder.UseLocalhostClustering(gatewayPort: 40000, serviceId: Constants.OrleansClusterId, clusterId: Constants.OrleansClusterId); + hostBuilder.Configure(options => options.ExpectedClusterSize = 1); + } + }); + + config.ConfigureByOption("store:type", new Options + { + ["MongoDB"] = () => + { + var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); + + hostBuilder.UseMongoDBReminders(options => + { + options.ConnectionString = mongoConfiguration; + options.CollectionPrefix = "Orleans_"; + options.DatabaseName = mongoDatabaseName; + }); + } + }); + + IServiceProvider provider = null; + + hostBuilder.UseServiceProviderFactory((siloServices) => + { + foreach (var descriptor in services) + { + siloServices.Add(descriptor); + } + + provider = siloServices.BuildServiceProvider(); + + return provider; + }).Build(); - services.AddSingletonAs(c => c.GetRequiredService().Client) - .As(); + return provider; } } } diff --git a/src/Squidex/Config/Orleans/SiloHost.cs b/src/Squidex/Config/Orleans/SiloHost.cs new file mode 100644 index 000000000..1817dfa12 --- /dev/null +++ b/src/Squidex/Config/Orleans/SiloHost.cs @@ -0,0 +1,51 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Orleans.Hosting; +using Squidex.Config.Startup; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Orleans +{ + public sealed class SiloHost : SafeHostedService + { + private readonly ISiloHost silo; + + public SiloHost(ISiloHost silo, ISemanticLog log, IApplicationLifetime lifetime) + : base(lifetime, log) + { + this.silo = silo; + } + + protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) + { + var watch = ValueStopwatch.StartNew(); + try + { + await silo.StartAsync(ct); + } + finally + { + var elapsedMs = watch.Stop(); + + log.LogInformation(w => w + .WriteProperty("message", "Silo started") + .WriteProperty("elapsedMs", elapsedMs)); + } + } + + protected override async Task StopAsync(ISemanticLog log, CancellationToken ct) + { + await silo.StopAsync(); + } + } +} diff --git a/src/Squidex/Config/Orleans/SiloServices.cs b/src/Squidex/Config/Orleans/SiloServices.cs deleted file mode 100644 index 865302883..000000000 --- a/src/Squidex/Config/Orleans/SiloServices.cs +++ /dev/null @@ -1,42 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Orleans.Hosting; - -namespace Squidex.Config.Orleans -{ - public static class SiloServices - { - public static void AddAppSiloServices(this IServiceCollection services, IConfiguration config) - { - config.ConfigureByOption("store:type", new Options - { - ["MongoDB"] = () => - { - var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); - - services.AddMongoDBMembershipTable(options => - { - options.ConnectionString = mongoConfiguration; - options.CollectionPrefix = "Orleans_"; - options.DatabaseName = mongoDatabaseName; - }); - - services.AddMongoDBReminders(options => - { - options.ConnectionString = mongoConfiguration; - options.CollectionPrefix = "Orleans_"; - options.DatabaseName = mongoDatabaseName; - }); - } - }); - } - } -} \ No newline at end of file diff --git a/src/Squidex/Config/Orleans/SiloWrapper.cs b/src/Squidex/Config/Orleans/SiloWrapper.cs deleted file mode 100644 index 55f8b6d11..000000000 --- a/src/Squidex/Config/Orleans/SiloWrapper.cs +++ /dev/null @@ -1,190 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Orleans; -using Orleans.Configuration; -using Orleans.Hosting; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Rules.UsageTracking; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Log.Adapter; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Config.Orleans -{ - public sealed class SiloWrapper : IHostedService - { - private readonly Lazy lazySilo; - private readonly ISemanticLog log; - private readonly IApplicationLifetime lifetime; - private bool isStopping; - - internal sealed class Source : IConfigurationSource - { - private readonly IConfigurationProvider configurationProvider; - - public Source(IConfigurationProvider configurationProvider) - { - this.configurationProvider = configurationProvider; - } - - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - return configurationProvider; - } - } - - public IClusterClient Client - { - get { return lazySilo.Value.Services.GetRequiredService(); } - } - - public SiloWrapper(IConfiguration config, ISemanticLog log, IApplicationLifetime lifetime) - { - this.lifetime = lifetime; - this.log = log; - - lazySilo = new Lazy(() => - { - var hostBuilder = new SiloHostBuilder() - .UseDashboard(options => options.HostSelf = false) - .EnableDirectClient() - .AddIncomingGrainCallFilter() - .AddStartupTask() - .AddStartupTask>() - .AddStartupTask>() - .AddStartupTask>() - .AddStartupTask>() - .Configure(options => - { - options.Configure(); - }) - .ConfigureApplicationParts(builder => - { - builder.AddMyParts(); - }) - .ConfigureLogging((hostingContext, builder) => - { - builder.AddConfiguration(hostingContext.Configuration.GetSection("logging")); - builder.AddSemanticLog(); - builder.AddFilter(); - }) - .ConfigureServices((context, services) => - { - services.AddAppSiloServices(context.Configuration); - services.AddAppServices(context.Configuration); - - services.Configure(options => options.FastKillOnProcessExit = false); - }) - .ConfigureAppConfiguration((hostContext, builder) => - { - if (config is IConfigurationRoot root) - { - foreach (var provider in root.Providers) - { - builder.Add(new Source(provider)); - } - } - }); - - config.ConfigureByOption("orleans:clustering", new Options - { - ["MongoDB"] = () => - { - hostBuilder.ConfigureEndpoints(Dns.GetHostName(), 11111, 40000, listenOnAnyHostAddress: true); - - var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); - - hostBuilder.UseMongoDBClustering(options => - { - options.ConnectionString = mongoConfiguration; - options.CollectionPrefix = "Orleans_"; - options.DatabaseName = mongoDatabaseName; - }); - }, - ["Development"] = () => - { - hostBuilder.UseLocalhostClustering(gatewayPort: 40000, serviceId: Constants.OrleansClusterId, clusterId: Constants.OrleansClusterId); - hostBuilder.Configure(options => options.ExpectedClusterSize = 1); - } - }); - - config.ConfigureByOption("store:type", new Options - { - ["MongoDB"] = () => - { - var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); - var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); - - hostBuilder.UseMongoDBReminders(options => - { - options.ConnectionString = mongoConfiguration; - options.CollectionPrefix = "Orleans_"; - options.DatabaseName = mongoDatabaseName; - }); - } - }); - - var silo = hostBuilder.Build(); - - silo.Stopped.ContinueWith(x => - { - if (!isStopping) - { - lifetime.StopApplication(); - } - }); - - return silo; - }); - } - - public async Task StartAsync(CancellationToken cancellationToken) - { - var watch = ValueStopwatch.StartNew(); - try - { - await lazySilo.Value.StartAsync(cancellationToken); - } - catch - { - lifetime.StopApplication(); - throw; - } - finally - { - var elapsedMs = watch.Stop(); - - log.LogInformation(w => w - .WriteProperty("message", "Silo started") - .WriteProperty("elapsedMs", elapsedMs)); - } - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - if (lazySilo.IsValueCreated) - { - isStopping = true; - - await lazySilo.Value.StopAsync(cancellationToken); - } - } - } -} diff --git a/src/Squidex/Config/Startup/InitializerHost.cs b/src/Squidex/Config/Startup/InitializerHost.cs new file mode 100644 index 000000000..9d8e2790a --- /dev/null +++ b/src/Squidex/Config/Startup/InitializerHost.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; + +namespace Squidex.Config.Startup +{ + public sealed class InitializerHost : SafeHostedService + { + private readonly IEnumerable targets; + + public InitializerHost(IEnumerable targets, IApplicationLifetime lifetime, ISemanticLog log) + : base(lifetime, log) + { + this.targets = targets; + } + + protected override async Task StartAsync(ISemanticLog log, CancellationToken ct) + { + foreach (var target in targets.Distinct()) + { + await target.InitializeAsync(ct); + + log.LogInformation(w => w.WriteProperty("initializedSystem", target.GetType().Name)); + } + } + } +} diff --git a/src/Squidex/Config/Startup/MigratorHost.cs b/src/Squidex/Config/Startup/MigratorHost.cs new file mode 100644 index 000000000..f65867da4 --- /dev/null +++ b/src/Squidex/Config/Startup/MigratorHost.cs @@ -0,0 +1,31 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Migrations; + +namespace Squidex.Config.Startup +{ + public sealed class MigratorHost : SafeHostedService + { + private readonly Migrator migrator; + + public MigratorHost(Migrator migrator, IApplicationLifetime lifetime, ISemanticLog log) + : base(lifetime, log) + { + this.migrator = migrator; + } + + protected override Task StartAsync(ISemanticLog log, CancellationToken ct) + { + return migrator.MigrateAsync(); + } + } +} diff --git a/src/Squidex/Config/Startup/SafeHostedService.cs b/src/Squidex/Config/Startup/SafeHostedService.cs new file mode 100644 index 000000000..90f39c691 --- /dev/null +++ b/src/Squidex/Config/Startup/SafeHostedService.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Config.Startup +{ + public abstract class SafeHostedService : IHostedService + { + private readonly IApplicationLifetime lifetime; + private readonly ISemanticLog log; + private bool isStarted; + + protected SafeHostedService(IApplicationLifetime lifetime, ISemanticLog log) + { + this.lifetime = lifetime; + + this.log = log; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + try + { + await StartAsync(log, cancellationToken); + + isStarted = true; + } + catch + { + lifetime.StopApplication(); + throw; + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (isStarted) + { + await StopAsync(log, cancellationToken); + } + } + + protected abstract Task StartAsync(ISemanticLog log, CancellationToken ct); + + protected virtual Task StopAsync(ISemanticLog log, CancellationToken ct) + { + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex/Pipeline/Squid/SquidMiddleware.cs b/src/Squidex/Pipeline/Squid/SquidMiddleware.cs index 2a5dd9dae..157c3c381 100644 --- a/src/Squidex/Pipeline/Squid/SquidMiddleware.cs +++ b/src/Squidex/Pipeline/Squid/SquidMiddleware.cs @@ -63,14 +63,9 @@ namespace Squidex.Pipeline.Squid background = backgroundValue; } - var isSmall = false; + var isSmall = request.Query.TryGetValue("small", out _); - if (request.Query.TryGetValue("small", out _)) - { - isSmall = true; - } - - var svg = string.Empty; + string svg; if (isSmall) { diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index cebe95677..cda49b8ed 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -91,7 +91,7 @@ - + diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index ff233f5f7..10b2546e3 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -5,17 +5,31 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Areas.Api; +using Squidex.Areas.Api.Config.Swagger; +using Squidex.Areas.Api.Controllers.Contents; using Squidex.Areas.Frontend; using Squidex.Areas.IdentityServer; +using Squidex.Areas.IdentityServer.Config; using Squidex.Areas.OrleansDashboard; using Squidex.Areas.Portal; +using Squidex.Config; +using Squidex.Config.Authentication; using Squidex.Config.Domain; using Squidex.Config.Orleans; +using Squidex.Config.Startup; using Squidex.Config.Web; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Extensions.Actions.Twitter; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Diagnostics; +using Squidex.Pipeline; +using Squidex.Pipeline.Robots; namespace Squidex { @@ -28,13 +42,63 @@ namespace Squidex this.configuration = configuration; } - public void ConfigureServices(IServiceCollection services) + public IServiceProvider ConfigureServices(IServiceCollection services) { - services.AddOrleansSilo(); - services.AddAppServices(configuration); + var config = configuration; - services.AddHostedService(); - services.AddHostedService(); + services.AddHttpClient(); + services.AddLogging(); + services.AddMemoryCache(); + services.AddOptions(); + + services.AddMyAssetServices(config); + services.AddMyAuthentication(config); + services.AddMyEntitiesServices(config); + services.AddMyEventPublishersServices(config); + services.AddMyEventStoreServices(config); + services.AddMyIdentityServer(); + services.AddMyInfrastructureServices(); + services.AddMyLoggingServices(config); + services.AddMyMigrationServices(); + services.AddMyMvc(); + services.AddMyRuleServices(); + services.AddMySerializers(); + services.AddMyStoreServices(config); + services.AddMySwaggerSettings(); + services.AddMySubscriptionServices(config); + + services.Configure( + config.GetSection("contents")); + services.Configure( + config.GetSection("assets")); + services.Configure( + config.GetSection("mode")); + services.Configure( + config.GetSection("twitter")); + services.Configure( + config.GetSection("robots")); + services.Configure( + config.GetSection("healthz:gc")); + services.Configure( + config.GetSection("etags")); + + services.Configure( + config.GetSection("contentsController")); + services.Configure( + config.GetSection("urls")); + services.Configure( + config.GetSection("identity")); + services.Configure( + config.GetSection("ui")); + services.Configure( + config.GetSection("usage")); + + services.AddHostedService(); + services.AddHostedService(); + + var provider = services.AddAndBuildOrleans(configuration); + + return provider; } public void Configure(IApplicationBuilder app) diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html index ce58ed3e2..59a73c5e1 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html @@ -6,7 +6,7 @@ - @@ -42,13 +42,13 @@ {{eventConsumer.position}} - - - diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index d7c4b44ce..c73493770 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -5,11 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Subscription, timer } from 'rxjs'; +import { Component, OnInit } from '@angular/core'; +import { timer } from 'rxjs'; import { onErrorResumeNext, switchMap } from 'rxjs/operators'; -import { DialogModel } from '@app/shared'; +import { DialogModel, ResourceOwner } from '@app/shared'; import { EventConsumerDto } from './../../services/event-consumers.service'; import { EventConsumersState } from './../../state/event-consumers.state'; @@ -19,27 +19,20 @@ import { EventConsumersState } from './../../state/event-consumers.state'; styleUrls: ['./event-consumers-page.component.scss'], templateUrl: './event-consumers-page.component.html' }) -export class EventConsumersPageComponent implements OnDestroy, OnInit { - private timerSubscription: Subscription; - +export class EventConsumersPageComponent extends ResourceOwner implements OnInit { public eventConsumerErrorDialog = new DialogModel(); public eventConsumerError = ''; constructor( public readonly eventConsumersState: EventConsumersState ) { - } - - public ngOnDestroy() { - this.timerSubscription.unsubscribe(); + super(); } public ngOnInit() { this.eventConsumersState.load().pipe(onErrorResumeNext()).subscribe(); - this.timerSubscription = - timer(2000, 2000).pipe(switchMap(x => this.eventConsumersState.load(true, true).pipe(onErrorResumeNext()))) - .subscribe(); + this.own(timer(2000, 2000).pipe(switchMap(() => this.eventConsumersState.load(true, true)))); } public reload() { diff --git a/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts b/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts index 026f0becb..c0f6304ab 100644 --- a/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts +++ b/src/Squidex/app/features/administration/pages/restore/restore-page.component.ts @@ -5,15 +5,16 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; -import { Subscription, timer } from 'rxjs'; -import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators'; +import { timer } from 'rxjs'; +import { onErrorResumeNext, switchMap } from 'rxjs/operators'; import { AuthService, BackupsService, DialogService, + ResourceOwner, RestoreDto, RestoreForm } from '@app/shared'; @@ -23,9 +24,7 @@ import { styleUrls: ['./restore-page.component.scss'], templateUrl: './restore-page.component.html' }) -export class RestorePageComponent implements OnDestroy, OnInit { - private timerSubscription: Subscription; - +export class RestorePageComponent extends ResourceOwner implements OnInit { public restoreJob: RestoreDto | null; public restoreForm = new RestoreForm(this.formBuilder); @@ -35,18 +34,17 @@ export class RestorePageComponent implements OnDestroy, OnInit { private readonly dialogs: DialogService, private readonly formBuilder: FormBuilder ) { - } - - public ngOnDestroy() { - this.timerSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.timerSubscription = - timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore().pipe(onErrorResumeNext())), filter(x => !!x)) - .subscribe(dto => { - this.restoreJob = dto!; - }); + this.own( + timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore().pipe(onErrorResumeNext()))) + .subscribe(job => { + if (job) { + this.restoreJob = job; + } + })); } public restore() { diff --git a/src/Squidex/app/features/administration/pages/users/user-page.component.ts b/src/Squidex/app/features/administration/pages/users/user-page.component.ts index 3be7e69a6..e009b6115 100644 --- a/src/Squidex/app/features/administration/pages/users/user-page.component.ts +++ b/src/Squidex/app/features/administration/pages/users/user-page.component.ts @@ -5,10 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { Subscription } from 'rxjs'; + +import { ResourceOwner } from '@app/shared'; import { UserDto } from './../../services/users.service'; import { UserForm, UsersState } from './../../state/users.state'; @@ -18,9 +19,7 @@ import { UserForm, UsersState } from './../../state/users.state'; styleUrls: ['./user-page.component.scss'], templateUrl: './user-page.component.html' }) -export class UserPageComponent implements OnDestroy, OnInit { - private selectedUserSubscription: Subscription; - +export class UserPageComponent extends ResourceOwner implements OnInit { public canUpdate = false; public user?: { user: UserDto, isCurrentUser: boolean }; @@ -32,22 +31,19 @@ export class UserPageComponent implements OnDestroy, OnInit { private readonly route: ActivatedRoute, private readonly router: Router ) { - } - - public ngOnDestroy() { - this.selectedUserSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.selectedUserSubscription = + this.own( this.usersState.selectedUser .subscribe(selectedUser => { - this.user = selectedUser; + this.user = selectedUser!; if (selectedUser) { this.userForm.load(selectedUser.user); } - }); + })); } public save() { diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.html b/src/Squidex/app/features/administration/pages/users/users-page.component.html index 4a8963e8f..0c51c3304 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.html @@ -6,7 +6,7 @@ - @@ -18,7 +18,7 @@ - @@ -61,10 +61,10 @@ - - @@ -81,7 +81,7 @@ diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.ts b/src/Squidex/app/features/administration/state/event-consumers.state.ts index e98fd0cf8..ae16e1f21 100644 --- a/src/Squidex/app/features/administration/state/event-consumers.state.ts +++ b/src/Squidex/app/features/administration/state/event-consumers.state.ts @@ -21,7 +21,7 @@ import { EventConsumerDto, EventConsumersService } from './../services/event-con interface Snapshot { eventConsumers: ImmutableArray; - isLoaded?: false; + isLoaded?: boolean; } @Injectable() diff --git a/src/Squidex/app/features/administration/state/users.state.ts b/src/Squidex/app/features/administration/state/users.state.ts index 85266dcb9..05482ff9a 100644 --- a/src/Squidex/app/features/administration/state/users.state.ts +++ b/src/Squidex/app/features/administration/state/users.state.ts @@ -98,7 +98,7 @@ interface Snapshot { isLoaded?: boolean; - selectedUser?: SnapshotUser; + selectedUser?: SnapshotUser | null; } @Injectable() diff --git a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html index 74f0d3054..d762911d5 100644 --- a/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html +++ b/src/Squidex/app/features/api/pages/graphql/graphql-page.component.html @@ -1,5 +1,5 @@ - +
\ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.html b/src/Squidex/app/features/assets/pages/assets-filters-page.component.html index 9453bc868..0a32d5244 100644 --- a/src/Squidex/app/features/assets/pages/assets-filters-page.component.html +++ b/src/Squidex/app/features/assets/pages/assets-filters-page.component.html @@ -14,7 +14,8 @@ - +
{{tag.name}} @@ -29,7 +30,8 @@

Saved queries

-
+ diff --git a/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts b/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts index 87cbae194..69c0852de 100644 --- a/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts +++ b/src/Squidex/app/features/assets/pages/assets-filters-page.component.ts @@ -47,4 +47,12 @@ export class AssetsFiltersPageComponent { public isSelectedQuery(query: string) { return query === this.assetsState.snapshot.assetsQuery || (!query && !this.assetsState.assetsQuery); } + + public trackByTag(index: number, tag: { name: string }) { + return tag.name; + } + + public trackByQuery(index: number, query: { name: string }) { + return query.name; + } } \ No newline at end of file diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.html b/src/Squidex/app/features/assets/pages/assets-page.component.html index 36e9e5e3d..8c27b3e4c 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.html +++ b/src/Squidex/app/features/assets/pages/assets-page.component.html @@ -1,6 +1,6 @@ - + Assets @@ -10,7 +10,7 @@
-
diff --git a/src/Squidex/app/features/content/declarations.ts b/src/Squidex/app/features/content/declarations.ts index f94dfd517..759513526 100644 --- a/src/Squidex/app/features/content/declarations.ts +++ b/src/Squidex/app/features/content/declarations.ts @@ -9,6 +9,7 @@ export * from './pages/comments/comments-page.component'; export * from './pages/content/content-field.component'; export * from './pages/content/content-history-page.component'; export * from './pages/content/content-page.component'; +export * from './pages/content/field-languages.component'; export * from './pages/contents/contents-filters-page.component'; export * from './pages/contents/contents-page.component'; export * from './pages/schemas/schemas-page.component'; @@ -17,6 +18,7 @@ export * from './shared/array-editor.component'; export * from './shared/assets-editor.component'; export * from './shared/array-item.component'; export * from './shared/content-item.component'; +export * from './shared/content-item-editor.component'; export * from './shared/content-status.component'; export * from './shared/contents-selector.component'; export * from './shared/due-time-selector.component'; diff --git a/src/Squidex/app/features/content/module.ts b/src/Squidex/app/features/content/module.ts index 1f9af170a..d47cb229a 100644 --- a/src/Squidex/app/features/content/module.ts +++ b/src/Squidex/app/features/content/module.ts @@ -29,6 +29,7 @@ import { ContentFieldComponent, ContentHistoryPageComponent, ContentItemComponent, + ContentItemEditorComponent, ContentPageComponent, ContentsFiltersPageComponent, ContentsPageComponent, @@ -36,6 +37,7 @@ import { ContentStatusComponent, DueTimeSelectorComponent, FieldEditorComponent, + FieldLanguagesComponent, PreviewButtonComponent, ReferencesEditorComponent, SchemasPageComponent @@ -112,6 +114,7 @@ const routes: Routes = [ ContentFieldComponent, ContentHistoryPageComponent, ContentItemComponent, + ContentItemEditorComponent, ContentPageComponent, ContentsFiltersPageComponent, ContentStatusComponent, @@ -119,6 +122,7 @@ const routes: Routes = [ ContentsSelectorComponent, DueTimeSelectorComponent, FieldEditorComponent, + FieldLanguagesComponent, PreviewButtonComponent, ReferencesEditorComponent, SchemasPageComponent diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.html b/src/Squidex/app/features/content/pages/content/content-field.component.html index 2dd8dd959..3ea27986d 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.html +++ b/src/Squidex/app/features/content/pages/content/content-field.component.html @@ -1,42 +1,83 @@ -
-
- - - - - - - - Please remember to check all languages when you see validation errors. - - +
+
+
+
+ + +
+ + +
+ + +
+
+ + + + + +
- -
- - -
-
+
+ - - - - +
+
+ + +
+ + +
+ + +
+
+ + + + + +
+
diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.scss b/src/Squidex/app/features/content/pages/content/content-field.component.scss index 9c13f51a7..821dd5465 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.scss +++ b/src/Squidex/app/features/content/pages/content/content-field.component.scss @@ -11,8 +11,26 @@ @include absolute(.7rem, 1.25rem, auto, auto); } -.invalid { - border-left-color: $color-theme-error; +.row { + margin-left: -1.5rem; + margin-right: -1.5rem; +} + +.col-12 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.col-6 { + & { + padding-left: 1.5rem; + padding-right: .5rem; + } + + &.col-right { + padding-left: .5rem; + padding-right: 1.5rem; + } } .field { @@ -20,9 +38,24 @@ color: $color-theme-error; } + &-invalid { + border-left-color: $color-theme-error; + } + &-disabled { color: $color-border-dark; font-size: .8rem; font-weight: normal; } + + &-copy { + @include absolute(1rem, auto, auto, -1rem); + z-index: 1000; + } +} + +.compare { + padding: .5rem 0; + border: 0; + border-bottom: 1px solid $color-border; } \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/content/content-field.component.ts b/src/Squidex/app/features/content/pages/content/content-field.component.ts index cf176d979..3d5f98d80 100644 --- a/src/Squidex/app/features/content/pages/content/content-field.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-field.component.ts @@ -5,20 +5,21 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, DoCheck, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { AbstractControl, FormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; +import { combineLatest } from 'rxjs/operators'; import { AppLanguageDto, EditContentForm, fieldInvariant, - ImmutableArray, + invalid$, LocalStoreService, RootFieldDto, SchemaDto, - Types + Types, + value$ } from '@app/shared'; @Component({ @@ -26,7 +27,7 @@ import { styleUrls: ['./content-field.component.scss'], templateUrl: './content-field.component.html' }) -export class ContentFieldComponent implements DoCheck, OnChanges { +export class ContentFieldComponent implements OnChanges { @Input() public form: EditContentForm; @@ -36,6 +37,9 @@ export class ContentFieldComponent implements DoCheck, OnChanges { @Input() public fieldForm: FormGroup; + @Input() + public fieldFormCompare?: FormGroup; + @Input() public schema: SchemaDto; @@ -43,15 +47,18 @@ export class ContentFieldComponent implements DoCheck, OnChanges { public language: AppLanguageDto; @Input() - public languages: ImmutableArray; + public languages: AppLanguageDto[]; @Output() public languageChange = new EventEmitter(); public selectedFormControl: AbstractControl; + public selectedFormControlCompare?: AbstractControl; + public showAllControls = false; public isInvalid: Observable; + public isDifferent: Observable; constructor( private readonly localStore: LocalStoreService @@ -60,36 +67,73 @@ export class ContentFieldComponent implements DoCheck, OnChanges { public ngOnChanges(changes: SimpleChanges) { if (changes['fieldForm']) { - this.isInvalid = this.fieldForm.statusChanges.pipe(startWith(this.fieldForm.invalid), map(x => this.fieldForm.invalid)); + this.isInvalid = invalid$(this.fieldForm); + } + + if ((changes['fieldForm'] || changes['fieldFormCompare']) && this.fieldFormCompare) { + this.isDifferent = + value$(this.fieldForm).pipe( + combineLatest(value$(this.fieldFormCompare), + (lhs, rhs) => !Types.jsJsonEquals(lhs, rhs))); } if (changes['field']) { this.showAllControls = this.localStore.getBoolean(this.configKey()); } + + const control = this.findControl(this.fieldForm); + + if (this.selectedFormControl !== control) { + if (this.selectedFormControl && Types.isFunction(this.selectedFormControl['_clearChangeFns'])) { + this.selectedFormControl['_clearChangeFns'](); + } + + this.selectedFormControl = control; + } + + if (this.fieldFormCompare) { + const controlCompare = this.findControl(this.fieldFormCompare); + + if (this.selectedFormControlCompare !== controlCompare) { + if (this.selectedFormControlCompare && Types.isFunction(this.selectedFormControlCompare['_clearChangeFns'])) { + this.selectedFormControlCompare['_clearChangeFns'](); + } + + this.selectedFormControlCompare = controlCompare; + } + } } - public toggleShowAll() { - this.showAllControls = !this.showAllControls; + public changeShowAllControls(value: boolean) { + this.showAllControls = value; this.localStore.setBoolean(this.configKey(), this.showAllControls); } - public ngDoCheck() { - let control: AbstractControl; + public copy() { + if (this.selectedFormControlCompare && this.fieldFormCompare) { + if (this.showAllControls) { + this.fieldForm.setValue(this.fieldFormCompare.value); + } else { + this.selectedFormControl.setValue(this.selectedFormControlCompare.value); + } + } + } + private findControl(form: FormGroup) { if (this.field.isLocalizable) { - control = this.fieldForm.controls[this.language.iso2Code]; + return form.controls[this.language.iso2Code]; } else { - control = this.fieldForm.controls[fieldInvariant]; + return form.controls[fieldInvariant]; } + } - if (this.selectedFormControl !== control) { - if (this.selectedFormControl && Types.isFunction(this.selectedFormControl['_clearChangeFns'])) { - this.selectedFormControl['_clearChangeFns'](); - } + public prefix(language: AppLanguageDto) { + return `(${language.iso2Code}`; + } - this.selectedFormControl = control; - } + public trackByLanguage(index: number, language: AppLanguageDto) { + return language.iso2Code; } private configKey() { diff --git a/src/Squidex/app/features/content/pages/content/content-history-page.component.html b/src/Squidex/app/features/content/pages/content/content-history-page.component.html index 3fec6c69d..7ca0c7449 100644 --- a/src/Squidex/app/features/content/pages/content/content-history-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-history-page.component.html @@ -15,7 +15,7 @@
{{event.created | sqxFromNow}}
- Load this Version + Load · Compare
diff --git a/src/Squidex/app/features/content/pages/content/content-history-page.component.ts b/src/Squidex/app/features/content/pages/content/content-history-page.component.ts index 0c79d722f..96d9fa43d 100644 --- a/src/Squidex/app/features/content/pages/content/content-history-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-history-page.component.ts @@ -62,7 +62,11 @@ export class ContentHistoryPageComponent { } public loadVersion(version: number) { - this.messageBus.emit(new ContentVersionSelected(new Version(version.toString()))); + this.messageBus.emit(new ContentVersionSelected(new Version(version.toString()), false)); + } + + public compareVersion(version: number) { + this.messageBus.emit(new ContentVersionSelected(new Version(version.toString()), true)); } public trackByEvent(index: number, event: HistoryEventDto) { diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.html b/src/Squidex/app/features/content/pages/content/content-page.component.html index bef584f93..a0fb8001b 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.html +++ b/src/Squidex/app/features/content/pages/content/content-page.component.html @@ -1,7 +1,7 @@
- + @@ -116,8 +116,9 @@ [form]="contentForm" [field]="field" [fieldForm]="contentForm.form.get(field.name)" + [fieldFormCompare]="contentFormCompare?.form.get(field.name)" [schema]="schema" - [languages]="languages" + [languages]="languages.mutableValues" [(language)]="language">
diff --git a/src/Squidex/app/features/content/pages/content/content-page.component.ts b/src/Squidex/app/features/content/pages/content/content-page.component.ts index a880fa5c2..610094887 100644 --- a/src/Squidex/app/features/content/pages/content/content-page.component.ts +++ b/src/Squidex/app/features/content/pages/content/content-page.component.ts @@ -5,10 +5,10 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { Observable, of, Subscription } from 'rxjs'; -import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators'; +import { Observable, of } from 'rxjs'; +import { onErrorResumeNext, switchMap } from 'rxjs/operators'; import { ContentVersionSelected } from './../messages'; @@ -25,6 +25,7 @@ import { LanguagesState, MessageBus, ModalModel, + ResourceOwner, SchemaDetailsDto, SchemasState, Version @@ -40,17 +41,13 @@ import { DueTimeSelectorComponent } from './../../shared/due-time-selector.compo fadeAnimation ] }) -export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit { - private languagesSubscription: Subscription; - private contentSubscription: Subscription; - private contentVersionSelectedSubscription: Subscription; - private selectedSchemaSubscription: Subscription; - +export class ContentPageComponent extends ResourceOwner implements CanComponentDeactivate, OnInit { public schema: SchemaDetailsDto; public content: ContentDto; public contentVersion: Version | null; public contentForm: EditContentForm; + public contentFormCompare: EditContentForm | null = null; public dropdown = new ModalModel(); @@ -70,44 +67,42 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, private readonly router: Router, private readonly schemasState: SchemasState ) { - } - - public ngOnDestroy() { - this.languagesSubscription.unsubscribe(); - this.contentSubscription.unsubscribe(); - this.contentVersionSelectedSubscription.unsubscribe(); - this.selectedSchemaSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.languagesSubscription = + this.own( this.languagesState.languages .subscribe(languages => { this.languages = languages.map(x => x.language); this.language = this.languages.at(0); - }); + })); - this.selectedSchemaSubscription = - this.schemasState.selectedSchema.pipe(filter(s => !!s)) + this.own( + this.schemasState.selectedSchema .subscribe(schema => { - this.schema = schema!; + if (schema) { + this.schema = schema!; - this.contentForm = new EditContentForm(this.schema, this.languages); - }); + this.contentForm = new EditContentForm(this.schema, this.languages); + } + })); - this.contentSubscription = - this.contentsState.selectedContent.pipe(filter(c => !!c)) + this.own( + this.contentsState.selectedContent .subscribe(content => { - this.content = content!; + if (content) { + this.content = content; - this.loadContent(this.content.dataDraft); - }); + this.loadContent(this.content.dataDraft); + } + })); - this.contentVersionSelectedSubscription = + this.own( this.messageBus.of(ContentVersionSelected) .subscribe(message => { - this.loadVersion(message.version); - }); + this.loadVersion(message.version, message.compare); + })); } public canDeactivate(): Observable { @@ -214,26 +209,36 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, .subscribe(); } - private loadVersion(version: Version) { - if (this.content) { + private loadVersion(version: Version | null, compare: boolean) { + if (!this.content || version === null || version.eq(this.content.version)) { + this.contentFormCompare = null; + this.contentVersion = null; + this.contentForm.load(this.content.dataDraft); + } else { this.contentsState.loadVersion(this.content, version) .subscribe(dto => { - if (this.content.version.value !== version.value) { - this.contentVersion = version; + if (compare) { + if (this.contentFormCompare === null) { + this.contentFormCompare = new EditContentForm(this.schema, this.languages); + this.contentFormCompare.form.disable(); + } + + this.contentFormCompare.load(dto.payload); + this.contentForm.load(this.content.dataDraft); } else { - this.contentVersion = null; + if (this.contentFormCompare) { + this.contentFormCompare = null; + } + + this.contentForm.load(dto.payload); } - this.loadContent(dto); + this.contentVersion = version; }); } } public showLatest() { - if (this.contentVersion) { - this.contentVersion = null; - - this.loadContent(this.content.dataDraft); - } + this.loadVersion(null, false); } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/content/field-languages.component.ts b/src/Squidex/app/features/content/pages/content/field-languages.component.ts new file mode 100644 index 000000000..08d6651d2 --- /dev/null +++ b/src/Squidex/app/features/content/pages/content/field-languages.component.ts @@ -0,0 +1,52 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; + +import { AppLanguageDto, RootFieldDto } from '@app/shared'; + +@Component({ + selector: 'sqx-field-languages', + template: ` + + + + + + + + + Please remember to check all languages when you see validation errors. + + + `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FieldLanguagesComponent { + @Input() + public field: RootFieldDto; + + @Input() + public showAllControls: boolean; + + @Input() + public language: AppLanguageDto; + + @Input() + public languages: AppLanguageDto[]; + + @Output() + public languageChange = new EventEmitter(); + + @Output() + public showAllControlsChange = new EventEmitter(); +} \ No newline at end of file diff --git a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html b/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html index abee1df85..cba1e9f8f 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html +++ b/src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html @@ -4,7 +4,8 @@ - + {{query.name}} @@ -13,7 +14,8 @@
- +
- @@ -73,23 +73,23 @@
{{selectionCount}} items selected:   - - - - -
diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts index d0f79bd98..d901cffcd 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.ts +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.ts @@ -5,8 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators'; import { @@ -18,6 +17,7 @@ import { LanguagesState, ModalModel, Queries, + ResourceOwner, SchemaDetailsDto, SchemasState, UIState @@ -30,11 +30,7 @@ import { DueTimeSelectorComponent } from './../../shared/due-time-selector.compo styleUrls: ['./contents-page.component.scss'], templateUrl: './contents-page.component.html' }) -export class ContentsPageComponent implements OnDestroy, OnInit { - private contentsSubscription: Subscription; - private languagesSubscription: Subscription; - private selectedSchemaSubscription: Subscription; - +export class ContentsPageComponent extends ResourceOwner implements OnInit { public schema: SchemaDetailsDto; public schemaQueries: Queries; @@ -61,16 +57,11 @@ export class ContentsPageComponent implements OnDestroy, OnInit { private readonly schemasState: SchemasState, private readonly uiState: UIState ) { - } - - public ngOnDestroy() { - this.contentsSubscription.unsubscribe(); - this.languagesSubscription.unsubscribe(); - this.selectedSchemaSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.selectedSchemaSubscription = + this.own( this.schemasState.selectedSchema .subscribe(schema => { this.resetSelection(); @@ -79,20 +70,20 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.schemaQueries = new Queries(this.uiState, `schemas.${this.schema.name}`); this.contentsState.init().pipe(onErrorResumeNext()).subscribe(); - }); + })); - this.contentsSubscription = + this.own( this.contentsState.contents .subscribe(() => { this.updateSelectionSummary(); - }); + })); - this.languagesSubscription = + this.own( this.languagesState.languages .subscribe(languages => { this.languages = languages.map(x => x.language); this.language = this.languages.at(0); - }); + })); } public reload() { @@ -209,7 +200,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit { this.updateSelectionSummary(); } - public trackByContent(content: ContentDto): string { + public trackByContent(index: number, content: ContentDto): string { return content.id; } diff --git a/src/Squidex/app/features/content/pages/messages.ts b/src/Squidex/app/features/content/pages/messages.ts index 115c27d43..3eca53b4b 100644 --- a/src/Squidex/app/features/content/pages/messages.ts +++ b/src/Squidex/app/features/content/pages/messages.ts @@ -9,7 +9,8 @@ import { Version } from '@app/shared'; export class ContentVersionSelected { constructor( - public readonly version: Version + public readonly version: Version, + public readonly compare: boolean ) { } } \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/array-editor.component.html b/src/Squidex/app/features/content/shared/array-editor.component.html index 81f476dfc..d4caa05a7 100644 --- a/src/Squidex/app/features/content/shared/array-editor.component.html +++ b/src/Squidex/app/features/content/shared/array-editor.component.html @@ -5,7 +5,7 @@
- diff --git a/src/Squidex/app/features/content/shared/array-editor.component.ts b/src/Squidex/app/features/content/shared/array-editor.component.ts index 64c957e14..b07d56a94 100644 --- a/src/Squidex/app/features/content/shared/array-editor.component.ts +++ b/src/Squidex/app/features/content/shared/array-editor.component.ts @@ -5,23 +5,27 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input } from '@angular/core'; import { AbstractControl, FormArray, FormGroup } from '@angular/forms'; import { AppLanguageDto, EditContentForm, - ImmutableArray, - RootFieldDto + RootFieldDto, + StatefulComponent } from '@app/shared'; +interface State { + isHidden: boolean; +} + @Component({ selector: 'sqx-array-editor', styleUrls: ['./array-editor.component.scss'], templateUrl: './array-editor.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class ArrayEditorComponent { +export class ArrayEditorComponent extends StatefulComponent { @Input() public form: EditContentForm; @@ -32,15 +36,19 @@ export class ArrayEditorComponent { public language: AppLanguageDto; @Input() - public languages: ImmutableArray; + public languages: AppLanguageDto[]; @Input() public arrayControl: FormArray; - public isHidden = false; + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + isHidden: false + }); + } - public hide(hide: boolean) { - this.isHidden = hide; + public hide(isHidden: boolean) { + this.next(s => ({ ...s, isHidden })); } public removeItem(index: number) { diff --git a/src/Squidex/app/features/content/shared/array-item.component.html b/src/Squidex/app/features/content/shared/array-item.component.html index ad3b3a507..2283e2e54 100644 --- a/src/Squidex/app/features/content/shared/array-item.component.html +++ b/src/Squidex/app/features/content/shared/array-item.component.html @@ -5,32 +5,32 @@ Item #{{index + 1}} - - - - - - - - diff --git a/src/Squidex/app/features/content/shared/array-item.component.ts b/src/Squidex/app/features/content/shared/array-item.component.ts index 16e95b0bc..666e63068 100644 --- a/src/Squidex/app/features/content/shared/array-item.component.ts +++ b/src/Squidex/app/features/content/shared/array-item.component.ts @@ -8,13 +8,12 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { AbstractControl, FormGroup } from '@angular/forms'; import { Observable } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; import { AppLanguageDto, EditContentForm, FieldDto, - ImmutableArray, + invalid$, RootFieldDto } from '@app/shared'; @@ -62,7 +61,7 @@ export class ArrayItemComponent implements OnChanges { public language: AppLanguageDto; @Input() - public languages: ImmutableArray; + public languages: AppLanguageDto[]; public isInvalid: Observable; @@ -70,7 +69,7 @@ export class ArrayItemComponent implements OnChanges { public ngOnChanges(changes: SimpleChanges) { if (changes['itemForm']) { - this.isInvalid = this.itemForm.statusChanges.pipe(startWith(this.itemForm.invalid), map(x => this.itemForm.invalid)); + this.isInvalid = invalid$(this.itemForm); } if (changes['itemForm'] || changes['field']) { diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.html b/src/Squidex/app/features/content/shared/assets-editor.component.html index eb98de9f4..c6b7f33a1 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.html +++ b/src/Squidex/app/features/content/shared/assets-editor.component.html @@ -1,17 +1,18 @@ -
+ +
- Drop files or click here to add assets. + Drop files or click
- -
@@ -20,12 +21,12 @@
- +
- -
@@ -33,15 +34,17 @@
-
-
- +
diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.scss b/src/Squidex/app/features/content/shared/assets-editor.component.scss index 498a0bcc2..866d10863 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.scss +++ b/src/Squidex/app/features/content/shared/assets-editor.component.scss @@ -44,12 +44,12 @@ & { @include transition(border-color .4s ease); @include border-radius; - @include flex-box; - @include truncate; + @include truncate-nowidth; border: 2px dashed darken($color-border, 10%); + padding: 5px .5rem; font-weight: normal; + font-size: 1rem; text-align: center; - padding: 5px 2rem; color: darken($color-border, 30%); cursor: pointer; } diff --git a/src/Squidex/app/features/content/shared/assets-editor.component.ts b/src/Squidex/app/features/content/shared/assets-editor.component.ts index bf725821b..9dc11ac56 100644 --- a/src/Squidex/app/features/content/shared/assets-editor.component.ts +++ b/src/Squidex/app/features/content/shared/assets-editor.component.ts @@ -5,9 +5,8 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnDestroy, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Subscription } from 'rxjs'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { AppsState, @@ -17,6 +16,7 @@ import { ImmutableArray, LocalStoreService, MessageBus, + StatefulControlComponent, Types } from '@app/shared'; @@ -32,6 +32,14 @@ class AssetUpdated { } } +interface State { + assetFiles: ImmutableArray; + + assets: ImmutableArray; + + isListView: boolean; +} + @Component({ selector: 'sqx-assets-editor', styleUrls: ['./assets-editor.component.scss'], @@ -39,39 +47,35 @@ class AssetUpdated { providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDestroy { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - private subscription: Subscription; - +export class AssetsEditorComponent extends StatefulControlComponent implements OnInit { public assetsDialog = new DialogModel(); - public newAssets = ImmutableArray.empty(); - public oldAssets = ImmutableArray.empty(); - - public isListView = false; - public isDisabled = false; + @Input() + public isCompact = false; - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly appsState: AppsState, private readonly assetsService: AssetsService, - private readonly changeDetector: ChangeDetectorRef, private readonly localStore: LocalStoreService, private readonly messageBus: MessageBus ) { - this.isListView = this.localStore.getBoolean('squidex.assets.list-view'); + super(changeDetector, { + assets: ImmutableArray.empty(), + assetFiles: ImmutableArray.empty(), + isListView: localStore.getBoolean('squidex.assets.list-view') + }); } public writeValue(obj: any) { if (Types.isArrayOfString(obj)) { - if (!Types.isEquals(obj, this.oldAssets.map(x => x.id).values)) { + if (!Types.isEquals(obj, this.snapshot.assets.map(x => x.id).values)) { const assetIds: string[] = obj; this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj) .subscribe(dtos => { this.setAssets(ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)!).filter(a => !!a))); - if (this.oldAssets.length !== assetIds.length) { + if (this.snapshot.assets.length !== assetIds.length) { this.updateValue(); } }, () => { @@ -87,54 +91,28 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe this.messageBus.emit(new AssetUpdated(asset, this)); } - public ngOnDestroy() { - this.subscription.unsubscribe(); - } - public ngOnInit() { - this.subscription = + this.own( this.messageBus.of(AssetUpdated) .subscribe(event => { if (event.source !== this) { - this.setAssets(this.oldAssets.replaceBy('id', event.asset)); + this.setAssets(this.snapshot.assets.replaceBy('id', event.asset)); } - }); + })); } - public setAssets(asset: ImmutableArray) { - this.oldAssets = asset; - - this.changeDetector.markForCheck(); - } - - public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - this.changeDetector.markForCheck(); - } - - public noop() { - return; - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; + public setAssets(assets: ImmutableArray) { + this.next(s => ({ ...s, assets })); } public addFiles(files: File[]) { for (let file of files) { - this.newAssets = this.newAssets.pushFront(file); + this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) })); } } public selectAssets(assets: AssetDto[]) { - for (let asset of assets) { - this.oldAssets = this.oldAssets.push(asset); - } + this.setAssets(this.snapshot.assets.push(...assets)); if (assets.length > 0) { this.updateValue(); @@ -145,8 +123,11 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe public addAsset(file: File, asset: AssetDto) { if (asset && file) { - this.newAssets = this.newAssets.remove(file); - this.oldAssets = this.oldAssets.pushFront(asset); + this.next(s => ({ + ...s, + assetFiles: s.assetFiles.remove(file), + assets: s.assets.pushFront(asset) + })); this.updateValue(); } @@ -154,7 +135,7 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe public sortAssets(assets: AssetDto[]) { if (assets) { - this.oldAssets = ImmutableArray.of(assets); + this.setAssets(ImmutableArray.of(assets)); this.updateValue(); } @@ -162,24 +143,24 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe public removeLoadedAsset(asset: AssetDto) { if (asset) { - this.oldAssets = this.oldAssets.remove(asset); + this.setAssets(this.snapshot.assets.remove(asset)); this.updateValue(); } } public removeLoadingAsset(file: File) { - this.newAssets = this.newAssets.remove(file); + this.next(s => ({ ...s, assetFiles: s.assetFiles.remove(file) })); } public changeView(isListView: boolean) { - this.isListView = isListView; + this.next(s => ({ ...s, isListView })); this.localStore.setBoolean('squidex.assets.list-view', isListView); } private updateValue() { - let ids: string[] | null = this.oldAssets.values.map(x => x.id); + let ids: string[] | null = this.snapshot.assets.values.map(x => x.id); if (ids.length === 0) { ids = null; @@ -187,8 +168,6 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe this.callTouched(); this.callChange(ids); - - this.changeDetector.markForCheck(); } public trackByAsset(index: number, asset: AssetDto) { diff --git a/src/Squidex/app/features/content/shared/content-item-editor.component.ts b/src/Squidex/app/features/content/shared/content-item-editor.component.ts new file mode 100644 index 000000000..8e8a95e93 --- /dev/null +++ b/src/Squidex/app/features/content/shared/content-item-editor.component.ts @@ -0,0 +1,69 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { FieldDto } from '@app/shared'; + +@Component({ + selector: 'sqx-content-item-editor', + template: ` +
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+ +
+
+
+
+
+
`, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ContentItemEditorComponent { + @Input() + public field: FieldDto; + + @Input() + public form: FormGroup; +} \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/content-item.component.html b/src/Squidex/app/features/content/shared/content-item.component.html index fea0c205f..5fd8b28c0 100644 --- a/src/Squidex/app/features/content/shared/content-item.component.html +++ b/src/Squidex/app/features/content/shared/content-item.component.html @@ -1,65 +1,26 @@ - - + + + + + + + + - - - + + + + - -
-
-
-
-
- -
-
- -
-
-
-
-
-
- -
-
- -
-
- -
-
-
-
-
-
- -
-
-
- -
-
-
-
-
-
-
- {{values[i]}} -
+ + {{values[i]}} + - + + {{content.lastModified | sqxFromNow}} - - - - - - - - + - + + + + + + + + + +
@@ -63,7 +63,7 @@
diff --git a/src/Squidex/app/features/content/shared/contents-selector.component.ts b/src/Squidex/app/features/content/shared/contents-selector.component.ts index bfb764f39..f88608fba 100644 --- a/src/Squidex/app/features/content/shared/contents-selector.component.ts +++ b/src/Squidex/app/features/content/shared/contents-selector.component.ts @@ -115,7 +115,7 @@ export class ContentsSelectorComponent implements OnInit { this.isAllSelected = this.selectionCount === this.contentsState.snapshot.contents.length; } - public trackByContent(content: ContentDto): string { + public trackByContent(index: number, content: ContentDto): string { return content.id; } } diff --git a/src/Squidex/app/features/content/shared/field-editor.component.html b/src/Squidex/app/features/content/shared/field-editor.component.html index 5bcf53a3c..12d8ca479 100644 --- a/src/Squidex/app/features/content/shared/field-editor.component.html +++ b/src/Squidex/app/features/content/shared/field-editor.component.html @@ -5,7 +5,7 @@ Disabled - +
@@ -101,7 +101,7 @@ - + @@ -122,7 +122,7 @@ @@ -132,7 +132,8 @@ [formControl]="control" [language]="language" [languages]="languages" - [schemaId]="field.properties['schemaId']"> + [schemaId]="field.properties['schemaId']" + [isCompact]="isCompact"> diff --git a/src/Squidex/app/features/content/shared/field-editor.component.ts b/src/Squidex/app/features/content/shared/field-editor.component.ts index e15497238..e3690276c 100644 --- a/src/Squidex/app/features/content/shared/field-editor.component.ts +++ b/src/Squidex/app/features/content/shared/field-editor.component.ts @@ -11,8 +11,7 @@ import { FormControl } from '@angular/forms'; import { AppLanguageDto, EditContentForm, - FieldDto, - ImmutableArray + FieldDto } from '@app/shared'; @Component({ @@ -34,7 +33,10 @@ export class FieldEditorComponent { public language: AppLanguageDto; @Input() - public languages: ImmutableArray; + public languages: AppLanguageDto[]; + + @Input() + public isCompact = false; @Input() public displaySuffix: string; diff --git a/src/Squidex/app/features/content/shared/preview-button.component.html b/src/Squidex/app/features/content/shared/preview-button.component.html index 013dc9abb..1c189d2c4 100644 --- a/src/Squidex/app/features/content/shared/preview-button.component.html +++ b/src/Squidex/app/features/content/shared/preview-button.component.html @@ -1,16 +1,16 @@ - + Preview:
- - diff --git a/src/Squidex/app/features/content/shared/preview-button.component.ts b/src/Squidex/app/features/content/shared/preview-button.component.ts index 215500f7d..197fe8b65 100644 --- a/src/Squidex/app/features/content/shared/preview-button.component.ts +++ b/src/Squidex/app/features/content/shared/preview-button.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; import { ContentDto, @@ -13,19 +13,26 @@ import { interpolate, LocalStoreService, ModalModel, - SchemaDetailsDto + SchemaDetailsDto, + StatefulComponent } from '@app/shared'; +interface State { + selectedName?: string; + + alternativeNames: string[]; +} + @Component({ selector: 'sqx-preview-button', styleUrls: ['./preview-button.component.scss'], templateUrl: './preview-button.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, animations: [ fadeAnimation - ] + ], + changeDetection: ChangeDetectionStrategy.OnPush }) -export class PreviewButtonComponent implements OnInit { +export class PreviewButtonComponent extends StatefulComponent implements OnInit { @Input() public content: ContentDto; @@ -34,13 +41,12 @@ export class PreviewButtonComponent implements OnInit { public dropdown = new ModalModel(); - public selectedName: string | undefined; - - public alternativeNames: string[]; - - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly localStore: LocalStoreService ) { + super(changeDetector, { + alternativeNames: [] + }); } public ngOnInit() { @@ -62,16 +68,23 @@ export class PreviewButtonComponent implements OnInit { } private selectUrl(selectedName: string) { - if (this.selectedName !== selectedName) { + this.next(s => { + if (selectedName === s.selectedName) { + return s; + } + const state = { ...s }; + const keys = Object.keys(this.schema.previewUrls); - this.selectedName = selectedName; + state.selectedName = selectedName; - this.alternativeNames = keys.filter(x => x !== this.selectedName); - this.alternativeNames.sort(); + state.alternativeNames = keys.filter(x => x !== s.selectedName); + state.alternativeNames.sort(); this.localStore.set(this.configKey(), selectedName); - } + + return state; + }); } private configKey() { diff --git a/src/Squidex/app/features/content/shared/references-editor.component.html b/src/Squidex/app/features/content/shared/references-editor.component.html index 7fbb63a25..83a7d5553 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.html +++ b/src/Squidex/app/features/content/shared/references-editor.component.html @@ -1,27 +1,28 @@ -
- +
+
Click here to link content items.
- - +
-
+
Schema not found or not configured yet.
@@ -30,7 +31,7 @@ \ No newline at end of file diff --git a/src/Squidex/app/features/content/shared/references-editor.component.ts b/src/Squidex/app/features/content/shared/references-editor.component.ts index 0fc562f6e..8cb079697 100644 --- a/src/Squidex/app/features/content/shared/references-editor.component.ts +++ b/src/Squidex/app/features/content/shared/references-editor.component.ts @@ -6,7 +6,7 @@ */ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input, OnInit } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { AppLanguageDto, @@ -18,6 +18,7 @@ import { MathHelper, SchemaDetailsDto, SchemasService, + StatefulControlComponent, Types } from '@app/shared'; @@ -25,6 +26,13 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferencesEditorComponent), multi: true }; +interface State { + schema?: SchemaDetailsDto | null; + schemaInvalid: boolean; + + contentItems: ImmutableArray; +} + @Component({ selector: 'sqx-references-editor', styleUrls: ['./references-editor.component.scss'], @@ -32,10 +40,7 @@ export const SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - +export class ReferencesEditorComponent extends StatefulControlComponent implements OnInit { @Input() public schemaId: string; @@ -43,53 +48,49 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { public language: AppLanguageDto; @Input() - public languages: ImmutableArray; - - public selectorDialog = new DialogModel(); + public languages: AppLanguageDto[]; - public schema: SchemaDetailsDto; - - public contentItems = ImmutableArray.empty(); + @Input() + public isCompact = false; - public isDisabled = false; - public isInvalidSchema = false; + public selectorDialog = new DialogModel(); - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly appsState: AppsState, - private readonly changeDetector: ChangeDetectorRef, private readonly contentsService: ContentsService, private readonly schemasService: SchemasService ) { + super(changeDetector, { + schemaInvalid: false, + schema: null, + contentItems: ImmutableArray.empty() + }); } public ngOnInit() { if (this.schemaId === MathHelper.EMPTY_GUID) { - this.isInvalidSchema = true; + this.next(s => ({ ...s, schemaInvalid: true })); return; } this.schemasService.getSchema(this.appsState.appName, this.schemaId) - .subscribe(dto => { - this.schema = dto; - - this.changeDetector.markForCheck(); + .subscribe(schema => { + this.next(s => ({ ...s, schema })); }, () => { - this.isInvalidSchema = true; - - this.changeDetector.markForCheck(); + this.next(s => ({ ...s, schemaInvalid: true })); }); } public writeValue(obj: any) { if (Types.isArrayOfString(obj)) { - if (!Types.isEquals(obj, this.contentItems.map(x => x.id).values)) { + if (!Types.isEquals(obj, this.snapshot.contentItems.map(x => x.id).values)) { const contentIds: string[] = obj; this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds) .subscribe(dtos => { this.setContentItems(ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)!).filter(r => !!r))); - if (this.contentItems.length !== contentIds.length) { + if (this.snapshot.contentItems.length !== contentIds.length) { this.updateValue(); } }, () => { @@ -101,29 +102,13 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { } } - public setContentItems(contents: ImmutableArray) { - this.contentItems = contents; - - this.changeDetector.markForCheck(); - } - - public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - this.changeDetector.markForCheck(); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; + public setContentItems(contentItems: ImmutableArray) { + this.next(s => ({ ...s, contentItems })); } public select(contents: ContentDto[]) { for (let content of contents) { - this.contentItems = this.contentItems.push(content); + this.setContentItems(this.snapshot.contentItems.push(content)); } if (contents.length > 0) { @@ -135,7 +120,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { public remove(content: ContentDto) { if (content) { - this.contentItems = this.contentItems.remove(content); + this.setContentItems(this.snapshot.contentItems.remove(content)); this.updateValue(); } @@ -143,14 +128,14 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { public sort(contents: ContentDto[]) { if (contents) { - this.contentItems = ImmutableArray.of(contents); + this.setContentItems(ImmutableArray.of(contents)); this.updateValue(); } } private updateValue() { - let ids: string[] | null = this.contentItems.values.map(x => x.id); + let ids: string[] | null = this.snapshot.contentItems.values.map(x => x.id); if (ids.length === 0) { ids = null; @@ -158,7 +143,5 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { this.callTouched(); this.callChange(ids); - - this.changeDetector.markForCheck(); } } \ No newline at end of file diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts index d4f6f202a..031d5d1b7 100644 --- a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts +++ b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts @@ -5,8 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { Component, OnInit } from '@angular/core'; import { filter, map, switchMap } from 'rxjs/operators'; import { @@ -17,6 +16,7 @@ import { fadeAnimation, HistoryEventDto, HistoryService, + ResourceOwner, UsagesService } from '@app/shared'; @@ -42,9 +42,7 @@ const COLORS = [ fadeAnimation ] }) -export class DashboardPageComponent implements OnDestroy, OnInit { - private subscriptions: Subscription[] = []; - +export class DashboardPageComponent extends ResourceOwner implements OnInit { public profileDisplayName = ''; public chartStorageCount: any; @@ -104,18 +102,11 @@ export class DashboardPageComponent implements OnDestroy, OnInit { private readonly historyService: HistoryService, private readonly usagesService: UsagesService ) { - } - - public ngOnDestroy() { - for (let subscription of this.subscriptions) { - subscription.unsubscribe(); - } - - this.subscriptions = []; + super(); } public ngOnInit() { - this.subscriptions.push( + this.own( this.app.pipe( switchMap(app => this.usagesService.getTodayStorage(app.name))) .subscribe(dto => { @@ -123,7 +114,7 @@ export class DashboardPageComponent implements OnDestroy, OnInit { this.assetsMax = dto.maxAllowed; })); - this.subscriptions.push( + this.own( this.app.pipe( switchMap(app => this.usagesService.getMonthCalls(app.name))) .subscribe(dto => { @@ -131,14 +122,14 @@ export class DashboardPageComponent implements OnDestroy, OnInit { this.callsMax = dto.maxAllowed; })); - this.subscriptions.push( + this.own( this.app.pipe( switchMap(app => this.historyService.getHistory(app.name, ''))) .subscribe(dto => { this.history = dto; })); - this.subscriptions.push( + this.own( this.app.pipe( switchMap(app => this.usagesService.getStorageUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))) .subscribe(dtos => { @@ -175,7 +166,7 @@ export class DashboardPageComponent implements OnDestroy, OnInit { }; })); - this.subscriptions.push( + this.own( this.app.pipe( switchMap(app => this.usagesService.getCallsUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))) .subscribe(dtos => { diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html index c39cde203..9c9c6f5cb 100644 --- a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.html @@ -6,7 +6,7 @@ - @@ -70,11 +70,11 @@ Next: {{event.nextAttempt | sqxFromNow}}
- -
@@ -90,7 +90,7 @@ - +
diff --git a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html index 9b9b92009..341d20043 100644 --- a/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html +++ b/src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html @@ -2,7 +2,7 @@
- diff --git a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html index 7e2636362..aff31a2ad 100644 --- a/src/Squidex/app/features/rules/pages/rules/rules-page.component.html +++ b/src/Squidex/app/features/rules/pages/rules/rules-page.component.html @@ -6,14 +6,14 @@ - - @@ -24,7 +24,7 @@
No Rule created yet. -
diff --git a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html index d454b070d..107009588 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html @@ -94,14 +94,14 @@
- - - + + +
- - + +
\ No newline at end of file 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 8a7eb1e5a..fce705986 100644 --- a/src/Squidex/app/features/schemas/pages/schema/field.component.html +++ b/src/Squidex/app/features/schemas/pages/schema/field.component.html @@ -28,7 +28,7 @@ -
- + {{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 3f1c137a0..77cd4f192 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 @@ -5,16 +5,17 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; -import { Observable, Subscription } from 'rxjs'; -import { map, startWith } from 'rxjs/operators'; +import { Observable } from 'rxjs'; import { AppPatternDto, FieldDto, + hasNoValue$, ImmutableArray, ModalModel, + ResourceOwner, RootFieldDto, StringFieldPropertiesDto, Types @@ -25,9 +26,7 @@ import { styleUrls: ['string-validation.component.scss'], templateUrl: 'string-validation.component.html' }) -export class StringValidationComponent implements OnDestroy, OnInit { - private patternSubscription: Subscription; - +export class StringValidationComponent extends ResourceOwner implements OnInit { @Input() public editForm: FormGroup; @@ -49,10 +48,6 @@ export class StringValidationComponent implements OnDestroy, OnInit { public showUnique: boolean; - public ngOnDestroy() { - this.patternSubscription.unsubscribe(); - } - public ngOnInit() { this.showUnique = Types.is(this.field, RootFieldDto) && !this.field.isLocalizable; @@ -77,24 +72,23 @@ export class StringValidationComponent implements OnDestroy, OnInit { new FormControl(this.properties.defaultValue)); this.showDefaultValue = - this.editForm.controls['isRequired'].valueChanges.pipe( - startWith(this.properties.isRequired), map(x => !x)); + hasNoValue$(this.editForm.controls['isRequired']); this.showPatternSuggestions = - this.editForm.controls['pattern'].valueChanges.pipe( - startWith(''), map(x => !x || x.trim().length === 0)); + hasNoValue$(this.editForm.controls['pattern']); this.showPatternMessage = this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim().length > 0; - this.patternSubscription = + this.own( this.editForm.controls['pattern'].valueChanges .subscribe((value: string) => { if (!value || value.length === 0) { this.editForm.controls['patternMessage'].setValue(undefined); } + this.setPatternName(); - }); + })); this.setPatternName(); } diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html index f13edf18f..d3ee7d5b5 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html @@ -9,7 +9,7 @@ - diff --git a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts index 6b212f8b5..cf0ac2093 100644 --- a/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts +++ b/src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts @@ -5,10 +5,9 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormControl } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { Subscription } from 'rxjs'; import { map, onErrorResumeNext } from 'rxjs/operators'; import { @@ -16,6 +15,7 @@ import { CreateCategoryForm, DialogModel, MessageBus, + ResourceOwner, SchemaDto, SchemasState } from '@app/shared'; @@ -27,9 +27,7 @@ import { SchemaCloning } from './../messages'; styleUrls: ['./schemas-page.component.scss'], templateUrl: './schemas-page.component.html' }) -export class SchemasPageComponent implements OnDestroy, OnInit { - private schemaCloningSubscription: Subscription; - +export class SchemasPageComponent extends ResourceOwner implements OnInit { public addSchemaDialog = new DialogModel(); public addCategoryForm = new CreateCategoryForm(this.formBuilder); @@ -45,27 +43,25 @@ export class SchemasPageComponent implements OnDestroy, OnInit { private readonly route: ActivatedRoute, private readonly router: Router ) { - } - - public ngOnDestroy() { - this.schemaCloningSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.schemaCloningSubscription = + this.own( this.messageBus.of(SchemaCloning) .subscribe(m => { this.import = m.schema; this.addSchemaDialog.show(); - }); - - this.route.params.pipe(map(q => q['showDialog'])) - .subscribe(showDialog => { - if (showDialog) { - this.addSchemaDialog.show(); - } - }); + })); + + this.own( + this.route.params.pipe(map(q => q['showDialog'])) + .subscribe(showDialog => { + if (showDialog) { + this.addSchemaDialog.show(); + } + })); this.schemasState.load().pipe(onErrorResumeNext()).subscribe(); } diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html index 7e9ef4470..5906e2444 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html @@ -6,13 +6,13 @@ - - @@ -27,7 +27,7 @@
No backups created yet. -
diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts index b2ef916a0..281c4f9e6 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.ts @@ -5,14 +5,15 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Subscription, timer } from 'rxjs'; +import { Component, OnInit } from '@angular/core'; +import { timer } from 'rxjs'; import { onErrorResumeNext, switchMap } from 'rxjs/operators'; import { AppsState, BackupDto, - BackupsState + BackupsState, + ResourceOwner } from '@app/shared'; @Component({ @@ -20,25 +21,20 @@ import { styleUrls: ['./backups-page.component.scss'], templateUrl: './backups-page.component.html' }) -export class BackupsPageComponent implements OnInit, OnDestroy { - private timerSubscription: Subscription; - +export class BackupsPageComponent extends ResourceOwner implements OnInit { constructor( public readonly appsState: AppsState, public readonly backupsState: BackupsState ) { - } - - public ngOnDestroy() { - this.timerSubscription.unsubscribe(); + super(); } public ngOnInit() { this.backupsState.load().pipe(onErrorResumeNext()).subscribe(); - this.timerSubscription = - timer(3000, 3000).pipe(switchMap(t => this.backupsState.load(true, true).pipe(onErrorResumeNext()))) - .subscribe(); + this.own( + timer(3000, 3000).pipe(switchMap(() => this.backupsState.load(true, true).pipe(onErrorResumeNext()))) + .subscribe()); } public reload() { diff --git a/src/Squidex/app/features/settings/pages/clients/client.component.html b/src/Squidex/app/features/settings/pages/clients/client.component.html index 8d0ac010c..748772511 100644 --- a/src/Squidex/app/features/settings/pages/clients/client.component.html +++ b/src/Squidex/app/features/settings/pages/clients/client.component.html @@ -11,7 +11,7 @@ - @@ -25,7 +25,7 @@
- +
diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html index 4fcace09b..f4f9a428c 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html @@ -6,7 +6,7 @@ - diff --git a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts index fddd713e5..b540f1b75 100644 --- a/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts +++ b/src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts @@ -8,7 +8,7 @@ import { Component, Injectable, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { Observable } from 'rxjs'; -import { filter, onErrorResumeNext, withLatestFrom } from 'rxjs/operators'; +import { onErrorResumeNext, withLatestFrom } from 'rxjs/operators'; import { AppContributorDto, @@ -34,7 +34,7 @@ export class UsersDataSource implements AutocompleteSource { public find(query: string): Observable { return this.usersService.getUsers(query).pipe( - withLatestFrom(this.contributorsState.contributors.pipe(filter(x => !!x)), (users, contributors) => { + withLatestFrom(this.contributorsState.contributors, (users, contributors) => { const results: any[] = []; for (let user of users) { diff --git a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html b/src/Squidex/app/features/settings/pages/languages/languages-page.component.html index d7d9981e9..25d942fc8 100644 --- a/src/Squidex/app/features/settings/pages/languages/languages-page.component.html +++ b/src/Squidex/app/features/settings/pages/languages/languages-page.component.html @@ -6,7 +6,7 @@ - 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 179323835..31634d509 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 @@ -5,16 +5,16 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { FormBuilder } from '@angular/forms'; -import { Subscription } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators'; import { AddLanguageForm, AppLanguageDto, AppsState, - LanguagesState + LanguagesState, + ResourceOwner } from '@app/shared'; @Component({ @@ -22,9 +22,7 @@ import { styleUrls: ['./languages-page.component.scss'], templateUrl: './languages-page.component.html' }) -export class LanguagesPageComponent implements OnDestroy, OnInit { - private newLanguagesSubscription: Subscription; - +export class LanguagesPageComponent extends ResourceOwner implements OnInit { public addLanguageForm = new AddLanguageForm(this.formBuilder); constructor( @@ -32,20 +30,17 @@ export class LanguagesPageComponent implements OnDestroy, OnInit { public readonly languagesState: LanguagesState, private readonly formBuilder: FormBuilder ) { - } - - public ngOnDestroy() { - this.newLanguagesSubscription.unsubscribe(); + super(); } public ngOnInit() { - this.newLanguagesSubscription = + this.own( this.languagesState.newLanguages .subscribe(languages => { if (languages.length > 0) { this.addLanguageForm.load({ language: languages.at(0) }); } - }); + })); this.languagesState.load().pipe(onErrorResumeNext()).subscribe(); } @@ -67,7 +62,7 @@ export class LanguagesPageComponent implements OnDestroy, OnInit { } } - public trackByLanguage(index: number, language: { language: AppLanguageDto }) { + public trackByLanguage(language: { language: AppLanguageDto }) { return language.language; } } diff --git a/src/Squidex/app/features/settings/pages/more/more-page.component.html b/src/Squidex/app/features/settings/pages/more/more-page.component.html index 43a4fb990..64ff6c060 100644 --- a/src/Squidex/app/features/settings/pages/more/more-page.component.html +++ b/src/Squidex/app/features/settings/pages/more/more-page.component.html @@ -17,7 +17,7 @@
Once you archive an app, there is no going back. Please be certain.
- diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html b/src/Squidex/app/features/settings/pages/plans/plans-page.component.html index 34f456c26..c4849ea5f 100644 --- a/src/Squidex/app/features/settings/pages/plans/plans-page.component.html +++ b/src/Squidex/app/features/settings/pages/plans/plans-page.component.html @@ -6,7 +6,7 @@ - diff --git a/src/Squidex/app/features/settings/pages/roles/roles-page.component.html b/src/Squidex/app/features/settings/pages/roles/roles-page.component.html index 1a0e1a884..1d44a7fd6 100644 --- a/src/Squidex/app/features/settings/pages/roles/roles-page.component.html +++ b/src/Squidex/app/features/settings/pages/roles/roles-page.component.html @@ -6,7 +6,7 @@ - diff --git a/src/Squidex/app/framework/angular/code.component.ts b/src/Squidex/app/framework/angular/code.component.ts index ec02060f5..f78d5b637 100644 --- a/src/Squidex/app/framework/angular/code.component.ts +++ b/src/Squidex/app/framework/angular/code.component.ts @@ -13,5 +13,4 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; templateUrl: './code.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class CodeComponent { -} \ No newline at end of file +export class CodeComponent { } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.html b/src/Squidex/app/framework/angular/forms/autocomplete.component.html index 882956d72..ea2df131d 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.html +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.html @@ -5,13 +5,13 @@ autocorrect="off" autocapitalize="off"> -
-
+
+ [sqxScrollActive]="i === snapshot.suggestedIndex"> {{item}} diff --git a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts index 164f74ec3..147199400 100644 --- a/src/Squidex/app/framework/angular/forms/autocomplete.component.ts +++ b/src/Squidex/app/framework/angular/forms/autocomplete.component.ts @@ -5,11 +5,13 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, ContentChild, ElementRef, forwardRef, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Observable, of, Subscription } from 'rxjs'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, ElementRef, forwardRef, Input, OnInit, TemplateRef, ViewChild } from '@angular/core'; +import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Observable, of } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap, tap } from 'rxjs/operators'; +import { StatefulControlComponent } from '@app/framework/internal'; + export interface AutocompleteSource { find(query: string): Observable; } @@ -23,6 +25,11 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => AutocompleteComponent), multi: true }; +interface State { + suggestedItems: any[]; + suggestedIndex: number; +} + @Component({ selector: 'sqx-autocomplete', styleUrls: ['./autocomplete.component.scss'], @@ -30,11 +37,7 @@ export const SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_AUTOCOMPLETE_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, OnInit { - private subscription: Subscription; - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - +export class AutocompleteComponent extends StatefulControlComponent implements OnInit { @Input() public source: AutocompleteSource; @@ -53,17 +56,17 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O @ViewChild('input') public inputControl: ElementRef; - public suggestedItems: any[] = []; - public suggestedIndex = -1; - public queryInput = new FormControl(); - public ngOnDestroy() { - this.subscription.unsubscribe(); + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + suggestedItems: [], + suggestedIndex: -1 + }); } public ngOnInit() { - this.subscription = + this.own( this.queryInput.valueChanges.pipe( tap(query => { this.callChange(query); @@ -80,9 +83,12 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O filter(query => !!query && !!this.source), switchMap(query => this.source.find(query)), catchError(() => of([]))) .subscribe(items => { - this.suggestedIndex = -1; - this.suggestedItems = items || []; - }); + this.next(s => ({ + ...s, + suggestedIndex: -1, + suggestedItems: items || [] + })); + })); } public onKeyDown(event: KeyboardEvent) { @@ -98,7 +104,7 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O this.reset(); return false; case KEY_ENTER: - if (this.suggestedItems.length > 0 && this.selectItem()) { + if (this.snapshot.suggestedItems.length > 0 && this.selectItem()) { return false; } break; @@ -149,11 +155,11 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O public selectItem(selection: any | null = null): boolean { if (!selection) { - selection = this.suggestedItems[this.suggestedIndex]; + selection = this.snapshot.suggestedItems[this.snapshot.suggestedIndex]; } - if (!selection && this.suggestedItems.length === 1) { - selection = this.suggestedItems[0]; + if (!selection && this.snapshot.suggestedItems.length === 1) { + selection = this.snapshot.suggestedItems[0]; } if (selection) { @@ -174,24 +180,24 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O return false; } - public selectIndex(selection: number) { - if (selection < 0) { - selection = 0; + public selectIndex(suggestedIndex: number) { + if (suggestedIndex < 0) { + suggestedIndex = 0; } - if (selection >= this.suggestedItems.length) { - selection = this.suggestedItems.length - 1; + if (suggestedIndex >= this.snapshot.suggestedItems.length) { + suggestedIndex = this.snapshot.suggestedItems.length - 1; } - this.suggestedIndex = selection; + this.next(s => ({ ...s, suggestedIndex })); } private up() { - this.selectIndex(this.suggestedIndex - 1); + this.selectIndex(this.snapshot.suggestedIndex - 1); } private down() { - this.selectIndex(this.suggestedIndex + 1); + this.selectIndex(this.snapshot.suggestedIndex + 1); } private resetForm() { @@ -199,7 +205,10 @@ export class AutocompleteComponent implements ControlValueAccessor, OnDestroy, O } private reset() { - this.suggestedItems = []; - this.suggestedIndex = -1; + this.next(s => ({ + ...s, + suggestedItems: [], + suggestedIndex: -1 + })); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.html b/src/Squidex/app/framework/angular/forms/checkbox-group.component.html index 0f2a18bc1..f3245e6f2 100644 --- a/src/Squidex/app/framework/angular/forms/checkbox-group.component.html +++ b/src/Squidex/app/framework/angular/forms/checkbox-group.component.html @@ -1,9 +1,9 @@ - + [disabled]="snapshot.isDisabled"> - + \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts b/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts index 34c311056..d34486b19 100644 --- a/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts +++ b/src/Squidex/app/framework/angular/forms/checkbox-group.component.ts @@ -6,16 +6,22 @@ */ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Types } from '@app/framework/internal'; - -import { MathHelper } from '../../utils/math-helper'; +import { + MathHelper, + StatefulControlComponent, + Types +} from '@app/framework/internal'; export const SQX_CHECKBOX_GROUP_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CheckboxGroupComponent), multi: true }; +interface State { + checkedValues: string[]; +} + @Component({ selector: 'sqx-checkbox-group', styleUrls: ['./checkbox-group.component.scss'], @@ -23,58 +29,39 @@ export const SQX_CHECKBOX_GROUP_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_CHECKBOX_GROUP_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class CheckboxGroupComponent implements ControlValueAccessor { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - private checkedValues: string[] = []; +export class CheckboxGroupComponent extends StatefulControlComponent { + public readonly controlId = MathHelper.guid(); @Input() public values: string[] = []; - public isDisabled = false; - - public control = MathHelper.guid(); - - constructor( - private readonly changeDetector: ChangeDetectorRef - ) { + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + checkedValues: [] + }); } public writeValue(obj: any) { - this.checkedValues = Types.isArrayOfString(obj) ? obj.filter(x => this.values.indexOf(x) >= 0) : []; - - this.changeDetector.markForCheck(); - } - - public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; + const checkedValues = Types.isArrayOfString(obj) ? obj.filter(x => this.values.indexOf(x) >= 0) : []; - this.changeDetector.markForCheck(); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - - public blur() { - this.callTouched(); + this.next(s => ({ ...s, checkedValues })); } public check(isChecked: boolean, value: string) { + let checkedValues = this.snapshot.checkedValues; + if (isChecked) { - this.checkedValues = [value, ...this.checkedValues]; + checkedValues = [value, ...checkedValues]; } else { - this.checkedValues = this.checkedValues.filter(x => x !== value); + checkedValues = checkedValues.filter(x => x !== value); } - this.callChange(this.checkedValues); + this.next(s => ({ ...s, checkedValues })); + + this.callChange(checkedValues); } public isChecked(value: string) { - return this.checkedValues.indexOf(value) >= 0; + return this.snapshot.checkedValues.indexOf(value) >= 0; } } diff --git a/src/Squidex/app/framework/angular/forms/code-editor.component.ts b/src/Squidex/app/framework/angular/forms/code-editor.component.ts index fd7cf17ec..6d99cdef0 100644 --- a/src/Squidex/app/framework/angular/forms/code-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/code-editor.component.ts @@ -5,12 +5,16 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, ViewChild } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; -import { ResourceLoaderService, Types } from '@app/framework/internal'; +import { + ExternalControlComponent, + ResourceLoaderService, + Types +} from '@app/framework/internal'; declare var ace: any; @@ -25,9 +29,7 @@ export const SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_JSCRIPT_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class CodeEditorComponent implements ControlValueAccessor, AfterViewInit { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class CodeEditorComponent extends ExternalControlComponent implements AfterViewInit { private valueChanged = new Subject(); private aceEditor: any; private value: string; @@ -39,9 +41,10 @@ export class CodeEditorComponent implements ControlValueAccessor, AfterViewInit @Input() public mode = 'ace/mode/javascript'; - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly resourceLoader: ResourceLoaderService ) { + super(changeDetector); } public writeValue(obj: any) { @@ -60,14 +63,6 @@ export class CodeEditorComponent implements ControlValueAccessor, AfterViewInit } } - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - public ngAfterViewInit() { this.valueChanged.pipe( debounceTime(500)) diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.html b/src/Squidex/app/framework/angular/forms/control-errors.component.html index 5cab06ca2..3626cf842 100644 --- a/src/Squidex/app/framework/angular/forms/control-errors.component.html +++ b/src/Squidex/app/framework/angular/forms/control-errors.component.html @@ -1,6 +1,6 @@ -
+
- + {{message}}
diff --git a/src/Squidex/app/framework/angular/forms/control-errors.component.ts b/src/Squidex/app/framework/angular/forms/control-errors.component.ts index 673975dfc..3a347ad06 100644 --- a/src/Squidex/app/framework/angular/forms/control-errors.component.ts +++ b/src/Squidex/app/framework/angular/forms/control-errors.component.ts @@ -7,12 +7,20 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Host, Input, OnChanges, OnDestroy, Optional } from '@angular/core'; import { AbstractControl, FormGroupDirective } from '@angular/forms'; -import { merge, Subscription } from 'rxjs'; +import { merge } from 'rxjs'; -import { fadeAnimation, Types } from '@app/framework/internal'; +import { + fadeAnimation, + StatefulComponent, + Types +} from '@app/framework/internal'; import { formatError } from './error-formatting'; +interface State { + errorMessages: string[]; +} + @Component({ selector: 'sqx-control-errors', styleUrls: ['./control-errors.component.scss'], @@ -22,10 +30,9 @@ import { formatError } from './error-formatting'; ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class ControlErrorsComponent implements OnChanges, OnDestroy { +export class ControlErrorsComponent extends StatefulComponent implements OnChanges, OnDestroy { private displayFieldName: string; private control: AbstractControl; - private controlSubscription: Subscription | null = null; private originalMarkAsTouched: any; @Input() @@ -43,16 +50,20 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy { @Input() public submitOnly = false; - public errorMessages: string[] = []; - - constructor( - @Optional() @Host() private readonly formGroupDirective: FormGroupDirective, - private readonly changeDetector: ChangeDetectorRef + constructor(changeDetector: ChangeDetectorRef, + @Optional() @Host() private readonly formGroupDirective: FormGroupDirective ) { + super(changeDetector, { + errorMessages: [] + }); } public ngOnDestroy() { - this.unsubscribe(); + super.ngOnDestroy(); + + if (this.control && this.originalMarkAsTouched) { + this.control['markAsTouched'] = this.originalMarkAsTouched; + } } public ngOnChanges() { @@ -75,16 +86,16 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy { } if (this.control !== control) { - this.unsubscribe(); + this.ngOnDestroy(); this.control = control; if (control) { - this.controlSubscription = + this.own( merge(control.valueChanges, control.statusChanges) .subscribe(() => { this.createMessages(); - }); + })); this.originalMarkAsTouched = this.control.markAsTouched; @@ -101,16 +112,6 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy { this.createMessages(); } - private unsubscribe() { - if (this.controlSubscription) { - this.controlSubscription.unsubscribe(); - } - - if (this.control && this.originalMarkAsTouched) { - this.control['markAsTouched'] = this.originalMarkAsTouched; - } - } - private createMessages() { const errors: string[] = []; @@ -126,8 +127,8 @@ export class ControlErrorsComponent implements OnChanges, OnDestroy { } } - this.errorMessages = errors; - - this.changeDetector.markForCheck(); + if (errors.length !== this.snapshot.errorMessages.length || errors.length > 0) { + this.next(s => ({ ...s, errorMessages: errors })); + } } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.html b/src/Squidex/app/framework/angular/forms/date-time-editor.component.html index 730e2f679..8d17ff0b8 100644 --- a/src/Squidex/app/framework/angular/forms/date-time-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/date-time-editor.component.html @@ -1,19 +1,19 @@
- +
- +
- +
- +
- +
diff --git a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts b/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts index eee5c1890..3508ee929 100644 --- a/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/date-time-editor.component.ts @@ -5,12 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import * as moment from 'moment'; -import { Subscription } from 'rxjs'; -import { Types } from '@app/framework/internal'; +import { StatefulControlComponent, Types } from '@app/framework/internal'; declare module 'pikaday/pikaday'; @@ -27,15 +26,11 @@ export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, OnInit, AfterViewInit { - private timeSubscription: Subscription; - private dateSubscription: Subscription; +export class DateTimeEditorComponent extends StatefulControlComponent<{}, string | null> implements OnInit, AfterViewInit { private picker: any; private timeValue: any | null = null; private dateValue: any | null = null; private suppressEvents = false; - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; @Input() public mode: string; @@ -49,8 +44,6 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, @ViewChild('dateInput') public dateInput: ElementRef; - public isDisabled = false; - public timeControl = new FormControl(); public dateControl = new FormControl(); @@ -62,18 +55,12 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, return !!this.dateValue; } - constructor( - private readonly changeDetector: ChangeDetectorRef - ) { - } - - public ngOnDestroy() { - this.dateSubscription.unsubscribe(); - this.timeSubscription.unsubscribe(); + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, {}); } public ngOnInit() { - this.timeSubscription = + this.own( this.timeControl.valueChanges.subscribe(value => { if (!value || value.length === 0) { this.timeValue = null; @@ -82,9 +69,9 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } this.updateValue(); - }); + })); - this.dateSubscription = + this.own( this.dateControl.valueChanges.subscribe(value => { if (!value || value.length === 0) { this.dateValue = null; @@ -93,7 +80,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } this.updateValue(); - }); + })); } public writeValue(obj: any) { @@ -114,7 +101,7 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, } public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; + super.setDisabledState(isDisabled); if (isDisabled) { this.dateControl.disable({ emitEvent: false }); @@ -123,8 +110,6 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, this.dateControl.enable({ emitEvent: false }); this.timeControl.enable({ emitEvent: false }); } - - this.changeDetector.markForCheck(); } public registerOnChange(fn: any) { @@ -144,25 +129,19 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, this.dateValue = this.picker.getMoment(); this.updateValue(); - this.touched(); - - this.changeDetector.markForCheck(); + this.callTouched(); } }); this.updateControls(); } - public touched() { - this.callTouched(); - } - public writeNow() { this.writeValue(new Date().toUTCString()); this.updateControls(); this.updateValue(); - this.touched(); + this.callTouched(); return false; } diff --git a/src/Squidex/app/framework/angular/forms/file-drop.directive.ts b/src/Squidex/app/framework/angular/forms/file-drop.directive.ts index 30bbcff60..52968da94 100644 --- a/src/Squidex/app/framework/angular/forms/file-drop.directive.ts +++ b/src/Squidex/app/framework/angular/forms/file-drop.directive.ts @@ -30,6 +30,9 @@ export class FileDropDirective { @Input() public onlyImages: boolean; + @Input() + public noDrop: boolean; + @Output('sqxFileDrop') public drop = new EventEmitter(); @@ -41,6 +44,10 @@ export class FileDropDirective { @HostListener('paste', ['$event']) public onPaste(event: ClipboardEvent) { + if (this.noDrop) { + return; + } + const result: File[] = []; for (let i = 0; i < event.clipboardData.items.length; i++) { diff --git a/src/Squidex/app/framework/angular/forms/forms-helper.ts b/src/Squidex/app/framework/angular/forms/forms-helper.ts index d38f8125e..8597769a2 100644 --- a/src/Squidex/app/framework/angular/forms/forms-helper.ts +++ b/src/Squidex/app/framework/angular/forms/forms-helper.ts @@ -6,10 +6,12 @@ */ import { AbstractControl, FormArray, FormGroup } from '@angular/forms'; + import { Observable } from 'rxjs'; + import { map, startWith } from 'rxjs/operators'; import { Types } from '@app/framework/internal'; -export const formControls = (form: AbstractControl): AbstractControl[] => { +export function formControls(form: AbstractControl): AbstractControl[] { if (Types.is(form, FormGroup)) { return Object.values(form.controls); } else if (Types.is(form, FormArray)) { @@ -17,9 +19,25 @@ export const formControls = (form: AbstractControl): AbstractControl[] => { } else { return []; } -}; +} -export const fullValue = (form: AbstractControl): any => { +export function invalid$(form: AbstractControl): Observable { + return form.statusChanges.pipe(map(_ => form.invalid), startWith(form.invalid)); +} + +export function value$(form: AbstractControl): Observable { + return form.valueChanges.pipe(startWith(form.value)); +} + +export function hasValue$(form: AbstractControl): Observable { + return value$(form).pipe(map(v => !!v)); +} + +export function hasNoValue$(form: AbstractControl): Observable { + return value$(form).pipe(map(v => !v)); +} + +export function fullValue(form: AbstractControl): any { if (Types.is(form, FormGroup)) { const groupValue = {}; @@ -41,4 +59,4 @@ export const fullValue = (form: AbstractControl): any => { } else { return form.value; } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts b/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts index 528eb663b..0a19695d5 100644 --- a/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/iframe-editor.component.ts @@ -5,11 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; -import { Types } from '@app/framework/internal'; +import { ExternalControlComponent, Types } from '@app/framework/internal'; export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => IFrameEditorComponent), multi: true @@ -22,10 +22,7 @@ export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class IFrameEditorComponent implements ControlValueAccessor, AfterViewInit, OnInit, OnDestroy { - private windowMessageListener: Function; - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class IFrameEditorComponent extends ExternalControlComponent implements AfterViewInit, OnInit { private value: any; private isDisabled = false; private isInitialized = false; @@ -41,14 +38,11 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni public sanitizedUrl: SafeResourceUrl; - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly sanitizer: DomSanitizer, private readonly renderer: Renderer2 ) { - } - - public ngOnDestroy() { - this.windowMessageListener(); + super(changeDetector); } public ngAfterViewInit() { @@ -56,7 +50,7 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni } public ngOnInit(): void { - this.windowMessageListener = + this.own( this.renderer.listen('window', 'message', (event: MessageEvent) => { if (event.source === this.plugin.contentWindow) { const { type } = event.data; @@ -84,7 +78,7 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni this.callTouched(); } } - }); + })); } public writeValue(obj: any) { @@ -102,12 +96,4 @@ export class IFrameEditorComponent implements ControlValueAccessor, AfterViewIni this.plugin.contentWindow.postMessage({ type: 'disabled', isDisabled: this.isDisabled }, '*'); } } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/json-editor.component.ts b/src/Squidex/app/framework/angular/forms/json-editor.component.ts index 35f84d0b5..e2baa6854 100644 --- a/src/Squidex/app/framework/angular/forms/json-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/json-editor.component.ts @@ -5,12 +5,12 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, ViewChild } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; -import { ResourceLoaderService } from '@app/framework/internal'; +import { ExternalControlComponent, ResourceLoaderService } from '@app/framework/internal'; declare var ace: any; @@ -25,9 +25,7 @@ export const SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_JSON_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class JsonEditorComponent extends ExternalControlComponent implements AfterViewInit { private valueChanged = new Subject(); private aceEditor: any; private value: any; @@ -35,11 +33,14 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit private isDisabled = false; @ViewChild('editor') - public editor: ElementRef; + public editor: ElementRef; - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly resourceLoader: ResourceLoaderService ) { + super(changeDetector); + + changeDetector.detach(); } public writeValue(obj: any) { @@ -64,14 +65,6 @@ export class JsonEditorComponent implements ControlValueAccessor, AfterViewInit } } - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - public ngAfterViewInit() { this.valueChanged.pipe( debounceTime(500)) diff --git a/src/Squidex/app/framework/angular/forms/progress-bar.component.ts b/src/Squidex/app/framework/angular/forms/progress-bar.component.ts index a5f300c0f..4a4d99649 100644 --- a/src/Squidex/app/framework/angular/forms/progress-bar.component.ts +++ b/src/Squidex/app/framework/angular/forms/progress-bar.component.ts @@ -5,7 +5,7 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Input, OnChanges, OnInit, Renderer2, SimpleChanges } from '@angular/core'; import * as ProgressBar from 'progressbar.js'; @@ -38,10 +38,11 @@ export class ProgressBarComponent implements OnChanges, OnInit { @Input() public value = 0; - constructor( + constructor(changeDetector: ChangeDetectorRef, private readonly element: ElementRef, private readonly renderer: Renderer2 ) { + changeDetector.detach(); } public ngOnInit() { diff --git a/src/Squidex/app/framework/angular/forms/slider.component.html b/src/Squidex/app/framework/angular/forms/slider.component.html deleted file mode 100644 index ab37c52ff..000000000 --- a/src/Squidex/app/framework/angular/forms/slider.component.html +++ /dev/null @@ -1,3 +0,0 @@ -
-
-
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/slider.component.scss b/src/Squidex/app/framework/angular/forms/slider.component.scss deleted file mode 100644 index da277a647..000000000 --- a/src/Squidex/app/framework/angular/forms/slider.component.scss +++ /dev/null @@ -1,51 +0,0 @@ -@import '_mixins'; -@import '_vars'; - -$bar-height: .8rem; - -$thumb-size: 1.25rem; -$thumb-margin: ($thumb-size - $bar-height) * .5; - -.slider { - &-bar { - & { - @include border-radius($bar-height * .5); - position: relative; - border: 1px solid $color-input-border; - margin-bottom: 1.25rem; - margin-top: .25rem; - margin-right: $thumb-size * .5; - background: $color-dark-foreground; - height: $bar-height; - } - - &.disabled { - background: lighten($color-border, 5%); - } - } - - &-thumb { - & { - @include border-radius($thumb-size * .5); - position: absolute; - width: $thumb-size; - height: $thumb-size; - border: 1px solid $color-input-border; - background: $color-dark-foreground; - margin-top: -$thumb-margin; - margin-left: -$thumb-size * .5; - } - - &.disabled { - background: lighten($color-border, 5%); - } - - &.focused { - border-color: $color-theme-blue; - } - } -} - -.disabled { - pointer-events: none; -} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/slider.component.ts b/src/Squidex/app/framework/angular/forms/slider.component.ts deleted file mode 100644 index 2dcb73854..000000000 --- a/src/Squidex/app/framework/angular/forms/slider.component.ts +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, Renderer2, ViewChild } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { Types } from '@app/framework/internal'; - -export const SQX_SLIDER_CONTROL_VALUE_ACCESSOR: any = { - provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SliderComponent), multi: true -}; - -@Component({ - selector: 'sqx-slider', - styleUrls: ['./slider.component.scss'], - templateUrl: './slider.component.html', - providers: [SQX_SLIDER_CONTROL_VALUE_ACCESSOR], - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class SliderComponent implements ControlValueAccessor { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - private windowMouseMoveListener: Function | null = null; - private windowMouseUpListener: Function | null = null; - private centerStartOffset = 0; - private lastValue: number; - private value: number; - private isDragging = false; - - @ViewChild('bar') - public bar: ElementRef; - - @ViewChild('thumb') - public thumb: ElementRef; - - @Input() - public min = 0; - - @Input() - public max = 100; - - @Input() - public step = 1; - - public isDisabled = false; - - constructor( - private readonly changeDetector: ChangeDetectorRef, - private readonly renderer: Renderer2 - ) { - } - - public writeValue(obj: any) { - this.lastValue = this.value = Types.isNumber(obj) ? obj : 0; - - this.updateThumbPosition(); - - this.changeDetector.markForCheck(); - } - - public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - this.changeDetector.markForCheck(); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - - public onBarMouseClick(event: MouseEvent): boolean { - if (this.windowMouseMoveListener) { - return true; - } - - const relativeValue = this.getRelativeX(event); - - this.value = Math.round((relativeValue * (this.max - this.min) + this.min) / this.step) * this.step; - - this.updateThumbPosition(); - this.updateTouched(); - this.updateValue(); - - return false; - } - - public onThumbMouseDown(event: MouseEvent): boolean { - this.centerStartOffset = event.offsetX - this.thumb.nativeElement.clientWidth * 0.5; - - this.windowMouseMoveListener = - this.renderer.listen('window', 'mousemove', (e: MouseEvent) => { - this.onMouseMove(e); - }); - - this.windowMouseUpListener = - this.renderer.listen('window', 'mouseup', () => { - this.onMouseUp(); - }); - - this.renderer.addClass(this.thumb.nativeElement, 'focused'); - - this.isDragging = true; - - return false; - } - - private onMouseMove(event: MouseEvent): boolean { - if (!this.isDragging) { - return true; - } - - const relativeValue = this.getRelativeX(event); - - this.value = Math.round((relativeValue * (this.max - this.min) + this.min) / this.step) * this.step; - - this.updateThumbPosition(); - this.updateTouched(); - - return false; - } - - private onMouseUp(): boolean { - this.updateValue(); - - setTimeout(() => { - this.releaseMouseHandlers(); - this.renderer.removeClass(this.thumb.nativeElement, 'focused'); - }, 10); - - this.centerStartOffset = 0; - - this.isDragging = false; - - return false; - } - - private getRelativeX(event: MouseEvent): number { - const parentOffsetX = this.getParentX(event, this.bar.nativeElement) - this.centerStartOffset; - const parentWidth = this.bar.nativeElement.clientWidth; - - const relativeValue = Math.min(1, Math.max(0, (parentOffsetX - this.centerStartOffset) / parentWidth)); - - return relativeValue; - } - - private getParentX(e: any, container: any): number { - const rect = container.getBoundingClientRect(); - - const x = - !!e.touches ? - e.touches[0].pageX : - e.pageX; - - return x - rect.left; - } - - private updateTouched() { - this.callTouched(); - } - - private updateValue() { - if (this.lastValue !== this.value) { - this.lastValue = this.value; - - this.callChange(this.value); - } - } - - private updateThumbPosition() { - const relativeValue = Math.min(1, Math.max(0, (this.value - this.min) / (this.max - this.min))); - - this.renderer.setStyle(this.thumb.nativeElement, 'left', relativeValue * 100 + '%'); - } - - private releaseMouseHandlers() { - if (this.windowMouseMoveListener) { - this.windowMouseMoveListener(); - this.windowMouseMoveListener = null; - } - - if (this.windowMouseUpListener) { - this.windowMouseUpListener(); - this.windowMouseUpListener = null; - } - - this.isDragging = false; - } -} \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/stars.component.html b/src/Squidex/app/framework/angular/forms/stars.component.html index 4f325a931..af4d6d7c9 100644 --- a/src/Squidex/app/framework/angular/forms/stars.component.html +++ b/src/Squidex/app/framework/angular/forms/stars.component.html @@ -4,10 +4,10 @@
- - + + - +
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/stars.component.ts b/src/Squidex/app/framework/angular/forms/stars.component.ts index 26f45ef20..69a0e24ca 100644 --- a/src/Squidex/app/framework/angular/forms/stars.component.ts +++ b/src/Squidex/app/framework/angular/forms/stars.component.ts @@ -6,14 +6,21 @@ */ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Types } from '@app/framework/internal'; +import { StatefulControlComponent, Types } from '@app/framework/internal'; export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => StarsComponent), multi: true }; +interface State { + stars: number; + starsArray: number[]; + + value: number | null; +} + @Component({ selector: 'sqx-stars', styleUrls: ['./stars.component.scss'], @@ -21,9 +28,7 @@ export const SQX_STARS_CONTROL_VALUE_ACCESSOR: any = { providers: [SQX_STARS_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class StarsComponent implements ControlValueAccessor { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; +export class StarsComponent extends StatefulControlComponent { private maximumStarsValue = 5; @Input() @@ -33,11 +38,13 @@ export class StarsComponent implements ControlValueAccessor { if (this.maximumStarsValue !== maxStars) { this.maximumStarsValue = value; - this.starsArray = []; + const starsArray: number[] = []; - for (let i = 1; i <= value; i++) { - this.starsArray.push(i); + for (let i = 1; i <= maxStars; i++) { + starsArray.push(i); } + + this.next(s => ({ ...s, starsArray })); } } @@ -45,64 +52,45 @@ export class StarsComponent implements ControlValueAccessor { return this.maximumStarsValue; } - public isDisabled = false; - - public stars: number; - public starsArray: number[] = [1, 2, 3, 4, 5]; - - public value: number | null = 1; - - constructor( - private readonly changeDetector: ChangeDetectorRef - ) { + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + stars: -1, + starsArray: [1, 2, 3, 4, 5], + value: 1 + }); } public writeValue(obj: any) { - this.value = this.stars = Types.isNumber(obj) ? obj : 0; - - this.changeDetector.markForCheck(); - } - - public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; - - this.changeDetector.markForCheck(); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } + const value = Types.isNumber(obj) ? obj : 0; - public registerOnTouched(fn: any) { - this.callTouched = fn; + this.next(s => ({ ...s, stars: value, value })); } - public setPreview(value: number) { - if (this.isDisabled) { + public setPreview(stars: number) { + if (this.snapshot.isDisabled) { return; } - this.stars = value; + this.next(s => ({ ...s, stars })); } public stopPreview() { - if (this.isDisabled) { + if (this.snapshot.isDisabled) { return; } - this.stars = this.value || 0; + this.next(s => ({ ...s, stars: s.value || 0 })); } public reset() { - if (this.isDisabled) { + if (this.snapshot.isDisabled) { return false; } - if (this.value) { - this.value = null; - this.stars = 0; + if (this.snapshot.value) { + this.next(s => ({ ...s, stars: -1, value: null })); - this.callChange(this.value); + this.callChange(null); this.callTouched(); } @@ -110,14 +98,14 @@ export class StarsComponent implements ControlValueAccessor { } public setValue(value: number) { - if (this.isDisabled) { + if (this.snapshot.isDisabled) { return false; } - if (this.value !== value) { - this.value = this.stars = value; + if (this.snapshot.value !== value) { + this.next(s => ({ ...s, stars: value, value })); - this.callChange(this.value); + this.callChange(value); this.callTouched(); } diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.html b/src/Squidex/app/framework/angular/forms/tag-editor.component.html index 5aa03539b..1877ad601 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.html +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.html @@ -1,9 +1,9 @@
- + {{item}} @@ -23,13 +23,13 @@ spellcheck="false">
-
-
+
+ [sqxScrollActive]="i === snapshot.suggestedIndex"> {{item}}
diff --git a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts index 0fa053881..9f70a8fc3 100644 --- a/src/Squidex/app/framework/angular/forms/tag-editor.component.ts +++ b/src/Squidex/app/framework/angular/forms/tag-editor.component.ts @@ -5,12 +5,11 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Subscription } from 'rxjs'; +import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnInit, ViewChild } from '@angular/core'; +import { FormControl, NG_VALUE_ACCESSOR } from '@angular/forms'; import { distinctUntilChanged, map, tap } from 'rxjs/operators'; -import { Types } from '@app/framework/internal'; +import { StatefulControlComponent, Types } from '@app/framework/internal'; const KEY_COMMA = 188; const KEY_DELETE = 8; @@ -75,6 +74,15 @@ const CACHED_SIZES: { [key: string]: number } = {}; let CACHED_FONT: string; +interface State { + hasFocus: boolean; + + suggestedItems: string[]; + suggestedIndex: number; + + items: any[]; +} + @Component({ selector: 'sqx-tag-editor', styleUrls: ['./tag-editor.component.scss'], @@ -82,11 +90,7 @@ let CACHED_FONT: string; providers: [SQX_TAG_EDITOR_CONTROL_VALUE_ACCESSOR], changeDetection: ChangeDetectionStrategy.OnPush }) -export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, OnDestroy, OnInit { - private subscription: Subscription; - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - +export class TagEditorComponent extends StatefulControlComponent implements AfterViewInit, OnInit { @Input() public converter: Converter = new StringConverter(); @@ -115,27 +119,20 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, public inputName = 'tag-editor'; @ViewChild('form') - public formElement: ElementRef; + public formElement: ElementRef; @ViewChild('input') public inputElement: ElementRef; - public hasFocus = false; - - public suggestedItems: string[] = []; - public suggestedIndex = 0; - - public items: any[] = []; - public addInput = new FormControl(); - constructor( - private readonly changeDetector: ChangeDetectorRef - ) { - } - - public ngOnDestroy() { - this.subscription.unsubscribe(); + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + hasFocus: false, + suggestedItems: [], + suggestedIndex: 0, + items: [] + }); } public ngAfterViewInit() { @@ -149,7 +146,7 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } public ngOnInit() { - this.subscription = + this.own( this.addInput.valueChanges.pipe( tap(() => { this.resetSize(); @@ -164,15 +161,18 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, distinctUntilChanged(), map(query => { if (Types.isArray(this.suggestions) && query && query.length > 0) { - return this.suggestions.filter(s => s.indexOf(query) >= 0 && this.items.indexOf(s) < 0); + return this.suggestions.filter(s => s.indexOf(query) >= 0 && this.snapshot.items.indexOf(s) < 0); } else { return []; } })) .subscribe(items => { - this.suggestedIndex = -1; - this.suggestedItems = items || []; - }); + this.next(s => ({ + ...s, + suggestedIndex: -1, + suggestedItems: items || [] + })); + })); } public writeValue(obj: any) { @@ -180,15 +180,15 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, this.resetSize(); if (this.converter && Types.isArrayOf(obj, v => this.converter.isValidValue(v))) { - this.items = obj; + this.next(s => ({ ...s, items: obj })); } else { - this.items = []; + this.next(s => ({ ...s, items: [] })); } - - this.changeDetector.markForCheck(); } public setDisabledState(isDisabled: boolean): void { + super.setDisabledState(isDisabled); + if (isDisabled) { this.addInput.disable(); } else { @@ -196,17 +196,9 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } } - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; - } - public focus() { if (this.addInput.enabled) { - this.hasFocus = true; + this.next(s => ({ ...s, hasFocus: true })); } } @@ -220,7 +212,7 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } public remove(index: number) { - this.updateItems([...this.items.slice(0, index), ...this.items.splice(index + 1)]); + this.updateItems(this.snapshot.items.filter((_, i) => i !== index)); } public resetSize() { @@ -274,7 +266,7 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, const value = this.addInput.value; if (!value || value.length === 0) { - this.updateItems(this.items.slice(0, this.items.length - 1)); + this.updateItems(this.snapshot.items.slice(0, this.snapshot.items.length - 1)); return false; } @@ -285,8 +277,8 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, this.down(); return false; } else if (key === KEY_ENTER) { - if (this.suggestedIndex >= 0) { - if (this.selectValue(this.suggestedItems[this.suggestedIndex])) { + if (this.snapshot.suggestedIndex >= 0) { + if (this.selectValue(this.snapshot.suggestedItems[this.snapshot.suggestedIndex])) { return false; } } else if (this.acceptEnter) { @@ -307,8 +299,8 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, if (value && this.converter.isValidInput(value)) { const converted = this.converter.convert(value); - if (this.allowDuplicates || this.items.indexOf(converted) < 0) { - this.updateItems([...this.items, converted]); + if (this.allowDuplicates || this.snapshot.items.indexOf(converted) < 0) { + this.updateItems([...this.snapshot.items, converted]); } this.resetForm(); @@ -318,24 +310,27 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } private resetAutocompletion() { - this.suggestedItems = []; - this.suggestedIndex = -1; + this.next(s => ({ + ...s, + suggestedItems: [], + suggestedIndex: -1 + })); } - public selectIndex(selection: number) { - if (selection < 0) { - selection = 0; + public selectIndex(suggestedIndex: number) { + if (suggestedIndex < 0) { + suggestedIndex = 0; } - if (selection >= this.suggestedItems.length) { - selection = this.suggestedItems.length - 1; + if (suggestedIndex >= this.snapshot.suggestedItems.length) { + suggestedIndex = this.snapshot.suggestedItems.length - 1; } - this.suggestedIndex = selection; + this.next(s => ({ ...s, suggestedIndex })); } public resetFocus(): any { - this.hasFocus = false; + this.next(s => ({ ...s, hasFocus: false })); } private resetForm() { @@ -343,11 +338,11 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } private up() { - this.selectIndex(this.suggestedIndex - 1); + this.selectIndex(this.snapshot.suggestedIndex - 1); } private down() { - this.selectIndex(this.suggestedIndex + 1); + this.selectIndex(this.snapshot.suggestedIndex + 1); } public onCut(event: ClipboardEvent) { @@ -360,7 +355,7 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, public onCopy(event: ClipboardEvent) { if (!this.hasSelection()) { - event.clipboardData.setData('text/plain', this.items.filter(x => !!x).join(',')); + event.clipboardData.setData('text/plain', this.snapshot.items.filter(x => !!x).join(',')); event.preventDefault(); } @@ -372,7 +367,7 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, if (value) { this.resetForm(); - const values = [...this.items]; + const values = [...this.snapshot.items]; for (let part of value.split(',')) { const converted = this.converter.convert(part); @@ -396,12 +391,12 @@ export class TagEditorComponent implements AfterViewInit, ControlValueAccessor, } private updateItems(items: any[]) { - this.items = items; + this.next(s => ({ ...s, items })); if (items.length === 0 && this.undefinedWhenEmpty) { this.callChange(undefined); } else { - this.callChange(this.items); + this.callChange(items); } this.resetSize(); diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.html b/src/Squidex/app/framework/angular/forms/toggle.component.html index 8c774acff..ecdde36e5 100644 --- a/src/Squidex/app/framework/angular/forms/toggle.component.html +++ b/src/Squidex/app/framework/angular/forms/toggle.component.html @@ -1,6 +1,6 @@
+ [class.disabled]="snapshot.isDisabled" + [class.checked]="snapshot.isChecked === true" + [class.unchecked]="snapshot.isChecked === false">
\ No newline at end of file diff --git a/src/Squidex/app/framework/angular/forms/toggle.component.ts b/src/Squidex/app/framework/angular/forms/toggle.component.ts index 1524d908b..7105ada32 100644 --- a/src/Squidex/app/framework/angular/forms/toggle.component.ts +++ b/src/Squidex/app/framework/angular/forms/toggle.component.ts @@ -5,75 +5,63 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; -import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ChangeDetectorRef, Component, forwardRef, Input } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { Types } from '@app/framework/internal'; +import { StatefulControlComponent, Types } from '@app/framework/internal'; export const SQX_TOGGLE_CONTROL_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ToggleComponent), multi: true }; +interface State { + isChecked: boolean | null; +} + @Component({ selector: 'sqx-toggle', styleUrls: ['./toggle.component.scss'], templateUrl: './toggle.component.html', - providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR], - changeDetection: ChangeDetectionStrategy.OnPush + providers: [SQX_TOGGLE_CONTROL_VALUE_ACCESSOR] }) -export class ToggleComponent implements ControlValueAccessor { - private callChange = (v: any) => { /* NOOP */ }; - private callTouched = () => { /* NOOP */ }; - +export class ToggleComponent extends StatefulControlComponent { @Input() public threeStates = false; - public isChecked: boolean | null = null; - public isDisabled = false; - - constructor( - private readonly changeDetector: ChangeDetectorRef - ) { + constructor(changeDetector: ChangeDetectorRef) { + super(changeDetector, { + isChecked: null + }); } public writeValue(obj: any) { - this.isChecked = Types.isBoolean(obj) ? obj : null; - - this.changeDetector.markForCheck(); - } - - public setDisabledState(isDisabled: boolean): void { - this.isDisabled = isDisabled; + const isChecked = Types.isBoolean(obj) ? obj : null; - this.changeDetector.markForCheck(); - } - - public registerOnChange(fn: any) { - this.callChange = fn; - } - - public registerOnTouched(fn: any) { - this.callTouched = fn; + this.next(s => ({ ...s, isChecked })); } public changeState(event: MouseEvent) { - if (this.isDisabled) { + let { isDisabled, isChecked } = this.snapshot; + + if (isDisabled) { return; } if (this.threeStates && (event.ctrlKey || event.shiftKey)) { - if (this.isChecked) { - this.isChecked = null; - } else if (this.isChecked === null) { - this.isChecked = false; + if (isChecked) { + isChecked = null; + } else if (isChecked === null) { + isChecked = false; } else { - this.isChecked = true; + isChecked = true; } } else { - this.isChecked = !(this.isChecked === true); + isChecked = !(isChecked === true); } - this.callChange(this.isChecked); + this.next(s => ({ ...s, isChecked })); + + this.callChange(isChecked); this.callTouched(); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts b/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts index 384bcb25c..c9d1ec641 100644 --- a/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts +++ b/src/Squidex/app/framework/angular/ignore-scrollbar.directive.ts @@ -5,27 +5,23 @@ * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. */ -import { AfterViewInit, Directive, ElementRef, OnDestroy, OnInit, Renderer2 } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, OnInit, Renderer2 } from '@angular/core'; +import { timer } from 'rxjs'; + +import { ResourceOwner } from '@app/framework/internal'; @Directive({ selector: '[sqxIgnoreScrollbar]' }) -export class IgnoreScrollbarDirective implements OnDestroy, OnInit, AfterViewInit { - private resizeListener: Function; +export class IgnoreScrollbarDirective extends ResourceOwner implements OnInit, AfterViewInit { private parent: any; - private checkTimer: any; private scollbarWidth = 0; constructor( private readonly element: ElementRef, private readonly renderer: Renderer2 ) { - } - - public ngOnDestroy() { - clearTimeout(this.checkTimer); - - this.resizeListener(); + super(); } public ngOnInit() { @@ -33,15 +29,12 @@ export class IgnoreScrollbarDirective implements OnDestroy, OnInit, AfterViewIni this.parent = this.renderer.parentNode(this.element.nativeElement); } - this.resizeListener = + this.own( this.renderer.listen(this.element.nativeElement, 'resize', () => { this.reposition(); - }); + })); - this.checkTimer = - setTimeout(() => { - this.reposition(); - }, 100); + this.own(timer(100, 100).subscribe(() => this.reposition)); } public ngAfterViewInit() { diff --git a/src/Squidex/app/framework/angular/image-source.directive.ts b/src/Squidex/app/framework/angular/image-source.directive.ts index c9406b524..ad4022b12 100644 --- a/src/Squidex/app/framework/angular/image-source.directive.ts +++ b/src/Squidex/app/framework/angular/image-source.directive.ts @@ -7,18 +7,16 @@ import { AfterViewInit, Directive, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, Renderer2 } from '@angular/core'; -import { MathHelper } from './../utils/math-helper'; +import { MathHelper, ResourceOwner } from '@app/framework/internal'; const LAYOUT_CACHE: { [key: string]: { width: number, height: number } } = {}; @Directive({ selector: '[sqxImageSource]' }) -export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, AfterViewInit { - private parentResizeListener: Function; - - private loadingTimer: any; +export class ImageSourceDirective extends ResourceOwner implements OnChanges, OnDestroy, OnInit, AfterViewInit { private size: any; + private loadTimer: any; private loadRetries = 0; private loadQuery: string | null = null; @@ -38,12 +36,13 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After private readonly element: ElementRef, private readonly renderer: Renderer2 ) { + super(); } public ngOnDestroy() { - clearTimeout(this.loadingTimer); + super.ngOnDestroy(); - this.parentResizeListener(); + clearTimeout(this.loadTimer); } public ngOnInit() { @@ -51,10 +50,10 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After this.parent = this.renderer.parentNode(this.element.nativeElement); } - this.parentResizeListener = + this.own( this.renderer.listen(this.parent, 'resize', () => { this.resize(); - }); + })); } public ngAfterViewInit() { @@ -127,7 +126,7 @@ export class ImageSourceDirective implements OnChanges, OnDestroy, OnInit, After this.loadRetries++; if (this.loadRetries <= 10) { - this.loadingTimer = + this.loadTimer = setTimeout(() => { this.loadQuery = MathHelper.guid(); diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.html b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.html index f39ac71d5..0dcb911a0 100644 --- a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.html +++ b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.html @@ -2,11 +2,11 @@ - {{dialogRequest?.title}} + {{snapshot.dialogRequest?.title}} - {{dialogRequest?.text}} + {{snapshot.dialogRequest?.text}} @@ -16,7 +16,7 @@
-
+
diff --git a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts index 17c184f0c..594c3fc9a 100644 --- a/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts +++ b/src/Squidex/app/framework/angular/modals/dialog-renderer.component.ts @@ -5,17 +5,24 @@ * Copyright (c) Sebastian Stehle. All rights r vbeserved */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core'; +import { timer } from 'rxjs'; import { DialogModel, DialogRequest, DialogService, fadeAnimation, - Notification + Notification, + StatefulComponent } from '@app/framework/internal'; +interface State { + dialogRequest?: DialogRequest | null; + + notifications: Notification[]; +} + @Component({ selector: 'sqx-dialog-renderer', styleUrls: ['./dialog-renderer.component.scss'], @@ -25,89 +32,74 @@ import { ], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DialogRendererComponent implements OnDestroy, OnInit { - private dialogSubscription: Subscription; - private dialogsSubscription: Subscription; - private notificationsSubscription: Subscription; - - public dialogView = new DialogModel(); - public dialogRequest: DialogRequest | null = null; - - public notifications: Notification[] = []; - +export class DialogRendererComponent extends StatefulComponent implements OnInit { @Input() public position = 'bottomright'; - constructor( - private readonly changeDetector: ChangeDetectorRef, + public dialogView = new DialogModel(); + + constructor(changeDetector: ChangeDetectorRef, private readonly dialogs: DialogService ) { - } - - public ngOnDestroy() { - this.notificationsSubscription.unsubscribe(); - this.dialogSubscription.unsubscribe(); - this.dialogsSubscription.unsubscribe(); + super(changeDetector, { notifications: [] }); } public ngOnInit() { - this.dialogSubscription = + this.own( this.dialogView.isOpen.subscribe(isOpen => { if (!isOpen) { - this.cancel(); - - this.changeDetector.detectChanges(); + this.finishRequest(false); } - }); + })); - this.notificationsSubscription = + this.own( this.dialogs.notifications.subscribe(notification => { - this.notifications.push(notification); + this.next(s => ({ + ...s, + notifications: [...s.notifications, notification] + })); if (notification.displayTime > 0) { - setTimeout(() => { + this.own(timer(notification.displayTime).subscribe(() => { this.close(notification); - }, notification.displayTime); + })); } + })); - this.changeDetector.detectChanges(); - }); - - this.dialogsSubscription = + this.own( this.dialogs.dialogs - .subscribe(request => { + .subscribe(dialogRequest => { this.cancel(); - this.dialogRequest = request; this.dialogView.show(); - this.changeDetector.detectChanges(); - }); + this.next(s => ({ ...s, dialogRequest })); + })); } public cancel() { - if (this.dialogRequest) { - this.dialogRequest.complete(false); - this.dialogRequest = null; - this.dialogView.hide(); - } + this.finishRequest(false); + + this.dialogView.hide(); } public confirm() { - if (this.dialogRequest) { - this.dialogRequest.complete(true); - this.dialogRequest = null; - this.dialogView.hide(); - } + this.finishRequest(true); + + this.dialogView.hide(); } - public close(notification: Notification) { - const index = this.notifications.indexOf(notification); + private finishRequest(value: boolean) { + this.next(s => { + if (s.dialogRequest) { + s.dialogRequest.complete(value); + } - if (index >= 0) { - this.notifications.splice(index, 1); + return { ...s, dialogRequest: null }; + }); + } - this.changeDetector.detectChanges(); - } + public close(notification: Notification) { + this.next(s => ({ ...s, notifications: s.notifications.filter(n => notification !== n) })); } } \ No newline at end of file diff --git a/src/Squidex/app/framework/angular/modals/modal-dialog.component.html b/src/Squidex/app/framework/angular/modals/modal-dialog.component.html index d7f3e5955..bf7383621 100644 --- a/src/Squidex/app/framework/angular/modals/modal-dialog.component.html +++ b/src/Squidex/app/framework/angular/modals/modal-dialog.component.html @@ -13,7 +13,7 @@
- -