Browse Source

Merge pull request #345 from Squidex/refactoring/sc

Refactoring/sc
pull/346/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
e842c08c29
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      extensions/Squidex.Extensions/Squidex.Extensions.csproj
  2. 4
      src/Squidex.Domain.Apps.Core.Model/Schemas/ArrayField.cs
  3. 4
      src/Squidex.Domain.Apps.Core.Operations/EventSynchronization/SchemaSynchronizer.cs
  4. 2
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  5. 8
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleTriggerHandler.cs
  6. 2
      src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/JsonMapper.cs
  7. 57
      src/Squidex.Domain.Apps.Core.Operations/Scripting/DefaultConverter.cs
  8. 43
      src/Squidex.Domain.Apps.Core.Operations/Scripting/JintScriptEngine.cs
  9. 2
      src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj
  10. 2
      src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs
  11. 3
      src/Squidex.Domain.Apps.Entities/EntityMapper.cs
  12. 4
      src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs
  13. 4
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  14. 2
      src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
  15. 4
      src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs
  16. 2
      src/Squidex.Infrastructure/Log/LockingLogStore.cs
  17. 2
      src/Squidex.Infrastructure/States/IStore.cs
  18. 79
      src/Squidex/AppServices.cs
  19. 1
      src/Squidex/Areas/Api/Controllers/Contents/Generator/SchemaSwaggerGenerator.cs
  20. 1
      src/Squidex/Config/Domain/EventStoreServices.cs
  21. 14
      src/Squidex/Config/Domain/LoggingServices.cs
  22. 87
      src/Squidex/Config/Domain/SystemExtensions.cs
  23. 27
      src/Squidex/Config/Orleans/Extensions.cs
  24. 99
      src/Squidex/Config/Orleans/OrleansServices.cs
  25. 51
      src/Squidex/Config/Orleans/SiloHost.cs
  26. 42
      src/Squidex/Config/Orleans/SiloServices.cs
  27. 190
      src/Squidex/Config/Orleans/SiloWrapper.cs
  28. 38
      src/Squidex/Config/Startup/InitializerHost.cs
  29. 31
      src/Squidex/Config/Startup/MigratorHost.cs
  30. 59
      src/Squidex/Config/Startup/SafeHostedService.cs
  31. 9
      src/Squidex/Pipeline/Squid/SquidMiddleware.cs
  32. 2
      src/Squidex/Squidex.csproj
  33. 74
      src/Squidex/WebStartup.cs
  34. 8
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  35. 19
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  36. 28
      src/Squidex/app/features/administration/pages/restore/restore-page.component.ts
  37. 20
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  38. 10
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  39. 2
      src/Squidex/app/features/administration/state/event-consumers.state.ts
  40. 2
      src/Squidex/app/features/administration/state/users.state.ts
  41. 2
      src/Squidex/app/features/api/pages/graphql/graphql-page.component.html
  42. 6
      src/Squidex/app/features/assets/pages/assets-filters-page.component.html
  43. 8
      src/Squidex/app/features/assets/pages/assets-filters-page.component.ts
  44. 4
      src/Squidex/app/features/assets/pages/assets-page.component.html
  45. 2
      src/Squidex/app/features/content/declarations.ts
  46. 4
      src/Squidex/app/features/content/module.ts
  47. 79
      src/Squidex/app/features/content/pages/content/content-field.component.html
  48. 37
      src/Squidex/app/features/content/pages/content/content-field.component.scss
  49. 80
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  50. 2
      src/Squidex/app/features/content/pages/content/content-history-page.component.html
  51. 6
      src/Squidex/app/features/content/pages/content/content-history-page.component.ts
  52. 5
      src/Squidex/app/features/content/pages/content/content-page.component.html
  53. 83
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  54. 52
      src/Squidex/app/features/content/pages/content/field-languages.component.ts
  55. 6
      src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html
  56. 25
      src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts
  57. 20
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  58. 31
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  59. 3
      src/Squidex/app/features/content/pages/messages.ts
  60. 4
      src/Squidex/app/features/content/shared/array-editor.component.html
  61. 24
      src/Squidex/app/features/content/shared/array-editor.component.ts
  62. 16
      src/Squidex/app/features/content/shared/array-item.component.html
  63. 7
      src/Squidex/app/features/content/shared/array-item.component.ts
  64. 25
      src/Squidex/app/features/content/shared/assets-editor.component.html
  65. 6
      src/Squidex/app/features/content/shared/assets-editor.component.scss
  66. 99
      src/Squidex/app/features/content/shared/assets-editor.component.ts
  67. 69
      src/Squidex/app/features/content/shared/content-item-editor.component.ts
  68. 105
      src/Squidex/app/features/content/shared/content-item.component.html
  69. 13
      src/Squidex/app/features/content/shared/content-item.component.ts
  70. 6
      src/Squidex/app/features/content/shared/contents-selector.component.html
  71. 2
      src/Squidex/app/features/content/shared/contents-selector.component.ts
  72. 7
      src/Squidex/app/features/content/shared/field-editor.component.html
  73. 8
      src/Squidex/app/features/content/shared/field-editor.component.ts
  74. 10
      src/Squidex/app/features/content/shared/preview-button.component.html
  75. 43
      src/Squidex/app/features/content/shared/preview-button.component.ts
  76. 17
      src/Squidex/app/features/content/shared/references-editor.component.html
  77. 81
      src/Squidex/app/features/content/shared/references-editor.component.ts
  78. 27
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  79. 8
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.html
  80. 2
      src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html
  81. 6
      src/Squidex/app/features/rules/pages/rules/rules-page.component.html
  82. 10
      src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html
  83. 4
      src/Squidex/app/features/schemas/pages/schema/field.component.html
  84. 6
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.html
  85. 28
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  86. 10
      src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts
  87. 14
      src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts
  88. 38
      src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts
  89. 5
      src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts
  90. 37
      src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts
  91. 2
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html
  92. 26
      src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.ts
  93. 2
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html
  94. 20
      src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.ts
  95. 6
      src/Squidex/app/features/settings/pages/backups/backups-page.component.html
  96. 22
      src/Squidex/app/features/settings/pages/backups/backups-page.component.ts
  97. 4
      src/Squidex/app/features/settings/pages/clients/client.component.html
  98. 2
      src/Squidex/app/features/settings/pages/clients/clients-page.component.html
  99. 2
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  100. 4
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

2
extensions/Squidex.Extensions/Squidex.Extensions.csproj

@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.3.1" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" />
<PackageReference Include="Elasticsearch.Net" Version="6.4.2" />
<PackageReference Include="Elasticsearch.Net" Version="6.5.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="2.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />

4
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));

4
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<NestedField>.Empty;
var fields = ((IArrayField)sourceField)?.FieldCollection ?? FieldCollection<NestedField>.Empty;
var events = SyncFields(fields, targetArrayField.FieldCollection, serializer, idGenerator, id, options);

2
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;
}

8
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);
}

2
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<string> s:
return new JsString(s.Value);

57
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;
}
}
}

43
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<string, Func<string>> 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));

2
src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj

@ -15,7 +15,7 @@
<ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-1427" />
<PackageReference Include="Jint" Version="3.0.0-beta-1469" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="NJsonSchema" Version="9.13.17" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />

2
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<string> contributors = new HashSet<string>();
private readonly Dictionary<string, RefToken> userMapping = new Dictionary<string, RefToken>();
private Dictionary<string, string> usersWithEmail = new Dictionary<string, string>();
private Dictionary<string, RefToken> userMapping = new Dictionary<string, RefToken>();
private bool isReserved;
private string appName;

3
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();
}

4
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)
{

4
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));
}

2
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);
}
}

4
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();

2
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)

2
src/Squidex.Infrastructure/States/IStore.cs

@ -12,7 +12,7 @@ namespace Squidex.Infrastructure.States
{
public delegate void HandleEvent(Envelope<IEvent> @event);
public delegate void HandleSnapshot<T>(T state);
public delegate void HandleSnapshot<in T>(T state);
public interface IStore<in TKey>
{

79
src/Squidex/AppServices.cs

@ -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<ContentOptions>(
config.GetSection("contents"));
services.Configure<AssetOptions>(
config.GetSection("assets"));
services.Configure<ReadonlyOptions>(
config.GetSection("mode"));
services.Configure<TwitterOptions>(
config.GetSection("twitter"));
services.Configure<RobotsTxtOptions>(
config.GetSection("robots"));
services.Configure<GCHealthCheckOptions>(
config.GetSection("healthz:gc"));
services.Configure<ETagOptions>(
config.GetSection("etags"));
services.Configure<MyContentsControllerOptions>(
config.GetSection("contentsController"));
services.Configure<MyUrlsOptions>(
config.GetSection("urls"));
services.Configure<MyIdentityOptions>(
config.GetSection("identity"));
services.Configure<MyUIOptions>(
config.GetSection("ui"));
services.Configure<MyUsageOptions>(
config.GetSection("usage"));
}
}
}

1
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;

1
src/Squidex/Config/Domain/EventStoreServices.cs

@ -37,7 +37,6 @@ namespace Squidex.Config.Domain
return new MongoEventStore(mongDatabase, c.GetRequiredService<IEventNotifier>());
})
.As<IInitializable>()
.As<IEventStore>();
},
["GetEventStore"] = () =>

14
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<bool>("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<ILogChannel>();
}
var useColors = config.GetValue<bool>("logging:colors");
if (console == null)
{
console = new ConsoleLogChannel(useColors);
}
services.AddSingletonAs(console)
services.AddSingletonAs(new ConsoleLogChannel(useColors))
.As<ILogChannel>();
services.AddSingletonAs(c => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid()))

87
src/Squidex/Config/Domain/SystemExtensions.cs

@ -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<IInitializable> targets;
private readonly IApplicationLifetime lifetime;
private readonly ISemanticLog log;
public InitializeHostedService(IEnumerable<IInitializable> 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;
}
}
}
}

27
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<DashboardOptions> configurator = null)
{
builder.AddStartupTask<Dashboard>();
builder.ConfigureApplicationParts(appParts =>
appParts
.AddFrameworkPart(typeof(Dashboard).Assembly)
.AddFrameworkPart(typeof(DashboardClient).Assembly));
builder.ConfigureServices(services =>
{
services.AddDashboard(options =>
{
options.HostSelf = false;
});
});
builder.AddIncomingGrainCallFilter<GrainProfiler>();
return builder;
}
}
}

99
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<SiloWrapper>()
.As<IHostedService>()
.AsSelf();
services.Configure<ClusterOptions>(options =>
{
options.Configure();
});
services.Configure<ProcessExitHandlingOptions>(options =>
{
options.FastKillOnProcessExit = false;
});
services.AddServicesForSelfHostedDashboard(null, options =>
{
options.HideTrace = true;
});
services.AddSingletonAs(c => c.GetRequiredService<IClusterClient>())
.As<IClusterClient>();
services.AddHostedService<SiloHost>();
var hostBuilder = new SiloHostBuilder()
.UseDashboardEx()
.EnableDirectClient()
.AddIncomingGrainCallFilter<LocalCacheFilter>()
.AddStartupTask<Bootstrap<IContentSchedulerGrain>>()
.AddStartupTask<Bootstrap<IEventConsumerManagerGrain>>()
.AddStartupTask<Bootstrap<IRuleDequeuerGrain>>()
.AddStartupTask<Bootstrap<IUsageTrackerGrain>>()
.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<ClusterMembershipOptions>(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<SiloWrapper>().Client)
.As<IGrainFactory>();
return provider;
}
}
}

51
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();
}
}
}

42
src/Squidex/Config/Orleans/SiloServices.cs

@ -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;
});
}
});
}
}
}

190
src/Squidex/Config/Orleans/SiloWrapper.cs

@ -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<ISiloHost> 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<IClusterClient>(); }
}
public SiloWrapper(IConfiguration config, ISemanticLog log, IApplicationLifetime lifetime)
{
this.lifetime = lifetime;
this.log = log;
lazySilo = new Lazy<ISiloHost>(() =>
{
var hostBuilder = new SiloHostBuilder()
.UseDashboard(options => options.HostSelf = false)
.EnableDirectClient()
.AddIncomingGrainCallFilter<LocalCacheFilter>()
.AddStartupTask<InitializerStartup>()
.AddStartupTask<Bootstrap<IContentSchedulerGrain>>()
.AddStartupTask<Bootstrap<IEventConsumerManagerGrain>>()
.AddStartupTask<Bootstrap<IRuleDequeuerGrain>>()
.AddStartupTask<Bootstrap<IUsageTrackerGrain>>()
.Configure<ClusterOptions>(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<ProcessExitHandlingOptions>(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<ClusterMembershipOptions>(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);
}
}
}
}

38
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<IInitializable> targets;
public InitializerHost(IEnumerable<IInitializable> 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));
}
}
}
}

31
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();
}
}
}

59
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;
}
}
}

9
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)
{

2
src/Squidex/Squidex.csproj

@ -91,7 +91,7 @@
<PackageReference Include="NSwag.AspNetCore" Version="12.0.13" />
<PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.1" />
<PackageReference Include="OrleansDashboard" Version="2.1.3" />
<PackageReference Include="OrleansDashboard" Version="2.2.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="4.0.9" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.6" />

74
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<SystemExtensions.InitializeHostedService>();
services.AddHostedService<SystemExtensions.MigratorHostedService>();
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<ContentOptions>(
config.GetSection("contents"));
services.Configure<AssetOptions>(
config.GetSection("assets"));
services.Configure<ReadonlyOptions>(
config.GetSection("mode"));
services.Configure<TwitterOptions>(
config.GetSection("twitter"));
services.Configure<RobotsTxtOptions>(
config.GetSection("robots"));
services.Configure<GCHealthCheckOptions>(
config.GetSection("healthz:gc"));
services.Configure<ETagOptions>(
config.GetSection("etags"));
services.Configure<MyContentsControllerOptions>(
config.GetSection("contentsController"));
services.Configure<MyUrlsOptions>(
config.GetSection("urls"));
services.Configure<MyIdentityOptions>(
config.GetSection("identity"));
services.Configure<MyUIOptions>(
config.GetSection("ui"));
services.Configure<MyUsageOptions>(
config.GetSection("usage"));
services.AddHostedService<InitializerHost>();
services.AddHostedService<MigratorHost>();
var provider = services.AddAndBuildOrleans(configuration);
return provider;
}
public void Configure(IApplicationBuilder app)

8
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html

@ -6,7 +6,7 @@
</ng-container>
<ng-container menu>
<button class="btn btn-text-secondary" (click)="reload()" title="Refresh event consumers (CTRL + SHIFT + R)">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh event consumers (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
@ -42,13 +42,13 @@
<span>{{eventConsumer.position}}</span>
</td>
<td class="cell-actions-lg">
<button class="btn btn-text" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer">
<button type="button" class="btn btn-text" (click)="reset(eventConsumer)" *ngIf="!eventConsumer.isResetting" title="Reset Event Consumer">
<i class="icon icon-reset"></i>
</button>
<button class="btn btn-text" (click)="start(eventConsumer)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer">
<button type="button" class="btn btn-text" (click)="start(eventConsumer)" *ngIf="eventConsumer.isStopped" title="Start Event Consumer">
<i class="icon icon-play"></i>
</button>
<button class="btn btn-text" (click)="stop(eventConsumer)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer">
<button type="button" class="btn btn-text" (click)="stop(eventConsumer)" *ngIf="!eventConsumer.isStopped" title="Stop Event Consumer">
<i class="icon icon-pause"></i>
</button>
</td>

19
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() {

28
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() {

20
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() {

10
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -6,7 +6,7 @@
</ng-container>
<ng-container menu>
<button class="btn btn-text-secondary mr-1" (click)="reload()" title="Refresh Users (CTRL + SHIFT + R)">
<button type="button" class="btn btn-text-secondary mr-1" (click)="reload()" title="Refresh Users (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
@ -18,7 +18,7 @@
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" />
</form>
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)">
<button type="button" class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)">
<i class="icon-plus"></i> New
</button>
</ng-container>
@ -61,10 +61,10 @@
</td>
<td class="cell-actions">
<ng-container *ngIf="!userInfo.isCurrentUser">
<button class="btn btn-text" (click)="lock(userInfo.user); $event.stopPropagation();" *ngIf="!userInfo.user.isLocked" title="Lock User">
<button type="button" class="btn btn-text" (click)="lock(userInfo.user)" *ngIf="!userInfo.user.isLocked" title="Lock User">
<i class="icon icon-unlocked"></i>
</button>
<button class="btn btn-text" (click)="unlock(userInfo.user); $event.stopPropagation();" *ngIf="userInfo.user.isLocked" title="Unlock User">
<button type="button" class="btn btn-text" (click)="unlock(userInfo.user)" *ngIf="userInfo.user.isLocked" title="Unlock User">
<i class="icon icon-lock"></i>
</button>
</ng-container>
@ -81,7 +81,7 @@
</div>
<div class="grid-footer">
<sqx-pager [pager]="usersState.usersPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager>
<sqx-pager [pager]="usersState.usersPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager>
</div>
</ng-container>
</sqx-panel>

2
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<EventConsumerDto>;
isLoaded?: false;
isLoaded?: boolean;
}
@Injectable()

2
src/Squidex/app/features/administration/state/users.state.ts

@ -98,7 +98,7 @@ interface Snapshot {
isLoaded?: boolean;
selectedUser?: SnapshotUser;
selectedUser?: SnapshotUser | null;
}
@Injectable()

2
src/Squidex/app/features/api/pages/graphql/graphql-page.component.html

@ -1,5 +1,5 @@
<sqx-title message="{app} | API | GraphQL" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel desiredWidth="*" isFullSize="true">
<sqx-panel desiredWidth="*" minWidth="50rem" isFullSize="true">
<div inner #graphiQLContainer></div>
</sqx-panel>

6
src/Squidex/app/features/assets/pages/assets-filters-page.component.html

@ -14,7 +14,8 @@
</div>
</a>
<a class="sidebar-item" *ngFor="let tag of assetsState.tags | async" (click)="toggleTag(tag.name)" [class.active]="assetsState.isTagSelected(tag.name)">
<a class="sidebar-item" *ngFor="let tag of assetsState.tags | async; trackBy: trackByTag" (click)="toggleTag(tag.name)"
[class.active]="assetsState.isTagSelected(tag.name)">
<div class="row">
<div class="col">
{{tag.name}}
@ -29,7 +30,8 @@
<h3>Saved queries</h3>
<a class="sidebar-item" *ngFor="let query of queries.queries | async" (click)="search(query.filter)" [class.active]="isSelectedQuery(query.filter)">
<a class="sidebar-item" *ngFor="let query of queries.queries | async; trackBy: trackByQuery" (click)="search(query.filter)"
[class.active]="isSelectedQuery(query.filter)">
<a class="sidebar-item-remove float-right" (click)="queries.remove(query.name)">
<i class="icon-close"></i>
</a>

8
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;
}
}

4
src/Squidex/app/features/assets/pages/assets-page.component.html

@ -1,6 +1,6 @@
<sqx-title message="{app} | Assets" parameter1="app" [value1]="appsState.appName"></sqx-title>
<sqx-panel desiredWidth="*" showSidebar="true">
<sqx-panel desiredWidth="*" minWidth="50rem" showSidebar="true">
<ng-container title>
Assets
</ng-container>
@ -10,7 +10,7 @@
<div class="col-auto offset-xl-4">
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<button class="btn btn-text-secondary" (click)="reload()" title="Refresh Contents (CTRL + SHIFT + R)">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh Contents (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
</div>

2
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';

4
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

79
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -1,30 +1,26 @@
<div class="table-items-row" [class.invalid]="isInvalid | async">
<div class="languages-buttons" *ngIf="field.isLocalizable && languages.length > 1">
<button *ngIf="!field.properties.isComplexUI" type="button" class="btn btn-secondary btn-sm btn-text mr-1" (click)="toggleShowAll()">
{{showAllControls ? 'Single Language' : 'All Languages'}}
</button>
<ng-container *ngIf="field.properties.isComplexUI || !showAllControls">
<sqx-language-selector size="sm" #buttonLanguages
[selectedLanguage]="language"
(selectedLanguageChange)="languageChange.emit($event)"
[languages]="languages.values">
</sqx-language-selector>
<sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="topRight" after="120000">
Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip>
</ng-container>
<div class="row no-gutters" [class.compare]="fieldFormCompare">
<div [class.col-12]="!fieldFormCompare" [class.col-6]="fieldFormCompare">
<div class="table-items-row" [class.field-invalid]="isInvalid | async">
<div class="languages-buttons">
<sqx-field-languages
[field]="field"
[language]="language"
(languageChange)="languageChange.emit($event)"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)">
</sqx-field-languages>
</div>
<ng-container *ngIf="showAllControls; else singleControl">
<div class="form-group" *ngFor="let language of languages">
<div class="form-group" *ngFor="let language of languages; trackBy: trackByLanguage">
<sqx-field-editor
[displaySuffix]="'(' + language.iso2Code + ')'"
[displaySuffix]="prefix(language)"
[form]="form"
[field]="field"
[language]="language"
[languages]="languages"
[isCompact]="!!fieldFormCompare"
[control]="fieldForm.controls[language.iso2Code]">
</sqx-field-editor>
</div>
@ -36,7 +32,52 @@
[field]="field"
[language]="language"
[languages]="languages"
[isCompact]="!!fieldFormCompare"
[control]="selectedFormControl">
</sqx-field-editor>
</ng-template>
</div>
</div>
<div class="col-6 col-right" *ngIf="fieldFormCompare">
<button type="button" class="btn btn-primary btn-sm field-copy" (click)="copy()" *ngIf="isDifferent | async">
<i class="icon-arrow_back"></i>
</button>
<div class="table-items-row">
<div class="languages-buttons">
<sqx-field-languages
[field]="field"
[language]="language"
(languageChange)="languageChange.emit($event)"
[languages]="languages"
[showAllControls]="showAllControls"
(showAllControlsChange)="changeShowAllControls($event)">
</sqx-field-languages>
</div>
<ng-container *ngIf="showAllControls; else singleControlCompare">
<div class="form-group" *ngFor="let language of languages; trackBy: trackByLanguage">
<sqx-field-editor
[displaySuffix]="prefix(language)"
[field]="field"
[language]="language"
[languages]="languages"
[isCompact]="true"
[control]="fieldFormCompare?.controls[language.iso2Code]">
</sqx-field-editor>
</div>
</ng-container>
<ng-template #singleControlCompare>
<sqx-field-editor
[field]="field"
[language]="language"
[languages]="languages"
[isCompact]="true"
[control]="selectedFormControlCompare">
</sqx-field-editor>
</ng-template>
</div>
</div>
</div>

37
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;
}

80
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<AppLanguageDto>;
public languages: AppLanguageDto[];
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
public selectedFormControl: AbstractControl;
public selectedFormControlCompare?: AbstractControl;
public showAllControls = false;
public isInvalid: Observable<boolean>;
public isDifferent: Observable<boolean>;
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']();
}
this.selectedFormControl = control;
public prefix(language: AppLanguageDto) {
return `(${language.iso2Code}`;
}
public trackByLanguage(index: number, language: AppLanguageDto) {
return language.iso2Code;
}
private configKey() {

2
src/Squidex/app/features/content/pages/content/content-history-page.component.html

@ -15,7 +15,7 @@
</div>
<div class="event-created">{{event.created | sqxFromNow}}</div>
<a class="event-load force" (click)="loadVersion(event.version)">Load this Version</a>
<a class="event-load force" (click)="loadVersion(event.version)">Load</a> &middot; <a class="event-load force" (click)="compareVersion(event.version)">Compare</a>
</div>
</div>
</ng-container>

6
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) {

5
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -1,7 +1,7 @@
<sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="appsState.appName" [value2]="schema.displayName"></sqx-title>
<form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()">
<sqx-panel desiredWidth="*" [showSidebar]="content">
<sqx-panel desiredWidth="*" minWidth="60rem" [showSidebar]="content">
<ng-container title>
<a class="btn btn-text" (click)="back()" *ngIf="!schema.isSingleton">
<i class="icon-angle-left"></i>
@ -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">
</sqx-content-field>
</div>

83
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 => {
if (schema) {
this.schema = schema!;
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.contentVersionSelectedSubscription =
this.own(
this.messageBus.of(ContentVersionSelected)
.subscribe(message => {
this.loadVersion(message.version);
});
this.loadVersion(message.version, message.compare);
}));
}
public canDeactivate(): Observable<boolean> {
@ -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);
}
}

52
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: `
<ng-container *ngIf="field.isLocalizable && languages.length > 1">
<button *ngIf="!field.properties.isComplexUI" type="button" class="btn btn-text-secondary btn-sm mr-1" (click)="showAllControlsChange.emit(!showAllControls)">
{{showAllControls ? 'Single Language' : 'All Languages'}}
</button>
<ng-container *ngIf="field.properties.isComplexUI || !showAllControls">
<sqx-language-selector size="sm" #buttonLanguages
[selectedLanguage]="language"
(selectedLanguageChange)="languageChange.emit($event)"
[languages]="languages">
</sqx-language-selector>
<sqx-onboarding-tooltip helpId="languages" [for]="buttonLanguages" position="topRight" after="120000">
Please remember to check all languages when you see validation errors.
</sqx-onboarding-tooltip>
</ng-container>
</ng-container>`,
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<AppLanguageDto>();
@Output()
public showAllControlsChange = new EventEmitter<AppLanguageDto>();
}

6
src/Squidex/app/features/content/pages/contents/contents-filters-page.component.html

@ -4,7 +4,8 @@
</ng-container>
<ng-container content>
<a class="sidebar-item" *ngFor="let query of schemaQueries.defaultQueries" (click)="search(query.filter)" [class.active]="isSelectedQuery(query.filter)">
<a class="sidebar-item" *ngFor="let query of schemaQueries.defaultQueries; trackBy: trackByTag" (click)="search(query.filter)"
[class.active]="isSelectedQuery(query.filter)">
{{query.name}}
</a>
@ -13,7 +14,8 @@
<div class="sidebar-section">
<h3>Saved queries</h3>
<a class="sidebar-item" *ngFor="let query of schemaQueries.queries | async" (click)="search(query.filter)" [class.active]="isSelectedQuery(query.filter)">
<a class="sidebar-item" *ngFor="let query of schemaQueries.queries | async; trackByQuery" (click)="search(query.filter)"
[class.active]="isSelectedQuery(query.filter)">
{{query.name}}
<a class="sidebar-item-remove float-right" (click)="schemaQueries.remove(query.name)">

25
src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts

@ -5,13 +5,13 @@
* 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 { onErrorResumeNext } from 'rxjs/operators';
import {
ContentsState,
Queries,
ResourceOwner,
SchemasState,
UIState
} from '@app/shared';
@ -21,9 +21,7 @@ import {
styleUrls: ['./contents-filters-page.component.scss'],
templateUrl: './contents-filters-page.component.html'
})
export class ContentsFiltersPageComponent implements OnDestroy, OnInit {
private selectedSchemaSubscription: Subscription;
export class ContentsFiltersPageComponent extends ResourceOwner implements OnInit {
public schemaQueries: Queries;
constructor(
@ -31,20 +29,17 @@ export class ContentsFiltersPageComponent implements OnDestroy, OnInit {
private readonly schemasState: SchemasState,
private readonly uiState: UIState
) {
}
public ngOnDestroy() {
this.selectedSchemaSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.selectedSchemaSubscription =
this.own(
this.schemasState.selectedSchema
.subscribe(schema => {
if (schema) {
this.schemaQueries = new Queries(this.uiState, `schemas.${schema.name}`);
}
});
}));
}
public search(query: string) {
@ -54,4 +49,12 @@ export class ContentsFiltersPageComponent implements OnDestroy, OnInit {
public isSelectedQuery(query: string) {
return query === this.contentsState.snapshot.contentsQuery || (!query && !this.contentsState.snapshot.contentsQuery);
}
public trackByTag(index: number, tag: { name: string }) {
return tag.name;
}
public trackByQuery(index: number, query: { name: string }) {
return query.name;
}
}

20
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -1,6 +1,6 @@
<sqx-title message="{app} | {schema} | Contents" parameter1="app" parameter2="schema" [value1]="appsState.appName" [value2]="schema.displayName"></sqx-title>
<sqx-panel desiredWidth="*" contentClass="grid" showSidebar="true">
<sqx-panel desiredWidth="*" minWidth="50rem" contentClass="grid" showSidebar="true">
<ng-container title>
<ng-container *ngIf="contentsState.isArchive | async; else noArchive">
Archive
@ -16,7 +16,7 @@
<div class="col-auto offset-xl-4">
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<button class="btn btn-text-secondary" (click)="reload()" title="Refresh Contents (CTRL + SHIFT + R)">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh Contents (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
</div>
@ -33,10 +33,10 @@
</sqx-search-form>
</div>
<div class="col-auto pl-1" *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.values"></sqx-language-selector>
<sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.mutableValues"></sqx-language-selector>
</div>
<div class="col-auto pl-1">
<button class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)">
<button type="button" class="btn btn-success" #newButton routerLink="new" title="New Content (CTRL + SHIFT + G)">
<i class="icon-plus"></i> New
</button>
@ -73,23 +73,23 @@
<div class="selection" *ngIf="selectionCount > 0">
{{selectionCount}} items selected:&nbsp;&nbsp;
<button class="btn btn-secondary mr-1" (click)="publishSelected()" *ngIf="canPublish">
<button type="button" class="btn btn-secondary mr-1" (click)="publishSelected()" *ngIf="canPublish">
Publish
</button>
<button class="btn btn-secondary mr-1" (click)="unpublishSelected()" *ngIf="canUnpublish">
<button type="button" class="btn btn-secondary mr-1" (click)="unpublishSelected()" *ngIf="canUnpublish">
Unpublish
</button>
<button class="btn btn-secondary mr-1" (click)="archiveSelected()" *ngIf="(contentsState.isArchive | async) === false">
<button type="button" class="btn btn-secondary mr-1" (click)="archiveSelected()" *ngIf="(contentsState.isArchive | async) === false">
Archive
</button>
<button class="btn btn-secondary mr-1" (click)="restoreSelected()" *ngIf="contentsState.isArchive | async">
<button type="button" class="btn btn-secondary mr-1" (click)="restoreSelected()" *ngIf="contentsState.isArchive | async">
Restore
</button>
<button class="btn btn-danger"
<button type="button" class="btn btn-danger"
(sqxConfirmClick)="deleteSelected()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the selected content items?">
@ -120,7 +120,7 @@
</div>
<div class="grid-footer">
<sqx-pager [pager]="contentsState.contentsPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager>
<sqx-pager [pager]="contentsState.contentsPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager>
</div>
</ng-container>

31
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;
}

3
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
) {
}
}

4
src/Squidex/app/features/content/shared/array-editor.component.html

@ -5,7 +5,7 @@
<sqx-array-item
[form]="form"
[field]="field"
[isHidden]="isHidden"
[isHidden]="snapshot.isHidden"
[isFirst]="i === 0"
[isLast]="i === arrayControl.controls.length - 1"
[index]="i"
@ -20,7 +20,7 @@
</div>
</div>
<button class="btn btn-success" [disabled]="field.nested.length === 0" (click)="addItem(undefined); $event.preventDefault()">
<button type="button" class="btn btn-success" [disabled]="field.nested.length === 0" (click)="addItem(undefined)">
Add Item
</button>

24
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<State> {
@Input()
public form: EditContentForm;
@ -32,15 +36,19 @@ export class ArrayEditorComponent {
public language: AppLanguageDto;
@Input()
public languages: ImmutableArray<AppLanguageDto>;
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) {

16
src/Squidex/app/features/content/shared/array-item.component.html

@ -5,32 +5,32 @@
<span class="header-text text-decent">Item #{{index + 1}}</span>
<button class="btn btn-text-secondary" [disabled]="isFirst" (click)="moveTop(); $event.preventDefault()">
<button type="button" class="btn btn-text-secondary" [disabled]="isFirst" (click)="moveTop()">
<i class="icon-caret-top"></i>
</button>
<button class="btn btn-text-secondary" [disabled]="isFirst" (click)="moveUp(); $event.preventDefault()">
<button type="button" class="btn btn-text-secondary" [disabled]="isFirst" (click)="moveUp()">
<i class="icon-caret-up"></i>
</button>
<button class="btn btn-text-secondary" [disabled]="isLast" (click)="moveDown(); $event.preventDefault()">
<button type="button" class="btn btn-text-secondary" [disabled]="isLast" (click)="moveDown()">
<i class="icon-caret-down"></i>
</button>
<button class="btn btn-text-secondary" [disabled]="isLast" (click)="moveBottom(); $event.preventDefault()">
<button type="button" class="btn btn-text-secondary" [disabled]="isLast" (click)="moveBottom()">
<i class="icon-caret-bottom"></i>
</button>
<button class="btn btn-text-secondary" [class.hidden]="!isHidden" (click)="toggle.emit(false); $event.preventDefault()" title="Open all items">
<button type="button" class="btn btn-text-secondary" [class.hidden]="!isHidden" (click)="toggle.emit(false)" title="Open all items">
<i class="icon-plus-square"></i>
</button>
<button class="btn btn-text-secondary" [class.hidden]="isHidden" (click)="toggle.emit(true); $event.preventDefault()" title="Close all items">
<button type="button" class="btn btn-text-secondary" [class.hidden]="isHidden" (click)="toggle.emit(true)" title="Close all items">
<i class="icon-minus-square"></i>
</button>
</span>
<span class="float-right">
<button type="button" class="btn btn-text-secondary" (click)="cloning.emit(); $event.preventDefault()">
<button type="button" class="btn btn-text-secondary" (click)="cloning.emit()">
<i class="icon-clone"></i>
</button>
<button type="button" class="btn btn-text-danger" (click)="removing.emit(); $event.preventDefault()">
<button type="button" class="btn btn-text-danger" (click)="removing.emit()">
<i class="icon-bin2"></i>
</button>

7
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<AppLanguageDto>;
public languages: AppLanguageDto[];
public isInvalid: Observable<boolean>;
@ -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']) {

25
src/Squidex/app/features/content/shared/assets-editor.component.html

@ -1,17 +1,18 @@
<div class="assets-container" [class.disabled]="isDisabled" (sqxFileDrop)="addFiles($event)" tabindex="1000">
<div class="assets-container" [class.disabled]="snapshot.isDisabled" (sqxFileDrop)="addFiles($event)" tabindex="1000">
<div class="header list">
<div class="row no-gutters">
<div class="col">
<div class="drop-area align-items-center" (click)="assetsDialog.show()" (sqxFileDrop)="addFiles($event)">
Drop files or click here to add assets.
Drop files or click
</div>
</div>
<div class="col-auto pl-1">
<div class="btn-group">
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="isListView" [disabled]="isListView" (click)="changeView(true)">
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="snapshot.isListView" [disabled]="snapshot.isListView" (click)="changeView(true)">
<i class="icon-list"></i>
</button>
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="!isListView" [disabled]="!isListView" (click)="changeView(false)">
<button type="button" class="btn btn-secondary btn-toggle" [class.btn-primary]="!snapshot.isListView" [disabled]="!snapshot.isListView" (click)="changeView(false)">
<i class="icon-grid"></i>
</button>
</div>
@ -20,12 +21,12 @@
</div>
<div class="body">
<ng-container *ngIf="!isListView; else listTemplate">
<ng-container *ngIf="!snapshot.isListView; else listTemplate">
<div class="row no-gutters">
<sqx-asset *ngFor="let file of newAssets" [initFile]="file"
<sqx-asset *ngFor="let file of snapshot.assetFiles" [initFile]="file"
(failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)">
</sqx-asset>
<sqx-asset *ngFor="let asset of oldAssets; trackBy: trackByAsset" [asset]="asset" removeMode="true"
<sqx-asset *ngFor="let asset of snapshot.assets; trackBy: trackByAsset" [asset]="asset" [isCompact]="isCompact" removeMode="true"
(updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)">
</sqx-asset>
</div>
@ -33,15 +34,17 @@
<ng-template #listTemplate>
<div class="list-view">
<sqx-asset *ngFor="let file of newAssets" [initFile]="file"
<sqx-asset *ngFor="let file of snapshot.assetFiles" [initFile]="file"
[isListView]="true" (failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)">
</sqx-asset>
<div
[sqxSortModel]="oldAssets.values"
[sqxSortModel]="snapshot.assets.mutableValues"
(sqxSorted)="sortAssets($event)">
<div *ngFor="let asset of oldAssets; trackBy: trackByAsset">
<sqx-asset [asset]="asset" removeMode="true" [isListView]="true"
<div *ngFor="let asset of snapshot.assets; trackBy: trackByAsset">
<sqx-asset [asset]="asset" removeMode="true"
[isListView]="true"
[isCompact]="isCompact"
(updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)">
</sqx-asset>
</div>

6
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;
}

99
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<File>;
assets: ImmutableArray<AssetDto>;
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<State, string[]> implements OnInit {
public assetsDialog = new DialogModel();
public newAssets = ImmutableArray.empty<File>();
public oldAssets = ImmutableArray.empty<AssetDto>();
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<AssetDto>) {
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<AssetDto>) {
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) {

69
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: `
<div [formGroup]="form">
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>
<div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify" />
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>
<div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Toggle'">
<sqx-toggle [formControlName]="field.name" [threeStates]="!field.properties.isRequired"></sqx-toggle>
</div>
<div *ngSwitchCase="'Checkbox'">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControlName]="field.name" sqxIndeterminateValue />
</div>
</div>
</div>
</div>
</div>
</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ContentItemEditorComponent {
@Input()
public field: FieldDto;
@Input()
public form: FormGroup;
}

105
src/Squidex/app/features/content/shared/content-item.component.html

@ -1,65 +1,26 @@
<td class="cell-select" *ngIf="!isReference" (click)="shouldStop($event)">
<td class="cell-select" (click)="stop($event)">
<ng-container *ngIf="!isReference; else referenceTemplate">
<input type="checkbox" class="form-check"
[ngModel]="selected"
(ngModelChange)="selectedChange.emit($event);"
(click)="$event.stopPropagation()" />
</td>
(ngModelChange)="selectedChange.emit($event)" />
</ng-container>
<td class="cell-select" *ngIf="isReference" (click)="shouldStop($event)">
<ng-template #referenceTemplate>
<i class="icon-drag2 drag-handle"></i>
</ng-template>
</td>
<td class="cell-auto" *ngFor="let field of schema.listFields; let i = index" (click)="shouldStop($event)">
<div *ngIf="field.isInlineEditable && !isReadOnly" [formGroup]="patchForm.form" (click)="$event.stopPropagation()">
<div [ngSwitch]="field.properties.fieldType">
<div *ngSwitchCase="'Number'">
<div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="number" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>
<div *ngSwitchCase="'String'">
<div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Input'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" />
</div>
<div *ngSwitchCase="'Slug'">
<input class="form-control" type="text" [formControlName]="field.name" [placeholder]="field.displayPlaceholder" sqxTransformInput="Slugify" />
</div>
<div *ngSwitchCase="'Dropdown'">
<select class="form-control" [formControlName]="field.name">
<option [ngValue]="null"></option>
<option *ngFor="let value of field.properties['allowedValues']" [ngValue]="value">{{value}}</option>
</select>
</div>
</div>
</div>
<div *ngSwitchCase="'Boolean'">
<div [ngSwitch]="field.properties['editor']">
<div *ngSwitchCase="'Toggle'">
<sqx-toggle [formControlName]="field.name" [threeStates]="!field.properties.isRequired"></sqx-toggle>
</div>
<div *ngSwitchCase="'Checkbox'">
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" [formControlName]="field.name" sqxIndeterminateValue />
</div>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="!field.isInlineEditable || isReadOnly" class="truncate">
{{values[i]}}
</div>
<td class="cell-auto" *ngFor="let field of schema.listFields; let i = index" (click)="shouldStop($event, field)">
<ng-container *ngIf="field.isInlineEditable && !isReadOnly; else displayTemplate">
<sqx-content-item-editor [form]="patchForm.form" [field]="field"></sqx-content-item-editor>
</ng-container>
<ng-template #displayTemplate>
<span class="truncate">{{values[i]}}</span>
</ng-template>
</td>
<td class="cell-time" (click)="shouldStop($event)">
<td class="cell-time" *ngIf="!isCompact" (click)="shouldStop($event)">
<sqx-content-status
[status]="content.status"
[scheduledTo]="content.scheduleJob?.status"
@ -70,40 +31,42 @@
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td>
<td class="cell-user" *ngIf="patchForm.form.dirty" (click)="shouldStop($event)">
<button type="button" class="btn btn-success" (click)="save(); $event.stopPropagation()">
<td class="cell-user" *ngIf="!isCompact && patchForm.form.pristine" (click)="shouldStop($event)">
<img class="user-picture" [attr.title]="content.lastModifiedBy | sqxUserNameRef" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td>
<ng-container *ngIf="patchForm.form.dirty">
<td class="cell-user" (click)="stop($event)">
<button type="button" class="btn btn-success" (click)="save()">
<i class="icon-checkmark"></i>
</button>
</td>
<td class="cell-actions" *ngIf="patchForm.form.dirty" (click)="shouldStop($event)">
<button type="button" class="btn btn-text-secondary btn-cancel" (click)="cancel(); $event.stopPropagation()">
<td class="cell-actions" (click)="stop($event)">
<button type="button" class="btn btn-text-secondary btn-cancel" (click)="cancel()">
<i class="icon-close"></i>
</button>
</td>
</ng-container>
<td class="cell-user" *ngIf="patchForm.form.pristine" (click)="shouldStop($event)">
<img class="user-picture" [attr.title]="content.lastModifiedBy | sqxUserNameRef" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td>
<td class="cell-actions" *ngIf="!isReadOnly && patchForm.form.pristine" (click)="shouldStop($event)">
<td class="cell-actions" *ngIf="!isReadOnly && patchForm.form.pristine" (click)="stop($event)">
<div class="dropdown dropdown-options" *ngIf="content">
<button type="button" class="btn btn-text-secondary" (click)="dropdown.toggle(); $event.stopPropagation()" [class.active]="dropdown.isOpen | async" #optionsButton>
<button type="button" class="btn btn-text-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #optionsButton>
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade>
<a class="dropdown-item" (click)="publishing.emit(); $event.stopPropagation()" *ngIf="content.status === 'Draft'">
<a class="dropdown-item" (click)="publishing.emit()" *ngIf="content.status === 'Draft'">
Publish
</a>
<a class="dropdown-item" (click)="unpublishing.emit(); $event.stopPropagation()" *ngIf="content.status === 'Published'">
<a class="dropdown-item" (click)="unpublishing.emit()" *ngIf="content.status === 'Published'">
Unpublish
</a>
<a class="dropdown-item" (click)="archiving.emit(); $event.stopPropagation()" *ngIf="content.status !== 'Archived'">
<a class="dropdown-item" (click)="archiving.emit()" *ngIf="content.status !== 'Archived'">
Archive
</a>
<a class="dropdown-item" (click)="restoring.emit(); $event.stopPropagation()" *ngIf="content.status === 'Archived'">
<a class="dropdown-item" (click)="restoring.emit()" *ngIf="content.status === 'Archived'">
Restore
</a>
<a class="dropdown-item" (click)="cloning.emit(); dropdown.hide(); $event.stopPropagation()" *ngIf="content.status !== 'Archived'">
<a class="dropdown-item" (click)="cloning.emit(); dropdown.hide()" *ngIf="content.status !== 'Archived'">
Clone
</a>
@ -119,7 +82,7 @@
</div>
</td>
<td class="cell-actions" *ngIf="isReference" (click)="shouldStop($event)">
<button type="button" class="btn btn-text-secondary" (click)="deleting.emit(); $event.stopPropagation()">
<button type="button" class="btn btn-text-secondary" (click)="deleting.emit()">
<i class="icon-close"></i>
</button>
</td>

13
src/Squidex/app/features/content/shared/content-item.component.ts

@ -12,6 +12,7 @@ import {
ContentDto,
ContentsState,
fadeAnimation,
FieldDto,
FieldFormatter,
fieldInvariant,
ModalModel,
@ -69,6 +70,9 @@ export class ContentItemComponent implements OnChanges {
@Input()
public isReference = false;
@Input()
public isCompact = false;
@Input('sqxContent')
public content: ContentDto;
@ -93,13 +97,18 @@ export class ContentItemComponent implements OnChanges {
}
}
public shouldStop(event: Event) {
if (this.patchForm.form.dirty) {
public shouldStop(event: Event, field?: FieldDto) {
if (this.patchForm.form.dirty || (field && field.isInlineEditable)) {
event.stopPropagation();
event.stopImmediatePropagation();
}
}
public stop(event: Event) {
event.stopPropagation();
event.stopImmediatePropagation();
}
public save() {
const value = this.patchForm.submit();

6
src/Squidex/app/features/content/shared/contents-selector.component.html

@ -6,7 +6,7 @@
<ng-container tabs>
<div class="row no-gutters">
<div class="col-auto offset-lg-4">
<button class="btn btn-text-secondary" (click)="reload()">
<button type="button" class="btn btn-text-secondary" (click)="reload()">
<i class="icon-reset"></i> Refresh
</button>
</div>
@ -19,7 +19,7 @@
</div>
<div class="col pl-1" *ngIf="languages.length > 1">
<sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages.values"></sqx-language-selector>
<sqx-language-selector class="languages-buttons" (selectedLanguageChange)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</div>
</div>
</ng-container>
@ -63,7 +63,7 @@
</div>
<div class="grid-footer">
<sqx-pager [pager]="contentsState.contentsPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager>
<sqx-pager [pager]="contentsState.contentsPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager>
</div>
</ng-container>

2
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;
}
}

7
src/Squidex/app/features/content/shared/field-editor.component.html

@ -5,7 +5,7 @@
<span class="field-disabled pl-1" *ngIf="field.isDisabled">Disabled</span>
<sqx-control-errors [for]="control" [fieldName]="field.displayName" [submitted]="form.submitted | async"></sqx-control-errors>
<sqx-control-errors *ngIf="form" [for]="control" [fieldName]="field.displayName" [submitted]="form.submitted | async"></sqx-control-errors>
<div>
<ng-container *ngIf="field.properties.editorUrl; else noEditor">
@ -101,7 +101,7 @@
<sqx-json-editor [formControl]="control"></sqx-json-editor>
</ng-container>
<ng-container *ngSwitchCase="'Assets'">
<sqx-assets-editor [formControl]="control"></sqx-assets-editor>
<sqx-assets-editor [formControl]="control" [isCompact]="isCompact"></sqx-assets-editor>
</ng-container>
<ng-container *ngSwitchCase="'Tags'">
<ng-container [ngSwitch]="field.properties['editor']">
@ -132,7 +132,8 @@
[formControl]="control"
[language]="language"
[languages]="languages"
[schemaId]="field.properties['schemaId']">
[schemaId]="field.properties['schemaId']"
[isCompact]="isCompact">
</sqx-references-editor>
</ng-container>
</ng-container>

8
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<AppLanguageDto>;
public languages: AppLanguageDto[];
@Input()
public isCompact = false;
@Input()
public displaySuffix: string;

10
src/Squidex/app/features/content/shared/preview-button.component.html

@ -1,16 +1,16 @@
<ng-container *ngIf="selectedName">
<ng-container *ngIf="snapshot.selectedName">
<span>Preview: </span>
<div class="btn-group ml-1" #buttonGroup>
<button type="button" class="btn btn-secondary" (click)="follow(selectedName)">
<i class="icon-external-link"></i> {{selectedName}}
<button type="button" class="btn btn-secondary" (click)="follow(snapshot.selectedName)">
<i class="icon-external-link"></i> {{snapshot.selectedName}}
</button>
<div class="btn-group" *ngIf="alternativeNames.length > 0">
<div class="btn-group" *ngIf="snapshot.alternativeNames.length > 0">
<button type="button" class="btn btn-secondary dropdown-toggle" (click)="dropdown.toggle()"></button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="buttonGroup" @fade>
<a *ngFor="let name of alternativeNames" class="dropdown-item" (click)="follow(name)">{{name}}</a>
<a *ngFor="let name of snapshot.alternativeNames" class="dropdown-item" (click)="follow(name)">{{name}}</a>
</div>
</div>
</div>

43
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<State> 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() {

17
src/Squidex/app/features/content/shared/references-editor.component.html

@ -1,27 +1,28 @@
<div class="references-container" [class.disabled]="isDisabled">
<ng-container *ngIf="schema">
<div class="references-container" [class.disabled]="snapshot.isDisabled">
<ng-container *ngIf="snapshot.schema">
<div class="drop-area-container">
<div class="drop-area" (click)="selectorDialog.show()">
Click here to link content items.
</div>
</div>
<table class="table table-items table-fixed" [class.disabled]="isDisabled" *ngIf="schema && contentItems && contentItems.length > 0"
[sqxSortModel]="contentItems.values"
<table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="snapshot.schema && snapshot.contentItems && snapshot.contentItems.length > 0"
[sqxSortModel]="snapshot.contentItems.mutableValues"
(sqxSorted)="sort($event)">
<tbody *ngFor="let content of contentItems">
<tbody *ngFor="let content of snapshot.contentItems">
<tr [sqxContent]="content"
[language]="language"
[isReadOnly]="true"
[isReference]="true"
[schema]="schema"
[isCompact]="isCompact"
[schema]="snapshot.schema"
(deleting)="remove(content)"></tr>
<tr class="spacer"></tr>
</tbody>
</table>
</ng-container>
<div class="invalid" *ngIf="isInvalidSchema">
<div class="invalid" *ngIf="snapshot.schemaInvalid">
Schema not found or not configured yet.
</div>
</div>
@ -30,7 +31,7 @@
<sqx-contents-selector
[language]="language"
[languages]="languages"
[schema]="schema"
[schema]="snapshot.schema"
(selected)="select($event)">
</sqx-contents-selector>
</ng-container>

81
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<ContentDto>;
}
@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<State, string[]> implements OnInit {
@Input()
public schemaId: string;
@ -43,53 +48,49 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
public language: AppLanguageDto;
@Input()
public languages: ImmutableArray<AppLanguageDto>;
public selectorDialog = new DialogModel();
public languages: AppLanguageDto[];
public schema: SchemaDetailsDto;
public contentItems = ImmutableArray.empty<ContentDto>();
@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<ContentDto>) {
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<ContentDto>) {
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();
}
}

27
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 => {

8
src/Squidex/app/features/rules/pages/events/rule-events-page.component.html

@ -6,7 +6,7 @@
</ng-container>
<ng-container menu>
<button class="btn btn-text-secondary" (click)="reload()" title="Refresh Events (CTRL + SHIFT + R)">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh Events (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
@ -70,11 +70,11 @@
Next: <ng-container *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</ng-container>
</div>
<div class="col-3 text-right">
<button class="btn btn-outline-danger btn-sm mr-1" (click)="cancel(event)" [class.hidden]="!event.nextAttempt">
<button type="button" class="btn btn-outline-danger btn-sm mr-1" (click)="cancel(event)" [class.hidden]="!event.nextAttempt">
Cancel
</button>
<button class="btn btn-success btn-sm" (click)="enqueue(event)">
<button type="button" class="btn btn-success btn-sm" (click)="enqueue(event)">
Enqueue
</button>
</div>
@ -90,7 +90,7 @@
</tbody>
</table>
<sqx-pager [pager]="ruleEventsState.ruleEventsPager | async" (prev)="goPrev()" (next)="goNext()"></sqx-pager>
<sqx-pager [pager]="ruleEventsState.ruleEventsPager | async" (prevPage)="goPrev()" (nextPage)="goNext()"></sqx-pager>
</ng-container>
</sqx-panel>

2
src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html

@ -2,7 +2,7 @@
<div class="form-group row">
<div class="col-9 offset-3">
<ng-container *ngIf="!isRedirected">
<button class="btn btn-twitter" [disabled]="isAuthenticating" (click)="auth()">
<button type="button" class="btn btn-twitter" [disabled]="isAuthenticating" (click)="auth()">
Request access token with twitter
</button>
</ng-container>

6
src/Squidex/app/features/rules/pages/rules/rules-page.component.html

@ -6,14 +6,14 @@
</ng-container>
<ng-container menu>
<button class="btn btn-text-secondary mr-1" (click)="reload()" title="Refresh Assets (CTRL + SHIFT + R)">
<button type="button" class="btn btn-text-secondary mr-1" (click)="reload()" title="Refresh Assets (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut>
<button class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)">
<button type="button" class="btn btn-success" #buttonNew (click)="createNew()" title="New Rule (CTRL + M)">
<i class="icon-plus"></i> New
</button>
</ng-container>
@ -24,7 +24,7 @@
<div class="table-items-row table-items-row-empty" *ngIf="rules.length === 0">
No Rule created yet.
<button class="btn btn-success btn-sm ml-2" (click)="createNew()">
<button type="button" class="btn btn-success btn-sm ml-2" (click)="createNew()">
<i class="icon icon-plus"></i> Add Rule
</button>
</div>

10
src/Squidex/app/features/schemas/pages/schema/field-wizard.component.html

@ -94,14 +94,14 @@
<button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button>
<div class="float-right" *ngIf="!isEditing">
<button class="btn btn-outline-success mr-1" (click)="addField(false, false)">Create and close</button>
<button class="btn btn-success mr-1" (click)="addField(true, false)">Create and add field</button>
<button class="btn btn-success" (click)="addField(false, true)">Create and edit field</button>
<button type="button" class="btn btn-outline-success mr-1" (click)="addField(false, false)">Create and close</button>
<button type="button" class="btn btn-success mr-1" (click)="addField(true, false)">Create and add field</button>
<button type="button" class="btn btn-success" (click)="addField(false, true)">Create and edit field</button>
</div>
<div class="float-right" *ngIf="isEditing">
<button class="btn btn-success mr-1" (click)="save(true)">Save and add field</button>
<button class="btn btn-primary" (click)="save()">Save and close</button>
<button type="button" class="btn btn-success mr-1" (click)="save(true)">Save and add field</button>
<button type="button" class="btn btn-primary" (click)="save()">Save and close</button>
</div>
</ng-container>
</sqx-modal-dialog>

4
src/Squidex/app/features/schemas/pages/schema/field.component.html

@ -28,7 +28,7 @@
<button type="button" class="btn btn-text-secondary ml-1" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #optionsButton>
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="dropdown" [sqxModalTarget]="optionsButton" @fade>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade>
<a class="dropdown-item" (click)="enableField()" *ngIf="field.isDisabled">
Enable in UI
</a>
@ -118,7 +118,7 @@
<div class="nested-field nested-field-add">
<span class="nested-field-line-h"></span>
<button class="btn btn-success btn-sm" (click)="addFieldDialog.show()">
<button type="button" class="btn btn-success btn-sm" (click)="addFieldDialog.show()">
<i class="icon icon-plus"></i> Add Nested Field
</button>
</div>

6
src/Squidex/app/features/schemas/pages/schema/schema-page.component.html

@ -23,7 +23,7 @@
<button type="button" class="btn btn-text-secondary ml-1" (click)="editOptionsDropdown.toggle()" [class.active]="editOptionsDropdown.isOpen | async" #buttonOptions>
<i class="icon-dots"></i>
</button>
<div class="dropdown-menu" *sqxModalView="editOptionsDropdown" [sqxModalTarget]="buttonOptions" @fade>
<div class="dropdown-menu" *sqxModalView="editOptionsDropdown;closeAlways:true" [sqxModalTarget]="buttonOptions" @fade>
<a class="dropdown-item" (click)="configureScriptsDialog.show()">
Edit Scripts
</a>
@ -61,7 +61,7 @@
<div class="table-items-row table-items-row-empty" *ngIf="schema && schema.fields.length === 0">
No field created yet.
<button class="btn btn-success btn-sm ml-2" (click)="addFieldDialog.show()">
<button type="button" class="btn btn-success btn-sm ml-2" (click)="addFieldDialog.show()">
<i class="icon icon-plus"></i> Add Field
</button>
</div>
@ -75,7 +75,7 @@
</div>
</div>
<button class="btn btn-success field-button" (click)="addFieldDialog.show()">
<button type="button" class="btn btn-success field-button" (click)="addFieldDialog.show()">
<i class="icon icon-plus field-button-icon"></i> <div class="field-button-text">Add Field</div>
</button>
</ng-container>

28
src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts

@ -7,10 +7,9 @@
// tslint:disable:no-shadowed-variable
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { filter, onErrorResumeNext } from 'rxjs/operators';
import { onErrorResumeNext } from 'rxjs/operators';
import {
AppsState,
@ -21,6 +20,7 @@ import {
MessageBus,
ModalModel,
PatternsState,
ResourceOwner,
SchemaDetailsDto,
SchemasState,
Types
@ -38,9 +38,7 @@ import {
fadeAnimation
]
})
export class SchemaPageComponent implements OnDestroy, OnInit {
private selectedSchemaSubscription: Subscription;
export class SchemaPageComponent extends ResourceOwner implements OnInit {
public fieldTypes = fieldTypes;
public schemaExport: any;
@ -64,22 +62,21 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
private readonly router: Router,
private readonly messageBus: MessageBus
) {
}
public ngOnDestroy() {
this.selectedSchemaSubscription.unsubscribe();
super();
}
public ngOnInit() {
this.patternsState.load().pipe(onErrorResumeNext()).subscribe();
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.export();
});
}
}));
}
public publish() {
@ -95,8 +92,7 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
}
public trackByField(index: number, field: FieldDto) {
const a = this;
return field.fieldId + a.schema.id;
return field.fieldId + this.schema.id;
}
public deleteSchema() {

10
src/Squidex/app/features/schemas/pages/schema/types/boolean-validation.component.ts

@ -8,9 +8,12 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { BooleanFieldPropertiesDto, FieldDto } from '@app/shared';
import {
BooleanFieldPropertiesDto,
FieldDto,
hasNoValue$
} from '@app/shared';
@Component({
selector: 'sqx-boolean-validation',
@ -37,7 +40,6 @@ export class BooleanValidationComponent implements OnInit {
new FormControl(this.properties.inlineEditable));
this.showDefaultValue =
this.editForm.controls['isRequired'].valueChanges.pipe(
startWith(this.properties.isRequired), map(x => !x));
hasNoValue$(this.editForm.controls['isRequired']);
}
}

14
src/Squidex/app/features/schemas/pages/schema/types/date-time-validation.component.ts

@ -8,9 +8,13 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { DateTimeFieldPropertiesDto, FieldDto, ValidatorsEx } from '@app/shared';
import {
DateTimeFieldPropertiesDto,
FieldDto,
hasNoValue$,
ValidatorsEx
} from '@app/shared';
@Component({
selector: 'sqx-date-time-validation',
@ -52,11 +56,9 @@ export class DateTimeValidationComponent implements OnInit {
]));
this.showDefaultValues =
this.editForm.controls['isRequired'].valueChanges.pipe(
startWith(this.properties.isRequired), map(x => !x));
hasNoValue$(this.editForm.controls['isRequired']);
this.showDefaultValue =
this.editForm.controls['calculatedDefaultValue'].valueChanges.pipe(
startWith(this.properties.calculatedDefaultValue), map(x => !x));
hasNoValue$(this.editForm.controls['calculatedDefaultValue']);
}
}

38
src/Squidex/app/features/schemas/pages/schema/types/number-ui.component.ts

@ -5,22 +5,25 @@
* 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, Validators } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { FieldDto, FloatConverter, NumberFieldPropertiesDto } from '@app/shared';
import {
FieldDto,
FloatConverter,
NumberFieldPropertiesDto,
ResourceOwner,
value$
} from '@app/shared';
@Component({
selector: 'sqx-number-ui',
styleUrls: ['number-ui.component.scss'],
templateUrl: 'number-ui.component.html'
})
export class NumberUIComponent implements OnDestroy, OnInit {
private hideAllowedValuesSubscription: Subscription;
private hideInlineEditableSubscription: Subscription;
export class NumberUIComponent extends ResourceOwner implements OnInit {
@Input()
public editForm: FormGroup;
@ -35,11 +38,6 @@ export class NumberUIComponent implements OnDestroy, OnInit {
public hideAllowedValues: Observable<boolean>;
public hideInlineEditable: Observable<boolean>;
public ngOnDestroy() {
this.hideAllowedValuesSubscription.unsubscribe();
this.hideInlineEditableSubscription.unsubscribe();
}
public ngOnInit() {
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
@ -53,25 +51,23 @@ export class NumberUIComponent implements OnDestroy, OnInit {
new FormControl(this.properties.inlineEditable));
this.hideAllowedValues =
this.editForm.controls['editor'].valueChanges.pipe(
startWith(this.properties.editor), map(x => !(x && (x === 'Radio' || x === 'Dropdown'))));
value$<string>(this.editForm.controls['editor']).pipe(map(x => !(x && (x === 'Radio' || x === 'Dropdown'))));
this.hideInlineEditable =
this.editForm.controls['editor'].valueChanges.pipe(
startWith(this.properties.editor), map(x => !(x && (x === 'Input' || x === 'Dropdown'))));
value$<string>(this.editForm.controls['editor']).pipe(map(x => !(x === 'Input' || x === 'Dropdown')));
this.hideAllowedValuesSubscription =
this.own(
this.hideAllowedValues.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['allowedValues'].setValue(undefined);
}
});
}));
this.hideInlineEditableSubscription =
this.own(
this.hideInlineEditable.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['inlineEditable'].setValue(false);
}
});
}));
}
}

5
src/Squidex/app/features/schemas/pages/schema/types/number-validation.component.ts

@ -8,10 +8,10 @@
import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import {
FieldDto,
hasNoValue$,
NumberFieldPropertiesDto,
RootFieldDto,
Types
@ -54,7 +54,6 @@ export class NumberValidationComponent implements 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']);
}
}

37
src/Squidex/app/features/schemas/pages/schema/types/string-ui.component.ts

@ -5,22 +5,24 @@
* 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, Validators } from '@angular/forms';
import { Observable, Subscription } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { FieldDto, StringFieldPropertiesDto } from '@app/shared';
import {
FieldDto,
ResourceOwner,
StringFieldPropertiesDto,
value$
} from '@app/shared';
@Component({
selector: 'sqx-string-ui',
styleUrls: ['string-ui.component.scss'],
templateUrl: 'string-ui.component.html'
})
export class StringUIComponent implements OnDestroy, OnInit {
private hideAllowedValuesSubscription: Subscription;
private hideInlineEditableSubscription: Subscription;
export class StringUIComponent extends ResourceOwner implements OnInit {
@Input()
public editForm: FormGroup;
@ -33,11 +35,6 @@ export class StringUIComponent implements OnDestroy, OnInit {
public hideAllowedValues: Observable<boolean>;
public hideInlineEditable: Observable<boolean>;
public ngOnDestroy() {
this.hideAllowedValuesSubscription.unsubscribe();
this.hideInlineEditableSubscription.unsubscribe();
}
public ngOnInit() {
this.editForm.setControl('editor',
new FormControl(this.properties.editor, [
@ -51,25 +48,23 @@ export class StringUIComponent implements OnDestroy, OnInit {
new FormControl(this.properties.inlineEditable));
this.hideAllowedValues =
this.editForm.controls['editor'].valueChanges.pipe(
startWith(this.properties.editor), map(x => !(x && (x === 'Radio' || x === 'Dropdown'))));
value$<string>(this.editForm.controls['editor']).pipe(map(x => !(x && (x === 'Radio' || x === 'Dropdown'))));
this.hideInlineEditable =
this.editForm.controls['editor'].valueChanges.pipe(
startWith(this.properties.editor), map(x => !(x && (x === 'Input' || x === 'Dropdown' || x === 'Slug'))));
value$<string>(this.editForm.controls['editor']).pipe(map(x => !(x && (x === 'Input' || x === 'Dropdown' || x === 'Slug'))));
this.hideAllowedValuesSubscription =
this.own(
this.hideAllowedValues.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['allowedValues'].setValue(undefined);
}
});
}));
this.hideInlineEditableSubscription =
this.own(
this.hideInlineEditable.subscribe(isSelection => {
if (isSelection) {
this.editForm.controls['inlineEditable'].setValue(false);
}
});
}));
}
}

2
src/Squidex/app/features/schemas/pages/schema/types/string-validation.component.html

@ -53,7 +53,7 @@
</div>
</div>
</div>
<small class="col-3" style="align-self: center;">
<small class="col-3" style="align-self: center">
{{patternName}}
</small>
</div>

26
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();
}

2
src/Squidex/app/features/schemas/pages/schemas/schemas-page.component.html

@ -9,7 +9,7 @@
<sqx-shortcut keys="ctrl+shift+g" (trigger)="addSchemaDialog.show()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<button class="btn btn-success subheader-button" (click)="createSchema()" title="New Schema (CTRL + SHIFT + G)">
<button type="button" class="btn btn-success subheader-button" (click)="createSchema()" title="New Schema (CTRL + SHIFT + G)">
<i class="icon-plus"></i>
</button>

20
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.own(
this.route.params.pipe(map(q => q['showDialog']))
.subscribe(showDialog => {
if (showDialog) {
this.addSchemaDialog.show();
}
});
}));
this.schemasState.load().pipe(onErrorResumeNext()).subscribe();
}

6
src/Squidex/app/features/settings/pages/backups/backups-page.component.html

@ -6,13 +6,13 @@
</ng-container>
<ng-container menu>
<button class="btn btn-text-secondary mr-1" (click)="reload()" title="Refresh backups (CTRL + SHIFT + R)">
<button type="button" class="btn btn-text-secondary mr-1" (click)="reload()" title="Refresh backups (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<button class="btn btn-success" [disabled]="backupsState.maxBackupsReached | async" (click)="start()">
<button type="button" class="btn btn-success" [disabled]="backupsState.maxBackupsReached | async" (click)="start()">
Start Backup
</button>
</ng-container>
@ -27,7 +27,7 @@
<div class="table-items-row table-items-row-empty" *ngIf="backups.length === 0">
No backups created yet.
<button class="btn btn-success btn-sm ml-2" (click)="start()">
<button type="button" class="btn btn-success btn-sm ml-2" (click)="start()">
Start Backup
</button>
</div>

22
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() {

4
src/Squidex/app/features/settings/pages/clients/client.component.html

@ -11,7 +11,7 @@
<button type="submit" class="btn btn-primary" [disabled]="!renameForm.form.valid || !renameForm.form.dirty">Save</button>
<button class="btn btn-text-secondary btn-cancel" (click)="toggleRename()">
<button type="button" class="btn btn-text-secondary btn-cancel" (click)="toggleRename()">
<i class="icon-close"></i>
</button>
</form>
@ -25,7 +25,7 @@
</ng-container>
</div>
<div class="col-auto">
<button class="btn btn-primary" (click)="connect()">Connect</button>
<button type="button" class="btn btn-primary" (click)="connect()">Connect</button>
</div>
<div class="col-auto cell-actions">
<button type="button" class="btn btn-text-danger"

2
src/Squidex/app/features/settings/pages/clients/clients-page.component.html

@ -6,7 +6,7 @@
</ng-container>
<ng-container menu>
<button class="btn btn-text-secondary" (click)="reload()" title="Refresh clients (CTRL + SHIFT + R)">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh clients (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>

2
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -6,7 +6,7 @@
</ng-container>
<ng-container menu>
<button class="btn btn-text-secondary" (click)="reload()" title="Refresh contributors (CTRL + SHIFT + R)">
<button type="button" class="btn btn-text-secondary" (click)="reload()" title="Refresh contributors (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>

4
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<any[]> {
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) {

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save