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. 117
      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. 82
      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. 89
      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. 119
      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. 9
      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. 30
      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. 32
      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> <ItemGroup>
<PackageReference Include="Algolia.Search" Version="5.3.1" /> <PackageReference Include="Algolia.Search" Version="5.3.1" />
<PackageReference Include="CoreTweet" Version="1.0.0.483" /> <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.Extensions.Http" Version="2.2.0" />
<PackageReference Include="Microsoft.OData.Core" Version="7.5.3" /> <PackageReference Include="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> <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) 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) 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)); 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) 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(); var id = sourceField.NamedId();
@ -184,7 +184,7 @@ namespace Squidex.Domain.Apps.Core.EventSynchronization
if ((sourceField == null || sourceField is IArrayField) && targetField is IArrayField targetArrayField) 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); 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; return null;
} }
if (!(@event.Payload is AppEvent appEvent)) if (!(@event.Payload is AppEvent))
{ {
return null; 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) bool IRuleTriggerHandler.Trigger(EnrichedEvent @event, RuleTrigger trigger)
{ {
var typed = @event as TEnrichedEvent; if (@event is TEnrichedEvent typed)
if (typed != null)
{ {
return Trigger(typed, (TTrigger)trigger); return Trigger(typed, (TTrigger)trigger);
} }
@ -45,9 +43,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules
bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId) bool IRuleTriggerHandler.Trigger(AppEvent @event, RuleTrigger trigger, Guid ruleId)
{ {
var typed = @event as TEvent; if (@event is TEvent typed)
if (typed != null)
{ {
return Trigger(typed, (TTrigger)trigger, ruleId); 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) switch (value)
{ {
case JsonNull n: case JsonNull _:
return JsValue.Null; return JsValue.Null;
case JsonScalar<string> s: case JsonScalar<string> s:
return new JsString(s.Value); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Security.Claims;
using Jint; using Jint;
using Jint.Native; using Jint.Native;
using Jint.Native.Date; using Jint.Native.Date;
using Jint.Native.Object; using Jint.Native.Object;
using Jint.Runtime; using Jint.Runtime;
using Jint.Runtime.Interop; using Jint.Runtime.Interop;
using NodaTime;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting.ContentWrapper; using Squidex.Domain.Apps.Core.Scripting.ContentWrapper;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Core.Scripting 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 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) public void Execute(ScriptContext context, string script)
{ {
Guard.NotNull(context, nameof(context)); 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) private Engine CreateScriptEngine(IReferenceResolver resolver = null, Dictionary<string, Func<string>> customFormatters = null)
{ {
var converter = new Converter();
var engine = new Engine(options => var engine = new Engine(options =>
{ {
if (resolver != null) if (resolver != null)
@ -207,7 +168,7 @@ namespace Squidex.Domain.Apps.Core.Scripting
options.SetReferencesResolver(resolver); options.SetReferencesResolver(resolver);
} }
options.TimeoutInterval(Timeout).Strict().AddObjectConverter(converter); options.TimeoutInterval(Timeout).Strict().AddObjectConverter(DefaultConverter.Instance);
}); });
if (customFormatters != null) 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("slugify", new ClrFunctionInstance(engine, "slugify", Slugify));
engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate)); engine.SetValue("formatTime", new ClrFunctionInstance(engine, "formatTime", FormatDate));
engine.SetValue("formatDate", new ClrFunctionInstance(engine, "formatDate", 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" /> <ProjectReference Include="..\Squidex.Shared\Squidex.Shared.csproj" />
</ItemGroup> </ItemGroup>
<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="Microsoft.OData.Core" Version="7.5.3" />
<PackageReference Include="NJsonSchema" Version="9.13.17" /> <PackageReference Include="NJsonSchema" Version="9.13.17" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" /> <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 IUserResolver userResolver;
private readonly IAppsByNameIndex appsByNameIndex; private readonly IAppsByNameIndex appsByNameIndex;
private readonly HashSet<string> contributors = new HashSet<string>(); 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, string> usersWithEmail = new Dictionary<string, string>();
private Dictionary<string, RefToken> userMapping = new Dictionary<string, RefToken>();
private bool isReserved; private bool isReserved;
private string appName; private string appName;

3
src/Squidex.Domain.Apps.Entities/EntityMapper.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System; using System;
using NodaTime;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
@ -46,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities
private static void SetCreated(IEntity entity, EnvelopeHeaders headers) 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(); 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 target = kvp.Value;
var (from, to) = GetDateRange(today, target.NumDays); var (from, _) = GetDateRange(today, target.NumDays);
if (!target.Triggered.HasValue || target.Triggered < from) if (!target.Triggered.HasValue || target.Triggered < from)
{ {
@ -107,7 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
await WriteStateAsync(); await WriteStateAsync();
} }
private (DateTime, DateTime) GetDateRange(DateTime today, int? numDays) private static (DateTime, DateTime) GetDateRange(DateTime today, int? numDays)
{ {
if (numDays.HasValue) 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 schemaSource = Snapshot.SchemaDef;
var schemaTarget = command.ToSchema(schemaSource.Name, schemaSource.IsSingleton); 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)); 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 (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 => Task.Delay(ReconnectWaitMs, timerCts.Token).ContinueWith(t =>
{ {
dispatcher.DispatchAsync(() => Subscribe()); dispatcher.DispatchAsync(Subscribe);
}).Forget(); }).Forget();
} }
else else
@ -104,7 +104,7 @@ namespace Squidex.Infrastructure.EventSourcing
public async Task StopAsync() public async Task StopAsync()
{ {
await dispatcher.DispatchAsync(() => Unsubscribe()); await dispatcher.DispatchAsync(Unsubscribe);
await dispatcher.StopAndWaitAsync(); await dispatcher.StopAndWaitAsync();
timerCts.Cancel(); timerCts.Cancel();

2
src/Squidex.Infrastructure/Log/LockingLogStore.cs

@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.Log
break; break;
} }
await Task.Delay(2000); await Task.Delay(2000, cts.Token);
} }
if (!cts.IsCancellationRequested) 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 HandleEvent(Envelope<IEvent> @event);
public delegate void HandleSnapshot<T>(T state); public delegate void HandleSnapshot<in T>(T state);
public interface IStore<in TKey> 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;
using Squidex.Domain.Apps.Core.GenerateJsonSchema; using Squidex.Domain.Apps.Core.GenerateJsonSchema;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Pipeline.Swagger; using Squidex.Pipeline.Swagger;
using Squidex.Shared; 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>()); return new MongoEventStore(mongDatabase, c.GetRequiredService<IEventNotifier>());
}) })
.As<IInitializable>()
.As<IEventStore>(); .As<IEventStore>();
}, },
["GetEventStore"] = () => ["GetEventStore"] = () =>

14
src/Squidex/Config/Domain/LoggingServices.cs

@ -12,15 +12,10 @@ using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Pipeline; using Squidex.Pipeline;
#pragma warning disable RECS0092 // Convert field to readonly
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
{ {
public static class LoggingServices public static class LoggingServices
{ {
private static ILogChannel console = new ConsoleLogChannel();
private static ILogChannel file;
public static void AddMyLoggingServices(this IServiceCollection services, IConfiguration config) public static void AddMyLoggingServices(this IServiceCollection services, IConfiguration config)
{ {
if (config.GetValue<bool>("logging:human")) if (config.GetValue<bool>("logging:human"))
@ -38,18 +33,13 @@ namespace Squidex.Config.Domain
if (!string.IsNullOrWhiteSpace(loggingFile)) if (!string.IsNullOrWhiteSpace(loggingFile))
{ {
services.AddSingletonAs(file ?? (file = new FileChannel(loggingFile))) services.AddSingletonAs(new FileChannel(loggingFile))
.As<ILogChannel>(); .As<ILogChannel>();
} }
var useColors = config.GetValue<bool>("logging:colors"); var useColors = config.GetValue<bool>("logging:colors");
if (console == null) services.AddSingletonAs(new ConsoleLogChannel(useColors))
{
console = new ConsoleLogChannel(useColors);
}
services.AddSingletonAs(console)
.As<ILogChannel>(); .As<ILogChannel>();
services.AddSingletonAs(c => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid())) 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Orleans; using Orleans;
using Orleans.ApplicationParts; using Orleans.ApplicationParts;
using Orleans.Configuration; using Orleans.Configuration;
using Orleans.Hosting;
using OrleansDashboard;
using OrleansDashboard.Client;
using OrleansDashboard.Metrics;
using Squidex.Domain.Apps.Entities; using Squidex.Domain.Apps.Entities;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -26,5 +31,27 @@ namespace Squidex.Config.Orleans
options.ClusterId = Constants.OrleansClusterId; options.ClusterId = Constants.OrleansClusterId;
options.ServiceId = 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 // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Net;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Orleans; 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 namespace Squidex.Config.Orleans
{ {
public static class OrleansServices public static class OrleansServices
{ {
public static void AddOrleansSilo(this IServiceCollection services) public static IServiceProvider AddAndBuildOrleans(this IServiceCollection services, IConfiguration config)
{ {
services.AddSingletonAs<SiloWrapper>() services.Configure<ClusterOptions>(options =>
.As<IHostedService>() {
.AsSelf(); options.Configure();
});
services.Configure<ProcessExitHandlingOptions>(options =>
{
options.FastKillOnProcessExit = false;
});
services.AddServicesForSelfHostedDashboard(null, options => services.AddServicesForSelfHostedDashboard(null, options =>
{ {
options.HideTrace = true; options.HideTrace = true;
}); });
services.AddSingletonAs(c => c.GetRequiredService<IClusterClient>()) services.AddHostedService<SiloHost>();
.As<IClusterClient>();
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) return provider;
.As<IGrainFactory>();
} }
} }
} }

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; background = backgroundValue;
} }
var isSmall = false; var isSmall = request.Query.TryGetValue("small", out _);
if (request.Query.TryGetValue("small", out _)) string svg;
{
isSmall = true;
}
var svg = string.Empty;
if (isSmall) if (isSmall)
{ {

2
src/Squidex/Squidex.csproj

@ -91,7 +91,7 @@
<PackageReference Include="NSwag.AspNetCore" Version="12.0.13" /> <PackageReference Include="NSwag.AspNetCore" Version="12.0.13" />
<PackageReference Include="OpenCover" Version="4.6.519" /> <PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="Orleans.Providers.MongoDB" Version="2.0.1" /> <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="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="4.0.9" /> <PackageReference Include="ReportGenerator" Version="4.0.9" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.6" /> <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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Areas.Api; using Squidex.Areas.Api;
using Squidex.Areas.Api.Config.Swagger;
using Squidex.Areas.Api.Controllers.Contents;
using Squidex.Areas.Frontend; using Squidex.Areas.Frontend;
using Squidex.Areas.IdentityServer; using Squidex.Areas.IdentityServer;
using Squidex.Areas.IdentityServer.Config;
using Squidex.Areas.OrleansDashboard; using Squidex.Areas.OrleansDashboard;
using Squidex.Areas.Portal; using Squidex.Areas.Portal;
using Squidex.Config;
using Squidex.Config.Authentication;
using Squidex.Config.Domain; using Squidex.Config.Domain;
using Squidex.Config.Orleans; using Squidex.Config.Orleans;
using Squidex.Config.Startup;
using Squidex.Config.Web; 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 namespace Squidex
{ {
@ -28,13 +42,63 @@ namespace Squidex
this.configuration = configuration; this.configuration = configuration;
} }
public void ConfigureServices(IServiceCollection services) public IServiceProvider ConfigureServices(IServiceCollection services)
{ {
services.AddOrleansSilo(); var config = configuration;
services.AddAppServices(configuration);
services.AddHostedService<SystemExtensions.InitializeHostedService>(); services.AddHttpClient();
services.AddHostedService<SystemExtensions.MigratorHostedService>(); 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) 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>
<ng-container menu> <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 <i class="icon-reset"></i> Refresh
</button> </button>
@ -42,13 +42,13 @@
<span>{{eventConsumer.position}}</span> <span>{{eventConsumer.position}}</span>
</td> </td>
<td class="cell-actions-lg"> <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> <i class="icon icon-reset"></i>
</button> </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> <i class="icon icon-play"></i>
</button> </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> <i class="icon icon-pause"></i>
</button> </button>
</td> </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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Subscription, timer } from 'rxjs'; import { timer } from 'rxjs';
import { onErrorResumeNext, switchMap } from 'rxjs/operators'; 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 { EventConsumerDto } from './../../services/event-consumers.service';
import { EventConsumersState } from './../../state/event-consumers.state'; import { EventConsumersState } from './../../state/event-consumers.state';
@ -19,27 +19,20 @@ import { EventConsumersState } from './../../state/event-consumers.state';
styleUrls: ['./event-consumers-page.component.scss'], styleUrls: ['./event-consumers-page.component.scss'],
templateUrl: './event-consumers-page.component.html' templateUrl: './event-consumers-page.component.html'
}) })
export class EventConsumersPageComponent implements OnDestroy, OnInit { export class EventConsumersPageComponent extends ResourceOwner implements OnInit {
private timerSubscription: Subscription;
public eventConsumerErrorDialog = new DialogModel(); public eventConsumerErrorDialog = new DialogModel();
public eventConsumerError = ''; public eventConsumerError = '';
constructor( constructor(
public readonly eventConsumersState: EventConsumersState public readonly eventConsumersState: EventConsumersState
) { ) {
} super();
public ngOnDestroy() {
this.timerSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.eventConsumersState.load().pipe(onErrorResumeNext()).subscribe(); this.eventConsumersState.load().pipe(onErrorResumeNext()).subscribe();
this.timerSubscription = this.own(timer(2000, 2000).pipe(switchMap(() => this.eventConsumersState.load(true, true))));
timer(2000, 2000).pipe(switchMap(x => this.eventConsumersState.load(true, true).pipe(onErrorResumeNext())))
.subscribe();
} }
public reload() { 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. * 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 { FormBuilder } from '@angular/forms';
import { Subscription, timer } from 'rxjs'; import { timer } from 'rxjs';
import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators'; import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import { import {
AuthService, AuthService,
BackupsService, BackupsService,
DialogService, DialogService,
ResourceOwner,
RestoreDto, RestoreDto,
RestoreForm RestoreForm
} from '@app/shared'; } from '@app/shared';
@ -23,9 +24,7 @@ import {
styleUrls: ['./restore-page.component.scss'], styleUrls: ['./restore-page.component.scss'],
templateUrl: './restore-page.component.html' templateUrl: './restore-page.component.html'
}) })
export class RestorePageComponent implements OnDestroy, OnInit { export class RestorePageComponent extends ResourceOwner implements OnInit {
private timerSubscription: Subscription;
public restoreJob: RestoreDto | null; public restoreJob: RestoreDto | null;
public restoreForm = new RestoreForm(this.formBuilder); public restoreForm = new RestoreForm(this.formBuilder);
@ -35,18 +34,17 @@ export class RestorePageComponent implements OnDestroy, OnInit {
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
private readonly formBuilder: FormBuilder private readonly formBuilder: FormBuilder
) { ) {
} super();
public ngOnDestroy() {
this.timerSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.timerSubscription = this.own(
timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore().pipe(onErrorResumeNext())), filter(x => !!x)) timer(0, 2000).pipe(switchMap(() => this.backupsService.getRestore().pipe(onErrorResumeNext())))
.subscribe(dto => { .subscribe(job => {
this.restoreJob = dto!; if (job) {
}); this.restoreJob = job;
}
}));
} }
public restore() { 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. * 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 { FormBuilder } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { ResourceOwner } from '@app/shared';
import { UserDto } from './../../services/users.service'; import { UserDto } from './../../services/users.service';
import { UserForm, UsersState } from './../../state/users.state'; import { UserForm, UsersState } from './../../state/users.state';
@ -18,9 +19,7 @@ import { UserForm, UsersState } from './../../state/users.state';
styleUrls: ['./user-page.component.scss'], styleUrls: ['./user-page.component.scss'],
templateUrl: './user-page.component.html' templateUrl: './user-page.component.html'
}) })
export class UserPageComponent implements OnDestroy, OnInit { export class UserPageComponent extends ResourceOwner implements OnInit {
private selectedUserSubscription: Subscription;
public canUpdate = false; public canUpdate = false;
public user?: { user: UserDto, isCurrentUser: boolean }; public user?: { user: UserDto, isCurrentUser: boolean };
@ -32,22 +31,19 @@ export class UserPageComponent implements OnDestroy, OnInit {
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router private readonly router: Router
) { ) {
} super();
public ngOnDestroy() {
this.selectedUserSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.selectedUserSubscription = this.own(
this.usersState.selectedUser this.usersState.selectedUser
.subscribe(selectedUser => { .subscribe(selectedUser => {
this.user = selectedUser; this.user = selectedUser!;
if (selectedUser) { if (selectedUser) {
this.userForm.load(selectedUser.user); this.userForm.load(selectedUser.user);
} }
}); }));
} }
public save() { public save() {

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

@ -6,7 +6,7 @@
</ng-container> </ng-container>
<ng-container menu> <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 <i class="icon-reset"></i> Refresh
</button> </button>
@ -18,7 +18,7 @@
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" /> <input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" />
</form> </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 <i class="icon-plus"></i> New
</button> </button>
</ng-container> </ng-container>
@ -61,10 +61,10 @@
</td> </td>
<td class="cell-actions"> <td class="cell-actions">
<ng-container *ngIf="!userInfo.isCurrentUser"> <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> <i class="icon icon-unlocked"></i>
</button> </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> <i class="icon icon-lock"></i>
</button> </button>
</ng-container> </ng-container>
@ -81,7 +81,7 @@
</div> </div>
<div class="grid-footer"> <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> </div>
</ng-container> </ng-container>
</sqx-panel> </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 { interface Snapshot {
eventConsumers: ImmutableArray<EventConsumerDto>; eventConsumers: ImmutableArray<EventConsumerDto>;
isLoaded?: false; isLoaded?: boolean;
} }
@Injectable() @Injectable()

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

@ -98,7 +98,7 @@ interface Snapshot {
isLoaded?: boolean; isLoaded?: boolean;
selectedUser?: SnapshotUser; selectedUser?: SnapshotUser | null;
} }
@Injectable() @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-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> <div inner #graphiQLContainer></div>
</sqx-panel> </sqx-panel>

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

@ -14,7 +14,8 @@
</div> </div>
</a> </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="row">
<div class="col"> <div class="col">
{{tag.name}} {{tag.name}}
@ -29,7 +30,8 @@
<h3>Saved queries</h3> <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)"> <a class="sidebar-item-remove float-right" (click)="queries.remove(query.name)">
<i class="icon-close"></i> <i class="icon-close"></i>
</a> </a>

8
src/Squidex/app/features/assets/pages/assets-filters-page.component.ts

@ -47,4 +47,12 @@ export class AssetsFiltersPageComponent {
public isSelectedQuery(query: string) { public isSelectedQuery(query: string) {
return query === this.assetsState.snapshot.assetsQuery || (!query && !this.assetsState.assetsQuery); 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-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> <ng-container title>
Assets Assets
</ng-container> </ng-container>
@ -10,7 +10,7 @@
<div class="col-auto offset-xl-4"> <div class="col-auto offset-xl-4">
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <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 <i class="icon-reset"></i> Refresh
</button> </button>
</div> </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-field.component';
export * from './pages/content/content-history-page.component'; export * from './pages/content/content-history-page.component';
export * from './pages/content/content-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-filters-page.component';
export * from './pages/contents/contents-page.component'; export * from './pages/contents/contents-page.component';
export * from './pages/schemas/schemas-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/assets-editor.component';
export * from './shared/array-item.component'; export * from './shared/array-item.component';
export * from './shared/content-item.component'; export * from './shared/content-item.component';
export * from './shared/content-item-editor.component';
export * from './shared/content-status.component'; export * from './shared/content-status.component';
export * from './shared/contents-selector.component'; export * from './shared/contents-selector.component';
export * from './shared/due-time-selector.component'; export * from './shared/due-time-selector.component';

4
src/Squidex/app/features/content/module.ts

@ -29,6 +29,7 @@ import {
ContentFieldComponent, ContentFieldComponent,
ContentHistoryPageComponent, ContentHistoryPageComponent,
ContentItemComponent, ContentItemComponent,
ContentItemEditorComponent,
ContentPageComponent, ContentPageComponent,
ContentsFiltersPageComponent, ContentsFiltersPageComponent,
ContentsPageComponent, ContentsPageComponent,
@ -36,6 +37,7 @@ import {
ContentStatusComponent, ContentStatusComponent,
DueTimeSelectorComponent, DueTimeSelectorComponent,
FieldEditorComponent, FieldEditorComponent,
FieldLanguagesComponent,
PreviewButtonComponent, PreviewButtonComponent,
ReferencesEditorComponent, ReferencesEditorComponent,
SchemasPageComponent SchemasPageComponent
@ -112,6 +114,7 @@ const routes: Routes = [
ContentFieldComponent, ContentFieldComponent,
ContentHistoryPageComponent, ContentHistoryPageComponent,
ContentItemComponent, ContentItemComponent,
ContentItemEditorComponent,
ContentPageComponent, ContentPageComponent,
ContentsFiltersPageComponent, ContentsFiltersPageComponent,
ContentStatusComponent, ContentStatusComponent,
@ -119,6 +122,7 @@ const routes: Routes = [
ContentsSelectorComponent, ContentsSelectorComponent,
DueTimeSelectorComponent, DueTimeSelectorComponent,
FieldEditorComponent, FieldEditorComponent,
FieldLanguagesComponent,
PreviewButtonComponent, PreviewButtonComponent,
ReferencesEditorComponent, ReferencesEditorComponent,
SchemasPageComponent SchemasPageComponent

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

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

37
src/Squidex/app/features/content/pages/content/content-field.component.scss

@ -11,8 +11,26 @@
@include absolute(.7rem, 1.25rem, auto, auto); @include absolute(.7rem, 1.25rem, auto, auto);
} }
.invalid { .row {
border-left-color: $color-theme-error; 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 { .field {
@ -20,9 +38,24 @@
color: $color-theme-error; color: $color-theme-error;
} }
&-invalid {
border-left-color: $color-theme-error;
}
&-disabled { &-disabled {
color: $color-border-dark; color: $color-border-dark;
font-size: .8rem; font-size: .8rem;
font-weight: normal; 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;
} }

82
src/Squidex/app/features/content/pages/content/content-field.component.ts

@ -5,20 +5,21 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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 { AbstractControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators'; import { combineLatest } from 'rxjs/operators';
import { import {
AppLanguageDto, AppLanguageDto,
EditContentForm, EditContentForm,
fieldInvariant, fieldInvariant,
ImmutableArray, invalid$,
LocalStoreService, LocalStoreService,
RootFieldDto, RootFieldDto,
SchemaDto, SchemaDto,
Types Types,
value$
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -26,7 +27,7 @@ import {
styleUrls: ['./content-field.component.scss'], styleUrls: ['./content-field.component.scss'],
templateUrl: './content-field.component.html' templateUrl: './content-field.component.html'
}) })
export class ContentFieldComponent implements DoCheck, OnChanges { export class ContentFieldComponent implements OnChanges {
@Input() @Input()
public form: EditContentForm; public form: EditContentForm;
@ -36,6 +37,9 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
@Input() @Input()
public fieldForm: FormGroup; public fieldForm: FormGroup;
@Input()
public fieldFormCompare?: FormGroup;
@Input() @Input()
public schema: SchemaDto; public schema: SchemaDto;
@ -43,15 +47,18 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public languages: ImmutableArray<AppLanguageDto>; public languages: AppLanguageDto[];
@Output() @Output()
public languageChange = new EventEmitter<AppLanguageDto>(); public languageChange = new EventEmitter<AppLanguageDto>();
public selectedFormControl: AbstractControl; public selectedFormControl: AbstractControl;
public selectedFormControlCompare?: AbstractControl;
public showAllControls = false; public showAllControls = false;
public isInvalid: Observable<boolean>; public isInvalid: Observable<boolean>;
public isDifferent: Observable<boolean>;
constructor( constructor(
private readonly localStore: LocalStoreService private readonly localStore: LocalStoreService
@ -60,36 +67,73 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['fieldForm']) { 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']) { if (changes['field']) {
this.showAllControls = this.localStore.getBoolean(this.configKey()); 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() { public changeShowAllControls(value: boolean) {
this.showAllControls = !this.showAllControls; this.showAllControls = value;
this.localStore.setBoolean(this.configKey(), this.showAllControls); this.localStore.setBoolean(this.configKey(), this.showAllControls);
} }
public ngDoCheck() { public copy() {
let control: AbstractControl; 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) { if (this.field.isLocalizable) {
control = this.fieldForm.controls[this.language.iso2Code]; return form.controls[this.language.iso2Code];
} else { } else {
control = this.fieldForm.controls[fieldInvariant]; return form.controls[fieldInvariant];
} }
}
if (this.selectedFormControl !== control) { public prefix(language: AppLanguageDto) {
if (this.selectedFormControl && Types.isFunction(this.selectedFormControl['_clearChangeFns'])) { return `(${language.iso2Code}`;
this.selectedFormControl['_clearChangeFns'](); }
}
this.selectedFormControl = control; public trackByLanguage(index: number, language: AppLanguageDto) {
} return language.iso2Code;
} }
private configKey() { private configKey() {

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

@ -15,7 +15,7 @@
</div> </div>
<div class="event-created">{{event.created | sqxFromNow}}</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>
</div> </div>
</ng-container> </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) { 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) { 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> <sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="appsState.appName" [value2]="schema.displayName"></sqx-title>
<form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()"> <form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()">
<sqx-panel desiredWidth="*" [showSidebar]="content"> <sqx-panel desiredWidth="*" minWidth="60rem" [showSidebar]="content">
<ng-container title> <ng-container title>
<a class="btn btn-text" (click)="back()" *ngIf="!schema.isSingleton"> <a class="btn btn-text" (click)="back()" *ngIf="!schema.isSingleton">
<i class="icon-angle-left"></i> <i class="icon-angle-left"></i>
@ -116,8 +116,9 @@
[form]="contentForm" [form]="contentForm"
[field]="field" [field]="field"
[fieldForm]="contentForm.form.get(field.name)" [fieldForm]="contentForm.form.get(field.name)"
[fieldFormCompare]="contentFormCompare?.form.get(field.name)"
[schema]="schema" [schema]="schema"
[languages]="languages" [languages]="languages.mutableValues"
[(language)]="language"> [(language)]="language">
</sqx-content-field> </sqx-content-field>
</div> </div>

89
src/Squidex/app/features/content/pages/content/content-page.component.ts

@ -5,10 +5,10 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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 { ActivatedRoute, Router } from '@angular/router';
import { Observable, of, Subscription } from 'rxjs'; import { Observable, of } from 'rxjs';
import { filter, onErrorResumeNext, switchMap } from 'rxjs/operators'; import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import { ContentVersionSelected } from './../messages'; import { ContentVersionSelected } from './../messages';
@ -25,6 +25,7 @@ import {
LanguagesState, LanguagesState,
MessageBus, MessageBus,
ModalModel, ModalModel,
ResourceOwner,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState, SchemasState,
Version Version
@ -40,17 +41,13 @@ import { DueTimeSelectorComponent } from './../../shared/due-time-selector.compo
fadeAnimation fadeAnimation
] ]
}) })
export class ContentPageComponent implements CanComponentDeactivate, OnDestroy, OnInit { export class ContentPageComponent extends ResourceOwner implements CanComponentDeactivate, OnInit {
private languagesSubscription: Subscription;
private contentSubscription: Subscription;
private contentVersionSelectedSubscription: Subscription;
private selectedSchemaSubscription: Subscription;
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
public content: ContentDto; public content: ContentDto;
public contentVersion: Version | null; public contentVersion: Version | null;
public contentForm: EditContentForm; public contentForm: EditContentForm;
public contentFormCompare: EditContentForm | null = null;
public dropdown = new ModalModel(); public dropdown = new ModalModel();
@ -70,44 +67,42 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
private readonly router: Router, private readonly router: Router,
private readonly schemasState: SchemasState private readonly schemasState: SchemasState
) { ) {
} super();
public ngOnDestroy() {
this.languagesSubscription.unsubscribe();
this.contentSubscription.unsubscribe();
this.contentVersionSelectedSubscription.unsubscribe();
this.selectedSchemaSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.languagesSubscription = this.own(
this.languagesState.languages this.languagesState.languages
.subscribe(languages => { .subscribe(languages => {
this.languages = languages.map(x => x.language); this.languages = languages.map(x => x.language);
this.language = this.languages.at(0); this.language = this.languages.at(0);
}); }));
this.selectedSchemaSubscription = this.own(
this.schemasState.selectedSchema.pipe(filter(s => !!s)) this.schemasState.selectedSchema
.subscribe(schema => { .subscribe(schema => {
this.schema = schema!; if (schema) {
this.schema = schema!;
this.contentForm = new EditContentForm(this.schema, this.languages); this.contentForm = new EditContentForm(this.schema, this.languages);
}); }
}));
this.contentSubscription = this.own(
this.contentsState.selectedContent.pipe(filter(c => !!c)) this.contentsState.selectedContent
.subscribe(content => { .subscribe(content => {
this.content = content!; if (content) {
this.content = content;
this.loadContent(this.content.dataDraft); this.loadContent(this.content.dataDraft);
}); }
}));
this.contentVersionSelectedSubscription = this.own(
this.messageBus.of(ContentVersionSelected) this.messageBus.of(ContentVersionSelected)
.subscribe(message => { .subscribe(message => {
this.loadVersion(message.version); this.loadVersion(message.version, message.compare);
}); }));
} }
public canDeactivate(): Observable<boolean> { public canDeactivate(): Observable<boolean> {
@ -214,26 +209,36 @@ export class ContentPageComponent implements CanComponentDeactivate, OnDestroy,
.subscribe(); .subscribe();
} }
private loadVersion(version: Version) { private loadVersion(version: Version | null, compare: boolean) {
if (this.content) { 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) this.contentsState.loadVersion(this.content, version)
.subscribe(dto => { .subscribe(dto => {
if (this.content.version.value !== version.value) { if (compare) {
this.contentVersion = version; 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 { } else {
this.contentVersion = null; if (this.contentFormCompare) {
this.contentFormCompare = null;
}
this.contentForm.load(dto.payload);
} }
this.loadContent(dto); this.contentVersion = version;
}); });
} }
} }
public showLatest() { public showLatest() {
if (this.contentVersion) { this.loadVersion(null, false);
this.contentVersion = null;
this.loadContent(this.content.dataDraft);
}
} }
} }

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>
<ng-container content> <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}} {{query.name}}
</a> </a>
@ -13,7 +14,8 @@
<div class="sidebar-section"> <div class="sidebar-section">
<h3>Saved queries</h3> <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}} {{query.name}}
<a class="sidebar-item-remove float-right" (click)="schemaQueries.remove(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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { onErrorResumeNext } from 'rxjs/operators'; import { onErrorResumeNext } from 'rxjs/operators';
import { import {
ContentsState, ContentsState,
Queries, Queries,
ResourceOwner,
SchemasState, SchemasState,
UIState UIState
} from '@app/shared'; } from '@app/shared';
@ -21,9 +21,7 @@ import {
styleUrls: ['./contents-filters-page.component.scss'], styleUrls: ['./contents-filters-page.component.scss'],
templateUrl: './contents-filters-page.component.html' templateUrl: './contents-filters-page.component.html'
}) })
export class ContentsFiltersPageComponent implements OnDestroy, OnInit { export class ContentsFiltersPageComponent extends ResourceOwner implements OnInit {
private selectedSchemaSubscription: Subscription;
public schemaQueries: Queries; public schemaQueries: Queries;
constructor( constructor(
@ -31,20 +29,17 @@ export class ContentsFiltersPageComponent implements OnDestroy, OnInit {
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
private readonly uiState: UIState private readonly uiState: UIState
) { ) {
} super();
public ngOnDestroy() {
this.selectedSchemaSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.selectedSchemaSubscription = this.own(
this.schemasState.selectedSchema this.schemasState.selectedSchema
.subscribe(schema => { .subscribe(schema => {
if (schema) { if (schema) {
this.schemaQueries = new Queries(this.uiState, `schemas.${schema.name}`); this.schemaQueries = new Queries(this.uiState, `schemas.${schema.name}`);
} }
}); }));
} }
public search(query: string) { public search(query: string) {
@ -54,4 +49,12 @@ export class ContentsFiltersPageComponent implements OnDestroy, OnInit {
public isSelectedQuery(query: string) { public isSelectedQuery(query: string) {
return query === this.contentsState.snapshot.contentsQuery || (!query && !this.contentsState.snapshot.contentsQuery); 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-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 title>
<ng-container *ngIf="contentsState.isArchive | async; else noArchive"> <ng-container *ngIf="contentsState.isArchive | async; else noArchive">
Archive Archive
@ -16,7 +16,7 @@
<div class="col-auto offset-xl-4"> <div class="col-auto offset-xl-4">
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <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 <i class="icon-reset"></i> Refresh
</button> </button>
</div> </div>
@ -33,10 +33,10 @@
</sqx-search-form> </sqx-search-form>
</div> </div>
<div class="col-auto pl-1" *ngIf="languages.length > 1"> <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>
<div class="col-auto pl-1"> <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 <i class="icon-plus"></i> New
</button> </button>
@ -73,23 +73,23 @@
<div class="selection" *ngIf="selectionCount > 0"> <div class="selection" *ngIf="selectionCount > 0">
{{selectionCount}} items selected:&nbsp;&nbsp; {{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 Publish
</button> </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 Unpublish
</button> </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 Archive
</button> </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 Restore
</button> </button>
<button class="btn btn-danger" <button type="button" class="btn btn-danger"
(sqxConfirmClick)="deleteSelected()" (sqxConfirmClick)="deleteSelected()"
confirmTitle="Delete content" confirmTitle="Delete content"
confirmText="Do you really want to delete the selected content items?"> confirmText="Do you really want to delete the selected content items?">
@ -120,7 +120,7 @@
</div> </div>
<div class="grid-footer"> <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> </div>
</ng-container> </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. * 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 { Subscription } from 'rxjs';
import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators'; import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators';
import { import {
@ -18,6 +17,7 @@ import {
LanguagesState, LanguagesState,
ModalModel, ModalModel,
Queries, Queries,
ResourceOwner,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState, SchemasState,
UIState UIState
@ -30,11 +30,7 @@ import { DueTimeSelectorComponent } from './../../shared/due-time-selector.compo
styleUrls: ['./contents-page.component.scss'], styleUrls: ['./contents-page.component.scss'],
templateUrl: './contents-page.component.html' templateUrl: './contents-page.component.html'
}) })
export class ContentsPageComponent implements OnDestroy, OnInit { export class ContentsPageComponent extends ResourceOwner implements OnInit {
private contentsSubscription: Subscription;
private languagesSubscription: Subscription;
private selectedSchemaSubscription: Subscription;
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
public schemaQueries: Queries; public schemaQueries: Queries;
@ -61,16 +57,11 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
private readonly uiState: UIState private readonly uiState: UIState
) { ) {
} super();
public ngOnDestroy() {
this.contentsSubscription.unsubscribe();
this.languagesSubscription.unsubscribe();
this.selectedSchemaSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.selectedSchemaSubscription = this.own(
this.schemasState.selectedSchema this.schemasState.selectedSchema
.subscribe(schema => { .subscribe(schema => {
this.resetSelection(); this.resetSelection();
@ -79,20 +70,20 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.schemaQueries = new Queries(this.uiState, `schemas.${this.schema.name}`); this.schemaQueries = new Queries(this.uiState, `schemas.${this.schema.name}`);
this.contentsState.init().pipe(onErrorResumeNext()).subscribe(); this.contentsState.init().pipe(onErrorResumeNext()).subscribe();
}); }));
this.contentsSubscription = this.own(
this.contentsState.contents this.contentsState.contents
.subscribe(() => { .subscribe(() => {
this.updateSelectionSummary(); this.updateSelectionSummary();
}); }));
this.languagesSubscription = this.own(
this.languagesState.languages this.languagesState.languages
.subscribe(languages => { .subscribe(languages => {
this.languages = languages.map(x => x.language); this.languages = languages.map(x => x.language);
this.language = this.languages.at(0); this.language = this.languages.at(0);
}); }));
} }
public reload() { public reload() {
@ -209,7 +200,7 @@ export class ContentsPageComponent implements OnDestroy, OnInit {
this.updateSelectionSummary(); this.updateSelectionSummary();
} }
public trackByContent(content: ContentDto): string { public trackByContent(index: number, content: ContentDto): string {
return content.id; return content.id;
} }

3
src/Squidex/app/features/content/pages/messages.ts

@ -9,7 +9,8 @@ import { Version } from '@app/shared';
export class ContentVersionSelected { export class ContentVersionSelected {
constructor( 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 <sqx-array-item
[form]="form" [form]="form"
[field]="field" [field]="field"
[isHidden]="isHidden" [isHidden]="snapshot.isHidden"
[isFirst]="i === 0" [isFirst]="i === 0"
[isLast]="i === arrayControl.controls.length - 1" [isLast]="i === arrayControl.controls.length - 1"
[index]="i" [index]="i"
@ -20,7 +20,7 @@
</div> </div>
</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 Add Item
</button> </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. * 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 { AbstractControl, FormArray, FormGroup } from '@angular/forms';
import { import {
AppLanguageDto, AppLanguageDto,
EditContentForm, EditContentForm,
ImmutableArray, RootFieldDto,
RootFieldDto StatefulComponent
} from '@app/shared'; } from '@app/shared';
interface State {
isHidden: boolean;
}
@Component({ @Component({
selector: 'sqx-array-editor', selector: 'sqx-array-editor',
styleUrls: ['./array-editor.component.scss'], styleUrls: ['./array-editor.component.scss'],
templateUrl: './array-editor.component.html', templateUrl: './array-editor.component.html',
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ArrayEditorComponent { export class ArrayEditorComponent extends StatefulComponent<State> {
@Input() @Input()
public form: EditContentForm; public form: EditContentForm;
@ -32,15 +36,19 @@ export class ArrayEditorComponent {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public languages: ImmutableArray<AppLanguageDto>; public languages: AppLanguageDto[];
@Input() @Input()
public arrayControl: FormArray; public arrayControl: FormArray;
public isHidden = false; constructor(changeDetector: ChangeDetectorRef) {
super(changeDetector, {
isHidden: false
});
}
public hide(hide: boolean) { public hide(isHidden: boolean) {
this.isHidden = hide; this.next(s => ({ ...s, isHidden }));
} }
public removeItem(index: number) { 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> <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> <i class="icon-caret-top"></i>
</button> </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> <i class="icon-caret-up"></i>
</button> </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> <i class="icon-caret-down"></i>
</button> </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> <i class="icon-caret-bottom"></i>
</button> </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> <i class="icon-plus-square"></i>
</button> </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> <i class="icon-minus-square"></i>
</button> </button>
</span> </span>
<span class="float-right"> <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> <i class="icon-clone"></i>
</button> </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> <i class="icon-bin2"></i>
</button> </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 { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AbstractControl, FormGroup } from '@angular/forms'; import { AbstractControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { import {
AppLanguageDto, AppLanguageDto,
EditContentForm, EditContentForm,
FieldDto, FieldDto,
ImmutableArray, invalid$,
RootFieldDto RootFieldDto
} from '@app/shared'; } from '@app/shared';
@ -62,7 +61,7 @@ export class ArrayItemComponent implements OnChanges {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public languages: ImmutableArray<AppLanguageDto>; public languages: AppLanguageDto[];
public isInvalid: Observable<boolean>; public isInvalid: Observable<boolean>;
@ -70,7 +69,7 @@ export class ArrayItemComponent implements OnChanges {
public ngOnChanges(changes: SimpleChanges) { public ngOnChanges(changes: SimpleChanges) {
if (changes['itemForm']) { 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']) { 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="header list">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col"> <div class="col">
<div class="drop-area align-items-center" (click)="assetsDialog.show()" (sqxFileDrop)="addFiles($event)"> <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> </div>
<div class="col-auto pl-1"> <div class="col-auto pl-1">
<div class="btn-group"> <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> <i class="icon-list"></i>
</button> </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> <i class="icon-grid"></i>
</button> </button>
</div> </div>
@ -20,12 +21,12 @@
</div> </div>
<div class="body"> <div class="body">
<ng-container *ngIf="!isListView; else listTemplate"> <ng-container *ngIf="!snapshot.isListView; else listTemplate">
<div class="row no-gutters"> <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)"> (failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)">
</sqx-asset> </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)"> (updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)">
</sqx-asset> </sqx-asset>
</div> </div>
@ -33,15 +34,17 @@
<ng-template #listTemplate> <ng-template #listTemplate>
<div class="list-view"> <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)"> [isListView]="true" (failed)="removeLoadingAsset(file)" (loaded)="addAsset(file, $event)">
</sqx-asset> </sqx-asset>
<div <div
[sqxSortModel]="oldAssets.values" [sqxSortModel]="snapshot.assets.mutableValues"
(sqxSorted)="sortAssets($event)"> (sqxSorted)="sortAssets($event)">
<div *ngFor="let asset of oldAssets; trackBy: trackByAsset"> <div *ngFor="let asset of snapshot.assets; trackBy: trackByAsset">
<sqx-asset [asset]="asset" removeMode="true" [isListView]="true" <sqx-asset [asset]="asset" removeMode="true"
[isListView]="true"
[isCompact]="isCompact"
(updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)"> (updated)="notifyOthers($event)" (removing)="removeLoadedAsset($event)">
</sqx-asset> </sqx-asset>
</div> </div>

6
src/Squidex/app/features/content/shared/assets-editor.component.scss

@ -44,12 +44,12 @@
& { & {
@include transition(border-color .4s ease); @include transition(border-color .4s ease);
@include border-radius; @include border-radius;
@include flex-box; @include truncate-nowidth;
@include truncate;
border: 2px dashed darken($color-border, 10%); border: 2px dashed darken($color-border, 10%);
padding: 5px .5rem;
font-weight: normal; font-weight: normal;
font-size: 1rem;
text-align: center; text-align: center;
padding: 5px 2rem;
color: darken($color-border, 30%); color: darken($color-border, 30%);
cursor: pointer; 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, forwardRef, OnDestroy, OnInit } from '@angular/core'; 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 { Subscription } from 'rxjs';
import { import {
AppsState, AppsState,
@ -17,6 +16,7 @@ import {
ImmutableArray, ImmutableArray,
LocalStoreService, LocalStoreService,
MessageBus, MessageBus,
StatefulControlComponent,
Types Types
} from '@app/shared'; } from '@app/shared';
@ -32,6 +32,14 @@ class AssetUpdated {
} }
} }
interface State {
assetFiles: ImmutableArray<File>;
assets: ImmutableArray<AssetDto>;
isListView: boolean;
}
@Component({ @Component({
selector: 'sqx-assets-editor', selector: 'sqx-assets-editor',
styleUrls: ['./assets-editor.component.scss'], styleUrls: ['./assets-editor.component.scss'],
@ -39,39 +47,35 @@ class AssetUpdated {
providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR], providers: [SQX_ASSETS_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDestroy { export class AssetsEditorComponent extends StatefulControlComponent<State, string[]> implements OnInit {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private subscription: Subscription;
public assetsDialog = new DialogModel(); public assetsDialog = new DialogModel();
public newAssets = ImmutableArray.empty<File>(); @Input()
public oldAssets = ImmutableArray.empty<AssetDto>(); public isCompact = false;
public isListView = false;
public isDisabled = false;
constructor( constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly assetsService: AssetsService, private readonly assetsService: AssetsService,
private readonly changeDetector: ChangeDetectorRef,
private readonly localStore: LocalStoreService, private readonly localStore: LocalStoreService,
private readonly messageBus: MessageBus 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) { public writeValue(obj: any) {
if (Types.isArrayOfString(obj)) { 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; const assetIds: string[] = obj;
this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj) this.assetsService.getAssets(this.appsState.appName, 0, 0, undefined, undefined, obj)
.subscribe(dtos => { .subscribe(dtos => {
this.setAssets(ImmutableArray.of(assetIds.map(id => dtos.items.find(x => x.id === id)!).filter(a => !!a))); 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(); this.updateValue();
} }
}, () => { }, () => {
@ -87,54 +91,28 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
this.messageBus.emit(new AssetUpdated(asset, this)); this.messageBus.emit(new AssetUpdated(asset, this));
} }
public ngOnDestroy() {
this.subscription.unsubscribe();
}
public ngOnInit() { public ngOnInit() {
this.subscription = this.own(
this.messageBus.of(AssetUpdated) this.messageBus.of(AssetUpdated)
.subscribe(event => { .subscribe(event => {
if (event.source !== this) { 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>) { public setAssets(assets: ImmutableArray<AssetDto>) {
this.oldAssets = asset; this.next(s => ({ ...s, assets }));
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 addFiles(files: File[]) { public addFiles(files: File[]) {
for (let file of files) { for (let file of files) {
this.newAssets = this.newAssets.pushFront(file); this.next(s => ({ ...s, assetFiles: s.assetFiles.pushFront(file) }));
} }
} }
public selectAssets(assets: AssetDto[]) { public selectAssets(assets: AssetDto[]) {
for (let asset of assets) { this.setAssets(this.snapshot.assets.push(...assets));
this.oldAssets = this.oldAssets.push(asset);
}
if (assets.length > 0) { if (assets.length > 0) {
this.updateValue(); this.updateValue();
@ -145,8 +123,11 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
public addAsset(file: File, asset: AssetDto) { public addAsset(file: File, asset: AssetDto) {
if (asset && file) { if (asset && file) {
this.newAssets = this.newAssets.remove(file); this.next(s => ({
this.oldAssets = this.oldAssets.pushFront(asset); ...s,
assetFiles: s.assetFiles.remove(file),
assets: s.assets.pushFront(asset)
}));
this.updateValue(); this.updateValue();
} }
@ -154,7 +135,7 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
public sortAssets(assets: AssetDto[]) { public sortAssets(assets: AssetDto[]) {
if (assets) { if (assets) {
this.oldAssets = ImmutableArray.of(assets); this.setAssets(ImmutableArray.of(assets));
this.updateValue(); this.updateValue();
} }
@ -162,24 +143,24 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
public removeLoadedAsset(asset: AssetDto) { public removeLoadedAsset(asset: AssetDto) {
if (asset) { if (asset) {
this.oldAssets = this.oldAssets.remove(asset); this.setAssets(this.snapshot.assets.remove(asset));
this.updateValue(); this.updateValue();
} }
} }
public removeLoadingAsset(file: File) { public removeLoadingAsset(file: File) {
this.newAssets = this.newAssets.remove(file); this.next(s => ({ ...s, assetFiles: s.assetFiles.remove(file) }));
} }
public changeView(isListView: boolean) { public changeView(isListView: boolean) {
this.isListView = isListView; this.next(s => ({ ...s, isListView }));
this.localStore.setBoolean('squidex.assets.list-view', isListView); this.localStore.setBoolean('squidex.assets.list-view', isListView);
} }
private updateValue() { 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) { if (ids.length === 0) {
ids = null; ids = null;
@ -187,8 +168,6 @@ export class AssetsEditorComponent implements ControlValueAccessor, OnInit, OnDe
this.callTouched(); this.callTouched();
this.callChange(ids); this.callChange(ids);
this.changeDetector.markForCheck();
} }
public trackByAsset(index: number, asset: AssetDto) { 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;
}

119
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)">
<input type="checkbox" class="form-check" <ng-container *ngIf="!isReference; else referenceTemplate">
[ngModel]="selected" <input type="checkbox" class="form-check"
(ngModelChange)="selectedChange.emit($event);" [ngModel]="selected"
(click)="$event.stopPropagation()" /> (ngModelChange)="selectedChange.emit($event)" />
</ng-container>
<ng-template #referenceTemplate>
<i class="icon-drag2 drag-handle"></i>
</ng-template>
</td> </td>
<td class="cell-select" *ngIf="isReference" (click)="shouldStop($event)"> <td class="cell-auto" *ngFor="let field of schema.listFields; let i = index" (click)="shouldStop($event, field)">
<i class="icon-drag2 drag-handle"></i> <ng-container *ngIf="field.isInlineEditable && !isReadOnly; else displayTemplate">
</td> <sqx-content-item-editor [form]="patchForm.form" [field]="field"></sqx-content-item-editor>
</ng-container>
<td class="cell-auto" *ngFor="let field of schema.listFields; let i = index" (click)="shouldStop($event)"> <ng-template #displayTemplate>
<div *ngIf="field.isInlineEditable && !isReadOnly" [formGroup]="patchForm.form" (click)="$event.stopPropagation()"> <span class="truncate">{{values[i]}}</span>
<div [ngSwitch]="field.properties.fieldType"> </ng-template>
<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> </td>
<td class="cell-time" (click)="shouldStop($event)">
<td class="cell-time" *ngIf="!isCompact" (click)="shouldStop($event)">
<sqx-content-status <sqx-content-status
[status]="content.status" [status]="content.status"
[scheduledTo]="content.scheduleJob?.status" [scheduledTo]="content.scheduleJob?.status"
@ -70,40 +31,42 @@
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small> <small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td> </td>
<td class="cell-user" *ngIf="patchForm.form.dirty" (click)="shouldStop($event)"> <td class="cell-user" *ngIf="!isCompact && patchForm.form.pristine" (click)="shouldStop($event)">
<button type="button" class="btn btn-success" (click)="save(); $event.stopPropagation()">
<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()">
<i class="icon-close"></i>
</button>
</td>
<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" /> <img class="user-picture" [attr.title]="content.lastModifiedBy | sqxUserNameRef" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td> </td>
<td class="cell-actions" *ngIf="!isReadOnly && patchForm.form.pristine" (click)="shouldStop($event)"> <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" (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-actions" *ngIf="!isReadOnly && patchForm.form.pristine" (click)="stop($event)">
<div class="dropdown dropdown-options" *ngIf="content"> <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> <i class="icon-dots"></i>
</button> </button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="optionsButton" @fade> <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 Publish
</a> </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 Unpublish
</a> </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 Archive
</a> </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 Restore
</a> </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 Clone
</a> </a>
@ -119,7 +82,7 @@
</div> </div>
</td> </td>
<td class="cell-actions" *ngIf="isReference" (click)="shouldStop($event)"> <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> <i class="icon-close"></i>
</button> </button>
</td> </td>

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

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

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

@ -6,7 +6,7 @@
<ng-container tabs> <ng-container tabs>
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-auto offset-lg-4"> <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 <i class="icon-reset"></i> Refresh
</button> </button>
</div> </div>
@ -19,7 +19,7 @@
</div> </div>
<div class="col pl-1" *ngIf="languages.length > 1"> <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>
</div> </div>
</ng-container> </ng-container>
@ -63,7 +63,7 @@
</div> </div>
<div class="grid-footer"> <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> </div>
</ng-container> </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; this.isAllSelected = this.selectionCount === this.contentsState.snapshot.contents.length;
} }
public trackByContent(content: ContentDto): string { public trackByContent(index: number, content: ContentDto): string {
return content.id; return content.id;
} }
} }

9
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> <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> <div>
<ng-container *ngIf="field.properties.editorUrl; else noEditor"> <ng-container *ngIf="field.properties.editorUrl; else noEditor">
@ -101,7 +101,7 @@
<sqx-json-editor [formControl]="control"></sqx-json-editor> <sqx-json-editor [formControl]="control"></sqx-json-editor>
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Assets'"> <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>
<ng-container *ngSwitchCase="'Tags'"> <ng-container *ngSwitchCase="'Tags'">
<ng-container [ngSwitch]="field.properties['editor']"> <ng-container [ngSwitch]="field.properties['editor']">
@ -122,7 +122,7 @@
<sqx-array-editor <sqx-array-editor
[arrayControl]="control" [arrayControl]="control"
[form]="form" [form]="form"
[field]="field" [field]="field"
[language]="language" [language]="language"
[languages]="languages"> [languages]="languages">
</sqx-array-editor> </sqx-array-editor>
@ -132,7 +132,8 @@
[formControl]="control" [formControl]="control"
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
[schemaId]="field.properties['schemaId']"> [schemaId]="field.properties['schemaId']"
[isCompact]="isCompact">
</sqx-references-editor> </sqx-references-editor>
</ng-container> </ng-container>
</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 { import {
AppLanguageDto, AppLanguageDto,
EditContentForm, EditContentForm,
FieldDto, FieldDto
ImmutableArray
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -34,7 +33,10 @@ export class FieldEditorComponent {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public languages: ImmutableArray<AppLanguageDto>; public languages: AppLanguageDto[];
@Input()
public isCompact = false;
@Input() @Input()
public displaySuffix: string; 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> <span>Preview: </span>
<div class="btn-group ml-1" #buttonGroup> <div class="btn-group ml-1" #buttonGroup>
<button type="button" class="btn btn-secondary" (click)="follow(selectedName)"> <button type="button" class="btn btn-secondary" (click)="follow(snapshot.selectedName)">
<i class="icon-external-link"></i> {{selectedName}} <i class="icon-external-link"></i> {{snapshot.selectedName}}
</button> </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> <button type="button" class="btn btn-secondary dropdown-toggle" (click)="dropdown.toggle()"></button>
<div class="dropdown-menu" *sqxModalView="dropdown;closeAlways:true" [sqxModalTarget]="buttonGroup" @fade> <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> </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. * 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 { import {
ContentDto, ContentDto,
@ -13,19 +13,26 @@ import {
interpolate, interpolate,
LocalStoreService, LocalStoreService,
ModalModel, ModalModel,
SchemaDetailsDto SchemaDetailsDto,
StatefulComponent
} from '@app/shared'; } from '@app/shared';
interface State {
selectedName?: string;
alternativeNames: string[];
}
@Component({ @Component({
selector: 'sqx-preview-button', selector: 'sqx-preview-button',
styleUrls: ['./preview-button.component.scss'], styleUrls: ['./preview-button.component.scss'],
templateUrl: './preview-button.component.html', templateUrl: './preview-button.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [ animations: [
fadeAnimation fadeAnimation
] ],
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class PreviewButtonComponent implements OnInit { export class PreviewButtonComponent extends StatefulComponent<State> implements OnInit {
@Input() @Input()
public content: ContentDto; public content: ContentDto;
@ -34,13 +41,12 @@ export class PreviewButtonComponent implements OnInit {
public dropdown = new ModalModel(); public dropdown = new ModalModel();
public selectedName: string | undefined; constructor(changeDetector: ChangeDetectorRef,
public alternativeNames: string[];
constructor(
private readonly localStore: LocalStoreService private readonly localStore: LocalStoreService
) { ) {
super(changeDetector, {
alternativeNames: []
});
} }
public ngOnInit() { public ngOnInit() {
@ -62,16 +68,23 @@ export class PreviewButtonComponent implements OnInit {
} }
private selectUrl(selectedName: string) { 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); const keys = Object.keys(this.schema.previewUrls);
this.selectedName = selectedName; state.selectedName = selectedName;
this.alternativeNames = keys.filter(x => x !== this.selectedName); state.alternativeNames = keys.filter(x => x !== s.selectedName);
this.alternativeNames.sort(); state.alternativeNames.sort();
this.localStore.set(this.configKey(), selectedName); this.localStore.set(this.configKey(), selectedName);
}
return state;
});
} }
private configKey() { private configKey() {

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

@ -1,27 +1,28 @@
<div class="references-container" [class.disabled]="isDisabled"> <div class="references-container" [class.disabled]="snapshot.isDisabled">
<ng-container *ngIf="schema"> <ng-container *ngIf="snapshot.schema">
<div class="drop-area-container"> <div class="drop-area-container">
<div class="drop-area" (click)="selectorDialog.show()"> <div class="drop-area" (click)="selectorDialog.show()">
Click here to link content items. Click here to link content items.
</div> </div>
</div> </div>
<table class="table table-items table-fixed" [class.disabled]="isDisabled" *ngIf="schema && contentItems && contentItems.length > 0" <table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="snapshot.schema && snapshot.contentItems && snapshot.contentItems.length > 0"
[sqxSortModel]="contentItems.values" [sqxSortModel]="snapshot.contentItems.mutableValues"
(sqxSorted)="sort($event)"> (sqxSorted)="sort($event)">
<tbody *ngFor="let content of contentItems"> <tbody *ngFor="let content of snapshot.contentItems">
<tr [sqxContent]="content" <tr [sqxContent]="content"
[language]="language" [language]="language"
[isReadOnly]="true" [isReadOnly]="true"
[isReference]="true" [isReference]="true"
[schema]="schema" [isCompact]="isCompact"
[schema]="snapshot.schema"
(deleting)="remove(content)"></tr> (deleting)="remove(content)"></tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>
</tbody> </tbody>
</table> </table>
</ng-container> </ng-container>
<div class="invalid" *ngIf="isInvalidSchema"> <div class="invalid" *ngIf="snapshot.schemaInvalid">
Schema not found or not configured yet. Schema not found or not configured yet.
</div> </div>
</div> </div>
@ -30,7 +31,7 @@
<sqx-contents-selector <sqx-contents-selector
[language]="language" [language]="language"
[languages]="languages" [languages]="languages"
[schema]="schema" [schema]="snapshot.schema"
(selected)="select($event)"> (selected)="select($event)">
</sqx-contents-selector> </sqx-contents-selector>
</ng-container> </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 { 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 { import {
AppLanguageDto, AppLanguageDto,
@ -18,6 +18,7 @@ import {
MathHelper, MathHelper,
SchemaDetailsDto, SchemaDetailsDto,
SchemasService, SchemasService,
StatefulControlComponent,
Types Types
} from '@app/shared'; } 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 provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ReferencesEditorComponent), multi: true
}; };
interface State {
schema?: SchemaDetailsDto | null;
schemaInvalid: boolean;
contentItems: ImmutableArray<ContentDto>;
}
@Component({ @Component({
selector: 'sqx-references-editor', selector: 'sqx-references-editor',
styleUrls: ['./references-editor.component.scss'], 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], providers: [SQX_REFERENCES_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ReferencesEditorComponent implements ControlValueAccessor, OnInit { export class ReferencesEditorComponent extends StatefulControlComponent<State, string[]> implements OnInit {
private callChange = (v: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
@Input() @Input()
public schemaId: string; public schemaId: string;
@ -43,53 +48,49 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
public language: AppLanguageDto; public language: AppLanguageDto;
@Input() @Input()
public languages: ImmutableArray<AppLanguageDto>; public languages: AppLanguageDto[];
public selectorDialog = new DialogModel();
public schema: SchemaDetailsDto; @Input()
public isCompact = false;
public contentItems = ImmutableArray.empty<ContentDto>();
public isDisabled = false; public selectorDialog = new DialogModel();
public isInvalidSchema = false;
constructor( constructor(changeDetector: ChangeDetectorRef,
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly changeDetector: ChangeDetectorRef,
private readonly contentsService: ContentsService, private readonly contentsService: ContentsService,
private readonly schemasService: SchemasService private readonly schemasService: SchemasService
) { ) {
super(changeDetector, {
schemaInvalid: false,
schema: null,
contentItems: ImmutableArray.empty()
});
} }
public ngOnInit() { public ngOnInit() {
if (this.schemaId === MathHelper.EMPTY_GUID) { if (this.schemaId === MathHelper.EMPTY_GUID) {
this.isInvalidSchema = true; this.next(s => ({ ...s, schemaInvalid: true }));
return; return;
} }
this.schemasService.getSchema(this.appsState.appName, this.schemaId) this.schemasService.getSchema(this.appsState.appName, this.schemaId)
.subscribe(dto => { .subscribe(schema => {
this.schema = dto; this.next(s => ({ ...s, schema }));
this.changeDetector.markForCheck();
}, () => { }, () => {
this.isInvalidSchema = true; this.next(s => ({ ...s, schemaInvalid: true }));
this.changeDetector.markForCheck();
}); });
} }
public writeValue(obj: any) { public writeValue(obj: any) {
if (Types.isArrayOfString(obj)) { 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; const contentIds: string[] = obj;
this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds) this.contentsService.getContents(this.appsState.appName, this.schemaId, 10000, 0, undefined, contentIds)
.subscribe(dtos => { .subscribe(dtos => {
this.setContentItems(ImmutableArray.of(contentIds.map(id => dtos.items.find(c => c.id === id)!).filter(r => !!r))); 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(); this.updateValue();
} }
}, () => { }, () => {
@ -101,29 +102,13 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
} }
} }
public setContentItems(contents: ImmutableArray<ContentDto>) { public setContentItems(contentItems: ImmutableArray<ContentDto>) {
this.contentItems = contents; this.next(s => ({ ...s, contentItems }));
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 select(contents: ContentDto[]) { public select(contents: ContentDto[]) {
for (let content of contents) { for (let content of contents) {
this.contentItems = this.contentItems.push(content); this.setContentItems(this.snapshot.contentItems.push(content));
} }
if (contents.length > 0) { if (contents.length > 0) {
@ -135,7 +120,7 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
public remove(content: ContentDto) { public remove(content: ContentDto) {
if (content) { if (content) {
this.contentItems = this.contentItems.remove(content); this.setContentItems(this.snapshot.contentItems.remove(content));
this.updateValue(); this.updateValue();
} }
@ -143,14 +128,14 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
public sort(contents: ContentDto[]) { public sort(contents: ContentDto[]) {
if (contents) { if (contents) {
this.contentItems = ImmutableArray.of(contents); this.setContentItems(ImmutableArray.of(contents));
this.updateValue(); this.updateValue();
} }
} }
private 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) { if (ids.length === 0) {
ids = null; ids = null;
@ -158,7 +143,5 @@ export class ReferencesEditorComponent implements ControlValueAccessor, OnInit {
this.callTouched(); this.callTouched();
this.callChange(ids); 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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { filter, map, switchMap } from 'rxjs/operators'; import { filter, map, switchMap } from 'rxjs/operators';
import { import {
@ -17,6 +16,7 @@ import {
fadeAnimation, fadeAnimation,
HistoryEventDto, HistoryEventDto,
HistoryService, HistoryService,
ResourceOwner,
UsagesService UsagesService
} from '@app/shared'; } from '@app/shared';
@ -42,9 +42,7 @@ const COLORS = [
fadeAnimation fadeAnimation
] ]
}) })
export class DashboardPageComponent implements OnDestroy, OnInit { export class DashboardPageComponent extends ResourceOwner implements OnInit {
private subscriptions: Subscription[] = [];
public profileDisplayName = ''; public profileDisplayName = '';
public chartStorageCount: any; public chartStorageCount: any;
@ -104,18 +102,11 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
private readonly historyService: HistoryService, private readonly historyService: HistoryService,
private readonly usagesService: UsagesService private readonly usagesService: UsagesService
) { ) {
} super();
public ngOnDestroy() {
for (let subscription of this.subscriptions) {
subscription.unsubscribe();
}
this.subscriptions = [];
} }
public ngOnInit() { public ngOnInit() {
this.subscriptions.push( this.own(
this.app.pipe( this.app.pipe(
switchMap(app => this.usagesService.getTodayStorage(app.name))) switchMap(app => this.usagesService.getTodayStorage(app.name)))
.subscribe(dto => { .subscribe(dto => {
@ -123,7 +114,7 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
this.assetsMax = dto.maxAllowed; this.assetsMax = dto.maxAllowed;
})); }));
this.subscriptions.push( this.own(
this.app.pipe( this.app.pipe(
switchMap(app => this.usagesService.getMonthCalls(app.name))) switchMap(app => this.usagesService.getMonthCalls(app.name)))
.subscribe(dto => { .subscribe(dto => {
@ -131,14 +122,14 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
this.callsMax = dto.maxAllowed; this.callsMax = dto.maxAllowed;
})); }));
this.subscriptions.push( this.own(
this.app.pipe( this.app.pipe(
switchMap(app => this.historyService.getHistory(app.name, ''))) switchMap(app => this.historyService.getHistory(app.name, '')))
.subscribe(dto => { .subscribe(dto => {
this.history = dto; this.history = dto;
})); }));
this.subscriptions.push( this.own(
this.app.pipe( this.app.pipe(
switchMap(app => this.usagesService.getStorageUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))) switchMap(app => this.usagesService.getStorageUsages(app.name, DateTime.today().addDays(-20), DateTime.today())))
.subscribe(dtos => { .subscribe(dtos => {
@ -175,7 +166,7 @@ export class DashboardPageComponent implements OnDestroy, OnInit {
}; };
})); }));
this.subscriptions.push( this.own(
this.app.pipe( this.app.pipe(
switchMap(app => this.usagesService.getCallsUsages(app.name, DateTime.today().addDays(-20), DateTime.today()))) switchMap(app => this.usagesService.getCallsUsages(app.name, DateTime.today().addDays(-20), DateTime.today())))
.subscribe(dtos => { .subscribe(dtos => {

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

@ -6,7 +6,7 @@
</ng-container> </ng-container>
<ng-container menu> <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 <i class="icon-reset"></i> Refresh
</button> </button>
@ -70,11 +70,11 @@
Next: <ng-container *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</ng-container> Next: <ng-container *ngIf="event.nextAttempt">{{event.nextAttempt | sqxFromNow}}</ng-container>
</div> </div>
<div class="col-3 text-right"> <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 Cancel
</button> </button>
<button class="btn btn-success btn-sm" (click)="enqueue(event)"> <button type="button" class="btn btn-success btn-sm" (click)="enqueue(event)">
Enqueue Enqueue
</button> </button>
</div> </div>
@ -90,7 +90,7 @@
</tbody> </tbody>
</table> </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> </ng-container>
</sqx-panel> </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="form-group row">
<div class="col-9 offset-3"> <div class="col-9 offset-3">
<ng-container *ngIf="!isRedirected"> <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 Request access token with twitter
</button> </button>
</ng-container> </ng-container>

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

@ -6,14 +6,14 @@
</ng-container> </ng-container>
<ng-container menu> <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 <i class="icon-reset"></i> Refresh
</button> </button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></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 <i class="icon-plus"></i> New
</button> </button>
</ng-container> </ng-container>
@ -24,7 +24,7 @@
<div class="table-items-row table-items-row-empty" *ngIf="rules.length === 0"> <div class="table-items-row table-items-row-empty" *ngIf="rules.length === 0">
No Rule created yet. 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 <i class="icon icon-plus"></i> Add Rule
</button> </button>
</div> </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> <button type="reset" class="float-left btn btn-secondary" (click)="complete()">Cancel</button>
<div class="float-right" *ngIf="!isEditing"> <div class="float-right" *ngIf="!isEditing">
<button class="btn btn-outline-success mr-1" (click)="addField(false, false)">Create and close</button> <button type="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 type="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-success" (click)="addField(false, true)">Create and edit field</button>
</div> </div>
<div class="float-right" *ngIf="isEditing"> <div class="float-right" *ngIf="isEditing">
<button class="btn btn-success mr-1" (click)="save(true)">Save and add field</button> <button type="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-primary" (click)="save()">Save and close</button>
</div> </div>
</ng-container> </ng-container>
</sqx-modal-dialog> </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> <button type="button" class="btn btn-text-secondary ml-1" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #optionsButton>
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </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"> <a class="dropdown-item" (click)="enableField()" *ngIf="field.isDisabled">
Enable in UI Enable in UI
</a> </a>
@ -118,7 +118,7 @@
<div class="nested-field nested-field-add"> <div class="nested-field nested-field-add">
<span class="nested-field-line-h"></span> <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 <i class="icon icon-plus"></i> Add Nested Field
</button> </button>
</div> </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> <button type="button" class="btn btn-text-secondary ml-1" (click)="editOptionsDropdown.toggle()" [class.active]="editOptionsDropdown.isOpen | async" #buttonOptions>
<i class="icon-dots"></i> <i class="icon-dots"></i>
</button> </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()"> <a class="dropdown-item" (click)="configureScriptsDialog.show()">
Edit Scripts Edit Scripts
</a> </a>
@ -61,7 +61,7 @@
<div class="table-items-row table-items-row-empty" *ngIf="schema && schema.fields.length === 0"> <div class="table-items-row table-items-row-empty" *ngIf="schema && schema.fields.length === 0">
No field created yet. 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 <i class="icon icon-plus"></i> Add Field
</button> </button>
</div> </div>
@ -75,7 +75,7 @@
</div> </div>
</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> <i class="icon icon-plus field-button-icon"></i> <div class="field-button-text">Add Field</div>
</button> </button>
</ng-container> </ng-container>

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

@ -7,10 +7,9 @@
// tslint:disable:no-shadowed-variable // 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 { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { onErrorResumeNext } from 'rxjs/operators';
import { filter, onErrorResumeNext } from 'rxjs/operators';
import { import {
AppsState, AppsState,
@ -21,6 +20,7 @@ import {
MessageBus, MessageBus,
ModalModel, ModalModel,
PatternsState, PatternsState,
ResourceOwner,
SchemaDetailsDto, SchemaDetailsDto,
SchemasState, SchemasState,
Types Types
@ -38,9 +38,7 @@ import {
fadeAnimation fadeAnimation
] ]
}) })
export class SchemaPageComponent implements OnDestroy, OnInit { export class SchemaPageComponent extends ResourceOwner implements OnInit {
private selectedSchemaSubscription: Subscription;
public fieldTypes = fieldTypes; public fieldTypes = fieldTypes;
public schemaExport: any; public schemaExport: any;
@ -64,22 +62,21 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
private readonly router: Router, private readonly router: Router,
private readonly messageBus: MessageBus private readonly messageBus: MessageBus
) { ) {
} super();
public ngOnDestroy() {
this.selectedSchemaSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.patternsState.load().pipe(onErrorResumeNext()).subscribe(); this.patternsState.load().pipe(onErrorResumeNext()).subscribe();
this.selectedSchemaSubscription = this.own(
this.schemasState.selectedSchema.pipe(filter(s => !!s)) this.schemasState.selectedSchema
.subscribe(schema => { .subscribe(schema => {
this.schema = schema!; if (schema) {
this.schema = schema;
this.export(); this.export();
}); }
}));
} }
public publish() { public publish() {
@ -95,8 +92,7 @@ export class SchemaPageComponent implements OnDestroy, OnInit {
} }
public trackByField(index: number, field: FieldDto) { public trackByField(index: number, field: FieldDto) {
const a = this; return field.fieldId + this.schema.id;
return field.fieldId + a.schema.id;
} }
public deleteSchema() { 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 { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { BooleanFieldPropertiesDto, FieldDto } from '@app/shared'; import {
BooleanFieldPropertiesDto,
FieldDto,
hasNoValue$
} from '@app/shared';
@Component({ @Component({
selector: 'sqx-boolean-validation', selector: 'sqx-boolean-validation',
@ -37,7 +40,6 @@ export class BooleanValidationComponent implements OnInit {
new FormControl(this.properties.inlineEditable)); new FormControl(this.properties.inlineEditable));
this.showDefaultValue = this.showDefaultValue =
this.editForm.controls['isRequired'].valueChanges.pipe( hasNoValue$(this.editForm.controls['isRequired']);
startWith(this.properties.isRequired), map(x => !x));
} }
} }

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

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

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. * 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 { FormControl, FormGroup, Validators } from '@angular/forms';
import { Observable, Subscription } from 'rxjs'; import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { FieldDto, StringFieldPropertiesDto } from '@app/shared'; import {
FieldDto,
ResourceOwner,
StringFieldPropertiesDto,
value$
} from '@app/shared';
@Component({ @Component({
selector: 'sqx-string-ui', selector: 'sqx-string-ui',
styleUrls: ['string-ui.component.scss'], styleUrls: ['string-ui.component.scss'],
templateUrl: 'string-ui.component.html' templateUrl: 'string-ui.component.html'
}) })
export class StringUIComponent implements OnDestroy, OnInit { export class StringUIComponent extends ResourceOwner implements OnInit {
private hideAllowedValuesSubscription: Subscription;
private hideInlineEditableSubscription: Subscription;
@Input() @Input()
public editForm: FormGroup; public editForm: FormGroup;
@ -33,11 +35,6 @@ export class StringUIComponent implements OnDestroy, OnInit {
public hideAllowedValues: Observable<boolean>; public hideAllowedValues: Observable<boolean>;
public hideInlineEditable: Observable<boolean>; public hideInlineEditable: Observable<boolean>;
public ngOnDestroy() {
this.hideAllowedValuesSubscription.unsubscribe();
this.hideInlineEditableSubscription.unsubscribe();
}
public ngOnInit() { public ngOnInit() {
this.editForm.setControl('editor', this.editForm.setControl('editor',
new FormControl(this.properties.editor, [ new FormControl(this.properties.editor, [
@ -51,25 +48,23 @@ export class StringUIComponent implements OnDestroy, OnInit {
new FormControl(this.properties.inlineEditable)); new FormControl(this.properties.inlineEditable));
this.hideAllowedValues = this.hideAllowedValues =
this.editForm.controls['editor'].valueChanges.pipe( value$<string>(this.editForm.controls['editor']).pipe(map(x => !(x && (x === 'Radio' || x === 'Dropdown'))));
startWith(this.properties.editor), map(x => !(x && (x === 'Radio' || x === 'Dropdown'))));
this.hideInlineEditable = this.hideInlineEditable =
this.editForm.controls['editor'].valueChanges.pipe( value$<string>(this.editForm.controls['editor']).pipe(map(x => !(x && (x === 'Input' || x === 'Dropdown' || x === 'Slug'))));
startWith(this.properties.editor), map(x => !(x && (x === 'Input' || x === 'Dropdown' || x === 'Slug'))));
this.hideAllowedValuesSubscription = this.own(
this.hideAllowedValues.subscribe(isSelection => { this.hideAllowedValues.subscribe(isSelection => {
if (isSelection) { if (isSelection) {
this.editForm.controls['allowedValues'].setValue(undefined); this.editForm.controls['allowedValues'].setValue(undefined);
} }
}); }));
this.hideInlineEditableSubscription = this.own(
this.hideInlineEditable.subscribe(isSelection => { this.hideInlineEditable.subscribe(isSelection => {
if (isSelection) { if (isSelection) {
this.editForm.controls['inlineEditable'].setValue(false); 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> </div>
</div> </div>
<small class="col-3" style="align-self: center;"> <small class="col-3" style="align-self: center">
{{patternName}} {{patternName}}
</small> </small>
</div> </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. * 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 { FormControl, FormGroup } from '@angular/forms';
import { Observable, Subscription } from 'rxjs'; import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { import {
AppPatternDto, AppPatternDto,
FieldDto, FieldDto,
hasNoValue$,
ImmutableArray, ImmutableArray,
ModalModel, ModalModel,
ResourceOwner,
RootFieldDto, RootFieldDto,
StringFieldPropertiesDto, StringFieldPropertiesDto,
Types Types
@ -25,9 +26,7 @@ import {
styleUrls: ['string-validation.component.scss'], styleUrls: ['string-validation.component.scss'],
templateUrl: 'string-validation.component.html' templateUrl: 'string-validation.component.html'
}) })
export class StringValidationComponent implements OnDestroy, OnInit { export class StringValidationComponent extends ResourceOwner implements OnInit {
private patternSubscription: Subscription;
@Input() @Input()
public editForm: FormGroup; public editForm: FormGroup;
@ -49,10 +48,6 @@ export class StringValidationComponent implements OnDestroy, OnInit {
public showUnique: boolean; public showUnique: boolean;
public ngOnDestroy() {
this.patternSubscription.unsubscribe();
}
public ngOnInit() { public ngOnInit() {
this.showUnique = Types.is(this.field, RootFieldDto) && !this.field.isLocalizable; 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)); new FormControl(this.properties.defaultValue));
this.showDefaultValue = this.showDefaultValue =
this.editForm.controls['isRequired'].valueChanges.pipe( hasNoValue$(this.editForm.controls['isRequired']);
startWith(this.properties.isRequired), map(x => !x));
this.showPatternSuggestions = this.showPatternSuggestions =
this.editForm.controls['pattern'].valueChanges.pipe( hasNoValue$(this.editForm.controls['pattern']);
startWith(''), map(x => !x || x.trim().length === 0));
this.showPatternMessage = this.showPatternMessage =
this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim().length > 0; this.editForm.controls['pattern'].value && this.editForm.controls['pattern'].value.trim().length > 0;
this.patternSubscription = this.own(
this.editForm.controls['pattern'].valueChanges this.editForm.controls['pattern'].valueChanges
.subscribe((value: string) => { .subscribe((value: string) => {
if (!value || value.length === 0) { if (!value || value.length === 0) {
this.editForm.controls['patternMessage'].setValue(undefined); this.editForm.controls['patternMessage'].setValue(undefined);
} }
this.setPatternName(); this.setPatternName();
}); }));
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+g" (trigger)="addSchemaDialog.show()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></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> <i class="icon-plus"></i>
</button> </button>

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

@ -5,10 +5,9 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * 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 { FormBuilder, FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { map, onErrorResumeNext } from 'rxjs/operators'; import { map, onErrorResumeNext } from 'rxjs/operators';
import { import {
@ -16,6 +15,7 @@ import {
CreateCategoryForm, CreateCategoryForm,
DialogModel, DialogModel,
MessageBus, MessageBus,
ResourceOwner,
SchemaDto, SchemaDto,
SchemasState SchemasState
} from '@app/shared'; } from '@app/shared';
@ -27,9 +27,7 @@ import { SchemaCloning } from './../messages';
styleUrls: ['./schemas-page.component.scss'], styleUrls: ['./schemas-page.component.scss'],
templateUrl: './schemas-page.component.html' templateUrl: './schemas-page.component.html'
}) })
export class SchemasPageComponent implements OnDestroy, OnInit { export class SchemasPageComponent extends ResourceOwner implements OnInit {
private schemaCloningSubscription: Subscription;
public addSchemaDialog = new DialogModel(); public addSchemaDialog = new DialogModel();
public addCategoryForm = new CreateCategoryForm(this.formBuilder); public addCategoryForm = new CreateCategoryForm(this.formBuilder);
@ -45,27 +43,25 @@ export class SchemasPageComponent implements OnDestroy, OnInit {
private readonly route: ActivatedRoute, private readonly route: ActivatedRoute,
private readonly router: Router private readonly router: Router
) { ) {
} super();
public ngOnDestroy() {
this.schemaCloningSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.schemaCloningSubscription = this.own(
this.messageBus.of(SchemaCloning) this.messageBus.of(SchemaCloning)
.subscribe(m => { .subscribe(m => {
this.import = m.schema; this.import = m.schema;
this.addSchemaDialog.show(); this.addSchemaDialog.show();
}); }));
this.route.params.pipe(map(q => q['showDialog'])) this.own(
.subscribe(showDialog => { this.route.params.pipe(map(q => q['showDialog']))
if (showDialog) { .subscribe(showDialog => {
this.addSchemaDialog.show(); if (showDialog) {
} this.addSchemaDialog.show();
}); }
}));
this.schemasState.load().pipe(onErrorResumeNext()).subscribe(); 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>
<ng-container menu> <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 <i class="icon-reset"></i> Refresh
</button> </button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut> <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 Start Backup
</button> </button>
</ng-container> </ng-container>
@ -27,7 +27,7 @@
<div class="table-items-row table-items-row-empty" *ngIf="backups.length === 0"> <div class="table-items-row table-items-row-empty" *ngIf="backups.length === 0">
No backups created yet. 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 Start Backup
</button> </button>
</div> </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. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Subscription, timer } from 'rxjs'; import { timer } from 'rxjs';
import { onErrorResumeNext, switchMap } from 'rxjs/operators'; import { onErrorResumeNext, switchMap } from 'rxjs/operators';
import { import {
AppsState, AppsState,
BackupDto, BackupDto,
BackupsState BackupsState,
ResourceOwner
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -20,25 +21,20 @@ import {
styleUrls: ['./backups-page.component.scss'], styleUrls: ['./backups-page.component.scss'],
templateUrl: './backups-page.component.html' templateUrl: './backups-page.component.html'
}) })
export class BackupsPageComponent implements OnInit, OnDestroy { export class BackupsPageComponent extends ResourceOwner implements OnInit {
private timerSubscription: Subscription;
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,
public readonly backupsState: BackupsState public readonly backupsState: BackupsState
) { ) {
} super();
public ngOnDestroy() {
this.timerSubscription.unsubscribe();
} }
public ngOnInit() { public ngOnInit() {
this.backupsState.load().pipe(onErrorResumeNext()).subscribe(); this.backupsState.load().pipe(onErrorResumeNext()).subscribe();
this.timerSubscription = this.own(
timer(3000, 3000).pipe(switchMap(t => this.backupsState.load(true, true).pipe(onErrorResumeNext()))) timer(3000, 3000).pipe(switchMap(() => this.backupsState.load(true, true).pipe(onErrorResumeNext())))
.subscribe(); .subscribe());
} }
public reload() { 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 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> <i class="icon-close"></i>
</button> </button>
</form> </form>
@ -25,7 +25,7 @@
</ng-container> </ng-container>
</div> </div>
<div class="col-auto"> <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>
<div class="col-auto cell-actions"> <div class="col-auto cell-actions">
<button type="button" class="btn btn-text-danger" <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>
<ng-container menu> <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 <i class="icon-reset"></i> Refresh
</button> </button>

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

@ -6,7 +6,7 @@
</ng-container> </ng-container>
<ng-container menu> <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 <i class="icon-reset"></i> Refresh
</button> </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 { Component, Injectable, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, onErrorResumeNext, withLatestFrom } from 'rxjs/operators'; import { onErrorResumeNext, withLatestFrom } from 'rxjs/operators';
import { import {
AppContributorDto, AppContributorDto,
@ -34,7 +34,7 @@ export class UsersDataSource implements AutocompleteSource {
public find(query: string): Observable<any[]> { public find(query: string): Observable<any[]> {
return this.usersService.getUsers(query).pipe( 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[] = []; const results: any[] = [];
for (let user of users) { for (let user of users) {

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

Loading…
Cancel
Save