Browse Source

Merge branch 'master' of github.com:Squidex/squidex

# Conflicts:
#	src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs
#	src/Squidex/app/features/content/shared/content-item.component.html
#	src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
#	src/Squidex/app/features/settings/pages/contributors/contributors-page.component.scss
#	src/Squidex/app/shared/components/saved-queries.component.ts
#	src/Squidex/app/shared/state/queries.spec.ts
#	src/Squidex/app/shared/state/ui.state.ts
pull/419/head
Sebastian Stehle 6 years ago
parent
commit
a923dafff5
  1. 197
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  2. 30
      src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs
  3. 4
      src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs
  4. 72
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs
  5. 122
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
  6. 80
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs
  7. 50
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs
  8. 286
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs
  9. 35
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs
  10. 12
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs
  11. 17
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs
  12. 39
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs
  13. 14
      src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  14. 18
      src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs
  15. 44
      src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs
  16. 2
      src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs
  17. 31
      src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs
  18. 2
      src/Squidex.Domain.Apps.Entities/Comments/ICommentsGrain.cs
  19. 18
      src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs
  20. 14
      src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs
  21. 5
      src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs
  22. 8
      src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs
  23. 6
      src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs
  24. 4
      src/Squidex.Domain.Apps.Entities/IAppProvider.cs
  25. 15
      src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs
  26. 2
      src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs
  27. 17
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs
  28. 15
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs
  29. 56
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs
  30. 55
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs
  31. 118
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs
  32. 4
      src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs
  33. 13
      src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs
  34. 9
      src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs
  35. 17
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs
  36. 17
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs
  37. 56
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs
  38. 62
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs
  39. 181
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs
  40. 10
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  41. 27
      src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs
  42. 35
      src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs
  43. 62
      src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs
  44. 16
      src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs
  45. 136
      src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs
  46. 16
      src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs
  47. 2
      src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  48. 9
      src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs
  49. 35
      src/Squidex/Config/Domain/EntitiesServices.cs
  50. 6
      src/Squidex/app-config/webpack.config.js
  51. 2
      src/Squidex/app/app.module.ts
  52. 2
      src/Squidex/app/features/administration/declarations.ts
  53. 4
      src/Squidex/app/features/administration/module.ts
  54. 66
      src/Squidex/app/features/administration/pages/event-consumers/event-consumer.component.ts
  55. 27
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  56. 12
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  57. 5
      src/Squidex/app/features/administration/pages/users/user-page.component.html
  58. 55
      src/Squidex/app/features/administration/pages/users/user.component.ts
  59. 23
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  60. 11
      src/Squidex/app/features/administration/pages/users/users-page.component.ts
  61. 4
      src/Squidex/app/features/administration/services/event-consumers.service.ts
  62. 2
      src/Squidex/app/features/api/api-area.component.html
  63. 2
      src/Squidex/app/features/api/pages/graphql/graphql-page.component.html
  64. 5
      src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts
  65. 6
      src/Squidex/app/features/apps/pages/news-dialog.component.ts
  66. 2
      src/Squidex/app/features/assets/pages/assets-page.component.html
  67. 5
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  68. 2
      src/Squidex/app/features/content/declarations.ts
  69. 4
      src/Squidex/app/features/content/module.ts
  70. 3
      src/Squidex/app/features/content/pages/comments/comments-page.component.ts
  71. 9
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  72. 14
      src/Squidex/app/features/content/pages/content/content-page.component.html
  73. 8
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  74. 12
      src/Squidex/app/features/content/pages/content/field-languages.component.ts
  75. 4
      src/Squidex/app/features/content/pages/contents/contents-filters-page.component.ts
  76. 29
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  77. 7
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  78. 2
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.html
  79. 10
      src/Squidex/app/features/content/pages/schemas/schemas-page.component.ts
  80. 8
      src/Squidex/app/features/content/shared/assets-editor.component.html
  81. 98
      src/Squidex/app/features/content/shared/content-item.component.html
  82. 3
      src/Squidex/app/features/content/shared/content-status.component.ts
  83. 3
      src/Squidex/app/features/content/shared/content-value.component.ts
  84. 101
      src/Squidex/app/features/content/shared/content.component.html
  85. 0
      src/Squidex/app/features/content/shared/content.component.scss
  86. 12
      src/Squidex/app/features/content/shared/content.component.ts
  87. 19
      src/Squidex/app/features/content/shared/contents-selector.component.html
  88. 9
      src/Squidex/app/features/content/shared/contents-selector.component.ts
  89. 3
      src/Squidex/app/features/content/shared/due-time-selector.component.ts
  90. 19
      src/Squidex/app/features/content/shared/references-editor.component.html
  91. 4
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.html
  92. 15
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  93. 1
      src/Squidex/app/features/rules/declarations.ts
  94. 2
      src/Squidex/app/features/rules/module.ts
  95. 2
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.html
  96. 5
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts
  97. 24
      src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts
  98. 87
      src/Squidex/app/features/rules/pages/rules/rule.component.ts
  99. 42
      src/Squidex/app/features/rules/pages/rules/rules-page.component.html
  100. 2
      src/Squidex/app/features/rules/pages/rules/rules-page.component.ts

197
src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -7,9 +7,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Indexes; using Squidex.Domain.Apps.Entities.Apps.Indexes;
using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules;
@ -18,50 +16,49 @@ using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Domain.Apps.Entities.Schemas.Indexes;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Security; using Squidex.Infrastructure.Security;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
{ {
public sealed class AppProvider : IAppProvider public sealed class AppProvider : IAppProvider
{ {
private readonly IGrainFactory grainFactory;
private readonly ILocalCache localCache; private readonly ILocalCache localCache;
private readonly IAppsIndex indexForApps;
private readonly IRulesIndex indexRules;
private readonly ISchemasIndex indexSchemas;
public AppProvider(IGrainFactory grainFactory, ILocalCache localCache) public AppProvider(ILocalCache localCache, IAppsIndex indexForApps, IRulesIndex indexRules, ISchemasIndex indexSchemas)
{ {
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(indexForApps, nameof(indexForApps));
Guard.NotNull(indexRules, nameof(indexRules));
Guard.NotNull(indexSchemas, nameof(indexSchemas));
Guard.NotNull(localCache, nameof(localCache)); Guard.NotNull(localCache, nameof(localCache));
this.grainFactory = grainFactory;
this.localCache = localCache; this.localCache = localCache;
this.indexForApps = indexForApps;
this.indexRules = indexRules;
this.indexSchemas = indexSchemas;
} }
public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id)
{ {
return localCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () => return localCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () =>
{ {
using (Profiler.TraceMethod<AppProvider>()) var app = await GetAppAsync(appId);
{
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
if (!IsExisting(app))
{
return (null, null);
}
var schema = await GetSchemaAsync(appId, id, false); if (app == null)
{
return (null, null);
}
if (schema == null) var schema = await GetSchemaAsync(appId, id, false);
{
return (null, null);
}
return (app.Value, schema); if (schema == null)
{
return (null, null);
} }
return (app, schema);
}); });
} }
@ -69,10 +66,7 @@ namespace Squidex.Domain.Apps.Entities
{ {
return localCache.GetOrCreateAsync($"GetAppAsync({appId})", async () => return localCache.GetOrCreateAsync($"GetAppAsync({appId})", async () =>
{ {
using (Profiler.TraceMethod<AppProvider>()) return await indexForApps.GetAppAsync(appId);
{
return await GetAppByIdAsync(appId);
}
}); });
} }
@ -80,17 +74,15 @@ namespace Squidex.Domain.Apps.Entities
{ {
return localCache.GetOrCreateAsync($"GetAppAsync({appName})", async () => return localCache.GetOrCreateAsync($"GetAppAsync({appName})", async () =>
{ {
using (Profiler.TraceMethod<AppProvider>()) return await indexForApps.GetAppAsync(appName);
{ });
var appId = await GetAppIdAsync(appName); }
if (appId == Guid.Empty)
{
return null;
}
return await GetAppByIdAsync(appId); public Task<List<IAppEntity>> GetUserAppsAsync(string userId, PermissionSet permissions)
} {
return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () =>
{
return await indexForApps.GetAppsForUserAsync(userId, permissions);
}); });
} }
@ -98,17 +90,7 @@ namespace Squidex.Domain.Apps.Entities
{ {
return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () => return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () =>
{ {
using (Profiler.TraceMethod<AppProvider>()) return await indexSchemas.GetSchemaAsync(appId, name);
{
var schemaId = await GetSchemaIdAsync(appId, name);
if (schemaId == Guid.Empty)
{
return null;
}
return await GetSchemaAsync(appId, schemaId, false);
}
}); });
} }
@ -116,17 +98,7 @@ namespace Squidex.Domain.Apps.Entities
{ {
return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () => return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () =>
{ {
using (Profiler.TraceMethod<AppProvider>()) return await indexSchemas.GetSchemaAsync(appId, id, allowDeleted);
{
var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync();
if (!IsExisting(schema, allowDeleted) || schema.Value.AppId.Id != appId)
{
return null;
}
return schema.Value;
}
}); });
} }
@ -134,16 +106,7 @@ namespace Squidex.Domain.Apps.Entities
{ {
return localCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () => return localCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () =>
{ {
using (Profiler.TraceMethod<AppProvider>()) return await indexSchemas.GetSchemasAsync(appId);
{
var ids = await grainFactory.GetGrain<ISchemasByAppIndex>(appId).GetSchemaIdsAsync();
var schemas =
await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync()));
return schemas.Where(s => IsFound(s.Value)).Select(s => s.Value).ToList();
}
}); });
} }
@ -151,100 +114,8 @@ namespace Squidex.Domain.Apps.Entities
{ {
return localCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () => return localCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () =>
{ {
using (Profiler.TraceMethod<AppProvider>()) return await indexRules.GetRulesAsync(appId);
{
var ids = await grainFactory.GetGrain<IRulesByAppIndex>(appId).GetRuleIdsAsync();
var rules =
await Task.WhenAll(
ids.Select(id => grainFactory.GetGrain<IRuleGrain>(id).GetStateAsync()));
return rules.Where(r => IsFound(r.Value)).Select(r => r.Value).ToList();
}
}); });
} }
public Task<List<IAppEntity>> GetUserApps(string userId, PermissionSet permissions)
{
Guard.NotNull(userId, nameof(userId));
Guard.NotNull(permissions, nameof(permissions));
return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () =>
{
using (Profiler.TraceMethod<AppProvider>())
{
var ids =
await Task.WhenAll(
GetAppIdsByUserAsync(userId),
GetAppIdsAsync(permissions.ToAppNames()));
var apps =
await Task.WhenAll(ids
.SelectMany(x => x)
.Select(id => grainFactory.GetGrain<IAppGrain>(id).GetStateAsync()));
return apps.Where(a => IsFound(a.Value)).Select(a => a.Value).ToList();
}
});
}
private async Task<IAppEntity> GetAppByIdAsync(Guid appId)
{
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
if (!IsExisting(app))
{
return null;
}
return app.Value;
}
private async Task<List<Guid>> GetAppIdsByUserAsync(string userId)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByUserIndex>(userId).GetAppIdsAsync();
}
}
private async Task<List<Guid>> GetAppIdsAsync(IEnumerable<string> names)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id).GetAppIdsAsync(names.ToArray());
}
}
private async Task<Guid> GetAppIdAsync(string name)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id).GetAppIdAsync(name);
}
}
private async Task<Guid> GetSchemaIdAsync(Guid appId, string name)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<ISchemasByAppIndex>(appId).GetSchemaIdAsync(name);
}
}
private static bool IsFound(IEntityWithVersion entity)
{
return entity.Version > EtagVersion.Empty;
}
private static bool IsExisting(J<IAppEntity> app)
{
return IsFound(app.Value) && !app.Value.IsArchived;
}
private static bool IsExisting(J<ISchemaEntity> schema, bool allowDeleted)
{
return IsFound(schema.Value) && (!schema.Value.IsDeleted || allowDeleted);
}
} }
} }

30
src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs

@ -16,7 +16,6 @@ using Squidex.Domain.Apps.Events.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Orleans;
using Squidex.Shared.Users; using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.Apps namespace Squidex.Domain.Apps.Entities.Apps
@ -25,29 +24,26 @@ namespace Squidex.Domain.Apps.Entities.Apps
{ {
private const string UsersFile = "Users.json"; private const string UsersFile = "Users.json";
private const string SettingsFile = "Settings.json"; private const string SettingsFile = "Settings.json";
private readonly IGrainFactory grainFactory;
private readonly IAppUISettings appUISettings; private readonly IAppUISettings appUISettings;
private readonly IAppsIndex appsIndex;
private readonly IUserResolver userResolver; private readonly IUserResolver userResolver;
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 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 bool isReserved; private string appReservation;
private string appName; private string appName;
public override string Name { get; } = "Apps"; public override string Name { get; } = "Apps";
public BackupApps(IGrainFactory grainFactory, IAppUISettings appUISettings, IUserResolver userResolver) public BackupApps(IAppUISettings appUISettings, IAppsIndex appsIndex, IUserResolver userResolver)
{ {
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(appsIndex, nameof(appsIndex));
Guard.NotNull(userResolver, nameof(userResolver)); Guard.NotNull(userResolver, nameof(userResolver));
Guard.NotNull(appUISettings, nameof(appUISettings)); Guard.NotNull(appUISettings, nameof(appUISettings));
this.grainFactory = grainFactory; this.appsIndex = appsIndex;
this.appUISettings = appUISettings; this.appUISettings = appUISettings;
this.userResolver = userResolver; this.userResolver = userResolver;
appsByNameIndex = grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id);
} }
public override async Task BackupEventAsync(Envelope<IEvent> @event, Guid appId, BackupWriter writer) public override async Task BackupEventAsync(Envelope<IEvent> @event, Guid appId, BackupWriter writer)
@ -128,7 +124,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
private async Task ReserveAppAsync(Guid appId) private async Task ReserveAppAsync(Guid appId)
{ {
if (!(isReserved = await appsByNameIndex.ReserveAppAsync(appId, appName))) appReservation = await appsIndex.ReserveAsync(appId, appName);
if (appReservation == null)
{ {
throw new BackupRestoreException("The app id or name is not available."); throw new BackupRestoreException("The app id or name is not available.");
} }
@ -136,10 +134,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
public override async Task CleanupRestoreErrorAsync(Guid appId) public override async Task CleanupRestoreErrorAsync(Guid appId)
{ {
if (isReserved) await appsIndex.RemoveReservationAsync(appReservation);
{
await appsByNameIndex.RemoveReservationAsync(appId, appName);
}
} }
private RefToken MapUser(string userId, RefToken fallback) private RefToken MapUser(string userId, RefToken fallback)
@ -199,12 +194,9 @@ namespace Squidex.Domain.Apps.Entities.Apps
public override async Task CompleteRestoreAsync(Guid appId, BackupReader reader) public override async Task CompleteRestoreAsync(Guid appId, BackupReader reader)
{ {
await appsByNameIndex.AddAppAsync(appId, appName); await appsIndex.AddAsync(appReservation);
foreach (var user in contributors) await appsIndex.RebuildByContributorsAsync(appId, contributors);
{
await grainFactory.GetGrain<IAppsByUserIndex>(user).AddAppAsync(appId);
}
} }
} }
} }

4
src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs

@ -17,13 +17,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics
{ {
public sealed class OrleansAppsHealthCheck : IHealthCheck public sealed class OrleansAppsHealthCheck : IHealthCheck
{ {
private readonly IAppsByNameIndex index; private readonly IAppsByNameIndexGrain index;
public OrleansAppsHealthCheck(IGrainFactory grainFactory) public OrleansAppsHealthCheck(IGrainFactory grainFactory)
{ {
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(grainFactory, nameof(grainFactory));
index = grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id); index = grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id);
} }
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)

72
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexCommandMiddleware.cs

@ -1,72 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{
public sealed class AppsByNameIndexCommandMiddleware : ICommandMiddleware
{
private readonly IAppsByNameIndex index;
public AppsByNameIndexCommandMiddleware(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
index = grainFactory.GetGrain<IAppsByNameIndex>(SingleGrain.Id);
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
var createApp = context.Command as CreateApp;
var isReserved = false;
try
{
if (createApp != null)
{
isReserved = await index.ReserveAppAsync(createApp.AppId, createApp.Name);
if (!isReserved)
{
var error = new ValidationError("An app with the same name already exists.", nameof(createApp.Name));
throw new ValidationException("Cannot create app.", error);
}
}
await next();
if (context.IsCompleted)
{
if (createApp != null)
{
await index.AddAppAsync(createApp.AppId, createApp.Name);
}
else if (context.Command is ArchiveApp archiveApp)
{
await index.RemoveAppAsync(archiveApp.AppId);
}
}
}
finally
{
if (isReserved)
{
await index.RemoveReservationAsync(createApp.AppId, createApp.Name);
}
}
}
}
}

122
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs

@ -6,128 +6,22 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Orleans.Indexes;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{ {
public sealed class AppsByNameIndexGrain : GrainOfString, IAppsByNameIndex public sealed class AppsByNameIndexGrain : UniqueNameIndexGrain<AppsByNameIndexState, Guid>, IAppsByNameIndexGrain
{ {
private readonly HashSet<Guid> reservedIds = new HashSet<Guid>(); public AppsByNameIndexGrain(IGrainState<AppsByNameIndexState> state)
private readonly HashSet<string> reservedNames = new HashSet<string>(); : base(state)
private readonly IGrainState<GrainState> state;
[CollectionName("Index_AppsByName")]
public sealed class GrainState
{
public Dictionary<string, Guid> Apps { get; set; } = new Dictionary<string, Guid>(StringComparer.Ordinal);
}
public AppsByNameIndexGrain(IGrainState<GrainState> state)
{
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task RebuildAsync(Dictionary<string, Guid> apps)
{
state.Value = new GrainState { Apps = apps };
return state.WriteAsync();
}
public Task<bool> ReserveAppAsync(Guid appId, string name)
{
var canReserve = !IsInUse(appId, name) && !IsReserved(appId, name);
if (canReserve)
{
reservedIds.Add(appId);
reservedNames.Add(name);
}
return Task.FromResult(canReserve);
}
private bool IsInUse(Guid appId, string name)
{
return state.Value.Apps.ContainsKey(name) || state.Value.Apps.Any(x => x.Value == appId);
}
private bool IsReserved(Guid appId, string name)
{ {
return reservedIds.Contains(appId) || reservedNames.Contains(name);
}
public Task RemoveReservationAsync(Guid appId, string name)
{
reservedIds.Remove(appId);
reservedNames.Remove(name);
return TaskHelper.Done;
}
public Task AddAppAsync(Guid appId, string name)
{
state.Value.Apps[name] = appId;
reservedIds.Remove(appId);
reservedNames.Remove(name);
return state.WriteAsync();
}
public Task RemoveAppAsync(Guid appId)
{
var name = state.Value.Apps.FirstOrDefault(x => x.Value == appId).Key;
if (!string.IsNullOrWhiteSpace(name))
{
state.Value.Apps.Remove(name);
reservedIds.Remove(appId);
reservedNames.Remove(name);
}
return state.WriteAsync();
}
public Task<List<Guid>> GetAppIdsAsync(params string[] names)
{
var appIds = new List<Guid>();
foreach (var appName in names)
{
if (state.Value.Apps.TryGetValue(appName, out var appId))
{
appIds.Add(appId);
}
}
return Task.FromResult(appIds);
}
public Task<Guid> GetAppIdAsync(string appName)
{
state.Value.Apps.TryGetValue(appName, out var appId);
return Task.FromResult(appId);
}
public Task<List<Guid>> GetAppIdsAsync()
{
return Task.FromResult(state.Value.Apps.Values.ToList());
} }
}
public Task<long> CountAsync() [CollectionName("Index_AppsByName")]
{ public sealed class AppsByNameIndexState : UniqueNameIndexState<Guid>
return Task.FromResult((long)state.Value.Apps.Count); {
}
} }
} }

80
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexCommandMiddleware.cs

@ -1,80 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{
public sealed class AppsByUserIndexCommandMiddleware : ICommandMiddleware
{
private readonly IGrainFactory grainFactory;
public AppsByUserIndexCommandMiddleware(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory;
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (context.IsCompleted)
{
switch (context.Command)
{
case CreateApp createApp:
await Index(GetUserId(createApp)).AddAppAsync(createApp.AppId);
break;
case AssignContributor assignContributor:
await Index(GetUserId(assignContributor)).AddAppAsync(assignContributor.AppId);
break;
case RemoveContributor removeContributor:
await Index(GetUserId(removeContributor)).RemoveAppAsync(removeContributor.AppId);
break;
case ArchiveApp archiveApp:
{
var appState = await grainFactory.GetGrain<IAppGrain>(archiveApp.AppId).GetStateAsync();
foreach (var contributorId in appState.Value.Contributors.Keys)
{
await Index(contributorId).RemoveAppAsync(archiveApp.AppId);
}
break;
}
}
}
await next();
}
private static string GetUserId(CreateApp createApp)
{
return createApp.Actor.Identifier;
}
private static string GetUserId(AssignContributor assignContributor)
{
return assignContributor.ContributorId;
}
private static string GetUserId(RemoveContributor removeContributor)
{
return removeContributor.ContributorId;
}
private IAppsByUserIndex Index(string id)
{
return grainFactory.GetGrain<IAppsByUserIndex>(id);
}
}
}

50
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs

@ -6,56 +6,22 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Orleans.Indexes;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{ {
public sealed class AppsByUserIndexGrain : GrainOfString, IAppsByUserIndex public sealed class AppsByUserIndexGrain : IdsIndexGrain<AppsByUserIndex, Guid>, IAppsByUserIndexGrain
{ {
private readonly IGrainState<GrainState> state; public AppsByUserIndexGrain(IGrainState<AppsByUserIndex> state)
: base(state)
[CollectionName("Index_AppsByUser")]
public sealed class GrainState
{
public HashSet<Guid> Apps { get; set; } = new HashSet<Guid>();
}
public AppsByUserIndexGrain(IGrainState<GrainState> state)
{ {
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task RebuildAsync(HashSet<Guid> apps)
{
state.Value = new GrainState { Apps = apps };
return state.WriteAsync();
}
public Task AddAppAsync(Guid appId)
{
state.Value.Apps.Add(appId);
return state.WriteAsync();
}
public Task RemoveAppAsync(Guid appId)
{
state.Value.Apps.Remove(appId);
return state.WriteAsync();
} }
}
public Task<List<Guid>> GetAppIdsAsync() [CollectionName("Index_AppsByUser")]
{ public sealed class AppsByUserIndex : IdsIndexState<Guid>
return Task.FromResult(state.Value.Apps.ToList()); {
}
} }
} }

286
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs

@ -0,0 +1,286 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Security;
using Squidex.Infrastructure.Validation;
using Squidex.Shared;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{
public sealed class AppsIndex : IAppsIndex, ICommandMiddleware
{
private readonly IGrainFactory grainFactory;
public AppsIndex(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory;
}
public async Task RebuildByContributorsAsync(Guid appId, HashSet<string> contributors)
{
foreach (var contributorId in contributors)
{
await Index(contributorId).AddAsync(appId);
}
}
public Task RebuildByContributorsAsync(string contributorId, HashSet<Guid> apps)
{
return Index(contributorId).RebuildAsync(apps);
}
public Task RebuildAsync(Dictionary<string, Guid> appsByName)
{
return Index().RebuildAsync(appsByName);
}
public Task RemoveReservationAsync(string token)
{
return Index().RemoveReservationAsync(token);
}
public Task<List<Guid>> GetIdsAsync()
{
return Index().GetIdsAsync();
}
public Task<bool> AddAsync(string token)
{
return Index().AddAsync(token);
}
public Task<string> ReserveAsync(Guid id, string name)
{
return Index().ReserveAsync(id, name);
}
public async Task<List<IAppEntity>> GetAppsAsync()
{
using (Profiler.TraceMethod<AppsIndex>())
{
var ids = await GetAppIdsAsync();
var apps =
await Task.WhenAll(ids
.Select(id => GetAppAsync(id)));
return apps.Where(x => x != null).ToList();
}
}
public async Task<List<IAppEntity>> GetAppsForUserAsync(string userId, PermissionSet permissions)
{
using (Profiler.TraceMethod<AppsIndex>())
{
var ids =
await Task.WhenAll(
GetAppIdsByUserAsync(userId),
GetAppIdsAsync(permissions.ToAppNames()));
var apps =
await Task.WhenAll(ids
.SelectMany(x => x)
.Select(id => GetAppAsync(id)));
return apps.Where(x => x != null).ToList();
}
}
public async Task<IAppEntity> GetAppAsync(string name)
{
using (Profiler.TraceMethod<AppsIndex>())
{
var appId = await GetAppIdAsync(name);
if (appId == default)
{
return null;
}
return await GetAppAsync(appId);
}
}
public async Task<IAppEntity> GetAppAsync(Guid appId)
{
using (Profiler.TraceMethod<AppsIndex>())
{
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
if (IsFound(app.Value))
{
return app.Value;
}
return null;
}
}
private async Task<List<Guid>> GetAppIdsByUserAsync(string userId)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByUserIndexGrain>(userId).GetIdsAsync();
}
}
private async Task<List<Guid>> GetAppIdsAsync()
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id).GetIdsAsync();
}
}
private async Task<List<Guid>> GetAppIdsAsync(string[] names)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id).GetIdsAsync(names);
}
}
private async Task<Guid> GetAppIdAsync(string name)
{
using (Profiler.TraceMethod<AppProvider>())
{
return await grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id).GetIdAsync(name);
}
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (context.Command is CreateApp createApp)
{
var index = Index();
string token = await CheckAppAsync(index, createApp);
try
{
await next();
}
finally
{
if (token != null)
{
if (context.IsCompleted)
{
await index.AddAsync(token);
if (createApp.Actor.IsSubject)
{
await Index(createApp.Actor.Identifier).AddAsync(createApp.AppId);
}
}
else
{
await index.RemoveReservationAsync(token);
}
}
}
}
else
{
await next();
if (context.IsCompleted)
{
switch (context.Command)
{
case AssignContributor assignContributor:
await AssignContributorAsync(assignContributor);
break;
case RemoveContributor removeContributor:
await RemoveContributorAsync(removeContributor);
break;
case ArchiveApp archiveApp:
await ArchiveAppAsync(archiveApp);
break;
}
}
}
}
private async Task<string> CheckAppAsync(IAppsByNameIndexGrain index, CreateApp command)
{
var name = command.Name;
if (name.IsSlug())
{
var token = await index.ReserveAsync(command.AppId, name);
if (token == null)
{
var error = new ValidationError("An app with this already exists.");
throw new ValidationException("Cannot create app.", error);
}
return token;
}
return null;
}
private Task AssignContributorAsync(AssignContributor command)
{
return Index(command.ContributorId).AddAsync(command.AppId);
}
private Task RemoveContributorAsync(RemoveContributor command)
{
return Index(command.ContributorId).RemoveAsync(command.AppId);
}
private async Task ArchiveAppAsync(ArchiveApp command)
{
var appId = command.AppId;
var app = await grainFactory.GetGrain<IAppGrain>(appId).GetStateAsync();
if (IsFound(app.Value))
{
await Index().RemoveAsync(appId);
}
foreach (var contributorId in app.Value.Contributors.Keys)
{
await Index(contributorId).RemoveAsync(appId);
}
}
private static bool IsFound(IAppEntity app)
{
return app.Version > EtagVersion.Empty && !app.IsArchived;
}
private IAppsByNameIndexGrain Index()
{
return grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id);
}
private IAppsByUserIndexGrain Index(string id)
{
return grainFactory.GetGrain<IAppsByUserIndexGrain>(id);
}
}
}

35
src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndex.cs

@ -1,35 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{
public interface IAppsByNameIndex : IGrainWithStringKey
{
Task<long> CountAsync();
Task<bool> ReserveAppAsync(Guid appId, string name);
Task AddAppAsync(Guid appId, string name);
Task RemoveAppAsync(Guid appId);
Task RebuildAsync(Dictionary<string, Guid> apps);
Task RemoveReservationAsync(Guid appId, string name);
Task<List<Guid>> GetAppIdsAsync();
Task<List<Guid>> GetAppIdsAsync(string[] names);
Task<Guid> GetAppIdAsync(string name);
}
}

12
src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndex.cs → src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByNameIndexGrain.cs

@ -6,20 +6,12 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans; using Orleans;
using Squidex.Infrastructure.Orleans.Indexes;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{ {
public interface IAppsByUserIndex : IGrainWithStringKey public interface IAppsByNameIndexGrain : IUniqueNameIndexGrain<Guid>, IGrainWithStringKey
{ {
Task AddAppAsync(Guid appId);
Task RemoveAppAsync(Guid appId);
Task RebuildAsync(HashSet<Guid> apps);
Task<List<Guid>> GetAppIdsAsync();
} }
} }

17
src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsByUserIndexGrain.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Orleans;
using Squidex.Infrastructure.Orleans.Indexes;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{
public interface IAppsByUserIndexGrain : IIdsIndexGrain<Guid>, IGrainWithStringKey
{
}
}

39
src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs

@ -0,0 +1,39 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{
public interface IAppsIndex
{
Task<List<Guid>> GetIdsAsync();
Task<List<IAppEntity>> GetAppsAsync();
Task<List<IAppEntity>> GetAppsForUserAsync(string userId, PermissionSet permissions);
Task<IAppEntity> GetAppAsync(string name);
Task<IAppEntity> GetAppAsync(Guid appId);
Task<string> ReserveAsync(Guid id, string name);
Task<bool> AddAsync(string token);
Task RemoveReservationAsync(string token);
Task RebuildByContributorsAsync(string contributorId, HashSet<Guid> apps);
Task RebuildAsync(Dictionary<string, Guid> apps);
Task RebuildByContributorsAsync(Guid appId, HashSet<string> contributors);
}
}

14
src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs

@ -6,7 +6,6 @@
// ========================================================================== // ==========================================================================
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Core.Rules.Triggers;
@ -21,26 +20,23 @@ namespace Squidex.Domain.Apps.Entities.Assets
public sealed class AssetChangedTriggerHandler : RuleTriggerHandler<AssetChangedTriggerV2, AssetEvent, EnrichedAssetEvent> public sealed class AssetChangedTriggerHandler : RuleTriggerHandler<AssetChangedTriggerV2, AssetEvent, EnrichedAssetEvent>
{ {
private readonly IScriptEngine scriptEngine; private readonly IScriptEngine scriptEngine;
private readonly IGrainFactory grainFactory; private readonly IAssetLoader assetLoader;
public AssetChangedTriggerHandler(IScriptEngine scriptEngine, IGrainFactory grainFactory) public AssetChangedTriggerHandler(IScriptEngine scriptEngine, IAssetLoader assetLoader)
{ {
Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(assetLoader, nameof(assetLoader));
this.scriptEngine = scriptEngine; this.scriptEngine = scriptEngine;
this.grainFactory = grainFactory; this.assetLoader = assetLoader;
} }
protected override async Task<EnrichedAssetEvent> CreateEnrichedEventAsync(Envelope<AssetEvent> @event) protected override async Task<EnrichedAssetEvent> CreateEnrichedEventAsync(Envelope<AssetEvent> @event)
{ {
var result = new EnrichedAssetEvent(); var result = new EnrichedAssetEvent();
var asset = var asset = await assetLoader.GetAsync(@event.Payload.AssetId, @event.Headers.EventStreamNumber());
(await grainFactory
.GetGrain<IAssetGrain>(@event.Payload.AssetId)
.GetStateAsync(@event.Headers.EventStreamNumber())).Value;
SimpleMapper.Map(asset, result); SimpleMapper.Map(asset, result);

18
src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IAssetLoader
{
Task<IAssetEntity> GetAsync(Guid id, long version = EtagVersion.Any);
}
}

44
src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs

@ -0,0 +1,44 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
public sealed class AssetLoader : IAssetLoader
{
private readonly IGrainFactory grainFactory;
public AssetLoader(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory;
}
public async Task<IAssetEntity> GetAsync(Guid id, long version)
{
using (Profiler.TraceMethod<AssetLoader>())
{
var grain = grainFactory.GetGrain<IAssetGrain>(id);
var content = await grain.GetStateAsync(version);
if (content.Value == null || content.Value.Version != version)
{
throw new DomainObjectNotFoundException(id.ToString(), typeof(IAssetEntity));
}
return content.Value;
}
}
}
}

2
src/Squidex.Domain.Apps.Entities/Comments/CommentsGrain.cs

@ -21,7 +21,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Comments namespace Squidex.Domain.Apps.Entities.Comments
{ {
public sealed class CommentsGrain : DomainObjectGrainBase<CommentsState>, ICommentGrain public sealed class CommentsGrain : DomainObjectGrainBase<CommentsState>, ICommentsGrain
{ {
private readonly IStore<Guid> store; private readonly IStore<Guid> store;
private readonly List<Envelope<CommentsEvent>> events = new List<Envelope<CommentsEvent>>(); private readonly List<Envelope<CommentsEvent>> events = new List<Envelope<CommentsEvent>>();

31
src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs

@ -0,0 +1,31 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Comments
{
public sealed class CommentsLoader : ICommentsLoader
{
private readonly IGrainFactory grainFactory;
public CommentsLoader(IGrainFactory grainFactory)
{
this.grainFactory = grainFactory;
}
public Task<CommentsResult> GetCommentsAsync(Guid id, long version = EtagVersion.Any)
{
var grain = grainFactory.GetGrain<ICommentsGrain>(id);
return grain.GetCommentsAsync(version);
}
}
}

2
src/Squidex.Domain.Apps.Entities/Comments/ICommentGrain.cs → src/Squidex.Domain.Apps.Entities/Comments/ICommentsGrain.cs

@ -11,7 +11,7 @@ using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Comments namespace Squidex.Domain.Apps.Entities.Comments
{ {
public interface ICommentGrain : IDomainObjectGrain public interface ICommentsGrain : IDomainObjectGrain
{ {
Task<CommentsResult> GetCommentsAsync(long version = EtagVersion.Any); Task<CommentsResult> GetCommentsAsync(long version = EtagVersion.Any);
} }

18
src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Comments
{
public interface ICommentsLoader
{
Task<CommentsResult> GetCommentsAsync(Guid id, long version = EtagVersion.Any);
}
}

14
src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs

@ -7,7 +7,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents; using Squidex.Domain.Apps.Core.HandleRules.EnrichedEvents;
@ -23,26 +22,23 @@ namespace Squidex.Domain.Apps.Entities.Contents
public sealed class ContentChangedTriggerHandler : RuleTriggerHandler<ContentChangedTriggerV2, ContentEvent, EnrichedContentEvent> public sealed class ContentChangedTriggerHandler : RuleTriggerHandler<ContentChangedTriggerV2, ContentEvent, EnrichedContentEvent>
{ {
private readonly IScriptEngine scriptEngine; private readonly IScriptEngine scriptEngine;
private readonly IGrainFactory grainFactory; private readonly IContentLoader contentLoader;
public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IGrainFactory grainFactory) public ContentChangedTriggerHandler(IScriptEngine scriptEngine, IContentLoader contentLoader)
{ {
Guard.NotNull(scriptEngine, nameof(scriptEngine)); Guard.NotNull(scriptEngine, nameof(scriptEngine));
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(contentLoader, nameof(contentLoader));
this.scriptEngine = scriptEngine; this.scriptEngine = scriptEngine;
this.grainFactory = grainFactory; this.contentLoader = contentLoader;
} }
protected override async Task<EnrichedContentEvent> CreateEnrichedEventAsync(Envelope<ContentEvent> @event) protected override async Task<EnrichedContentEvent> CreateEnrichedEventAsync(Envelope<ContentEvent> @event)
{ {
var result = new EnrichedContentEvent(); var result = new EnrichedContentEvent();
var content = var content = await contentLoader.GetAsync(@event.Headers.AggregateId(), @event.Headers.EventStreamNumber());
(await grainFactory
.GetGrain<IContentGrain>(@event.Payload.ContentId)
.GetStateAsync(@event.Headers.EventStreamNumber())).Value;
SimpleMapper.Map(content, result); SimpleMapper.Map(content, result);

5
src/Squidex.Domain.Apps.Entities/Contents/IContentVersionLoader.cs → src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs

@ -7,11 +7,12 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public interface IContentVersionLoader public interface IContentLoader
{ {
Task<IContentEntity> LoadAsync(Guid id, long version); Task<IContentEntity> GetAsync(Guid id, long version = EtagVersion.Any);
} }
} }

8
src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentVersionLoader.cs → src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs

@ -13,20 +13,20 @@ using Squidex.Infrastructure.Log;
namespace Squidex.Domain.Apps.Entities.Contents.Queries namespace Squidex.Domain.Apps.Entities.Contents.Queries
{ {
public sealed class ContentVersionLoader : IContentVersionLoader public sealed class ContentLoader : IContentLoader
{ {
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
public ContentVersionLoader(IGrainFactory grainFactory) public ContentLoader(IGrainFactory grainFactory)
{ {
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
} }
public async Task<IContentEntity> LoadAsync(Guid id, long version) public async Task<IContentEntity> GetAsync(Guid id, long version)
{ {
using (Profiler.TraceMethod<ContentVersionLoader>()) using (Profiler.TraceMethod<ContentLoader>())
{ {
var grain = grainFactory.GetGrain<IContentGrain>(id); var grain = grainFactory.GetGrain<IContentGrain>(id);

6
src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs

@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private readonly IAssetUrlGenerator assetUrlGenerator; private readonly IAssetUrlGenerator assetUrlGenerator;
private readonly IContentEnricher contentEnricher; private readonly IContentEnricher contentEnricher;
private readonly IContentRepository contentRepository; private readonly IContentRepository contentRepository;
private readonly IContentVersionLoader contentVersionLoader; private readonly IContentLoader contentVersionLoader;
private readonly IScriptEngine scriptEngine; private readonly IScriptEngine scriptEngine;
private readonly ContentQueryParser queryParser; private readonly ContentQueryParser queryParser;
@ -41,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
IAssetUrlGenerator assetUrlGenerator, IAssetUrlGenerator assetUrlGenerator,
IContentEnricher contentEnricher, IContentEnricher contentEnricher,
IContentRepository contentRepository, IContentRepository contentRepository,
IContentVersionLoader contentVersionLoader, IContentLoader contentVersionLoader,
IScriptEngine scriptEngine, IScriptEngine scriptEngine,
ContentQueryParser queryParser) ContentQueryParser queryParser)
{ {
@ -330,7 +330,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
private Task<IContentEntity> FindByVersionAsync(Guid id, long version) private Task<IContentEntity> FindByVersionAsync(Guid id, long version)
{ {
return contentVersionLoader.LoadAsync(id, version); return contentVersionLoader.GetAsync(id, version);
} }
private static bool WithDraft(Context context) private static bool WithDraft(Context context)

4
src/Squidex.Domain.Apps.Entities/IAppProvider.cs

@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Entities
Task<IAppEntity> GetAppAsync(string appName); Task<IAppEntity> GetAppAsync(string appName);
Task<List<IAppEntity>> GetUserAppsAsync(string userId, PermissionSet permissions);
Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false); Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false);
Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name); Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name);
@ -30,7 +32,5 @@ namespace Squidex.Domain.Apps.Entities
Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId); Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId);
Task<List<IRuleEntity>> GetRulesAsync(Guid appId); Task<List<IRuleEntity>> GetRulesAsync(Guid appId);
Task<List<IAppEntity>> GetUserApps(string userId, PermissionSet permissions);
} }
} }

15
src/Squidex.Domain.Apps.Entities/Rules/BackupRules.cs

@ -8,10 +8,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Domain.Apps.Entities.Rules.Indexes; using Squidex.Domain.Apps.Entities.Rules.Indexes;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Events.Rules; using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
@ -22,16 +20,15 @@ namespace Squidex.Domain.Apps.Entities.Rules
public sealed class BackupRules : BackupHandler public sealed class BackupRules : BackupHandler
{ {
private readonly HashSet<Guid> ruleIds = new HashSet<Guid>(); private readonly HashSet<Guid> ruleIds = new HashSet<Guid>();
private readonly IGrainFactory grainFactory; private readonly IRulesIndex indexForRules;
public override string Name { get; } = "Rules"; public override string Name { get; } = "Rules";
public BackupRules(IGrainFactory grainFactory, IRuleEventRepository ruleEventRepository) public BackupRules(IRulesIndex indexForRules)
{ {
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(indexForRules, nameof(indexForRules));
Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository));
this.grainFactory = grainFactory; this.indexForRules = indexForRules;
} }
public override Task<bool> RestoreEventAsync(Envelope<IEvent> @event, Guid appId, BackupReader reader, RefToken actor) public override Task<bool> RestoreEventAsync(Envelope<IEvent> @event, Guid appId, BackupReader reader, RefToken actor)
@ -49,9 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Rules
return TaskHelper.True; return TaskHelper.True;
} }
public override async Task RestoreAsync(Guid appId, BackupReader reader) public override Task RestoreAsync(Guid appId, BackupReader reader)
{ {
await grainFactory.GetGrain<IRulesByAppIndex>(appId).RebuildAsync(ruleIds); return indexForRules.RebuildAsync(appId, ruleIds);
} }
} }
} }

2
src/Squidex.Domain.Apps.Entities/Rules/IRuleEntity.cs

@ -20,5 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
NamedId<Guid> AppId { get; set; } NamedId<Guid> AppId { get; set; }
Rule RuleDef { get; } Rule RuleDef { get; }
bool IsDeleted { get; }
} }
} }

17
src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndexGrain.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Orleans;
using Squidex.Infrastructure.Orleans.Indexes;
namespace Squidex.Domain.Apps.Entities.Rules.Indexes
{
public interface IRulesByAppIndexGrain : IIdsIndexGrain<Guid>, IGrainWithGuidKey
{
}
}

15
src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesByAppIndex.cs → src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesIndex.cs

@ -8,20 +8,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans;
namespace Squidex.Domain.Apps.Entities.Rules.Indexes namespace Squidex.Domain.Apps.Entities.Rules.Indexes
{ {
public interface IRulesByAppIndex : IGrainWithGuidKey public interface IRulesIndex
{ {
Task AddRuleAsync(Guid ruleId); Task<List<IRuleEntity>> GetRulesAsync(Guid appId);
Task RemoveRuleAsync(Guid ruleId); Task RebuildAsync(Guid appId, HashSet<Guid> rules);
Task RebuildAsync(HashSet<Guid> rules);
Task ClearAsync();
Task<List<Guid>> GetRuleIdsAsync();
} }
} }

56
src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexCommandMiddleware.cs

@ -1,56 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Rules.Indexes
{
public sealed class RulesByAppIndexCommandMiddleware : ICommandMiddleware
{
private readonly IGrainFactory grainFactory;
public RulesByAppIndexCommandMiddleware(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory;
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (context.IsCompleted)
{
switch (context.Command)
{
case CreateRule createRule:
await Index(createRule.AppId.Id).AddRuleAsync(createRule.RuleId);
break;
case DeleteRule deleteRule:
{
var schema = await grainFactory.GetGrain<IRuleGrain>(deleteRule.RuleId).GetStateAsync();
await Index(schema.Value.AppId.Id).RemoveRuleAsync(deleteRule.RuleId);
break;
}
}
}
await next();
}
private IRulesByAppIndex Index(Guid appId)
{
return grainFactory.GetGrain<IRulesByAppIndex>(appId);
}
}
}

55
src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs

@ -6,61 +6,22 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Orleans.Indexes;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Rules.Indexes namespace Squidex.Domain.Apps.Entities.Rules.Indexes
{ {
public sealed class RulesByAppIndexGrain : GrainOfGuid, IRulesByAppIndex public sealed class RulesByAppIndexGrain : IdsIndexGrain<RulesByAppIndexState, Guid>, IRulesByAppIndexGrain
{ {
private readonly IGrainState<GrainState> state; public RulesByAppIndexGrain(IGrainState<RulesByAppIndexState> state)
: base(state)
[CollectionName("Index_RulesByApp")]
public sealed class GrainState
{
public HashSet<Guid> Rules { get; set; } = new HashSet<Guid>();
}
public RulesByAppIndexGrain(IGrainState<GrainState> state)
{
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task ClearAsync()
{
return state.ClearAsync();
}
public Task RebuildAsync(HashSet<Guid> rules)
{
state.Value = new GrainState { Rules = rules };
return state.WriteAsync();
}
public Task AddRuleAsync(Guid ruleId)
{
state.Value.Rules.Add(ruleId);
return state.WriteAsync();
}
public Task RemoveRuleAsync(Guid ruleId)
{ {
state.Value.Rules.Remove(ruleId);
return state.WriteAsync();
} }
}
public Task<List<Guid>> GetRuleIdsAsync() [CollectionName("Index_RulesByApp")]
{ public sealed class RulesByAppIndexState : IdsIndexState<Guid>
return Task.FromResult(state.Value.Rules.ToList()); {
}
} }
} }

118
src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs

@ -0,0 +1,118 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Rules.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log;
namespace Squidex.Domain.Apps.Entities.Rules.Indexes
{
public sealed class RulesIndex : ICommandMiddleware, IRulesIndex
{
private readonly IGrainFactory grainFactory;
public RulesIndex(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory;
}
public Task RebuildAsync(Guid appId, HashSet<Guid> rues)
{
return Index(appId).RebuildAsync(rues);
}
public async Task<List<IRuleEntity>> GetRulesAsync(Guid appId)
{
using (Profiler.TraceMethod<RulesIndex>())
{
var ids = await GetRuleIdsAsync(appId);
var rules =
await Task.WhenAll(
ids.Select(GetRuleAsync));
return rules.Where(x => x != null).ToList();
}
}
private async Task<IRuleEntity> GetRuleAsync(Guid id)
{
using (Profiler.TraceMethod<RulesIndex>())
{
var ruleEntity = await grainFactory.GetGrain<IRuleGrain>(id).GetStateAsync();
if (IsFound(ruleEntity.Value))
{
return ruleEntity.Value;
}
return null;
}
}
private async Task<List<Guid>> GetRuleIdsAsync(Guid appId)
{
using (Profiler.TraceMethod<RulesIndex>())
{
return await Index(appId).GetIdsAsync();
}
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
await next();
if (context.IsCompleted)
{
switch (context.Command)
{
case CreateRule createRule:
await CreateRuleAsync(createRule);
break;
case DeleteRule deleteRule:
await DeleteRuleAsync(deleteRule);
break;
}
}
}
private async Task CreateRuleAsync(CreateRule command)
{
await Index(command.AppId.Id).AddAsync(command.RuleId);
}
private async Task DeleteRuleAsync(DeleteRule command)
{
var id = command.RuleId;
var rule = await grainFactory.GetGrain<IRuleGrain>(id).GetStateAsync();
if (IsFound(rule.Value))
{
await Index(rule.Value.AppId.Id).RemoveAsync(id);
}
}
private IRulesByAppIndexGrain Index(Guid appId)
{
return grainFactory.GetGrain<IRulesByAppIndexGrain>(appId);
}
private static bool IsFound(IRuleEntity rule)
{
return rule.Version > EtagVersion.Empty && !rule.IsDeleted;
}
}
}

4
src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs

@ -29,13 +29,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
public sealed class Target public sealed class Target
{ {
public NamedId<Guid> AppId { get; set; }
public int Limits { get; set; } public int Limits { get; set; }
public int? NumDays { get; set; } public int? NumDays { get; set; }
public DateTime? Triggered { get; set; } public DateTime? Triggered { get; set; }
public NamedId<Guid> AppId { get; set; }
} }
[CollectionName("UsageTracker")] [CollectionName("UsageTracker")]

13
src/Squidex.Domain.Apps.Entities/Schemas/BackupSchemas.cs

@ -8,7 +8,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Backup; using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Domain.Apps.Entities.Schemas.Indexes; using Squidex.Domain.Apps.Entities.Schemas.Indexes;
using Squidex.Domain.Apps.Events.Schemas; using Squidex.Domain.Apps.Events.Schemas;
@ -21,15 +20,15 @@ namespace Squidex.Domain.Apps.Entities.Schemas
public sealed class BackupSchemas : BackupHandler public sealed class BackupSchemas : BackupHandler
{ {
private readonly Dictionary<string, Guid> schemasByName = new Dictionary<string, Guid>(); private readonly Dictionary<string, Guid> schemasByName = new Dictionary<string, Guid>();
private readonly IGrainFactory grainFactory; private readonly ISchemasIndex indexSchemas;
public override string Name { get; } = "Schemas"; public override string Name { get; } = "Schemas";
public BackupSchemas(IGrainFactory grainFactory) public BackupSchemas(ISchemasIndex indexSchemas)
{ {
Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(indexSchemas, nameof(indexSchemas));
this.grainFactory = grainFactory; this.indexSchemas = indexSchemas;
} }
public override Task<bool> RestoreEventAsync(Envelope<IEvent> @event, Guid appId, BackupReader reader, RefToken actor) public override Task<bool> RestoreEventAsync(Envelope<IEvent> @event, Guid appId, BackupReader reader, RefToken actor)
@ -47,9 +46,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas
return TaskHelper.True; return TaskHelper.True;
} }
public override async Task RestoreAsync(Guid appId, BackupReader reader) public override Task RestoreAsync(Guid appId, BackupReader reader)
{ {
await grainFactory.GetGrain<ISchemasByAppIndex>(appId).RebuildAsync(schemasByName); return indexSchemas.RebuildAsync(appId, schemasByName);
} }
} }
} }

9
src/Squidex.Domain.Apps.Entities/Schemas/Guards/GuardSchema.cs

@ -7,7 +7,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core; using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands; using Squidex.Domain.Apps.Entities.Schemas.Commands;
@ -20,20 +19,16 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Guards
{ {
public static class GuardSchema public static class GuardSchema
{ {
public static Task CanCreate(CreateSchema command, IAppProvider appProvider) public static void CanCreate(CreateSchema command)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));
return Validate.It(() => "Cannot create schema.", async e => Validate.It(() => "Cannot create schema.", e =>
{ {
if (!command.Name.IsSlug()) if (!command.Name.IsSlug())
{ {
e(Not.ValidSlug("Name"), nameof(command.Name)); e(Not.ValidSlug("Name"), nameof(command.Name));
} }
else if (await appProvider.GetSchemaAsync(command.AppId.Id, command.Name) != null)
{
e("A schema with the same name already exists.");
}
ValidateUpsert(command, e); ValidateUpsert(command, e);
}); });

17
src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndexGrain.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Orleans;
using Squidex.Infrastructure.Orleans.Indexes;
namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
{
public interface ISchemasByAppIndexGrain : IUniqueNameIndexGrain<Guid>, IGrainWithGuidKey
{
}
}

17
src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasByAppIndex.cs → src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs

@ -8,22 +8,17 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans;
namespace Squidex.Domain.Apps.Entities.Schemas.Indexes namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
{ {
public interface ISchemasByAppIndex : IGrainWithGuidKey public interface ISchemasIndex
{ {
Task AddSchemaAsync(Guid schemaId, string name); Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false);
Task RemoveSchemaAsync(Guid schemaId); Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name, bool allowDeleted = false);
Task RebuildAsync(Dictionary<string, Guid> schemas); Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId, bool allowDeleted = false);
Task ClearAsync(); Task RebuildAsync(Guid appId, Dictionary<string, Guid> schemas);
Task<Guid> GetSchemaIdAsync(string name);
Task<List<Guid>> GetSchemaIdsAsync();
} }
} }

56
src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexCommandMiddleware.cs

@ -1,56 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
{
public sealed class SchemasByAppIndexCommandMiddleware : ICommandMiddleware
{
private readonly IGrainFactory grainFactory;
public SchemasByAppIndexCommandMiddleware(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory;
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (context.IsCompleted)
{
switch (context.Command)
{
case CreateSchema createSchema:
await Index(createSchema.AppId.Id).AddSchemaAsync(createSchema.SchemaId, createSchema.Name);
break;
case DeleteSchema deleteSchema:
{
var schema = await grainFactory.GetGrain<ISchemaGrain>(deleteSchema.SchemaId).GetStateAsync();
await Index(schema.Value.AppId.Id).RemoveSchemaAsync(deleteSchema.SchemaId);
break;
}
}
}
await next();
}
private ISchemasByAppIndex Index(Guid appId)
{
return grainFactory.GetGrain<ISchemasByAppIndex>(appId);
}
}
}

62
src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs

@ -6,68 +6,22 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Orleans.Indexes;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Schemas.Indexes namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
{ {
public sealed class SchemasByAppIndexGrain : GrainOfGuid, ISchemasByAppIndex public sealed class SchemasByAppIndexGrain : UniqueNameIndexGrain<SchemasByAppIndexGrainState, Guid>, ISchemasByAppIndexGrain
{ {
private readonly IGrainState<GrainState> state; public SchemasByAppIndexGrain(IGrainState<SchemasByAppIndexGrainState> state)
: base(state)
[CollectionName("Index_SchemasByApp")]
public sealed class GrainState
{
public Dictionary<string, Guid> Schemas { get; set; } = new Dictionary<string, Guid>();
}
public SchemasByAppIndexGrain(IGrainState<GrainState> state)
{
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task ClearAsync()
{
return state.ClearAsync();
}
public Task RebuildAsync(Dictionary<string, Guid> schemas)
{
state.Value = new GrainState { Schemas = schemas };
return state.WriteAsync();
}
public Task AddSchemaAsync(Guid schemaId, string name)
{
state.Value.Schemas[name] = schemaId;
return state.WriteAsync();
}
public Task RemoveSchemaAsync(Guid schemaId)
{
state.Value.Schemas.Remove(state.Value.Schemas.FirstOrDefault(x => x.Value == schemaId).Key ?? string.Empty);
return state.WriteAsync();
}
public Task<Guid> GetSchemaIdAsync(string name)
{ {
state.Value.Schemas.TryGetValue(name, out var schemaId);
return Task.FromResult(schemaId);
} }
}
public Task<List<Guid>> GetSchemaIdsAsync() [CollectionName("Index_SchemasByApp")]
{ public sealed class SchemasByAppIndexGrainState : UniqueNameIndexState<Guid>
return Task.FromResult(state.Value.Schemas.Values.ToList()); {
}
} }
} }

181
src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs

@ -0,0 +1,181 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Orleans;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
{
public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex
{
private readonly IGrainFactory grainFactory;
public SchemasIndex(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory;
}
public Task RebuildAsync(Guid appId, Dictionary<string, Guid> schemas)
{
return Index(appId).RebuildAsync(schemas);
}
public async Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId, bool allowDeleted = false)
{
using (Profiler.TraceMethod<SchemasIndex>())
{
var ids = await GetSchemaIdsAsync(appId);
var schemas =
await Task.WhenAll(
ids.Select(id => GetSchemaAsync(appId, id, allowDeleted)));
return schemas.Where(x => x != null).ToList();
}
}
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name, bool allowDeleted = false)
{
using (Profiler.TraceMethod<SchemasIndex>())
{
var id = await GetSchemaIdAsync(appId, name);
if (id == default)
{
return null;
}
return await GetSchemaAsync(appId, id, allowDeleted);
}
}
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false)
{
using (Profiler.TraceMethod<SchemasIndex>())
{
var schema = await grainFactory.GetGrain<ISchemaGrain>(id).GetStateAsync();
if (IsFound(schema.Value, allowDeleted))
{
return schema.Value;
}
return null;
}
}
private async Task<Guid> GetSchemaIdAsync(Guid appId, string name)
{
using (Profiler.TraceMethod<SchemasIndex>())
{
return await Index(appId).GetIdAsync(name);
}
}
private async Task<List<Guid>> GetSchemaIdsAsync(Guid appId)
{
using (Profiler.TraceMethod<SchemasIndex>())
{
return await Index(appId).GetIdsAsync();
}
}
public async Task HandleAsync(CommandContext context, Func<Task> next)
{
if (context.Command is CreateSchema createSchema)
{
var index = Index(createSchema.AppId.Id);
string token = await CheckSchemaAsync(index, createSchema);
try
{
await next();
}
finally
{
if (token != null)
{
if (context.IsCompleted)
{
await index.AddAsync(token);
}
else
{
await index.RemoveReservationAsync(token);
}
}
}
}
else
{
await next();
if (context.IsCompleted)
{
if (context.Command is DeleteSchema deleteSchema)
{
await DeleteSchemaAsync(deleteSchema);
}
}
}
}
private async Task<string> CheckSchemaAsync(ISchemasByAppIndexGrain index, CreateSchema command)
{
var name = command.Name;
if (name.IsSlug())
{
var token = await index.ReserveAsync(command.SchemaId, name);
if (token == null)
{
var error = new ValidationError("A schema with this name already exists.");
throw new ValidationException("Cannot create schema.", error);
}
return token;
}
return null;
}
private async Task DeleteSchemaAsync(DeleteSchema commmand)
{
var schemaId = commmand.SchemaId;
var schema = await grainFactory.GetGrain<ISchemaGrain>(schemaId).GetStateAsync();
if (IsFound(schema.Value, true))
{
await Index(schema.Value.AppId.Id).RemoveAsync(schemaId);
}
}
private ISchemasByAppIndexGrain Index(Guid appId)
{
return grainFactory.GetGrain<ISchemasByAppIndexGrain>(appId);
}
private static bool IsFound(ISchemaEntity entity, bool allowDeleted)
{
return entity.Version > EtagVersion.Empty && (!entity.IsDeleted || allowDeleted);
}
}
}

10
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -27,17 +27,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas
{ {
public sealed class SchemaGrain : DomainObjectGrain<SchemaState>, ISchemaGrain public sealed class SchemaGrain : DomainObjectGrain<SchemaState>, ISchemaGrain
{ {
private readonly IAppProvider appProvider;
private readonly IJsonSerializer serializer; private readonly IJsonSerializer serializer;
public SchemaGrain(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider, IJsonSerializer serializer) public SchemaGrain(IStore<Guid> store, ISemanticLog log, IJsonSerializer serializer)
: base(store, log) : base(store, log)
{ {
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(serializer, nameof(serializer)); Guard.NotNull(serializer, nameof(serializer));
this.appProvider = appProvider;
this.serializer = serializer; this.serializer = serializer;
} }
@ -69,9 +65,9 @@ namespace Squidex.Domain.Apps.Entities.Schemas
}); });
case CreateSchema createSchema: case CreateSchema createSchema:
return CreateReturnAsync(createSchema, async c => return CreateReturn(createSchema, c =>
{ {
await GuardSchema.CanCreate(c, appProvider); GuardSchema.CanCreate(c);
Create(c); Create(c);

27
src/Squidex.Infrastructure/Orleans/Indexes/IIdsIndexGrain.cs

@ -0,0 +1,27 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Orleans.Indexes
{
public interface IIdsIndexGrain<T>
{
Task<long> CountAsync();
Task RebuildAsync(HashSet<T> ids);
Task AddAsync(T id);
Task RemoveAsync(T id);
Task ClearAsync();
Task<List<T>> GetIdsAsync();
}
}

35
src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameIndexGrain.cs

@ -0,0 +1,35 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Orleans.Indexes
{
public interface IUniqueNameIndexGrain<T>
{
Task<string> ReserveAsync(T id, string name);
Task<bool> AddAsync(string token);
Task<long> CountAsync();
Task RemoveReservationAsync(string token);
Task RemoveAsync(T id);
Task RebuildAsync(Dictionary<string, T> values);
Task ClearAsync();
Task<T> GetIdAsync(string name);
Task<List<T>> GetIdsAsync(string[] names);
Task<List<T>> GetIdsAsync();
}
}

62
src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexGrain.cs

@ -0,0 +1,62 @@
// ==========================================================================
// 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.Tasks;
using Orleans;
namespace Squidex.Infrastructure.Orleans.Indexes
{
public class IdsIndexGrain<TState, T> : Grain, IIdsIndexGrain<T> where TState : IdsIndexState<T>, new()
{
private readonly IGrainState<TState> state;
public IdsIndexGrain(IGrainState<TState> state)
{
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task<long> CountAsync()
{
return Task.FromResult<long>(state.Value.Ids.Count);
}
public Task RebuildAsync(HashSet<T> ids)
{
state.Value = new TState { Ids = ids };
return state.WriteAsync();
}
public Task AddAsync(T id)
{
state.Value.Ids.Add(id);
return state.WriteAsync();
}
public Task RemoveAsync(T id)
{
state.Value.Ids.Remove(id);
return state.WriteAsync();
}
public Task ClearAsync()
{
return state.ClearAsync();
}
public Task<List<T>> GetIdsAsync()
{
return Task.FromResult(state.Value.Ids.ToList());
}
}
}

16
src/Squidex.Infrastructure/Orleans/Indexes/IdsIndexState.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Infrastructure.Orleans.Indexes
{
public class IdsIndexState<T>
{
public HashSet<T> Ids { get; set; } = new HashSet<T>();
}
}

136
src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexGrain.cs

@ -0,0 +1,136 @@
// ==========================================================================
// 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.Tasks;
using Orleans;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Orleans.Indexes
{
public class UniqueNameIndexGrain<TState, T> : Grain, IUniqueNameIndexGrain<T> where TState : UniqueNameIndexState<T>, new()
{
private readonly Dictionary<string, (string Name, T Id)> reservations = new Dictionary<string, (string Name, T Id)>();
private readonly IGrainState<TState> state;
public UniqueNameIndexGrain(IGrainState<TState> state)
{
Guard.NotNull(state, nameof(state));
this.state = state;
}
public Task<long> CountAsync()
{
return Task.FromResult<long>(state.Value.Names.Count);
}
public Task ClearAsync()
{
reservations.Clear();
return state.ClearAsync();
}
public Task RebuildAsync(Dictionary<string, T> names)
{
state.Value = new TState { Names = names };
return state.WriteAsync();
}
public Task<string> ReserveAsync(T id, string name)
{
string token = default;
if (!IsInUse(name) && !IsReserved(name))
{
token = RandomHash.Simple();
reservations.Add(token, (name, id));
}
return Task.FromResult(token);
}
public async Task<bool> AddAsync(string token)
{
if (reservations.TryGetValue(token ?? string.Empty, out var reservation))
{
state.Value.Names.Add(reservation.Name, reservation.Id);
await state.WriteAsync();
reservations.Remove(token);
return true;
}
return false;
}
public Task RemoveReservationAsync(string token)
{
reservations.Remove(token ?? string.Empty);
return TaskHelper.Done;
}
public async Task RemoveAsync(T id)
{
var name = state.Value.Names.FirstOrDefault(x => Equals(x.Value, id)).Key;
if (name != null)
{
state.Value.Names.Remove(name);
await state.WriteAsync();
}
}
public Task<List<T>> GetIdsAsync(string[] names)
{
var result = new List<T>();
if (names != null)
{
foreach (var name in names)
{
if (state.Value.Names.TryGetValue(name, out var id))
{
result.Add(id);
}
}
}
return Task.FromResult(result);
}
public Task<T> GetIdAsync(string name)
{
state.Value.Names.TryGetValue(name, out var id);
return Task.FromResult(id);
}
public Task<List<T>> GetIdsAsync()
{
return Task.FromResult(state.Value.Names.Values.ToList());
}
private bool IsInUse(string name)
{
return state.Value.Names.ContainsKey(name);
}
private bool IsReserved(string name)
{
return reservations.Values.Any(x => x.Name == name);
}
}
}

16
src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameIndexState.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Infrastructure.Orleans.Indexes
{
public class UniqueNameIndexState<T>
{
public Dictionary<string, T> Names { get; set; } = new Dictionary<string, T>();
}
}

2
src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs

@ -75,7 +75,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
var userOrClientId = HttpContext.User.UserOrClientId(); var userOrClientId = HttpContext.User.UserOrClientId();
var userPermissions = HttpContext.Permissions(); var userPermissions = HttpContext.Permissions();
var apps = await appProvider.GetUserApps(userOrClientId, userPermissions); var apps = await appProvider.GetUserAppsAsync(userOrClientId, userPermissions);
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {

9
src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs

@ -9,7 +9,6 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Orleans;
using Squidex.Areas.Api.Controllers.Comments.Models; using Squidex.Areas.Api.Controllers.Comments.Models;
using Squidex.Domain.Apps.Entities.Comments; using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Domain.Apps.Entities.Comments.Commands;
@ -26,12 +25,12 @@ namespace Squidex.Areas.Api.Controllers.Comments
[ApiExplorerSettings(GroupName = nameof(Comments))] [ApiExplorerSettings(GroupName = nameof(Comments))]
public sealed class CommentsController : ApiController public sealed class CommentsController : ApiController
{ {
private readonly IGrainFactory grainFactory; private readonly ICommentsLoader commentsLoader;
public CommentsController(ICommandBus commandBus, IGrainFactory grainFactory) public CommentsController(ICommandBus commandBus, ICommentsLoader commentsLoader)
: base(commandBus) : base(commandBus)
{ {
this.grainFactory = grainFactory; this.commentsLoader = commentsLoader;
} }
/// <summary> /// <summary>
@ -54,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Comments
[ApiCosts(0)] [ApiCosts(0)]
public async Task<IActionResult> GetComments(string app, Guid commentsId, [FromQuery] long version = EtagVersion.Any) public async Task<IActionResult> GetComments(string app, Guid commentsId, [FromQuery] long version = EtagVersion.Any)
{ {
var result = await grainFactory.GetGrain<ICommentGrain>(commentsId).GetCommentsAsync(version); var result = await commentsLoader.GetCommentsAsync(commentsId, version);
var response = Deferred.Response(() => var response = Deferred.Response(() =>
{ {

35
src/Squidex/Config/Domain/EntitiesServices.cs

@ -93,6 +93,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<TempFolderBackupArchiveLocation>() services.AddSingletonAs<TempFolderBackupArchiveLocation>()
.As<IBackupArchiveLocation>(); .As<IBackupArchiveLocation>();
services.AddSingletonAs<CommentsLoader>()
.As<ICommentsLoader>();
services.AddSingletonAs<AppProvider>() services.AddSingletonAs<AppProvider>()
.As<IAppProvider>(); .As<IAppProvider>();
@ -111,6 +114,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<AssetQueryService>() services.AddSingletonAs<AssetQueryService>()
.As<IAssetQueryService>(); .As<IAssetQueryService>();
services.AddSingletonAs<AssetLoader>()
.As<IAssetLoader>();
services.AddSingletonAs(c => new Lazy<IContentQueryService>(() => c.GetRequiredService<IContentQueryService>())) services.AddSingletonAs(c => new Lazy<IContentQueryService>(() => c.GetRequiredService<IContentQueryService>()))
.AsSelf(); .AsSelf();
@ -120,8 +126,8 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<ContentQueryService>() services.AddSingletonAs<ContentQueryService>()
.As<IContentQueryService>(); .As<IContentQueryService>();
services.AddSingletonAs<ContentVersionLoader>() services.AddSingletonAs<ContentLoader>()
.As<IContentVersionLoader>(); .As<IContentLoader>();
services.AddSingletonAs<AppHistoryEventsCreator>() services.AddSingletonAs<AppHistoryEventsCreator>()
.As<IHistoryEventsCreator>(); .As<IHistoryEventsCreator>();
@ -239,34 +245,31 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<InviteUserCommandMiddleware>() services.AddSingletonAs<InviteUserCommandMiddleware>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingletonAs<AssetCommandMiddleware>() services.AddSingletonAs<AppsIndex>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>().As<IAppsIndex>();
services.AddSingletonAs<ContentCommandMiddleware>() services.AddSingletonAs<RulesIndex>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>().As<IRulesIndex>();
services.AddSingletonAs<AppsByNameIndexCommandMiddleware>() services.AddSingletonAs<SchemasIndex>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>().As<ISchemasIndex>();
services.AddSingletonAs<AppCommandMiddleware>() services.AddSingletonAs<AppCommandMiddleware>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<CommentsCommand, ICommentGrain>>() services.AddSingletonAs<AssetCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<SchemaCommand, ISchemaGrain>>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<RuleCommand, IRuleGrain>>() services.AddSingletonAs<ContentCommandMiddleware>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingletonAs<AppsByUserIndexCommandMiddleware>() services.AddSingletonAs<GrainCommandMiddleware<CommentsCommand, ICommentsGrain>>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingletonAs<RulesByAppIndexCommandMiddleware>() services.AddSingletonAs<GrainCommandMiddleware<SchemaCommand, ISchemaGrain>>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingletonAs<SchemasByAppIndexCommandMiddleware>() services.AddSingletonAs<GrainCommandMiddleware<RuleCommand, IRuleGrain>>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingletonAs<SingletonCommandMiddleware>() services.AddSingletonAs<SingletonCommandMiddleware>()

6
src/Squidex/app-config/webpack.config.js

@ -125,7 +125,11 @@ module.exports = function (env) {
use: [{ use: [{
loader: 'raw-loader' loader: 'raw-loader'
}, { }, {
loader: 'sass-loader', options: { includePaths: [root('app', 'theme')] } loader: 'sass-loader', options: {
sassOptions: {
includePaths: [root('app', 'theme')]
}
}
}], }],
exclude: root('app', 'theme') exclude: root('app', 'theme')
}] }]

2
src/Squidex/app/app.module.ts

@ -55,7 +55,7 @@ export function configUIOptions() {
} }
export function configTitles() { export function configTitles() {
return new TitlesConfig({}, undefined, 'Squidex Headless CMS'); return new TitlesConfig(undefined, 'Squidex Headless CMS');
} }
export function configAnalyticsId() { export function configAnalyticsId() {

2
src/Squidex/app/features/administration/declarations.ts

@ -10,8 +10,10 @@ export * from './administration-area.component';
export * from './guards/user-must-exist.guard'; export * from './guards/user-must-exist.guard';
export * from './guards/unset-user.guard'; export * from './guards/unset-user.guard';
export * from './pages/event-consumers/event-consumer.component';
export * from './pages/event-consumers/event-consumers-page.component'; export * from './pages/event-consumers/event-consumers-page.component';
export * from './pages/restore/restore-page.component'; export * from './pages/restore/restore-page.component';
export * from './pages/users/user.component';
export * from './pages/users/user-page.component'; export * from './pages/users/user-page.component';
export * from './pages/users/users-page.component'; export * from './pages/users/users-page.component';

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

@ -15,11 +15,13 @@ import {
import { import {
AdministrationAreaComponent, AdministrationAreaComponent,
EventConsumerComponent,
EventConsumersPageComponent, EventConsumersPageComponent,
EventConsumersService, EventConsumersService,
EventConsumersState, EventConsumersState,
RestorePageComponent, RestorePageComponent,
UnsetUserGuard, UnsetUserGuard,
UserComponent,
UserMustExistGuard, UserMustExistGuard,
UserPageComponent, UserPageComponent,
UsersPageComponent, UsersPageComponent,
@ -73,8 +75,10 @@ const routes: Routes = [
], ],
declarations: [ declarations: [
AdministrationAreaComponent, AdministrationAreaComponent,
EventConsumerComponent,
EventConsumersPageComponent, EventConsumersPageComponent,
RestorePageComponent, RestorePageComponent,
UserComponent,
UserPageComponent, UserPageComponent,
UsersPageComponent UsersPageComponent
], ],

66
src/Squidex/app/features/administration/pages/event-consumers/event-consumer.component.ts

@ -0,0 +1,66 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable: component-selector
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { EventConsumerDto, EventConsumersState } from '@app/features/administration/internal';
@Component({
selector: '[sqxEventConsumer]',
template: `
<tr [class.faulted]="eventConsumer.error && eventConsumer.error?.length > 0">
<td class="auto-auto">
<span class="truncate">
<i class="faulted-icon icon icon-bug" (click)="error.emit()" [class.hidden]="!eventConsumer.error || eventConsumer.error?.length === 0"></i>
{{eventConsumer.name}}
</span>
</td>
<td class="cell-auto-right">
<span>{{eventConsumer.position}}</span>
</td>
<td class="cell-actions-lg">
<button type="button" class="btn btn-text" (click)="reset()" *ngIf="eventConsumer.canReset" title="Reset Event Consumer">
<i class="icon icon-reset"></i>
</button>
<button type="button" class="btn btn-text" (click)="start()" *ngIf="eventConsumer.canStart" title="Start Event Consumer">
<i class="icon icon-play"></i>
</button>
<button type="button" class="btn btn-text" (click)="stop()" *ngIf="eventConsumer.canStop" title="Stop Event Consumer">
<i class="icon icon-pause"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class EventConsumerComponent {
@Output()
public error = new EventEmitter();
@Input('sqxEventConsumer')
public eventConsumer: EventConsumerDto;
constructor(
public readonly eventConsumersState: EventConsumersState
) {
}
public start() {
this.eventConsumersState.start(this.eventConsumer);
}
public stop() {
this.eventConsumersState.stop(this.eventConsumer);
}
public reset() {
this.eventConsumersState.reset(this.eventConsumer);
}
}

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

@ -29,31 +29,8 @@
</tr> </tr>
</thead> </thead>
<tbody *ngFor="let eventConsumer of eventConsumersState.eventConsumers | async; trackBy: trackByEventConsumer"> <tbody *ngFor="let eventConsumer of eventConsumersState.eventConsumers | async; trackBy: trackByEventConsumer"
<tr [class.faulted]="eventConsumer.error && eventConsumer.error.length > 0"> [sqxEventConsumer]="eventConsumer" (error)="showError(eventConsumer)">
<td class="auto-auto">
<span class="truncate">
<i class="faulted-icon icon icon-bug" (click)="showError(eventConsumer)" [class.hidden]="!eventConsumer.error || eventConsumer.error.length === 0"></i>
{{eventConsumer.name}}
</span>
</td>
<td class="cell-auto-right">
<span>{{eventConsumer.position}}</span>
</td>
<td class="cell-actions-lg">
<button type="button" class="btn btn-text" (click)="reset(eventConsumer)" *ngIf="eventConsumer.canReset" title="Reset Event Consumer">
<i class="icon icon-reset"></i>
</button>
<button type="button" class="btn btn-text" (click)="start(eventConsumer)" *ngIf="eventConsumer.canStart" title="Start Event Consumer">
<i class="icon icon-play"></i>
</button>
<button type="button" class="btn btn-text" (click)="stop(eventConsumer)" *ngIf="eventConsumer.canStop" title="Stop Event Consumer">
<i class="icon icon-pause"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</tbody> </tbody>
</table> </table>
</ng-container> </ng-container>

12
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts

@ -38,18 +38,6 @@ export class EventConsumersPageComponent extends ResourceOwner implements OnInit
this.eventConsumersState.load(true, false); this.eventConsumersState.load(true, false);
} }
public start(eventConsumer: EventConsumerDto) {
this.eventConsumersState.start(eventConsumer);
}
public stop(eventConsumer: EventConsumerDto) {
this.eventConsumersState.stop(eventConsumer);
}
public reset(eventConsumer: EventConsumerDto) {
this.eventConsumersState.reset(eventConsumer);
}
public trackByEventConsumer(index: number, es: EventConsumerDto) { public trackByEventConsumer(index: number, es: EventConsumerDto) {
return es.name; return es.name;
} }

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

@ -1,4 +1,3 @@
<sqx-title message="User Management"></sqx-title>
<form [formGroup]="userForm.form" (ngSubmit)="save()"> <form [formGroup]="userForm.form" (ngSubmit)="save()">
<input style="display:none" type="password" name="foilautofill"/> <input style="display:none" type="password" name="foilautofill"/>
@ -6,10 +5,14 @@
<sqx-panel desiredWidth="26rem" isBlank="true" [isLazyLoaded]="false"> <sqx-panel desiredWidth="26rem" isBlank="true" [isLazyLoaded]="false">
<ng-container title> <ng-container title>
<ng-container *ngIf="usersState.selectedUser | async; else noUserTitle"> <ng-container *ngIf="usersState.selectedUser | async; else noUserTitle">
<sqx-title message="Edit User"></sqx-title>
Edit User Edit User
</ng-container> </ng-container>
<ng-template #noUserTitle> <ng-template #noUserTitle>
<sqx-title message="New User"></sqx-title>
New User New User
</ng-template> </ng-template>
</ng-container> </ng-container>

55
src/Squidex/app/features/administration/pages/users/user.component.ts

@ -0,0 +1,55 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable: component-selector
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { UserDto, UsersState } from '@app/features/administration/internal';
@Component({
selector: '[sqxUser]',
template: `
<tr [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user">
<img class="user-picture" title="{{user.displayName}}" [attr.src]="user | sqxUserDtoPicture" />
</td>
<td class="cell-auto">
<span class="user-name table-cell">{{user.displayName}}</span>
</td>
<td class="cell-auto">
<span class="user-email table-cell">{{user.email}}</span>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-text" (click)="lock()" sqxStopClick *ngIf="user.canLock" title="Lock User">
<i class="icon icon-unlocked"></i>
</button>
<button type="button" class="btn btn-text" (click)="unlock()" sqxStopClick *ngIf="user.canUnlock" title="Unlock User">
<i class="icon icon-lock"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserComponent {
@Input('sqxUser')
public user: UserDto;
constructor(
private readonly usersState: UsersState
) {
}
public lock() {
this.usersState.lock(this.user);
}
public unlock() {
this.usersState.unlock(this.user);
}
}

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

@ -51,27 +51,8 @@
<div class="grid-content"> <div class="grid-content">
<div sqxIgnoreScrollbar> <div sqxIgnoreScrollbar>
<table class="table table-items table-fixed" *ngIf="usersState.users | async; let users"> <table class="table table-items table-fixed" *ngIf="usersState.users | async; let users">
<tbody *ngFor="let user of users; trackBy: trackByUser"> <tbody *ngFor="let user of users; trackBy: trackByUser"
<tr [routerLink]="user.id" routerLinkActive="active"> [sqxUser]="user">
<td class="cell-user">
<img class="user-picture" title="{{user.name}}" [attr.src]="user | sqxUserDtoPicture" />
</td>
<td class="cell-auto">
<span class="user-name table-cell">{{user.displayName}}</span>
</td>
<td class="cell-auto">
<span class="user-email table-cell">{{user.email}}</span>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-text" (click)="lock(user)" sqxStopClick *ngIf="user.canLock" title="Lock User">
<i class="icon icon-unlocked"></i>
</button>
<button type="button" class="btn btn-text" (click)="unlock(user)" sqxStopClick *ngIf="user.canUnlock" title="Unlock User">
<i class="icon icon-lock"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</tbody> </tbody>
</table> </table>
</div> </div>

11
src/Squidex/app/features/administration/pages/users/users-page.component.ts

@ -43,16 +43,7 @@ export class UsersPageComponent implements OnInit {
this.usersState.goNext(); this.usersState.goNext();
} }
public lock(user: UserDto) {
this.usersState.lock(user);
}
public unlock(user: UserDto) {
this.usersState.unlock(user);
}
public trackByUser(index: number, user: UserDto) { public trackByUser(index: number, user: UserDto) {
return user.id; return user.id;
} }
} }

4
src/Squidex/app/features/administration/services/event-consumers.service.ts

@ -33,7 +33,7 @@ export class EventConsumerDto {
public readonly canStop: boolean; public readonly canStop: boolean;
public readonly canStart: boolean; public readonly canStart: boolean;
public readonly canRestart: boolean; public readonly canReset: boolean;
constructor(links: ResourceLinks, constructor(links: ResourceLinks,
public readonly name: string, public readonly name: string,
@ -46,7 +46,7 @@ export class EventConsumerDto {
this.canStop = hasAnyLink(links, 'stop'); this.canStop = hasAnyLink(links, 'stop');
this.canStart = hasAnyLink(links, 'start'); this.canStart = hasAnyLink(links, 'start');
this.canRestart = hasAnyLink(links, 'canReset'); this.canReset = hasAnyLink(links, 'reset');
} }
} }

2
src/Squidex/app/features/api/api-area.component.html

@ -1,4 +1,4 @@
<sqx-title message="{app} | Settings" parameter1="app" [value1]="appsState.appDisplayName"></sqx-title> <sqx-title message="API"></sqx-title>
<sqx-panel theme="dark" desiredWidth="12rem"> <sqx-panel theme="dark" desiredWidth="12rem">
<ng-container title> <ng-container title>

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

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

5
src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts

@ -26,7 +26,7 @@ export class GraphQLPageComponent implements AfterViewInit {
public graphiQLContainer: ElementRef; public graphiQLContainer: ElementRef;
constructor( constructor(
public readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly graphQlService: GraphQlService private readonly graphQlService: GraphQlService
) { ) {
} }
@ -45,5 +45,4 @@ export class GraphQLPageComponent implements AfterViewInit {
private request(params: any) { private request(params: any) {
return this.graphQlService.query(this.appsState.appName, params).pipe(catchError(response => of(response.error))).toPromise(); return this.graphQlService.query(this.appsState.appName, params).pipe(catchError(response => of(response.error))).toPromise();
} }
} }

6
src/Squidex/app/features/apps/pages/news-dialog.component.ts

@ -15,12 +15,12 @@ import { FeatureDto } from '@app/shared';
templateUrl: './news-dialog.component.html' templateUrl: './news-dialog.component.html'
}) })
export class NewsDialogComponent { export class NewsDialogComponent {
@Input()
public features: FeatureDto[];
@Output() @Output()
public close = new EventEmitter(); public close = new EventEmitter();
@Input()
public features: FeatureDto[];
public emitClose() { public emitClose() {
this.close.emit(); this.close.emit();
} }

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

@ -1,4 +1,4 @@
<sqx-title message="{app} | Assets" parameter1="app" [value1]="appsState.appDisplayName"></sqx-title> <sqx-title message="Assets"></sqx-title>
<sqx-panel desiredWidth="*" minWidth="50rem" showSidebar="true"> <sqx-panel desiredWidth="*" minWidth="50rem" showSidebar="true">
<ng-container title> <ng-container title>

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

@ -9,7 +9,6 @@ import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { import {
AppsState,
AssetsState, AssetsState,
LocalStoreService, LocalStoreService,
Queries, Queries,
@ -31,7 +30,6 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit {
public isListView: boolean; public isListView: boolean;
constructor( constructor(
public readonly appsState: AppsState,
public readonly assetsState: AssetsState, public readonly assetsState: AssetsState,
private readonly localStore: LocalStoreService, private readonly localStore: LocalStoreService,
private readonly uiState: UIState private readonly uiState: UIState
@ -74,5 +72,4 @@ export class AssetsPageComponent extends ResourceOwner implements OnInit {
this.localStore.setBoolean('squidex.assets.list-view', isListView); this.localStore.setBoolean('squidex.assets.list-view', isListView);
} }
} }

2
src/Squidex/app/features/content/declarations.ts

@ -17,7 +17,7 @@ export * from './pages/schemas/schemas-page.component';
export * from './shared/array-editor.component'; export * from './shared/array-editor.component';
export * from './shared/array-item.component'; export * from './shared/array-item.component';
export * from './shared/assets-editor.component'; export * from './shared/assets-editor.component';
export * from './shared/content-item.component'; export * from './shared/content.component';
export * from './shared/content-status.component'; export * from './shared/content-status.component';
export * from './shared/content-value.component'; export * from './shared/content-value.component';
export * from './shared/content-value-editor.component'; export * from './shared/content-value-editor.component';

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

@ -25,9 +25,9 @@ import {
ArrayItemComponent, ArrayItemComponent,
AssetsEditorComponent, AssetsEditorComponent,
CommentsPageComponent, CommentsPageComponent,
ContentComponent,
ContentFieldComponent, ContentFieldComponent,
ContentHistoryPageComponent, ContentHistoryPageComponent,
ContentItemComponent,
ContentPageComponent, ContentPageComponent,
ContentsFiltersPageComponent, ContentsFiltersPageComponent,
ContentsPageComponent, ContentsPageComponent,
@ -112,7 +112,7 @@ const routes: Routes = [
CommentsPageComponent, CommentsPageComponent,
ContentFieldComponent, ContentFieldComponent,
ContentHistoryPageComponent, ContentHistoryPageComponent,
ContentItemComponent, ContentComponent,
ContentPageComponent, ContentPageComponent,
ContentsFiltersPageComponent, ContentsFiltersPageComponent,
ContentsPageComponent, ContentsPageComponent,

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

@ -26,5 +26,4 @@ export class CommentsPageComponent implements OnInit {
public ngOnInit() { public ngOnInit() {
this.commentsId = allParams(this.route)['contentId']; this.commentsId = allParams(this.route)['contentId'];
} }
} }

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

@ -30,6 +30,9 @@ import {
templateUrl: './content-field.component.html' templateUrl: './content-field.component.html'
}) })
export class ContentFieldComponent implements DoCheck, OnChanges { export class ContentFieldComponent implements DoCheck, OnChanges {
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
@Input() @Input()
public form: EditContentForm; public form: EditContentForm;
@ -54,9 +57,6 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
@Input() @Input()
public languages: AppLanguageDto[]; public languages: AppLanguageDto[];
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
public selectedFormControl: AbstractControl; public selectedFormControl: AbstractControl;
public selectedFormControlCompare?: AbstractControl; public selectedFormControlCompare?: AbstractControl;
@ -195,5 +195,4 @@ export class ContentFieldComponent implements DoCheck, OnChanges {
private configKey() { private configKey() {
return `squidex.schemas.${this.schema.id}.fields.${this.field.fieldId}.show-all`; return `squidex.schemas.${this.schema.id}.fields.${this.field.fieldId}.show-all`;
} }
} }

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

@ -1,4 +1,4 @@
<sqx-title message="{app} | {schema} | Content" parameter1="app" parameter2="schema" [value1]="appsState.appDisplayName" [value2]="schema.displayName"></sqx-title> <sqx-title [message]="schema.displayName"></sqx-title>
<form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()"> <form [formGroup]="contentForm.form" (ngSubmit)="saveAndPublish()">
<sqx-panel desiredWidth="*" minWidth="60rem" [showSidebar]="content"> <sqx-panel desiredWidth="*" minWidth="60rem" [showSidebar]="content">
@ -7,11 +7,15 @@
<i class="icon-angle-left"></i> <i class="icon-angle-left"></i>
</a> </a>
<ng-container *ngIf="!content else notNewTitle"> <ng-container *ngIf="content else noContentTitle">
New Content <sqx-title message="Edit Content"></sqx-title>
Edit Content
</ng-container> </ng-container>
<ng-template #notNewTitle> <ng-template #noContentTitle>
Content <sqx-title message="New Content"></sqx-title>
New Content
</ng-template> </ng-template>
</ng-container> </ng-container>

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

@ -15,7 +15,6 @@ import { ContentVersionSelected } from './../messages';
import { import {
ApiUrlConfig, ApiUrlConfig,
AppLanguageDto, AppLanguageDto,
AppsState,
AuthService, AuthService,
AutoSaveKey, AutoSaveKey,
AutoSaveService, AutoSaveService,
@ -70,7 +69,6 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
public dueTimeSelector: DueTimeSelectorComponent; public dueTimeSelector: DueTimeSelectorComponent;
constructor(apiUrl: ApiUrlConfig, authService: AuthService, constructor(apiUrl: ApiUrlConfig, authService: AuthService,
public readonly appsState: AppsState,
public readonly contentsState: ContentsState, public readonly contentsState: ContentsState,
private readonly autoSaveService: AutoSaveService, private readonly autoSaveService: AutoSaveService,
private readonly dialogs: DialogService, private readonly dialogs: DialogService,
@ -100,11 +98,9 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
this.own( this.own(
this.schemasState.selectedSchema this.schemasState.selectedSchema
.subscribe(schema => { .subscribe(schema => {
if (schema) { this.schema = schema;
this.schema = schema!;
this.contentForm = new EditContentForm(this.languages, this.schema); this.contentForm = new EditContentForm(this.languages, this.schema);
}
})); }));
this.own( this.own(

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

@ -32,6 +32,12 @@ import { AppLanguageDto, RootFieldDto } from '@app/shared';
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class FieldLanguagesComponent { export class FieldLanguagesComponent {
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
@Output()
public showAllControlsChange = new EventEmitter<boolean>();
@Input() @Input()
public field: RootFieldDto; public field: RootFieldDto;
@ -43,10 +49,4 @@ export class FieldLanguagesComponent {
@Input() @Input()
public languages: AppLanguageDto[]; public languages: AppLanguageDto[];
@Output()
public languageChange = new EventEmitter<AppLanguageDto>();
@Output()
public showAllControlsChange = new EventEmitter<boolean>();
} }

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

@ -37,9 +37,7 @@ export class ContentsFiltersPageComponent extends ResourceOwner implements OnIni
this.own( this.own(
this.schemasState.selectedSchema this.schemasState.selectedSchema
.subscribe(schema => { .subscribe(schema => {
if (schema) { this.schemaQueries = new Queries(this.uiState, `schemas.${schema.name}`);
this.schemaQueries = new Queries(this.uiState, `schemas.${schema.name}`);
}
})); }));
} }

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

@ -1,4 +1,4 @@
<sqx-title message="{app} | {schema} | Contents" parameter1="app" parameter2="schema" [value1]="appsState.appDisplayName" [value2]="schema.displayName"></sqx-title> <sqx-title [message]="schema.displayName"></sqx-title>
<sqx-panel desiredWidth="*" minWidth="50rem" contentClass="grid" showSidebar="true"> <sqx-panel desiredWidth="*" minWidth="50rem" contentClass="grid" showSidebar="true">
<ng-container title> <ng-container title>
@ -91,21 +91,18 @@
<div class="grid-content" [sqxSyncScrolling]="header"> <div class="grid-content" [sqxSyncScrolling]="header">
<div class="table-container" sqxIgnoreScrollbar> <div class="table-container" sqxIgnoreScrollbar>
<table class="table table-items table-fixed" [style.minWidth]="minWidth"> <table class="table table-items table-fixed" [style.minWidth]="minWidth">
<tbody *ngFor="let content of contentsState.contents | async; trackBy: trackByContent"> <tbody *ngFor="let content of contentsState.contents | async; trackBy: trackByContent"
<tr [sqxContent]="content" [sqxContent]="content"
(delete)="delete(content)" (delete)="delete(content)"
[selected]="isItemSelected(content)" [selected]="isItemSelected(content)"
(selectedChange)="selectItem(content, $event)" (selectedChange)="selectItem(content, $event)"
(statusChange)="changeStatus(content, $event)" (statusChange)="changeStatus(content, $event)"
(clone)="clone(content)" (clone)="clone(content)"
[language]="language" [link]="[content.id]"
[canClone]="contentsState.snapshot.canCreate" [language]="language"
[routerLink]="[content.id]" [canClone]="contentsState.snapshot.canCreate"
[routerLinkActive]="'active'" [schema]="schema"
[schema]="schema" [schemaFields]="schema.listFields">
[schemaFields]="schema.listFields">
</tr>
<tr class="spacer"></tr>
</tbody> </tbody>
</table> </table>
</div> </div>

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

@ -10,7 +10,6 @@ import { onErrorResumeNext, switchMap, tap } from 'rxjs/operators';
import { import {
AppLanguageDto, AppLanguageDto,
AppsState,
ContentDto, ContentDto,
ContentsState, ContentsState,
ImmutableArray, ImmutableArray,
@ -57,7 +56,6 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
public dueTimeSelector: DueTimeSelectorComponent; public dueTimeSelector: DueTimeSelectorComponent;
constructor( constructor(
public readonly appsState: AppsState,
public readonly contentsState: ContentsState, public readonly contentsState: ContentsState,
private readonly languagesState: LanguagesState, private readonly languagesState: LanguagesState,
private readonly schemasState: SchemasState, private readonly schemasState: SchemasState,
@ -72,7 +70,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
.subscribe(schema => { .subscribe(schema => {
this.resetSelection(); this.resetSelection();
this.schema = schema!; this.schema = schema;
this.minWidth = `${300 + (200 * this.schema.listFields.length)}px`; this.minWidth = `${300 + (200 * this.schema.listFields.length)}px`;
@ -239,5 +237,4 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.queryModel = queryModelFromSchema(this.schema, this.languages.values, this.contentsState.snapshot.statuses); this.queryModel = queryModelFromSchema(this.schema, this.languages.values, this.contentsState.snapshot.statuses);
} }
} }
} }

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

@ -1,4 +1,4 @@
<sqx-title message="{app} | Schemas" parameter1="app" [value1]="appsState.appDisplayName"></sqx-title> <sqx-title message="Contents"></sqx-title>
<sqx-panel theme="dark" desiredWidth="16rem" showSecondHeader="true"> <sqx-panel theme="dark" desiredWidth="16rem" showSecondHeader="true">
<ng-container title> <ng-container title>

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

@ -8,11 +8,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { import { SchemaCategory, SchemasState } from '@app/shared';
AppsState,
SchemaCategory,
SchemasState
} from '@app/shared';
@Component({ @Component({
selector: 'sqx-schemas-page', selector: 'sqx-schemas-page',
@ -23,7 +19,6 @@ export class SchemasPageComponent implements OnInit {
public schemasFilter = new FormControl(); public schemasFilter = new FormControl();
constructor( constructor(
public readonly appsState: AppsState,
public readonly schemasState: SchemasState public readonly schemasState: SchemasState
) { ) {
} }
@ -35,5 +30,4 @@ export class SchemasPageComponent implements OnInit {
public trackByCategory(index: number, category: SchemaCategory) { public trackByCategory(index: number, category: SchemaCategory) {
return category.name; return category.name;
} }
} }

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

@ -27,8 +27,8 @@
(load)="addAsset(file, $event)"> (load)="addAsset(file, $event)">
</sqx-asset> </sqx-asset>
<sqx-asset *ngFor="let asset of snapshot.assets; trackBy: trackByAsset" [asset]="asset" [isCompact]="isCompact" removeMode="true" <sqx-asset *ngFor="let asset of snapshot.assets; trackBy: trackByAsset" [asset]="asset" [isCompact]="isCompact" removeMode="true"
(update)="notifyOthers($event)" (update)="notifyOthers(asset)"
(remove)="removeLoadedAsset($event)"> (remove)="removeLoadedAsset(asset)">
</sqx-asset> </sqx-asset>
</div> </div>
</ng-container> </ng-container>
@ -48,8 +48,8 @@
<sqx-asset [asset]="asset" removeMode="true" <sqx-asset [asset]="asset" removeMode="true"
[isListView]="true" [isListView]="true"
[isCompact]="isCompact" [isCompact]="isCompact"
(update)="notifyOthers($event)" (update)="notifyOthers(asset)"
(remove)="removeLoadedAsset($event)"> (remove)="removeLoadedAsset(asset)">
</sqx-asset> </sqx-asset>
</div> </div>
</div> </div>

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

@ -1,98 +0,0 @@
<td class="cell-select" sqxStopClick>
<ng-container *ngIf="!isReference; else referenceTemplate">
<input type="checkbox" class="form-check"
[disabled]="!selectable"
[ngModel]="selected || !selectable"
(ngModelChange)="selectedChange.emit($event)" />
</ng-container>
<ng-template #referenceTemplate>
<i class="icon-drag2 drag-handle"></i>
</ng-template>
</td>
<td class="cell-actions cell-actions-left" *ngIf="!isReadOnly && !isDirty" sqxStopClick>
<div class="dropdown dropdown-options" *ngIf="content">
<button type="button" class="btn btn-text-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
<i class="icon-dots"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" position="bottom-left" @fade>
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="emitChangeStatus(info.status)">
Change to <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}}
</a>
<a class="dropdown-item" (click)="emitClone(); dropdown.hide()" *ngIf="canClone">
Clone
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="emitDelete()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">
Delete
</a>
</div>
</ng-container>
</div>
</td>
<ng-container *ngIf="isDirty">
<td class="cell-actions" >
<button type="button" class="btn btn-text-secondary btn-cancel" (click)="cancel()" sqxStopClick>
<i class="icon-close"></i>
</button>
</td>
<td class="cell-user" >
<button type="button" class="btn btn-success" (click)="save()" sqxStopClick>
<i class="icon-checkmark"></i>
</button>
</td>
</ng-container>
<td class="cell-user" *ngIf="!isCompact && !isDirty" [sqxStopClick]="isDirty">
<img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td>
<td class="cell-auto cell-content" *ngFor="let field of schemaFields; let i = index; trackBy: trackByFieldFn" [sqxStopClick]="isDirty || (field.isInlineEditable && patchAllowed)">
<ng-container *ngIf="field.isInlineEditable && patchAllowed; else displayTemplate">
<sqx-content-value-editor [form]="patchForm.form" [field]="field"></sqx-content-value-editor>
</ng-container>
<ng-template #displayTemplate>
<sqx-content-value [value]="values[i]"></sqx-content-value>
</ng-template>
</td>
<td class="cell-time" *ngIf="!isCompact" [sqxStopClick]="isDirty">
<sqx-content-status
[status]="content.status"
[statusColor]="content.statusColor"
[scheduledTo]="content.scheduleJob?.status"
[scheduledAt]="content.scheduleJob?.dueTime"
[isPending]="content.isPending">
</sqx-content-status>
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td>
<td class="cell-actions" *ngIf="isReference" [sqxStopClick]="isDirty">
<div class="reference-edit">
<button type="button" class="btn btn-text-secondary">
<i class="icon-dots"></i>
</button>
<div class="reference-menu">
<a class="btn btn-text-secondary" [routerLink]="['../..', schema.name, content.id]">
<i class="icon-pencil"></i>
</a>
<button type="button" class="btn btn-text-secondary" (click)="emitDelete()">
<i class="icon-close"></i>
</button>
</div>
</div>
</td>

3
src/Squidex/app/features/content/shared/content-status.component.ts

@ -49,5 +49,4 @@ export class ContentStatusComponent {
return this.status; return this.status;
} }
} }
} }

3
src/Squidex/app/features/content/shared/content-value.component.ts

@ -17,8 +17,7 @@ import { HtmlValue, Types } from '@app/shared';
</ng-container> </ng-container>
<ng-template #html> <ng-template #html>
<span class="truncate" [innerHTML]="value.html"></span> <span class="truncate" [innerHTML]="value.html"></span>
</ng-template> </ng-template>`,
`,
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ContentValueComponent { export class ContentValueComponent {

101
src/Squidex/app/features/content/shared/content.component.html

@ -0,0 +1,101 @@
<tr [routerLink]="link" routerLinkActive="active">
<td class="cell-select" sqxStopClick>
<ng-container *ngIf="!isReference; else referenceTemplate">
<input type="checkbox" class="form-check"
[disabled]="!selectable"
[ngModel]="selected || !selectable"
(ngModelChange)="selectedChange.emit($event)" />
</ng-container>
<ng-template #referenceTemplate>
<i class="icon-drag2 drag-handle"></i>
</ng-template>
</td>
<td class="cell-actions cell-actions-left" *ngIf="!isReadOnly && !isDirty" sqxStopClick>
<div class="dropdown dropdown-options" *ngIf="content">
<button type="button" class="btn btn-text-secondary" (click)="dropdown.toggle()" [class.active]="dropdown.isOpen | async" #buttonOptions>
<i class="icon-dots"></i>
</button>
<ng-container *sqxModal="dropdown;closeAlways:true">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonOptions" position="bottom-left" @fade>
<a class="dropdown-item" *ngFor="let info of content.statusUpdates" (click)="emitChangeStatus(info.status)">
Change to <i class="icon-circle icon-sm" [style.color]="info.color"></i> {{info.status}}
</a>
<a class="dropdown-item" (click)="emitClone(); dropdown.hide()" *ngIf="canClone">
Clone
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="emitDelete()"
confirmTitle="Delete content"
confirmText="Do you really want to delete the content?">
Delete
</a>
</div>
</ng-container>
</div>
</td>
<ng-container *ngIf="isDirty">
<td class="cell-actions" >
<button type="button" class="btn btn-text-secondary btn-cancel" (click)="cancel()" sqxStopClick>
<i class="icon-close"></i>
</button>
</td>
<td class="cell-user" >
<button type="button" class="btn btn-success" (click)="save()" sqxStopClick>
<i class="icon-checkmark"></i>
</button>
</td>
</ng-container>
<td class="cell-user" *ngIf="!isCompact && !isDirty" [sqxStopClick]="isDirty">
<img class="user-picture" title="{{content.lastModifiedBy | sqxUserNameRef}}" [attr.src]="content.lastModifiedBy | sqxUserPictureRef" />
</td>
<td class="cell-auto cell-content" *ngFor="let field of schemaFields; let i = index; trackBy: trackByFieldFn" [sqxStopClick]="isDirty || (field.isInlineEditable && patchAllowed)">
<ng-container *ngIf="field.isInlineEditable && patchAllowed; else displayTemplate">
<sqx-content-value-editor [form]="patchForm.form" [field]="field"></sqx-content-value-editor>
</ng-container>
<ng-template #displayTemplate>
<sqx-content-value [value]="values[i]"></sqx-content-value>
</ng-template>
</td>
<td class="cell-time" *ngIf="!isCompact" [sqxStopClick]="isDirty">
<sqx-content-status
[status]="content.status"
[statusColor]="content.statusColor"
[scheduledTo]="content.scheduleJob?.status"
[scheduledAt]="content.scheduleJob?.dueTime"
[isPending]="content.isPending">
</sqx-content-status>
<small class="item-modified">{{content.lastModified | sqxFromNow}}</small>
</td>
<td class="cell-actions" *ngIf="isReference" [sqxStopClick]="isDirty">
<div class="reference-edit">
<button type="button" class="btn btn-text-secondary">
<i class="icon-dots"></i>
</button>
<div class="reference-menu">
<a class="btn btn-text-secondary" [routerLink]="['../..', schema.name, content.id]">
<i class="icon-pencil"></i>
</a>
<button type="button" class="btn btn-text-secondary" (click)="emitDelete()">
<i class="icon-close"></i>
</button>
</div>
</div>
</td>
</tr>
<tr class="spacer"></tr>

0
src/Squidex/app/features/content/shared/content-item.component.scss → src/Squidex/app/features/content/shared/content.component.scss

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

@ -24,14 +24,14 @@ import {
@Component({ @Component({
selector: '[sqxContent]', selector: '[sqxContent]',
styleUrls: ['./content-item.component.scss'], styleUrls: ['./content.component.scss'],
templateUrl: './content-item.component.html', templateUrl: './content.component.html',
animations: [ animations: [
fadeAnimation fadeAnimation
], ],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ContentItemComponent implements OnChanges { export class ContentComponent implements OnChanges {
@Output() @Output()
public clone = new EventEmitter(); public clone = new EventEmitter();
@ -71,6 +71,9 @@ export class ContentItemComponent implements OnChanges {
@Input() @Input()
public isCompact = false; public isCompact = false;
@Input()
public link: any = null;
@Input('sqxContent') @Input('sqxContent')
public content: ContentDto; public content: ContentDto;
@ -170,5 +173,4 @@ export class ContentItemComponent implements OnChanges {
public trackByField(field: FieldDto) { public trackByField(field: FieldDto) {
return field.fieldId + this.schema.id; return field.fieldId + this.schema.id;
} }
} }

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

@ -62,16 +62,15 @@
<div class="grid-content" [sqxSyncScrolling]="header"> <div class="grid-content" [sqxSyncScrolling]="header">
<div class="table-container" sqxIgnoreScrollbar> <div class="table-container" sqxIgnoreScrollbar>
<table class="table table-items table-fixed" [style.minWidth]="minWidth" *ngIf="contentsState.contents | async; let contents"> <table class="table table-items table-fixed" [style.minWidth]="minWidth" *ngIf="contentsState.contents | async; let contents">
<tbody *ngFor="let content of contents; trackBy: trackByContent"> <tbody *ngFor="let content of contents; trackBy: trackByContent"
<tr [sqxContent]="content" [sqxContent]="content"
[selectable]="!isItemAlreadySelected(content)" [selectable]="!isItemAlreadySelected(content)"
[selected]="isItemSelected(content)" [selected]="isItemSelected(content)"
(selectedChange)="selectContent(content)" (selectedChange)="selectContent(content)"
[language]="language" [language]="language"
[schema]="schema" [schema]="schema"
[schemaFields]="schema.referenceFields" [schemaFields]="schema.referenceFields"
isReadOnly="true"></tr> isReadOnly="true">
<tr class="spacer"></tr>
</tbody> </tbody>
</table> </table>
</div> </div>

9
src/Squidex/app/features/content/shared/contents-selector.component.ts

@ -27,6 +27,9 @@ import {
] ]
}) })
export class ContentsSelectorComponent extends ResourceOwner implements OnInit { export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
@Output()
public select = new EventEmitter<ContentDto[]>();
@Input() @Input()
public language: LanguageDto; public language: LanguageDto;
@ -42,9 +45,6 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
@Input() @Input()
public schema: SchemaDetailsDto; public schema: SchemaDetailsDto;
@Output()
public select = new EventEmitter<ContentDto[]>();
public queryModel: QueryModel; public queryModel: QueryModel;
public selectedItems: { [id: string]: ContentDto; } = {}; public selectedItems: { [id: string]: ContentDto; } = {};
@ -142,5 +142,4 @@ export class ContentsSelectorComponent extends ResourceOwner implements OnInit {
public trackByContent(content: ContentDto): string { public trackByContent(content: ContentDto): string {
return content.id; return content.id;
} }
} }

3
src/Squidex/app/features/content/shared/due-time-selector.component.ts

@ -48,5 +48,4 @@ export class DueTimeSelectorComponent {
this.dueTimeFunction = null!; this.dueTimeFunction = null!;
this.dueTime = null; this.dueTime = null;
} }
} }

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

@ -9,16 +9,15 @@
<table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="schema && snapshot.contentItems && snapshot.contentItems.length > 0" <table class="table table-items table-fixed" [class.disabled]="snapshot.isDisabled" *ngIf="schema && snapshot.contentItems && snapshot.contentItems.length > 0"
[sqxSortModel]="snapshot.contentItems.mutableValues" [sqxSortModel]="snapshot.contentItems.mutableValues"
(sqxSort)="sort($event)"> (sqxSort)="sort($event)">
<tbody *ngFor="let content of snapshot.contentItems; trackBy: trackByContent"> <tbody *ngFor="let content of snapshot.contentItems; trackBy: trackByContent"
<tr [sqxContent]="content" [sqxContent]="content"
[language]="language" [language]="language"
[isReadOnly]="true" [isReadOnly]="true"
[isReference]="true" [isReference]="true"
[isCompact]="isCompact" [isCompact]="isCompact"
[schema]="schema" [schema]="schema"
[schemaFields]="schema.referenceFields" [schemaFields]="schema.referenceFields"
(delete)="remove(content)"></tr> (delete)="remove(content)">
<tr class="spacer"></tr>
</tbody> </tbody>
</table> </table>
</ng-container> </ng-container>

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

@ -1,6 +1,6 @@
<ng-container *ngIf="app | async; let app"> <sqx-title message="Dashboard"></sqx-title>
<sqx-title message="{app} | Dashboard" parameter1="app" [value1]="app.displayName"></sqx-title>
<ng-container *ngIf="appsState.selectedApp | async; let app">
<div class="dashboard" @fade> <div class="dashboard" @fade>
<div class="dashboard-inner"> <div class="dashboard-inner">
<div> <div>

15
src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts

@ -51,8 +51,6 @@ export class DashboardPageComponent extends ResourceOwner implements OnInit {
public isPerformanceStacked = false; public isPerformanceStacked = false;
public app = this.appsState.selectedValidApp;
public chartOptions = { public chartOptions = {
responsive: true, responsive: true,
scales: { scales: {
@ -106,7 +104,7 @@ export class DashboardPageComponent extends ResourceOwner implements OnInit {
public ngOnInit() { public ngOnInit() {
this.own( this.own(
this.app.pipe( this.appsState.selectedApp.pipe(
switchMap(app => this.usagesService.getTodayStorage(app.name))) switchMap(app => this.usagesService.getTodayStorage(app.name)))
.subscribe(dto => { .subscribe(dto => {
this.assetsCurrent = dto.size; this.assetsCurrent = dto.size;
@ -114,7 +112,7 @@ export class DashboardPageComponent extends ResourceOwner implements OnInit {
})); }));
this.own( this.own(
this.app.pipe( this.appsState.selectedApp.pipe(
switchMap(app => this.usagesService.getMonthCalls(app.name))) switchMap(app => this.usagesService.getMonthCalls(app.name)))
.subscribe(dto => { .subscribe(dto => {
this.callsCurrent = dto.count; this.callsCurrent = dto.count;
@ -122,14 +120,14 @@ export class DashboardPageComponent extends ResourceOwner implements OnInit {
})); }));
this.own( this.own(
this.app.pipe( this.appsState.selectedApp.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.own( this.own(
this.app.pipe( this.appsState.selectedApp.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 => {
const labels = createLabels(dtos); const labels = createLabels(dtos);
@ -166,7 +164,7 @@ export class DashboardPageComponent extends ResourceOwner implements OnInit {
})); }));
this.own( this.own(
this.app.pipe( this.appsState.selectedApp.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 => {
const labels = createLabelsFromSet(dtos); const labels = createLabelsFromSet(dtos);
@ -215,5 +213,4 @@ function createLabels(dtos: { date: DateTime }[]): string[] {
function createLabelsFromSet(dtos: { [category: string]: { date: DateTime }[] }): string[] { function createLabelsFromSet(dtos: { [category: string]: { date: DateTime }[] }): string[] {
return createLabels(dtos[Object.keys(dtos)[0]]); return createLabels(dtos[Object.keys(dtos)[0]]);
} }

1
src/Squidex/app/features/rules/declarations.ts

@ -12,6 +12,7 @@ export * from './pages/rules/triggers/content-changed-trigger.component';
export * from './pages/rules/triggers/schema-changed-trigger.component'; export * from './pages/rules/triggers/schema-changed-trigger.component';
export * from './pages/rules/triggers/usage-trigger.component'; export * from './pages/rules/triggers/usage-trigger.component';
export * from './pages/rules/rule.component';
export * from './pages/rules/rule-element.component'; export * from './pages/rules/rule-element.component';
export * from './pages/rules/rule-wizard.component'; export * from './pages/rules/rule-wizard.component';
export * from './pages/rules/rules-page.component'; export * from './pages/rules/rules-page.component';

2
src/Squidex/app/features/rules/module.ts

@ -18,6 +18,7 @@ import {
AssetChangedTriggerComponent, AssetChangedTriggerComponent,
ContentChangedTriggerComponent, ContentChangedTriggerComponent,
GenericActionComponent, GenericActionComponent,
RuleComponent,
RuleElementComponent, RuleElementComponent,
RuleEventBadgeClassPipe, RuleEventBadgeClassPipe,
RuleEventsPageComponent, RuleEventsPageComponent,
@ -57,6 +58,7 @@ const routes: Routes = [
AssetChangedTriggerComponent, AssetChangedTriggerComponent,
ContentChangedTriggerComponent, ContentChangedTriggerComponent,
GenericActionComponent, GenericActionComponent,
RuleComponent,
RuleElementComponent, RuleElementComponent,
RuleEventBadgeClassPipe, RuleEventBadgeClassPipe,
RuleEventsPageComponent, RuleEventsPageComponent,

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

@ -1,4 +1,4 @@
<sqx-title message="{app} | Rules Events" parameter1="app" [value1]="appsState.appDisplayName"></sqx-title> <sqx-title message="Events"></sqx-title>
<sqx-panel desiredWidth="63rem"> <sqx-panel desiredWidth="63rem">
<ng-container title> <ng-container title>

5
src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts

@ -8,7 +8,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { import {
AppsState,
RuleEventDto, RuleEventDto,
RuleEventsState RuleEventsState
} from '@app/shared'; } from '@app/shared';
@ -22,7 +21,6 @@ export class RuleEventsPageComponent implements OnInit {
public selectedEventId: string | null = null; public selectedEventId: string | null = null;
constructor( constructor(
public readonly appsState: AppsState,
public readonly ruleEventsState: RuleEventsState public readonly ruleEventsState: RuleEventsState
) { ) {
} }
@ -58,5 +56,4 @@ export class RuleEventsPageComponent implements OnInit {
public trackByRuleEvent(index: number, ruleEvent: RuleEventDto) { public trackByRuleEvent(index: number, ruleEvent: RuleEventDto) {
return ruleEvent.id; return ruleEvent.id;
} }
} }

24
src/Squidex/app/features/rules/pages/rules/rule-wizard.component.ts

@ -27,18 +27,6 @@ export const MODE_EDIT_ACTION = 'EditAction';
templateUrl: './rule-wizard.component.html' templateUrl: './rule-wizard.component.html'
}) })
export class RuleWizardComponent implements AfterViewInit, OnInit { export class RuleWizardComponent implements AfterViewInit, OnInit {
public actionForm = new Form<FormGroup, any>(new FormGroup({}));
public actionType: string;
public action: any = {};
public triggerForm = new Form<FormGroup, any>(new FormGroup({}));
public triggerType: string;
public trigger: any = {};
public isEditable: boolean;
public step = 1;
@Output() @Output()
public complete = new EventEmitter(); public complete = new EventEmitter();
@ -57,6 +45,18 @@ export class RuleWizardComponent implements AfterViewInit, OnInit {
@Input() @Input()
public mode = MODE_WIZARD; public mode = MODE_WIZARD;
public actionForm = new Form<FormGroup, any>(new FormGroup({}));
public actionType: string;
public action: any = {};
public triggerForm = new Form<FormGroup, any>(new FormGroup({}));
public triggerType: string;
public trigger: any = {};
public isEditable: boolean;
public step = 1;
constructor( constructor(
private readonly rulesState: RulesState private readonly rulesState: RulesState
) { ) {

87
src/Squidex/app/features/rules/pages/rules/rule.component.ts

@ -0,0 +1,87 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable: component-selector
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import {
ActionsDto,
RuleDto,
RulesState,
TriggersDto
} from '@app/shared';
@Component({
selector: '[sqxRule]',
template: `
<tr>
<td class="cell-separator">
<h3>If</h3>
</td>
<td class="cell-auto">
<span (click)="editTrigger.emit()">
<sqx-rule-element [type]="rule.triggerType" [element]="ruleTriggers[rule.triggerType]"></sqx-rule-element>
</span>
</td>
<td class="cell-separator">
<h3>then</h3>
</td>
<td class="cell-auto">
<span (click)="editAction.emit()">
<sqx-rule-element [type]="rule.actionType" [element]="ruleActions[rule.actionType]"></sqx-rule-element>
</span>
</td>
<td class="cell-actions">
<sqx-toggle [disabled]="!rule.canDisable && !rule.canEnable" [ngModel]="rule.isEnabled" (ngModelChange)="toggle()"></sqx-toggle>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-text-danger"
[disabled]="!rule.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="Delete rule"
confirmText="Do you really want to delete the rule?">
<i class="icon-bin2"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class RuleComponent {
@Output()
public editTrigger = new EventEmitter();
@Output()
public editAction = new EventEmitter();
@Input()
public ruleTriggers: TriggersDto;
@Input()
public ruleActions: ActionsDto;
@Input('sqxRule')
public rule: RuleDto;
constructor(
private readonly rulesState: RulesState
) {
}
public delete() {
this.rulesState.delete(this.rule);
}
public toggle() {
if (this.rule.isEnabled) {
this.rulesState.disable(this.rule);
} else {
this.rulesState.enable(this.rule);
}
}
}

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

@ -1,4 +1,4 @@
<sqx-title message="{app} | Rules" parameter1="app" [value1]="appsState.appDisplayName"></sqx-title> <sqx-title message="Rules"></sqx-title>
<sqx-panel desiredWidth="54rem" showSidebar="true"> <sqx-panel desiredWidth="54rem" showSidebar="true">
<ng-container title> <ng-container title>
@ -32,38 +32,14 @@
</div> </div>
<table class="table table-items table-fixed"> <table class="table table-items table-fixed">
<tbody *ngFor="let rule of rules; trackBy: trackByRule"> <tbody *ngFor="let rule of rules; trackBy: trackByRule"
<tr> [sqxRule]="rule"
<td class="cell-separator"> [ruleActions]="ruleActions"
<h3>If</h3> [ruleTriggers]="ruleTriggers"
</td> (delete)="delete(rule)"
<td class="cell-auto"> (editAction)="editAction(rule)"
<span (click)="editTrigger(rule)"> (editTrigger)="editTrigger(rule)"
<sqx-rule-element [type]="rule.triggerType" [element]="ruleTriggers[rule.triggerType]"></sqx-rule-element> (toggle)="toggle(rule)">
</span>
</td>
<td class="cell-separator">
<h3>then</h3>
</td>
<td class="cell-auto">
<span (click)="editAction(rule)">
<sqx-rule-element [type]="rule.actionType" [element]="ruleActions[rule.actionType]"></sqx-rule-element>
</span>
</td>
<td class="cell-actions">
<sqx-toggle [disabled]="!rule.canDisable && !rule.canEnable" [ngModel]="rule.isEnabled" (ngModelChange)="toggle(rule)"></sqx-toggle>
</td>
<td class="cell-actions">
<button type="button" class="btn btn-text-danger"
[disabled]="!rule.canDelete"
(sqxConfirmClick)="delete(rule)"
confirmTitle="Delete rule"
confirmText="Do you really want to delete the rule?">
<i class="icon-bin2"></i>
</button>
</td>
</tr>
<tr class="spacer"></tr>
</tbody> </tbody>
</table> </table>

2
src/Squidex/app/features/rules/pages/rules/rules-page.component.ts

@ -9,7 +9,6 @@ import { Component, OnInit } from '@angular/core';
import { import {
ALL_TRIGGERS, ALL_TRIGGERS,
AppsState,
DialogModel, DialogModel,
RuleDto, RuleDto,
RuleElementDto, RuleElementDto,
@ -33,7 +32,6 @@ export class RulesPageComponent implements OnInit {
public ruleTriggers = ALL_TRIGGERS; public ruleTriggers = ALL_TRIGGERS;
constructor( constructor(
public readonly appsState: AppsState,
public readonly rulesState: RulesState, public readonly rulesState: RulesState,
public readonly rulesService: RulesService, public readonly rulesService: RulesService,
public readonly schemasState: SchemasState public readonly schemasState: SchemasState

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

Loading…
Cancel
Save