Browse Source

Feature/asset folders (#451)

Asset folders.
pull/456/merge
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
4c02d6cbcc
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  2. 65
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs
  3. 74
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs
  4. 75
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs
  5. 27
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  6. 18
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs
  7. 5
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs
  8. 11
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs
  9. 130
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs
  10. 24
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  11. 5
      backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs
  12. 22
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs
  13. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs
  14. 26
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs
  15. 13
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAssetFolder.cs
  16. 16
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs
  17. 16
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs
  18. 14
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAssetFolder.cs
  19. 39
      backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs
  20. 95
      backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs
  21. 2
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs
  22. 21
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderEntity.cs
  23. 15
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderGrain.cs
  24. 6
      backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs
  25. 42
      backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs
  26. 20
      backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs
  27. 6
      backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs
  28. 72
      backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs
  29. 13
      backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs
  30. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs
  31. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs
  32. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  33. 2
      backend/src/Squidex.Domain.Apps.Entities/IEntity.cs
  34. 3
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs
  35. 20
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderCreated.cs
  36. 16
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderDeleted.cs
  37. 16
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderEvent.cs
  38. 18
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderMoved.cs
  39. 17
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderRenamed.cs
  40. 18
      backend/src/Squidex.Domain.Apps.Events/Assets/AssetMoved.cs
  41. 17
      backend/src/Squidex.Infrastructure/CollectionExtensions.cs
  42. 2
      backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs
  43. 168
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs
  44. 35
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  45. 7
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs
  46. 69
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs
  47. 55
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs
  48. 33
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs
  49. 30
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetItemDto.cs
  50. 28
      backend/src/Squidex/Areas/Api/Controllers/Assets/Models/RenameAssetFolderDto.cs
  51. 2
      backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml
  52. 4
      backend/src/Squidex/Config/Domain/CommandsServices.cs
  53. 4
      backend/src/Squidex/Config/Domain/StoreServices.cs
  54. BIN
      backend/src/Squidex/wwwroot/images/add-app.png
  55. 59
      backend/src/Squidex/wwwroot/images/add-app.svg
  56. BIN
      backend/src/Squidex/wwwroot/images/add-blog.png
  57. 38
      backend/src/Squidex/wwwroot/images/add-blog.svg
  58. BIN
      backend/src/Squidex/wwwroot/images/add-identity.png
  59. 27
      backend/src/Squidex/wwwroot/images/add-identity.svg
  60. BIN
      backend/src/Squidex/wwwroot/images/add-profile.png
  61. 26
      backend/src/Squidex/wwwroot/images/add-profile.svg
  62. BIN
      backend/src/Squidex/wwwroot/images/asset_doc.png
  63. 7
      backend/src/Squidex/wwwroot/images/asset_doc.svg
  64. BIN
      backend/src/Squidex/wwwroot/images/asset_docx.png
  65. 7
      backend/src/Squidex/wwwroot/images/asset_docx.svg
  66. BIN
      backend/src/Squidex/wwwroot/images/asset_generic.png
  67. 7
      backend/src/Squidex/wwwroot/images/asset_generic.svg
  68. BIN
      backend/src/Squidex/wwwroot/images/asset_pdf.png
  69. 5
      backend/src/Squidex/wwwroot/images/asset_pdf.svg
  70. BIN
      backend/src/Squidex/wwwroot/images/asset_ppt.png
  71. 6
      backend/src/Squidex/wwwroot/images/asset_ppt.svg
  72. BIN
      backend/src/Squidex/wwwroot/images/asset_pptx.png
  73. 6
      backend/src/Squidex/wwwroot/images/asset_pptx.svg
  74. BIN
      backend/src/Squidex/wwwroot/images/asset_video.png
  75. 5
      backend/src/Squidex/wwwroot/images/asset_video.svg
  76. BIN
      backend/src/Squidex/wwwroot/images/asset_xls.png
  77. 10
      backend/src/Squidex/wwwroot/images/asset_xls.svg
  78. BIN
      backend/src/Squidex/wwwroot/images/asset_xlsx.png
  79. 10
      backend/src/Squidex/wwwroot/images/asset_xlsx.svg
  80. BIN
      backend/src/Squidex/wwwroot/images/dashboard-api.png
  81. 12
      backend/src/Squidex/wwwroot/images/dashboard-api.svg
  82. BIN
      backend/src/Squidex/wwwroot/images/dashboard-feedback.png
  83. BIN
      backend/src/Squidex/wwwroot/images/dashboard-github.png
  84. 6
      backend/src/Squidex/wwwroot/images/dashboard-github.svg
  85. BIN
      backend/src/Squidex/wwwroot/images/dashboard-schema.png
  86. 10
      backend/src/Squidex/wwwroot/images/dashboard-schema.svg
  87. 16
      backend/src/Squidex/wwwroot/images/dashboard_feedback.svg
  88. 10
      backend/src/Squidex/wwwroot/images/dashboard_schema.svg
  89. 5
      backend/src/Squidex/wwwroot/images/folder.svg
  90. 8
      backend/src/Squidex/wwwroot/images/logo.svg
  91. 10
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs
  92. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  93. 171
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs
  94. 53
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs
  95. 167
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetFolderTests.cs
  96. 94
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs
  97. 130
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs
  98. 14
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  99. 12
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs
  100. 25
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs

13
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs

@ -10,17 +10,12 @@ using System.Collections.Generic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using NodaTime;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed class MongoAssetEntity :
IAssetEntity,
IUpdateableEntityWithVersion,
IUpdateableEntityWithCreatedBy,
IUpdateableEntityWithLastModifiedBy
public sealed class MongoAssetEntity : IAssetEntity
{
[BsonId]
[BsonElement("_id")]
@ -32,6 +27,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonRepresentation(BsonType.String)]
public Guid IndexedAppId { get; set; }
[BsonIgnoreIfDefault]
[BsonElement("pi")]
public Guid ParentId { get; set; }
[BsonRequired]
[BsonElement("ct")]
public Instant Created { get; set; }
@ -100,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonElement("dl")]
public bool IsDeleted { get; set; }
Guid IAssetInfo.AssetId
public Guid AssetId
{
get { return Id; }
}

65
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderEntity.cs

@ -0,0 +1,65 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using NodaTime;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed class MongoAssetFolderEntity : IAssetFolderEntity
{
[BsonId]
[BsonElement("_id")]
[BsonRepresentation(BsonType.String)]
public Guid Id { get; set; }
[BsonRequired]
[BsonElement("_ai")]
[BsonRepresentation(BsonType.String)]
public Guid IndexedAppId { get; set; }
[BsonRequired]
[BsonElement("pi")]
public Guid ParentId { get; set; }
[BsonRequired]
[BsonElement("ct")]
public Instant Created { get; set; }
[BsonRequired]
[BsonElement("mt")]
public Instant LastModified { get; set; }
[BsonRequired]
[BsonElement("ai")]
public NamedId<Guid> AppId { get; set; }
[BsonRequired]
[BsonElement("fn")]
public string FolderName { get; set; }
[BsonRequired]
[BsonElement("vs")]
public long Version { get; set; }
[BsonRequired]
[BsonElement("cb")]
public RefToken CreatedBy { get; set; }
[BsonRequired]
[BsonElement("mb")]
public RefToken LastModifiedBy { get; set; }
[BsonRequired]
[BsonElement("dl")]
public bool IsDeleted { get; set; }
}
}

74
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs

@ -0,0 +1,74 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed partial class MongoAssetFolderRepository : MongoRepositoryBase<MongoAssetFolderEntity>, IAssetFolderRepository
{
public MongoAssetFolderRepository(IMongoDatabase database)
: base(database)
{
}
protected override string CollectionName()
{
return "States_AssetFolders";
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoAssetFolderEntity> collection, CancellationToken ct = default)
{
return collection.Indexes.CreateManyAsync(new[]
{
new CreateIndexModel<MongoAssetFolderEntity>(
Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.ParentId))
}, ct);
}
public async Task<IResultList<IAssetFolderEntity>> QueryAsync(Guid appId, Guid parentId)
{
using (Profiler.TraceMethod<MongoAssetFolderRepository>("QueryAsyncByQuery"))
{
var assetFolders =
await Collection
.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.ParentId == parentId).SortBy(x => x.FolderName)
.ToListAsync();
return ResultList.Create<IAssetFolderEntity>(assetFolders.Count, assetFolders);
}
}
public async Task<IAssetFolderEntity?> FindAssetFolderAsync(Guid id)
{
using (Profiler.TraceMethod<MongoAssetFolderRepository>())
{
var assetFolderEntity =
await Collection.Find(x => x.Id == id)
.FirstOrDefaultAsync();
if (assetFolderEntity?.IsDeleted == true)
{
return null;
}
return assetFolderEntity;
}
}
}
}

75
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository_SnapshotStore.cs

@ -0,0 +1,75 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed partial class MongoAssetFolderRepository : ISnapshotStore<AssetFolderState, Guid>
{
async Task<(AssetFolderState Value, long Version)> ISnapshotStore<AssetFolderState, Guid>.ReadAsync(Guid key)
{
using (Profiler.TraceMethod<MongoAssetFolderRepository>())
{
var existing =
await Collection.Find(x => x.Id == key)
.FirstOrDefaultAsync();
if (existing != null)
{
return (Map(existing), existing.Version);
}
return (null!, EtagVersion.NotFound);
}
}
async Task ISnapshotStore<AssetFolderState, Guid>.WriteAsync(Guid key, AssetFolderState value, long oldVersion, long newVersion)
{
using (Profiler.TraceMethod<MongoAssetFolderRepository>())
{
var entity = SimpleMapper.Map(value, new MongoAssetFolderEntity());
entity.Version = newVersion;
entity.IndexedAppId = value.AppId.Id;
await Collection.ReplaceOneAsync(x => x.Id == key && x.Version == oldVersion, entity, Upsert);
}
}
async Task ISnapshotStore<AssetFolderState, Guid>.ReadAllAsync(Func<AssetFolderState, long, Task> callback, CancellationToken ct)
{
using (Profiler.TraceMethod<MongoAssetFolderRepository>())
{
await Collection.Find(new BsonDocument(), options: Batching.Options).ForEachPipelineAsync(x => callback(Map(x), x.Version), ct);
}
}
async Task ISnapshotStore<AssetFolderState, Guid>.RemoveAsync(Guid key)
{
using (Profiler.TraceMethod<MongoAssetFolderRepository>())
{
await Collection.DeleteOneAsync(x => x.Id == key);
}
}
private static AssetFolderState Map(MongoAssetFolderEntity existing)
{
return SimpleMapper.Map(existing, new AssetFolderState());
}
}
}

27
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -41,6 +41,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
Index
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted)
.Ascending(x => x.ParentId)
.Ascending(x => x.Tags)
.Descending(x => x.LastModified)),
new CreateIndexModel<MongoAssetEntity>(
@ -51,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}, ct);
}
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, ClrQuery query)
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Guid? parentId, ClrQuery query)
{
using (Profiler.TraceMethod<MongoAssetRepository>("QueryAsyncByQuery"))
{
@ -59,19 +60,19 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
query = query.AdjustToModel();
var filter = query.BuildFilter(appId);
var filter = query.BuildFilter(appId, parentId);
var contentCount = Collection.Find(filter).CountDocumentsAsync();
var contentItems =
var assetCount = Collection.Find(filter).CountDocumentsAsync();
var assetItems =
Collection.Find(filter)
.AssetTake(query)
.AssetSkip(query)
.AssetSort(query)
.ToListAsync();
await Task.WhenAll(contentItems, contentCount);
await Task.WhenAll(assetItems, assetCount);
return ResultList.Create<IAssetEntity>(contentCount.Result, contentItems.Result);
return ResultList.Create<IAssetEntity>(assetCount.Result, assetItems.Result);
}
catch (MongoQueryException ex)
{
@ -123,26 +124,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
}
}
public async Task<IAssetEntity?> FindAssetAsync(Guid id, bool allowDeleted = false)
public async Task<IAssetEntity?> FindAssetAsync(Guid id)
{
using (Profiler.TraceMethod<MongoAssetRepository>())
{
var assetEntity =
await Collection.Find(x => x.Id == id)
await Collection.Find(x => x.Id == id && !x.IsDeleted)
.FirstOrDefaultAsync();
if (assetEntity?.IsDeleted == true && !allowDeleted)
{
return null;
}
return assetEntity;
}
}
public Task RemoveAsync(Guid key)
{
return Collection.DeleteOneAsync(x => x.Id == key);
}
}
}

18
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Visitors/FindExtensions.cs

@ -19,6 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
public static class FindExtensions
{
private static readonly FilterDefinitionBuilder<MongoAssetEntity> Filter = Builders<MongoAssetEntity>.Filter;
private static readonly SortDefinitionBuilder<MongoAssetEntity> Sorting = Builders<MongoAssetEntity>.Sort;
public static ClrQuery AdjustToModel(this ClrQuery query)
{
@ -52,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
return cursor.Skip(query);
}
public static FilterDefinition<MongoAssetEntity> BuildFilter(this ClrQuery query, Guid appId)
public static FilterDefinition<MongoAssetEntity> BuildFilter(this ClrQuery query, Guid appId, Guid? parentId)
{
var filters = new List<FilterDefinition<MongoAssetEntity>>
{
@ -60,6 +61,21 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets.Visitors
Filter.Eq(x => x.IsDeleted, false)
};
if (parentId.HasValue)
{
if (parentId == Guid.Empty)
{
filters.Add(
Filter.Or(
Filter.Exists(x => x.ParentId, false),
Filter.Eq(x => x.ParentId, Guid.Empty)));
}
else
{
filters.Add(Filter.Eq(x => x.ParentId, parentId.Value));
}
}
var (filter, last) = query.BuildFilter<MongoAssetEntity>(false);
if (filter != null)

5
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs

@ -34,6 +34,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
protected override async Task<EnrichedAssetEvent?> CreateEnrichedEventAsync(Envelope<AssetEvent> @event)
{
if (@event.Payload is AssetMoved)
{
return null;
}
var result = new EnrichedAssetEvent();
var asset = await assetLoader.GetAsync(@event.Payload.AssetId, @event.Headers.EventStreamNumber());

11
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetEntity.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
public Guid Id { get; set; }
public Guid AssetId { get; set; }
public Guid ParentId { get; set; }
public Instant Created { get; set; }
@ -34,12 +34,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
public long Version { get; set; }
public string MimeType { get; set; }
public string FileName { get; set; }
public string FileHash { get; set; }
public string MimeType { get; set; }
public string Slug { get; set; }
public long FileSize { get; set; }
@ -53,5 +53,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
public int? PixelWidth { get; set; }
public int? PixelHeight { get; set; }
public Guid AssetId
{
get { return Id; }
}
}
}

130
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderGrain.cs

@ -0,0 +1,130 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.Guards;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Assets
{
public sealed class AssetFolderGrain : DomainObjectGrain<AssetFolderState>, IAssetFolderGrain
{
private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5);
private readonly IAssetQueryService assetQuery;
public AssetFolderGrain(IStore<Guid> store, IAssetQueryService assetQuery, IActivationLimit limit, ISemanticLog log)
: base(store, log)
{
Guard.NotNull(assetQuery);
this.assetQuery = assetQuery;
limit?.SetLimit(5000, Lifetime);
}
protected override Task OnActivateAsync(Guid key)
{
TryDelayDeactivation(Lifetime);
return base.OnActivateAsync(key);
}
protected override Task<object?> ExecuteAsync(IAggregateCommand command)
{
VerifyNotDeleted();
switch (command)
{
case CreateAssetFolder createAssetFolder:
return CreateReturnAsync(createAssetFolder, async c =>
{
await GuardAssetFolder.CanCreate(c, assetQuery);
Create(c);
return Snapshot;
});
case MoveAssetFolder moveAssetFolder:
return UpdateReturnAsync(moveAssetFolder, async c =>
{
await GuardAssetFolder.CanMove(c, assetQuery, Snapshot.Id, Snapshot.ParentId);
Move(c);
return Snapshot;
});
case RenameAssetFolder renameAssetFolder:
return UpdateReturn(renameAssetFolder, c =>
{
GuardAssetFolder.CanRename(c, Snapshot.FolderName);
Rename(c);
return Snapshot;
});
case DeleteAssetFolder deleteAssetFolder:
return Update(deleteAssetFolder, c =>
{
GuardAssetFolder.CanDelete(c);
Delete(c);
});
default:
throw new NotSupportedException();
}
}
public void Create(CreateAssetFolder command)
{
RaiseEvent(SimpleMapper.Map(command, new AssetFolderCreated()));
}
public void Move(MoveAssetFolder command)
{
RaiseEvent(SimpleMapper.Map(command, new AssetFolderMoved()));
}
public void Rename(RenameAssetFolder command)
{
RaiseEvent(SimpleMapper.Map(command, new AssetFolderRenamed()));
}
public void Delete(DeleteAssetFolder command)
{
RaiseEvent(SimpleMapper.Map(command, new AssetFolderDeleted()));
}
private void RaiseEvent(AppEvent @event)
{
if (@event.AppId == null)
{
@event.AppId = Snapshot.AppId;
}
RaiseEvent(Envelope.Create(@event));
}
private void VerifyNotDeleted()
{
if (Snapshot.IsDeleted)
{
throw new DomainException("Asset folder has already been deleted");
}
}
}
}

24
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -28,14 +28,18 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5);
private readonly ITagService tagService;
private readonly IAssetQueryService assetQuery;
public AssetGrain(IStore<Guid> store, ITagService tagService, IActivationLimit limit, ISemanticLog log)
public AssetGrain(IStore<Guid> store, ITagService tagService, IAssetQueryService assetQuery, IActivationLimit limit, ISemanticLog log)
: base(store, log)
{
Guard.NotNull(tagService);
Guard.NotNull(assetQuery);
this.tagService = tagService;
this.assetQuery = assetQuery;
limit?.SetLimit(5000, Lifetime);
}
@ -55,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
case CreateAsset createAsset:
return CreateReturnAsync(createAsset, async c =>
{
GuardAsset.CanCreate(c);
await GuardAsset.CanCreate(c, assetQuery);
var tagIds = await NormalizeTagsAsync(c.AppId.Id, c.Tags);
@ -75,12 +79,21 @@ namespace Squidex.Domain.Apps.Entities.Assets
case AnnotateAsset annotateAsset:
return UpdateReturnAsync(annotateAsset, async c =>
{
GuardAsset.CanAnnotate(c, Snapshot.FileName, Snapshot.Slug);
GuardAsset.CanAnnotate(c, Snapshot.FileName!, Snapshot.Slug);
var tagIds = await NormalizeTagsAsync(Snapshot.AppId.Id, c.Tags);
Annotate(c, tagIds);
return Snapshot;
});
case MoveAsset moveAsset:
return UpdateReturnAsync(moveAsset, async c =>
{
await GuardAsset.CanMove(c, assetQuery, Snapshot.ParentId);
Move(c);
return Snapshot;
});
case DeleteAsset deleteAsset:
@ -152,6 +165,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
RaiseEvent(@event);
}
public void Move(MoveAsset command)
{
RaiseEvent(SimpleMapper.Map(command, new AssetMoved()));
}
public void Delete(DeleteAsset command)
{
RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = Snapshot.TotalSize }));

5
backend/src/Squidex.Domain.Apps.Entities/Assets/BackupAssets.cs

@ -24,6 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
private const string TagsFile = "AssetTags.json";
private readonly HashSet<Guid> assetIds = new HashSet<Guid>();
private readonly HashSet<Guid> assetFolderIds = new HashSet<Guid>();
private readonly IAssetStore assetStore;
private readonly ITagService tagService;
@ -62,6 +63,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
switch (@event.Payload)
{
case AssetFolderCreated assetFolderCreated:
assetFolderIds.Add(assetFolderCreated.AssetFolderId);
break;
case AssetCreated assetCreated:
await ReadAssetAsync(assetCreated.AssetId, assetCreated.FileVersion, reader);
break;
@ -78,6 +82,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
await RestoreTagsAsync(appId, reader);
await RebuildManyAsync(assetIds, RebuildAsync<AssetState, AssetGrain>);
await RebuildManyAsync(assetFolderIds, RebuildAsync<AssetFolderState, AssetFolderGrain>);
}
private async Task RestoreTagsAsync(Guid appId, BackupReader reader)

22
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/AssetFolderCommand.cs

@ -0,0 +1,22 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public abstract class AssetFolderCommand : SquidexCommand, IAggregateCommand
{
public Guid AssetFolderId { get; set; }
Guid IAggregateCommand.AggregateId
{
get { return AssetFolderId; }
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAsset.cs

@ -15,6 +15,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public NamedId<Guid> AppId { get; set; }
public Guid ParentId { get; set; }
public HashSet<string> Tags { get; set; }
public CreateAsset()

26
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/CreateAssetFolder.cs

@ -0,0 +1,26 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class CreateAssetFolder : AssetFolderCommand, IAppCommand
{
public NamedId<Guid> AppId { get; set; }
public string FolderName { get; set; }
public Guid ParentId { get; set; }
public CreateAssetFolder()
{
AssetFolderId = Guid.NewGuid();
}
}
}

13
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAssetFolder.cs

@ -0,0 +1,13 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class DeleteAssetFolder : AssetFolderCommand
{
}
}

16
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAsset.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class MoveAsset : AssetCommand
{
public Guid ParentId { get; set; }
}
}

16
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/MoveAssetFolder.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class MoveAssetFolder : AssetFolderCommand
{
public Guid ParentId { get; set; }
}
}

14
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/RenameAssetFolder.cs

@ -0,0 +1,14 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class RenameAssetFolder : AssetFolderCommand
{
public string FolderName { get; set; }
}
}

39
backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAsset.cs

@ -5,6 +5,8 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation;
@ -28,9 +30,31 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
});
}
public static void CanCreate(CreateAsset command)
public static Task CanCreate(CreateAsset command, IAssetQueryService assetQuery)
{
Guard.NotNull(command);
return Validate.It(() => "Cannot upload asset.", async e =>
{
await CheckPathAsync(command.ParentId, assetQuery, e);
});
}
public static Task CanMove(MoveAsset command, IAssetQueryService assetQuery, Guid oldParentId)
{
Guard.NotNull(command);
return Validate.It(() => "Cannot move asset.", async e =>
{
if (command.ParentId == oldParentId)
{
e("Asset is already part of this folder.", nameof(command.ParentId));
}
else
{
await CheckPathAsync(command.ParentId, assetQuery, e);
}
});
}
public static void CanUpdate(UpdateAsset command)
@ -42,5 +66,18 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
{
Guard.NotNull(command);
}
private static async Task CheckPathAsync(Guid parentId, IAssetQueryService assetQuery, AddValidation e)
{
if (parentId != default)
{
var path = await assetQuery.FindAssetFolderAsync(parentId);
if (path.Count == 0)
{
e("Asset folder does not exist.", nameof(MoveAsset.ParentId));
}
}
}
}
}

95
backend/src/Squidex.Domain.Apps.Entities/Assets/Guards/GuardAssetFolder.cs

@ -0,0 +1,95 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Validation;
namespace Squidex.Domain.Apps.Entities.Assets.Guards
{
public static class GuardAssetFolder
{
public static Task CanCreate(CreateAssetFolder command, IAssetQueryService assetQuery)
{
Guard.NotNull(command);
return Validate.It(() => "Cannot upload asset.", async e =>
{
if (string.IsNullOrWhiteSpace(command.FolderName))
{
e(Not.Defined("Folder name"), nameof(command.FolderName));
}
await CheckPathAsync(command.ParentId, assetQuery, Guid.Empty, e);
});
}
public static void CanRename(RenameAssetFolder command, string olderFolderName)
{
Guard.NotNull(command);
Validate.It(() => "Cannot rename asset.", e =>
{
if (string.IsNullOrWhiteSpace(command.FolderName))
{
e(Not.Defined("Folder name"), nameof(command.FolderName));
}
else if (string.Equals(command.FolderName, olderFolderName))
{
e(Not.New("Asset folder", "name"), nameof(command.FolderName));
}
});
}
public static Task CanMove(MoveAssetFolder command, IAssetQueryService assetQuery, Guid id, Guid oldParentId)
{
Guard.NotNull(command);
return Validate.It(() => "Cannot move asset.", async e =>
{
if (command.ParentId == oldParentId)
{
e("Asset folder is already part of this folder.", nameof(command.ParentId));
}
else
{
await CheckPathAsync(command.ParentId, assetQuery, id, e);
}
});
}
public static void CanDelete(DeleteAssetFolder command)
{
Guard.NotNull(command);
}
private static async Task CheckPathAsync(Guid parentId, IAssetQueryService assetQuery, Guid id, AddValidation e)
{
if (parentId != default)
{
var path = await assetQuery.FindAssetFolderAsync(parentId);
if (path.Count == 0)
{
e("Asset folder does not exist.", nameof(MoveAssetFolder.ParentId));
}
else if (id != default)
{
var indexOfThis = path.IndexOf(x => x.Id == id);
var indexOfParent = path.IndexOf(x => x.Id == parentId);
if (indexOfThis >= 0 && indexOfParent > indexOfThis)
{
e("Cannot add folder to its own child.", nameof(MoveAssetFolder.ParentId));
}
}
}
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetEntity.cs

@ -21,6 +21,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
NamedId<Guid> AppId { get; }
Guid ParentId { get; }
string MimeType { get; }
long FileVersion { get; }

21
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderEntity.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IAssetFolderEntity : IEntity, IEntityWithVersion
{
NamedId<Guid> AppId { get; set; }
string FolderName { get; set; }
Guid ParentId { get; set; }
}
}

15
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetFolderGrain.cs

@ -0,0 +1,15 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.Commands;
namespace Squidex.Domain.Apps.Entities.Assets
{
public interface IAssetFolderGrain : IDomainObjectGrain
{
}
}

6
backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetQueryService.cs

@ -16,7 +16,11 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
Task<IReadOnlyList<IEnrichedAssetEntity>> QueryByHashAsync(Context context, Guid appId, string hash);
Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, Q query);
Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, Guid? parentId, Q query);
Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, Guid parentId);
Task<IReadOnlyList<IAssetFolderEntity>> FindAssetFolderAsync(Guid id);
Task<IEnrichedAssetEntity?> FindAssetAsync(Context context, Guid id);
}

42
backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs

@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Infrastructure;
@ -17,19 +18,23 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
private readonly IAssetEnricher assetEnricher;
private readonly IAssetRepository assetRepository;
private readonly IAssetFolderRepository assetFolderRepository;
private readonly AssetQueryParser queryParser;
public AssetQueryService(
IAssetEnricher assetEnricher,
IAssetRepository assetRepository,
IAssetFolderRepository assetFolderRepository,
AssetQueryParser queryParser)
{
Guard.NotNull(assetEnricher);
Guard.NotNull(assetRepository);
Guard.NotNull(assetFolderRepository);
Guard.NotNull(queryParser);
this.assetEnricher = assetEnricher;
this.assetRepository = assetRepository;
this.assetFolderRepository = assetFolderRepository;
this.queryParser = queryParser;
}
@ -45,6 +50,35 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
return null;
}
public async Task<IReadOnlyList<IAssetFolderEntity>> FindAssetFolderAsync(Guid id)
{
var result = new List<IAssetFolderEntity>();
while (id != default)
{
var folder = await assetFolderRepository.FindAssetFolderAsync(id);
if (folder == null || result.Any(x => x.Id == folder.Id))
{
result.Clear();
break;
}
result.Insert(0, folder);
id = folder.ParentId;
}
return result;
}
public async Task<IResultList<IAssetFolderEntity>> QueryAssetFoldersAsync(Context context, Guid parentId)
{
var assetFolders = await assetFolderRepository.QueryAsync(context.App.Id, parentId);
return assetFolders;
}
public async Task<IReadOnlyList<IEnrichedAssetEntity>> QueryByHashAsync(Context context, Guid appId, string hash)
{
Guard.NotNull(hash);
@ -54,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
return await assetEnricher.EnrichAsync(assets, context);
}
public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, Q query)
public async Task<IResultList<IEnrichedAssetEntity>> QueryAsync(Context context, Guid? parentId, Q query)
{
Guard.NotNull(context);
Guard.NotNull(query);
@ -67,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
}
else
{
assets = await QueryByQueryAsync(context, query);
assets = await QueryByQueryAsync(context, parentId, query);
}
var enriched = await assetEnricher.EnrichAsync(assets, context);
@ -75,11 +109,11 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
return ResultList.Create(assets.Total, enriched);
}
private async Task<IResultList<IAssetEntity>> QueryByQueryAsync(Context context, Q query)
private async Task<IResultList<IAssetEntity>> QueryByQueryAsync(Context context, Guid? parentId, Q query)
{
var parsedQuery = queryParser.ParseQuery(context, query);
return await assetRepository.QueryAsync(context.App.Id, parsedQuery);
return await assetRepository.QueryAsync(context.App.Id, parentId, parsedQuery);
}
private async Task<IResultList<IAssetEntity>> QueryByIdsAsync(Context context, Q query)

20
backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetFolderRepository.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
public interface IAssetFolderRepository
{
Task<IResultList<IAssetFolderEntity>> QueryAsync(Guid appId, Guid parentId);
Task<IAssetFolderEntity?> FindAssetFolderAsync(Guid id);
}
}

6
backend/src/Squidex.Domain.Apps.Entities/Assets/Repositories/IAssetRepository.cs

@ -17,14 +17,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.Repositories
{
Task<IReadOnlyList<IAssetEntity>> QueryByHashAsync(Guid appId, string hash);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, ClrQuery query);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, Guid? parentId, ClrQuery query);
Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids);
Task<IAssetEntity?> FindAssetAsync(Guid id, bool allowDeleted = false);
Task<IAssetEntity?> FindAssetAsync(Guid id);
Task<IAssetEntity?> FindAssetBySlugAsync(Guid appId, string slug);
Task RemoveAsync(Guid appId);
}
}

72
backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetFolderState.cs

@ -0,0 +1,72 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Runtime.Serialization;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Reflection;
#pragma warning disable IDE0060 // Remove unused parameter
namespace Squidex.Domain.Apps.Entities.Assets.State
{
public class AssetFolderState : DomainObjectState<AssetFolderState>, IAssetFolderEntity
{
[DataMember]
public NamedId<Guid> AppId { get; set; }
[DataMember]
public string FolderName { get; set; }
[DataMember]
public bool IsDeleted { get; set; }
[DataMember]
public Guid ParentId { get; set; }
public void ApplyEvent(IEvent @event)
{
switch (@event)
{
case AssetFolderCreated e:
{
SimpleMapper.Map(e, this);
break;
}
case AssetFolderRenamed e:
{
SimpleMapper.Map(e, this);
break;
}
case AssetFolderMoved e:
{
ParentId = e.ParentId;
break;
}
case AssetFolderDeleted _:
{
IsDeleted = true;
break;
}
}
}
public override AssetFolderState Apply(Envelope<IEvent> @event)
{
return Clone().Update(@event, (e, s) => s.ApplyEvent(e));
}
}
}

13
backend/src/Squidex.Domain.Apps.Entities/Assets/State/AssetState.cs

@ -8,7 +8,6 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
@ -23,6 +22,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
[DataMember]
public NamedId<Guid> AppId { get; set; }
[DataMember]
public Guid ParentId { get; set; }
[DataMember]
public string FileName { get; set; }
@ -59,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
[DataMember]
public HashSet<string> Tags { get; set; }
Guid IAssetInfo.AssetId
public Guid AssetId
{
get { return Id; }
}
@ -117,6 +119,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.State
break;
}
case AssetMoved e:
{
ParentId = e.ParentId;
break;
}
case AssetDeleted _:
{
IsDeleted = true;

2
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetGraphType.cs

@ -111,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
Name = "fileType",
ResolvedType = AllTypes.NonNullString,
Resolver = Resolve(x => x.FileName.FileType()),
Resolver = Resolve(x => x.FileName!.FileType()),
Description = "The file type."
});

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentEnricher.cs

@ -322,7 +322,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
return EmptyAssets;
}
var assets = await assetQuery.QueryAsync(context.Clone().WithNoAssetEnrichment(true), Q.Empty.WithIds(ids));
var assets = await assetQuery.QueryAsync(context.Clone().WithNoAssetEnrichment(true), null, Q.Empty.WithIds(ids));
return assets.ToLookup(x => x.Id);
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs

@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
public virtual async Task<IResultList<IAssetEntity>> QueryAssetsAsync(string query)
{
var assets = await assetQuery.QueryAsync(context, Q.Empty.WithODataQuery(query));
var assets = await assetQuery.QueryAsync(context, null, Q.Empty.WithODataQuery(query));
foreach (var asset in assets)
{
@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
if (notLoadedAssets.Count > 0)
{
var assets = await assetQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedAssets));
var assets = await assetQuery.QueryAsync(context, null, Q.Empty.WithIds(notLoadedAssets));
foreach (var asset in assets)
{

2
backend/src/Squidex.Domain.Apps.Entities/IEntity.cs

@ -12,7 +12,7 @@ namespace Squidex.Domain.Apps.Entities
{
public interface IEntity
{
Guid Id { get; }
Guid Id { get; }
Instant Created { get; }

3
backend/src/Squidex.Domain.Apps.Events/Assets/AssetCreated.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using Squidex.Infrastructure.EventSourcing;
@ -13,6 +14,8 @@ namespace Squidex.Domain.Apps.Events.Assets
[EventType(nameof(AssetCreated))]
public sealed class AssetCreated : AssetEvent
{
public Guid ParentId { get; set; }
public string FileName { get; set; }
public string FileHash { get; set; }

20
backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderCreated.cs

@ -0,0 +1,20 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets
{
[EventType(nameof(AssetFolderCreated))]
public sealed class AssetFolderCreated : AssetFolderEvent
{
public Guid ParentId { get; set; }
public string FolderName { get; set; }
}
}

16
backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderDeleted.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets
{
[EventType(nameof(AssetFolderDeleted))]
public sealed class AssetFolderDeleted : AssetFolderEvent
{
}
}

16
backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderEvent.cs

@ -0,0 +1,16 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Domain.Apps.Events.Assets
{
public abstract class AssetFolderEvent : AppEvent
{
public Guid AssetFolderId { get; set; }
}
}

18
backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderMoved.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets
{
[EventType(nameof(AssetFolderMoved))]
public sealed class AssetFolderMoved : AssetFolderEvent
{
public Guid ParentId { get; set; }
}
}

17
backend/src/Squidex.Domain.Apps.Events/Assets/AssetFolderRenamed.cs

@ -0,0 +1,17 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets
{
[EventType(nameof(AssetFolderRenamed))]
public sealed class AssetFolderRenamed : AssetFolderEvent
{
public string FolderName { get; set; }
}
}

18
backend/src/Squidex.Domain.Apps.Events/Assets/AssetMoved.cs

@ -0,0 +1,18 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Events.Assets
{
[EventType(nameof(AssetMoved))]
public sealed class AssetMoved : AssetEvent
{
public Guid ParentId { get; set; }
}
}

17
backend/src/Squidex.Infrastructure/CollectionExtensions.cs

@ -33,6 +33,23 @@ namespace Squidex.Infrastructure
return input.GroupBy(x => x).Where(x => x.Count() > 1).Select(x => x.Key);
}
public static int IndexOf<T>(this IEnumerable<T> input, Func<T, bool> predicate)
{
var i = 0;
foreach (var item in input)
{
if (predicate(item))
{
return i;
}
i++;
}
return -1;
}
public static IEnumerable<TResult> Duplicates<TResult, T>(this IEnumerable<T> input, Func<T, TResult> selector)
{
return input.GroupBy(selector).Where(x => x.Count() > 1).Select(x => x.Key);

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

@ -89,7 +89,7 @@ namespace Squidex.Areas.Api.Controllers.Apps
/// <summary>
/// Create a new app.
/// </summary>
/// <param name="request">The app object that needs to be added to squidex.</param>
/// <param name="request">The app object that needs to be added to Squidex.</param>
/// <returns>
/// 201 => App created.
/// 400 => App request not valid.

168
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs

@ -0,0 +1,168 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Squidex.Areas.Api.Controllers.Assets.Models;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure.Commands;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets
{
/// <summary>
/// Uploads and retrieves assets.
/// </summary>
[ApiExplorerSettings(GroupName = nameof(Assets))]
public sealed class AssetFoldersController : ApiController
{
private readonly IAssetQueryService assetQuery;
public AssetFoldersController(ICommandBus commandBus, IAssetQueryService assetQuery)
: base(commandBus)
{
this.assetQuery = assetQuery;
}
/// <summary>
/// Get asset folders.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="parentId">The optional parent folder id.</param>
/// <returns>
/// 200 => Asset folders returned.
/// 404 => App not found.
/// </returns>
/// <remarks>
/// Get all asset folders for the app.
/// </remarks>
[HttpGet]
[Route("apps/{app}/assets/folders", Order = -1)]
[ProducesResponseType(typeof(AssetsDto), 200)]
[ApiPermission(Permissions.AppAssetsRead)]
[ApiCosts(1)]
public async Task<IActionResult> GetAssetFolders(string app, [FromQuery] Guid parentId)
{
var assetFolders = await assetQuery.QueryAssetFoldersAsync(Context, parentId);
var response = Deferred.Response(() =>
{
return AssetFoldersDto.FromAssets(assetFolders, this, app);
});
Response.Headers[HeaderNames.ETag] = assetFolders.ToEtag();
return Ok(response);
}
/// <summary>
/// Upload a new asset.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="request">The asset folder object that needs to be added to the App.</param>
/// <returns>
/// 201 => Asset folder created.
/// 404 => App not found.
/// </returns>
[HttpPost]
[Route("apps/{app}/assets/folders", Order = -1)]
[ProducesResponseType(typeof(AssetDto), 201)]
[AssetRequestSizeLimit]
[ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PostAssetFolder(string app, [FromBody] CreateAssetFolderDto request)
{
var command = request.ToCommand();
var response = await InvokeCommandAsync(app, command);
return Ok(response);
}
/// <summary>
/// Updates the asset folder.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset folder.</param>
/// <param name="request">The asset folder object that needs to updated.</param>
/// <returns>
/// 200 => Asset folder updated.
/// 400 => Asset folder name not valid.
/// 404 => Asset or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/assets/folders/{id}/", Order = -1)]
[ProducesResponseType(typeof(AssetDto), 200)]
[AssetRequestSizeLimit]
[ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutAssetFolder(string app, Guid id, [FromBody] RenameAssetFolderDto request)
{
var command = request.ToCommand(id);
var response = await InvokeCommandAsync(app, command);
return Ok(response);
}
/// <summary>
/// Moves the asset folder.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset folder.</param>
/// <param name="request">The asset folder object that needs to updated.</param>
/// <returns>
/// 200 => Asset folder moved.
/// 404 => Asset or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/assets/folders/{id}/parent", Order = -1)]
[ProducesResponseType(typeof(AssetDto), 200)]
[AssetRequestSizeLimit]
[ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutAssetFolderParent(string app, Guid id, [FromBody] MoveAssetItemDto request)
{
var command = request.ToFolderCommand(id);
var response = await InvokeCommandAsync(app, command);
return Ok(response);
}
/// <summary>
/// Delete an asset folder.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset folder to delete.</param>
/// <returns>
/// 204 => Asset folder deleted.
/// 404 => Asset or app not found.
/// </returns>
[HttpDelete]
[Route("apps/{app}/assets/folders/{id}/", Order = -1)]
[ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteAssetFolder(string app, Guid id)
{
await CommandBus.PublishAsync(new DeleteAssetFolder { AssetFolderId = id });
return NoContent();
}
private async Task<AssetFolderDto> InvokeCommandAsync(string app, ICommand command)
{
var context = await CommandBus.PublishAsync(command);
return AssetFolderDto.FromAssetFolder(context.Result<IAssetFolderEntity>(), this, app);
}
}
}

35
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs

@ -89,6 +89,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// Get assets.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="parentId">The optional parent folder id.</param>
/// <param name="ids">The optional asset ids.</param>
/// <param name="q">The optional json query.</param>
/// <returns>
@ -103,9 +104,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
[ProducesResponseType(typeof(AssetsDto), 200)]
[ApiPermission(Permissions.AppAssetsRead)]
[ApiCosts(1)]
public async Task<IActionResult> GetAssets(string app, [FromQuery] string? ids = null, [FromQuery] string? q = null)
public async Task<IActionResult> GetAssets(string app, [FromQuery] Guid? parentId, [FromQuery] string? ids = null, [FromQuery] string? q = null)
{
var assets = await assetQuery.QueryAsync(Context,
var assets = await assetQuery.QueryAsync(Context, parentId,
Q.Empty
.WithIds(ids)
.WithJsonQuery(q)
@ -168,6 +169,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// Upload a new asset.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="parentId">The optional parent folder id.</param>
/// <param name="file">The file to upload.</param>
/// <returns>
/// 201 => Asset created.
@ -183,11 +185,11 @@ namespace Squidex.Areas.Api.Controllers.Assets
[AssetRequestSizeLimit]
[ApiPermission(Permissions.AppAssetsCreate)]
[ApiCosts(1)]
public async Task<IActionResult> PostAsset(string app, [OpenApiIgnore] List<IFormFile> file)
public async Task<IActionResult> PostAsset(string app, [FromQuery] Guid parentId, [OpenApiIgnore] List<IFormFile> file)
{
var assetFile = await CheckAssetFileAsync(file);
var command = new CreateAsset { File = assetFile };
var command = new CreateAsset { File = assetFile, ParentId = parentId };
var response = await InvokeCommandAsync(app, command);
@ -250,6 +252,31 @@ namespace Squidex.Areas.Api.Controllers.Assets
return Ok(response);
}
/// <summary>
/// Moves the asset.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset.</param>
/// <param name="request">The asset object that needs to updated.</param>
/// <returns>
/// 200 => Asset moved.
/// 404 => Asset or app not found.
/// </returns>
[HttpPut]
[Route("apps/{app}/assets/{id}/parent")]
[ProducesResponseType(typeof(AssetDto), 200)]
[AssetRequestSizeLimit]
[ApiPermission(Permissions.AppAssetsUpdate)]
[ApiCosts(1)]
public async Task<IActionResult> PutAssetParent(string app, Guid id, [FromBody] MoveAssetItemDto request)
{
var command = request.ToCommand(id);
var response = await InvokeCommandAsync(app, command);
return Ok(response);
}
/// <summary>
/// Delete an asset.
/// </summary>

7
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetDto.cs

@ -25,6 +25,11 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The id of the parent folder. Empty for files without parent.
/// </summary>
public Guid ParentId { get; set; }
/// <summary>
/// The file name.
/// </summary>
@ -141,6 +146,8 @@ namespace Squidex.Areas.Api.Controllers.Assets.Models
{
response.AddPutLink("update", controller.Url<AssetsController>(x => nameof(x.PutAsset), values));
response.AddPutLink("upload", controller.Url<AssetsController>(x => nameof(x.PutAssetContent), values));
response.AddPutLink("move", controller.Url<AssetsController>(x => nameof(x.PutAssetParent), values));
}
if (controller.HasPermission(Permissions.AppAssetsDelete))

69
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFolderDto.cs

@ -0,0 +1,69 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class AssetFolderDto : Resource
{
/// <summary>
/// The id of the asset.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// The id of the parent folder. Empty for files without parent.
/// </summary>
public Guid ParentId { get; set; }
/// <summary>
/// The folder name.
/// </summary>
[Required]
public string FolderName { get; set; }
/// <summary>
/// The version of the asset folder.
/// </summary>
public long Version { get; set; }
public static AssetFolderDto FromAssetFolder(IAssetFolderEntity asset, ApiController controller, string app)
{
var response = SimpleMapper.Map(asset, new AssetFolderDto());
return CreateLinks(response, controller, app);
}
private static AssetFolderDto CreateLinks(AssetFolderDto response, ApiController controller, string app)
{
var values = new { app, id = response.Id };
response.AddSelfLink(controller.Url<AssetsController>(x => nameof(x.GetAsset), values));
if (controller.HasPermission(Permissions.AppAssetsUpdate))
{
response.AddPutLink("update", controller.Url<AssetFoldersController>(x => nameof(x.PutAssetFolder), values));
response.AddPutLink("move", controller.Url<AssetFoldersController>(x => nameof(x.PutAssetFolderParent), values));
}
if (controller.HasPermission(Permissions.AppAssetsUpdate))
{
response.AddDeleteLink("delete", controller.Url<AssetFoldersController>(x => nameof(x.DeleteAssetFolder), values));
}
return response;
}
}
}

55
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/AssetFoldersDto.cs

@ -0,0 +1,55 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Shared;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class AssetFoldersDto : Resource
{
/// <summary>
/// The total number of assets.
/// </summary>
public long Total { get; set; }
/// <summary>
/// The assets folders.
/// </summary>
[Required]
public AssetFolderDto[] Items { get; set; }
public static AssetFoldersDto FromAssets(IResultList<IAssetFolderEntity> assetFolders, ApiController controller, string app)
{
var response = new AssetFoldersDto
{
Total = assetFolders.Total,
Items = assetFolders.Select(x => AssetFolderDto.FromAssetFolder(x, controller, app)).ToArray()
};
return CreateLinks(response, controller, app);
}
private static AssetFoldersDto CreateLinks(AssetFoldersDto response, ApiController controller, string app)
{
var values = new { app };
response.AddSelfLink(controller.Url<AssetFoldersController>(x => nameof(x.GetAssetFolders), values));
if (controller.HasPermission(Permissions.AppAssetsUpdate))
{
response.AddPostLink("create", controller.Url<AssetFoldersController>(x => nameof(x.PostAssetFolder), values));
}
return response;
}
}
}

33
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/CreateAssetFolderDto.cs

@ -0,0 +1,33 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class CreateAssetFolderDto
{
/// <summary>
/// The name of the folder.
/// </summary>
[Required]
public string FolderName { get; set; }
/// <summary>
/// The id of the parent folder.
/// </summary>
public Guid ParentId { get; set; }
public CreateAssetFolder ToCommand()
{
return SimpleMapper.Map(this, new CreateAssetFolder());
}
}
}

30
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/MoveAssetItemDto.cs

@ -0,0 +1,30 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Squidex.Domain.Apps.Entities.Assets.Commands;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class MoveAssetItemDto
{
/// <summary>
/// The parent folder id.
/// </summary>
public Guid ParentId { get; set; }
public MoveAsset ToCommand(Guid id)
{
return new MoveAsset { AssetId = id, ParentId = ParentId };
}
public MoveAssetFolder ToFolderCommand(Guid id)
{
return new MoveAssetFolder { AssetFolderId = id, ParentId = ParentId };
}
}
}

28
backend/src/Squidex/Areas/Api/Controllers/Assets/Models/RenameAssetFolderDto.cs

@ -0,0 +1,28 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Areas.Api.Controllers.Assets.Models
{
public sealed class RenameAssetFolderDto
{
/// <summary>
/// The name of the folder.
/// </summary>
[Required]
public string FolderName { get; set; }
public RenameAssetFolder ToCommand(Guid id)
{
return SimpleMapper.Map(this, new RenameAssetFolder { AssetFolderId = id });
}
}
}

2
backend/src/Squidex/Areas/IdentityServer/Views/_Layout.cshtml

@ -19,7 +19,7 @@
</head>
<body class="white">
<div class="profile @ViewBag.Class">
<img class="profile-logo" src="@Url.RootContentUrl("~/images/logo-small.png")" />
<img class="profile-logo" src="@Url.RootContentUrl("~/images/logo.svg")" />
@RenderBody()
</div>

4
backend/src/Squidex/Config/Domain/CommandsServices.cs

@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Entities.Apps.Indexes;
using Squidex.Domain.Apps.Entities.Apps.Invitation;
using Squidex.Domain.Apps.Entities.Apps.Templates;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Comments;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Domain.Apps.Entities.Contents;
@ -83,6 +84,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<RuleCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<AssetFolderCommand, IAssetFolderGrain>>()
.As<ICommandMiddleware>();
services.AddSingletonAs<GrainCommandMiddleware<CommentsCommand, ICommentsGrain>>()
.As<ICommandMiddleware>();

4
backend/src/Squidex/Config/Domain/StoreServices.cs

@ -100,6 +100,10 @@ namespace Squidex.Config.Domain
.As<IAssetRepository>()
.As<ISnapshotStore<AssetState, Guid>>();
services.AddSingletonAs<MongoAssetFolderRepository>()
.As<IAssetFolderRepository>()
.As<ISnapshotStore<AssetFolderState, Guid>>();
services.AddSingletonAs(c => new MongoContentRepository(
c.GetRequiredService<IMongoClient>().GetDatabase(mongoContentDatabaseName),
c.GetRequiredService<IAppProvider>(),

BIN
backend/src/Squidex/wwwroot/images/add-app.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

59
backend/src/Squidex/wwwroot/images/add-app.svg

@ -0,0 +1,59 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 64 64" xml:space="preserve">
<style id="style8137" type="text/css">
.st6{fill:#b5b5ea}.st15{fill:#fff}
</style>
<g id="g8143">
<path id="path8145" fill="#b4bcc1" d="M11.8 16c.2.4.2.9.2 1.3V19h1v-1.6c0-.7-.1-1.4-.4-2l-.1-3.1c-.4-1.2-.3-2.5.4-3.5.6-1 1.6-1.6 2.7-1.7H23.2c2.4 0 4.6.9 6.3 2.6l4.7 4.7c2.1 2.1 4.8 3.2 7.7 3.2H50v-2h-8.1c-2.4 0-4.6-.9-6.3-2.6l-4.7-4.7C28.8 6.1 26.1 5 23.2 5h-7.8c-1.7.2-3.2 1.1-4.2 2.6-1 1.6-1.2 3.5-.6 5.3l1.2 3.1z"/>
</g>
<g id="g8147">
<path id="path8149" fill="#d7d7f9" d="M3 24v-4.5C3 15.9 5.9 13 9.5 13h12.8c1.7 0 3.4.7 4.6 1.9l7.2 9.2c1.2 1.2 2.9 1.9 4.6 1.9h7.8C55.1 26 61 30.9 61 39.5v15c0 3.6-3.9 6.5-7.5 6.5h-44C5.9 61 3 58.1 3 54.5V24z"/>
<path id="path8151" fill="#2e3842" d="M53.5 62h-44C5.4 62 2 58.6 2 54.5v-35C2 15.4 5.4 12 9.5 12h12.8c2 0 3.9.8 5.3 2.2l.1.1 7.2 9.2c1 1 2.4 1.6 3.8 1.6h7.8C55.9 25 62 30.7 62 39.5v15c0 4.3-4.5 7.5-8.5 7.5zm-44-48c-3 0-5.5 2.5-5.5 5.5v35.1c0 3 2.5 5.5 5.5 5.5h44.1c2.9 0 6.5-2.4 6.5-5.5v-15C60 31.8 54.8 27 46.5 27h-7.8c-2 0-3.9-.8-5.3-2.2l-.1-.1-7.2-9.2c-1-1-2.4-1.6-3.8-1.6H9.5z"/>
</g>
<g id="g8153">
<path id="path8155" fill="#ededf9" d="M53 60H10c-3.8 0-6-2.2-6-6V20c0-3.8 2.2-6 6-6h12c2 0 3.6 1.2 4.7 2.3l.1.1 7 8.9c.6.5 2.1 1.7 4.5 1.5h7.3C54.7 26.9 60 31.3 60 39v15c0 3.6-2.8 6-7 6zM10 16c-2.7 0-4 1.3-4 4v34c0 2.7 1.3 4 4 4h43c2.3 0 5-1 5-4V39c0-8.8-7.9-10.1-12.5-10.1h-7.2c-2.2.1-4.4-.6-6-2.1l-.1-.1-7-8.9C24 16.5 23.1 16 22 16H10z"/>
</g>
<g id="g8157">
<g id="g8159">
<path id="path8161" d="M22 41c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5zm0-8c-1.7 0-3 1.3-3 3s1.3 3 3 3 3-1.3 3-3-1.3-3-3-3z" class="st6"/>
</g>
</g>
<g id="g8163">
<g id="g8165">
<g id="g8167">
<path id="path8169" d="M17 36c0-.3 0-.7.1-1H14c-.6 0-1 .4-1 1s.4 1 1 1h3.1c-.1-.3-.1-.7-.1-1z" class="st6"/>
</g>
</g>
<g id="g8171">
<g id="g8173">
<path id="path8175" d="M49 35H26.9c.1.3.1.7.1 1s0 .7-.1 1H49c.6 0 1-.4 1-1s-.4-1-1-1z" class="st6"/>
</g>
</g>
</g>
<g id="g8177">
<g id="g8179">
<path id="path8181" d="M41 52c-2.8 0-5-2.2-5-5s2.2-5 5-5 5 2.2 5 5-2.2 5-5 5zm0-8c-1.7 0-3 1.3-3 3s1.3 3 3 3 3-1.3 3-3-1.3-3-3-3z" class="st6"/>
</g>
</g>
<g id="g8183">
<g id="g8185">
<g id="g8187">
<path id="path8189" d="M46 47c0-.3 0-.7-.1-1H49c.6 0 1 .4 1 1s-.4 1-1 1h-3.1c.1-.3.1-.7.1-1z" class="st6"/>
</g>
</g>
<g id="g8191">
<g id="g8193">
<path id="path8195" d="M14 46h22.1c-.1.3-.1.7-.1 1s0 .7.1 1H14c-.6 0-1-.4-1-1s.4-1 1-1z" class="st6"/>
</g>
</g>
</g>
<g id="g8197">
<circle id="circle8199" cx="51.9" cy="12.1" r="11.1" fill="#3389ff"/>
<path id="path8201" d="M51.9 24.2c-6.7 0-12.1-5.4-12.1-12.1S45.2 0 51.9 0 64 5.4 64 12.1s-5.4 12.1-12.1 12.1zm0-22.2c-5.6 0-10.1 4.5-10.1 10.1s4.5 10.1 10.1 10.1S62 17.7 62 12.1 57.5 2 51.9 2z" class="st15"/>
</g>
<g id="g8203">
<path id="path8205" d="M57 13H47c-.6 0-1-.4-1-1s.4-1 1-1h10c.6 0 1 .4 1 1s-.4 1-1 1z" class="st15"/>
</g>
<g id="g8207">
<path id="path8209" d="M52 18c-.6 0-1-.4-1-1V7c0-.6.4-1 1-1s1 .4 1 1v10c0 .6-.4 1-1 1z" class="st15"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
backend/src/Squidex/wwwroot/images/add-blog.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

38
backend/src/Squidex/wwwroot/images/add-blog.svg

@ -0,0 +1,38 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 64 64" xml:space="preserve">
<style id="style8137" type="text/css">
.st15{fill:#fff}
</style>
<style id="style8137-2" type="text/css">
.st15{fill:#fff}
</style>
<style id="style8137-0" type="text/css">
.st15{fill:#fff}
</style>
<g id="g4726" transform="translate(-67.875 31.5)">
<g id="g8143-8" transform="translate(67.875 -31.5)">
<path id="path8145-0" fill="#b4bcc1" d="M11.8 16c.2.4.2.9.2 1.3V19h1v-1.6c0-.7-.1-1.4-.4-2l-.1-3.1c-.4-1.2-.3-2.5.4-3.5.6-1 1.6-1.6 2.7-1.7H23.2c2.4 0 4.6.9 6.3 2.6l4.7 4.7c2.1 2.1 4.8 3.2 7.7 3.2H50v-2h-8.1c-2.4 0-4.6-.9-6.3-2.6l-4.7-4.7C28.8 6.1 26.1 5 23.2 5h-7.8c-1.7.2-3.2 1.1-4.2 2.6-1 1.6-1.2 3.5-.6 5.3l1.2 3.1z"/>
</g>
<g id="g8147-3" transform="translate(67.875 -31.5)">
<path id="path8149-6" fill="#d7d7f9" d="M3 24v-4.5C3 15.9 5.9 13 9.5 13h12.8c1.7 0 3.4.7 4.6 1.9l7.2 9.2c1.2 1.2 2.9 1.9 4.6 1.9h7.8C55.1 26 61 30.9 61 39.5v15c0 3.6-3.9 6.5-7.5 6.5h-44C5.9 61 3 58.1 3 54.5V24z"/>
<path id="path8151-3" fill="#2e3842" d="M53.5 62h-44C5.4 62 2 58.6 2 54.5v-35C2 15.4 5.4 12 9.5 12h12.8c2 0 3.9.8 5.3 2.2l.1.1 7.2 9.2c1 1 2.4 1.6 3.8 1.6h7.8C55.9 25 62 30.7 62 39.5v15c0 4.3-4.5 7.5-8.5 7.5zm-44-48c-3 0-5.5 2.5-5.5 5.5v35.1c0 3 2.5 5.5 5.5 5.5h44.1c2.9 0 6.5-2.4 6.5-5.5v-15C60 31.8 54.8 27 46.5 27h-7.8c-2 0-3.9-.8-5.3-2.2l-.1-.1-7.2-9.2c-1-1-2.4-1.6-3.8-1.6L9.633 14z"/>
</g>
<g id="g8153" transform="translate(67.875 -31.5)">
<path id="path8155" fill="#ededf9" d="M53 60H10c-3.8 0-6-2.2-6-6V20c0-3.8 2.2-6 6-6h12c2 0 3.6 1.2 4.7 2.3l.1.1 7 8.9c.6.5 2.1 1.7 4.5 1.5h7.3C54.7 26.9 60 31.3 60 39v15c0 3.6-2.8 6-7 6zM10 16c-2.7 0-4 1.3-4 4v34c0 2.7 1.3 4 4 4h43c2.3 0 5-1 5-4V39c0-8.8-7.9-10.1-12.5-10.1h-7.2c-2.2.1-4.4-.6-6-2.1l-.1-.1-7-8.9C24 16.5 23.1 16 22 16H10z"/>
</g>
<g id="g8197-0" transform="translate(67.875 -31.5)">
<circle id="circle8199-9" cx="51.9" cy="12.1" r="11.1" fill="#3389ff"/>
<path id="path8201-7" fill="#fff" d="M51.9 24.2c-6.7 0-12.1-5.4-12.1-12.1C39.8 5.4 45.2 0 51.9 0 58.6 0 64 5.4 64 12.1c0 6.7-5.4 12.1-12.1 12.1zm0-22.2c-5.6 0-10.1 4.5-10.1 10.1s4.5 10.1 10.1 10.1S62 17.7 62 12.1 57.5 2 51.9 2z" class="st15"/>
</g>
<g id="g8203-4" transform="translate(67.875 -31.5)">
<path id="path8205-6" fill="#fff" d="M57 13H47c-.6 0-1-.4-1-1s.4-1 1-1h10c.6 0 1 .4 1 1s-.4 1-1 1z" class="st15"/>
</g>
<g id="g8207-9" transform="translate(67.875 -31.5)">
<path id="path8209-3" fill="#fff" d="M52 18c-.6 0-1-.4-1-1V7c0-.6.4-1 1-1s1 .4 1 1v10c0 .6-.4 1-1 1z" class="st15"/>
</g>
<g id="g4685" stroke="#b5b5ea" stroke-opacity="1" transform="matrix(.77563 0 0 .7719 73.711 -21.893)">
<path id="path4174" fill="#b5b5ea" fill-opacity="1" stroke-width=".082" d="M12.888 47.387c-.96 0-1.775.34-2.447 1.018-.672.68-1.007 1.504-1.007 2.475 0 .97.335 1.795 1.007 2.474a3.313 3.313 0 002.447 1.019c.96 0 1.774-.34 2.446-1.019.672-.679 1.007-1.504 1.007-2.474 0-.97-.335-1.796-1.007-2.475a3.312 3.312 0 00-2.446-1.018z"/>
<path id="path4356" fill="none" fill-rule="evenodd" stroke-dasharray="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="4" stroke-width="3.877" d="M10.506 31.413C33.49 31.538 32.491 53.4 32.491 53.4"/>
<path id="path4356-4" fill="none" fill-rule="evenodd" stroke-dasharray="none" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="4" stroke-width="3.877" d="M10.344 39.427c14.616.08 13.982 13.986 13.982 13.986"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
backend/src/Squidex/wwwroot/images/add-identity.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

27
backend/src/Squidex/wwwroot/images/add-identity.svg

@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 64 64" xml:space="preserve">
<style>
.st7{fill:#fff}.st9{fill:none;stroke:#b5b5ea;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10}
</style>
<g id="g8143">
<path id="path8145" fill="#b4bcc1" d="M11.8 16c.2.4.2.9.2 1.3V19h1v-1.6c0-.7-.1-1.4-.4-2l-.1-3.1c-.4-1.2-.3-2.5.4-3.5.6-1 1.6-1.6 2.7-1.7H23.2c2.4 0 4.6.9 6.3 2.6l4.7 4.7c2.1 2.1 4.8 3.2 7.7 3.2H50v-2h-8.1c-2.4 0-4.6-.9-6.3-2.6l-4.7-4.7C28.8 6.1 26.1 5 23.2 5h-7.8c-1.7.2-3.2 1.1-4.2 2.6-1 1.6-1.2 3.5-.6 5.3l1.2 3.1z"/>
</g>
<g id="g8147">
<path id="path8149" fill="#d7d7f9" d="M3 24v-4.5C3 15.9 5.9 13 9.5 13h12.8c1.7 0 3.4.7 4.6 1.9l7.2 9.2c1.2 1.2 2.9 1.9 4.6 1.9h7.8C55.1 26 61 30.9 61 39.5v15c0 3.6-3.9 6.5-7.5 6.5h-44C5.9 61 3 58.1 3 54.5V24z"/>
<path id="path8151" fill="#2e3842" d="M53.5 62h-44C5.4 62 2 58.6 2 54.5v-35C2 15.4 5.4 12 9.5 12h12.8c2 0 3.9.8 5.3 2.2l.1.1 7.2 9.2c1 1 2.4 1.6 3.8 1.6h7.8C55.9 25 62 30.7 62 39.5v15c0 4.3-4.5 7.5-8.5 7.5zm-44-48c-3 0-5.5 2.5-5.5 5.5v35.1c0 3 2.5 5.5 5.5 5.5h44.1c2.9 0 6.5-2.4 6.5-5.5v-15C60 31.8 54.8 27 46.5 27h-7.8c-2 0-3.9-.8-5.3-2.2l-.1-.1-7.2-9.2c-1-1-2.4-1.6-3.8-1.6H9.5v.1z"/>
</g>
<g id="g8153">
<path id="path8155" fill="#ededf9" d="M53 60H10c-3.8 0-6-2.2-6-6V20c0-3.8 2.2-6 6-6h12c2 0 3.6 1.2 4.7 2.3l.1.1 7 8.9c.6.5 2.1 1.7 4.5 1.5h7.3C54.7 26.9 60 31.3 60 39v15c0 3.6-2.8 6-7 6zM10 16c-2.7 0-4 1.3-4 4v34c0 2.7 1.3 4 4 4h43c2.3 0 5-1 5-4V39c0-8.8-7.9-10.1-12.5-10.1h-7.2c-2.2.1-4.4-.6-6-2.1l-.1-.1-7-8.9C24 16.5 23.1 16 22 16H10z"/>
</g>
<g id="g8197">
<circle id="circle8199" cx="51.9" cy="12.1" r="11.1" fill="#3389ff"/>
<path id="path8201" d="M51.9 24.2c-6.7 0-12.1-5.4-12.1-12.1S45.2 0 51.9 0 64 5.4 64 12.1s-5.4 12.1-12.1 12.1zm0-22.2c-5.6 0-10.1 4.5-10.1 10.1s4.5 10.1 10.1 10.1S62 17.7 62 12.1 57.5 2 51.9 2z" class="st7"/>
</g>
<g id="g8203">
<path id="path8205" d="M57 13H47c-.6 0-1-.4-1-1s.4-1 1-1h10c.6 0 1 .4 1 1s-.4 1-1 1z" class="st7"/>
</g>
<g id="g8207">
<path id="path8209" d="M52 18c-.6 0-1-.4-1-1V7c0-.6.4-1 1-1s1 .4 1 1v10c0 .6-.4 1-1 1z" class="st7"/>
</g>
<circle cx="25.1" cy="39.2" r="7.2" fill="none" stroke="#b5b5ea" stroke-miterlimit="10" stroke-width="2"/>
<path d="M30.9 43.6l12.4 9.3M41 51.2l3.3-4.4M38.3 48.1l1.5-2.1" class="st9"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
backend/src/Squidex/wwwroot/images/add-profile.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

26
backend/src/Squidex/wwwroot/images/add-profile.svg

@ -0,0 +1,26 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" viewBox="0 0 64 64" xml:space="preserve">
<style id="style2" type="text/css">
.st7{fill:#fff}
</style>
<g id="g8143">
<path id="path8145" fill="#b4bcc1" d="M11.8 16c.2.4.2.9.2 1.3V19h1v-1.6c0-.7-.1-1.4-.4-2l-.1-3.1c-.4-1.2-.3-2.5.4-3.5.6-1 1.6-1.6 2.7-1.7H23.2c2.4 0 4.6.9 6.3 2.6l4.7 4.7c2.1 2.1 4.8 3.2 7.7 3.2H50v-2h-8.1c-2.4 0-4.6-.9-6.3-2.6l-4.7-4.7C28.8 6.1 26.1 5 23.2 5h-7.8c-1.7.2-3.2 1.1-4.2 2.6-1 1.6-1.2 3.5-.6 5.3l1.2 3.1z"/>
</g>
<g id="g8147">
<path id="path8149" fill="#d7d7f9" d="M3 24v-4.5C3 15.9 5.9 13 9.5 13h12.8c1.7 0 3.4.7 4.6 1.9l7.2 9.2c1.2 1.2 2.9 1.9 4.6 1.9h7.8C55.1 26 61 30.9 61 39.5v15c0 3.6-3.9 6.5-7.5 6.5h-44C5.9 61 3 58.1 3 54.5V24z"/>
<path id="path8151" fill="#2e3842" d="M53.5 62h-44C5.4 62 2 58.6 2 54.5v-35C2 15.4 5.4 12 9.5 12h12.8c2 0 3.9.8 5.3 2.2l.1.1 7.2 9.2c1 1 2.4 1.6 3.8 1.6h7.8C55.9 25 62 30.7 62 39.5v15c0 4.3-4.5 7.5-8.5 7.5zm-44-48c-3 0-5.5 2.5-5.5 5.5v35.1c0 3 2.5 5.5 5.5 5.5h44.1c2.9 0 6.5-2.4 6.5-5.5v-15C60 31.8 54.8 27 46.5 27h-7.8c-2 0-3.9-.8-5.3-2.2l-.1-.1-7.2-9.2c-1-1-2.4-1.6-3.8-1.6H9.5v.1z"/>
</g>
<g id="g8153">
<path id="path8155" fill="#ededf9" d="M53 60H10c-3.8 0-6-2.2-6-6V20c0-3.8 2.2-6 6-6h12c2 0 3.6 1.2 4.7 2.3l.1.1 7 8.9c.6.5 2.1 1.7 4.5 1.5h7.3C54.7 26.9 60 31.3 60 39v15c0 3.6-2.8 6-7 6zM10 16c-2.7 0-4 1.3-4 4v34c0 2.7 1.3 4 4 4h43c2.3 0 5-1 5-4V39c0-8.8-7.9-10.1-12.5-10.1h-7.2c-2.2.1-4.4-.6-6-2.1l-.1-.1-7-8.9C24 16.5 23.1 16 22 16H10z"/>
</g>
<g id="g8197">
<circle id="circle8199" cx="51.9" cy="12.1" r="11.1" fill="#3389ff"/>
<path id="path8201" d="M51.9 24.2c-6.7 0-12.1-5.4-12.1-12.1S45.2 0 51.9 0 64 5.4 64 12.1s-5.4 12.1-12.1 12.1zm0-22.2c-5.6 0-10.1 4.5-10.1 10.1s4.5 10.1 10.1 10.1S62 17.7 62 12.1 57.5 2 51.9 2z" class="st7"/>
</g>
<g id="g8203">
<path id="path8205" d="M57 13H47c-.6 0-1-.4-1-1s.4-1 1-1h10c.6 0 1 .4 1 1s-.4 1-1 1z" class="st7"/>
</g>
<g id="g8207">
<path id="path8209" d="M52 18c-.6 0-1-.4-1-1V7c0-.6.4-1 1-1s1 .4 1 1v10c0 .6-.4 1-1 1z" class="st7"/>
</g>
<path id="path36" fill="#b5b5ea" fill-opacity="1" stroke="#b5b5ea" stroke-dasharray="none" stroke-miterlimit="4" stroke-opacity="1" stroke-width=".5" d="M21.695 32.767c-3.123 0-5.67 2.547-5.67 5.667 0 1.56.588 3.48 1.55 5.105.466.787 1.04 1.514 1.701 2.065-1.409.179-2.892.223-4.081.759-1.893.852-3.423 2.247-4.087 4.235-.036.226 0 0-.036.214v.012a3.199 3.199 0 003.186 3.19h14.87a3.2 3.2 0 003.19-3.19l-.036-.222c-.66-1.998-2.188-3.394-4.081-4.245-1.19-.534-2.677-.58-4.087-.756.659-.552 1.233-1.277 1.697-2.063.962-1.625 1.551-3.544 1.551-5.105a5.678 5.678 0 00-5.667-5.667zm0 1.423a4.235 4.235 0 014.245 4.245c0 1.175-.519 2.973-1.352 4.381-.833 1.408-1.931 2.342-2.893 2.342-.962 0-2.06-.934-2.893-2.342-.832-1.407-1.354-3.206-1.354-4.38a4.238 4.238 0 014.247-4.246zm0 12.39c2.195 0 4.288.341 5.922 1.076 1.583.712 2.695 1.772 3.236 3.267-.058.93-.78 1.668-1.725 1.668h-14.87c-.94 0-1.662-.736-1.722-1.666.544-1.487 1.659-2.55 3.242-3.263 1.635-.736 3.728-1.082 5.916-1.082z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
backend/src/Squidex/wwwroot/images/asset_doc.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 911 B

7
backend/src/Squidex/wwwroot/images/asset_doc.svg

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 57 71.8">
<path id="path258" fill="#aee2f4" stroke="#64afdd" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.2 70.8H11.8C5.9 70.8 1 65.9 1 60V11.8C1 5.9 5.9 1 11.8 1h39.1C53.7 1 56 3.3 56 6.1V60c0 5.9-4.9 10.8-10.8 10.8z"/>
<path id="path260" fill="none" stroke="#dce9ef" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.4 68.8H11.5c-4.9 0-8.9-4-8.9-8.9V11.6c0-4.9 4-8.9 8.9-8.9H51c1.8 0 3.3 1.5 3.3 3.3v53.9c0 4.9-4 8.9-8.9 8.9z"/>
<g id="g272" transform="translate(-317 -144)">
<path id="path270" fill="#4ca9d8" d="M345.6 178.9l-3.9 10.7h-1.3l-6.2-16.7c-.2-.7-.8-.9-1.6-1v-.9h7.7v.9c-1.4.1-1.9.4-1.9 1.1 0 .4.2.8.3 1.3l3.6 10.1 2.6-7.1-1.4-4.3c-.2-.7-.7-.9-1.4-1v-.9h7.6v.9c-1.6 0-2.1.3-2.1 1.1 0 .4.1.8.3 1.3l3.2 9.8 3.3-9.8c.2-.5.3-.9.3-1.2 0-.8-.5-1.1-2.1-1.2v-.9h5.2v.9c-.7.1-1.3.3-1.5 1l-5.8 16.7h-1.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
backend/src/Squidex/wwwroot/images/asset_docx.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 958 B

7
backend/src/Squidex/wwwroot/images/asset_docx.svg

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 57 71.8">
<path id="path258" fill="#aee2f4" stroke="#64afdd" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.2 70.8H11.8C5.9 70.8 1 65.9 1 60V11.8C1 5.9 5.9 1 11.8 1h39.1C53.7 1 56 3.3 56 6.1V60c0 5.9-4.9 10.8-10.8 10.8z"/>
<path id="path260" fill="none" stroke="#dce9ef" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.4 68.8H11.5c-4.9 0-8.9-4-8.9-8.9V11.6c0-4.9 4-8.9 8.9-8.9H51c1.8 0 3.3 1.5 3.3 3.3v53.9c0 4.9-4 8.9-8.9 8.9z"/>
<g id="g272" transform="translate(-317 -144)">
<path id="path270" fill="#4ca9d8" d="M345.6 178.9l-3.9 10.7h-1.3l-6.2-16.7c-.2-.7-.8-.9-1.6-1v-.9h7.7v.9c-1.4.1-1.9.4-1.9 1.1 0 .4.2.8.3 1.3l3.6 10.1 2.6-7.1-1.4-4.3c-.2-.7-.7-.9-1.4-1v-.9h7.6v.9c-1.6 0-2.1.3-2.1 1.1 0 .4.1.8.3 1.3l3.2 9.8 3.3-9.8c.2-.5.3-.9.3-1.2 0-.8-.5-1.1-2.1-1.2v-.9h5.2v.9c-.7.1-1.3.3-1.5 1l-5.8 16.7h-1.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
backend/src/Squidex/wwwroot/images/asset_generic.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 514 B

7
backend/src/Squidex/wwwroot/images/asset_generic.svg

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 57 71.8">
<path id="path226" fill="#d6dde5" stroke="#92979b" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.2 70.8H11.8C5.9 70.8 1 65.9 1 60V11.8C1 5.9 5.9 1 11.8 1h39.1C53.7 1 56 3.3 56 6.1V60c0 5.9-4.9 10.8-10.8 10.8z" class="st34"/>
<path id="path228" fill="none" stroke="#e8ecef" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.4 68.8H11.5c-4.9 0-8.9-4-8.9-8.9V11.6c0-4.9 4-8.9 8.9-8.9H51c1.8 0 3.3 1.5 3.3 3.3v53.9c0 4.9-4 8.9-8.9 8.9z"/>
<path id="line230" fill="#d6dde5" stroke="#92979b" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M13.2 17h30.7" class="st34"/>
<path id="line232" fill="#d6dde5" stroke="#92979b" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M13.2 26.4h30.7" class="st34"/>
<path id="line234" fill="#d6dde5" stroke="#92979b" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M13.2 35.8h11.9" class="st34"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
backend/src/Squidex/wwwroot/images/asset_pdf.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

5
backend/src/Squidex/wwwroot/images/asset_pdf.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 57 71.8">
<path id="path222" fill="#f2bdbd" stroke="#d6516b" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.2 70.8H11.8C5.9 70.8 1 65.9 1 60V11.8C1 5.9 5.9 1 11.8 1h39.1C53.7 1 56 3.3 56 6.1V60c0 5.9-4.9 10.8-10.8 10.8z"/>
<path id="path224" fill="none" stroke="#f7dcdc" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.4 68.8H11.5c-4.9 0-8.9-4-8.9-8.9V11.6c0-4.9 4-8.9 8.9-8.9H51c1.8 0 3.3 1.5 3.3 3.3v53.9c0 4.9-4 8.9-8.9 8.9z"/>
<path id="path236" fill="#d6516b" d="M41.8 39.4c-.4-.9-1.5-1.3-3.3-1.3-1 0-2.3.1-3.8.4-2.1-2.2-4.2-5.4-5.7-8.6 1.5-6.4.8-7.3.4-7.7-.4-.5-.9-1.2-1.4-1.2-.2 0-.9.1-1.1.2-.6.2-1 .7-1.3 1.4-.8 1.9.3 5.1 1.4 7.5-1 3.9-2.6 8.5-4.3 12.2-4.3 2-6.5 3.9-6.7 5.8-.1.7.1 1.7 1.3 2.6.3.2.7.4 1.1.4 1 0 2-.8 3.1-2.4.8-1.2 1.7-2.8 2.7-4.8 3-1.3 6.7-2.5 9.9-3.2 1.8 1.7 3.4 2.6 4.7 2.6 1 0 1.9-.5 2.5-1.4.7-1 .9-1.8.5-2.5zm-23.4 9.7c-.5-.4-.5-.7-.5-.8.1-.6 1-1.8 3.5-3.1-1.9 3.4-2.8 3.8-3 3.9zm9.2-26s1.2 1.3.1 3.7c-1.6-1.8-.2-3.7-.1-3.7zm-2.3 18.1c1.1-2.7 2.2-5.8 3-8.6 1.3 2.3 2.8 4.5 4.3 6.3-2.4.6-5 1.4-7.3 2.3zm14.5-.5c-.3.5-1.1.5-1.4.5-.6 0-.8-.4-1.7-1.1.8-.1 1.5-.1 2-.1 1 0 1.2.1 1.3.2.1.1 0 .3-.2.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
backend/src/Squidex/wwwroot/images/asset_ppt.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

6
backend/src/Squidex/wwwroot/images/asset_ppt.svg

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 57 71.8">
<path id="path262" fill="#f7c38b" stroke="#e57e5e" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.2 70.8H11.8C5.9 70.8 1 65.9 1 60V11.8C1 5.9 5.9 1 11.8 1h39.1C53.7 1 56 3.3 56 6.1V60c0 5.9-4.9 10.8-10.8 10.8z"/>
<path id="path264" fill="none" stroke="#f4e5d9" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.4 68.8H11.5c-4.9 0-8.9-4-8.9-8.9V11.6c0-4.9 4-8.9 8.9-8.9H51c1.8 0 3.3 1.5 3.3 3.3v53.9c0 4.9-4 8.9-8.9 8.9z"/>
<path id="path266" fill="none" stroke="#e57e5e" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M32.5 23h-.3v9h9.7c-.3-5-4.4-9-9.4-9z" class="st45"/>
<path id="path268" fill="none" stroke="#e57e5e" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M28 36V23c-7.2.3-13 6.2-13 13.5C15 44 21 50 28.5 50S42 44 42 36.5V36z" class="st45"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
backend/src/Squidex/wwwroot/images/asset_pptx.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

6
backend/src/Squidex/wwwroot/images/asset_pptx.svg

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 57 71.8">
<path id="path262" fill="#f7c38b" stroke="#e57e5e" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.2 70.8H11.8C5.9 70.8 1 65.9 1 60V11.8C1 5.9 5.9 1 11.8 1h39.1C53.7 1 56 3.3 56 6.1V60c0 5.9-4.9 10.8-10.8 10.8z"/>
<path id="path264" fill="none" stroke="#f4e5d9" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.4 68.8H11.5c-4.9 0-8.9-4-8.9-8.9V11.6c0-4.9 4-8.9 8.9-8.9H51c1.8 0 3.3 1.5 3.3 3.3v53.9c0 4.9-4 8.9-8.9 8.9z"/>
<path id="path266" fill="none" stroke="#e57e5e" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M32.5 23h-.3v9h9.7c-.3-5-4.4-9-9.4-9z" class="st45"/>
<path id="path268" fill="none" stroke="#e57e5e" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M28 36V23c-7.2.3-13 6.2-13 13.5C15 44 21 50 28.5 50S42 44 42 36.5V36z" class="st45"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
backend/src/Squidex/wwwroot/images/asset_video.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 723 B

5
backend/src/Squidex/wwwroot/images/asset_video.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 57 71.8">
<path id="path254" fill="#c1bae5" stroke="#585ebf" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.2 70.8H11.8C5.9 70.8 1 65.9 1 60V11.8C1 5.9 5.9 1 11.8 1h39.1C53.7 1 56 3.3 56 6.1V60c0 5.9-4.9 10.8-10.8 10.8z" class="st39"/>
<path id="path256" fill="none" stroke="#e4ddf2" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.4 68.8H11.5c-4.9 0-8.9-4-8.9-8.9V11.6c0-4.9 4-8.9 8.9-8.9H51c1.8 0 3.3 1.5 3.3 3.3v53.9c0 4.9-4 8.9-8.9 8.9z"/>
<path id="path288" fill="#c1bae5" stroke="#585ebf" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M23 43.6v-16c0-1.5 1.7-2.3 2.9-1.4L37 34.6c1 .7.9 2.2-.1 2.9l-11.1 7.7c-1.2.7-2.8-.2-2.8-1.6z" class="st39"/>
</svg>

After

Width:  |  Height:  |  Size: 913 B

BIN
backend/src/Squidex/wwwroot/images/asset_xls.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

10
backend/src/Squidex/wwwroot/images/asset_xls.svg

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 57 71.8">
<path id="path238" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.2 70.8H11.8C5.9 70.8 1 65.9 1 60V11.8C1 5.9 5.9 1 11.8 1h39.1C53.7 1 56 3.3 56 6.1V60c0 5.9-4.9 10.8-10.8 10.8z" class="st37"/>
<path id="path240" fill="none" stroke="#dcf4e5" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.4 68.8H11.5c-4.9 0-8.9-4-8.9-8.9V11.6c0-4.9 4-8.9 8.9-8.9H51c1.8 0 3.3 1.5 3.3 3.3v53.9c0 4.9-4 8.9-8.9 8.9z"/>
<path id="line242" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M43 26H15" class="st37"/>
<path id="line244" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M15 36h28" class="st37"/>
<path id="line246" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M15 46h28" class="st37"/>
<path id="line248" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M15 26v20" class="st37"/>
<path id="line250" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M43 26v20" class="st37"/>
<path id="line252" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M29 26v20" class="st37"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
backend/src/Squidex/wwwroot/images/asset_xlsx.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 905 B

10
backend/src/Squidex/wwwroot/images/asset_xlsx.svg

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 57 71.8">
<path id="path238" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.2 70.8H11.8C5.9 70.8 1 65.9 1 60V11.8C1 5.9 5.9 1 11.8 1h39.1C53.7 1 56 3.3 56 6.1V60c0 5.9-4.9 10.8-10.8 10.8z" class="st37"/>
<path id="path240" fill="none" stroke="#dcf4e5" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M45.4 68.8H11.5c-4.9 0-8.9-4-8.9-8.9V11.6c0-4.9 4-8.9 8.9-8.9H51c1.8 0 3.3 1.5 3.3 3.3v53.9c0 4.9-4 8.9-8.9 8.9z"/>
<path id="line242" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M43 26H15" class="st37"/>
<path id="line244" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M15 36h28" class="st37"/>
<path id="line246" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M15 46h28" class="st37"/>
<path id="line248" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M15 26v20" class="st37"/>
<path id="line250" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M43 26v20" class="st37"/>
<path id="line252" fill="#bae8ca" stroke="#57c178" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M29 26v20" class="st37"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
backend/src/Squidex/wwwroot/images/dashboard-api.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

12
backend/src/Squidex/wwwroot/images/dashboard-api.svg

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 57 62">
<path id="path38" fill="#f2bdbd" stroke="#2e3842" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M38.2 53H18.8C12.9 53 8 48.1 8 42.2V11.8C8 5.9 12.9 1 18.8 1h25.1C46.7 1 49 3.3 49 6.1v36.1C49 48.1 44.1 53 38.2 53z"/>
<path id="path40" fill="none" stroke="#f7dcdc" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M38.1 53H18.9C14 53 10 49 10 44.1V11.9C10 7 14 3 18.9 3h24.9c1.8 0 3.3 1.5 3.3 3.3v37.9C47 49 43 53 38.1 53z"/>
<path id="line90" fill="none" stroke="#e29393" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M17.1 13H40" class="st13"/>
<path id="line92" fill="none" stroke="#e29393" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M17.1 20H40" class="st13"/>
<path id="line94" fill="none" stroke="#e29393" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M17.1 27H26" class="st13"/>
<g id="g100" transform="translate(-364 -444)">
<path id="path96" fill="#fff" d="M369 484h12.3c1.6 0 3.1 1 3.7 2.5.6 1.5 2.1 2.5 3.7 2.5H416c2.2 0 4 1.8 4 4v8c0 2.2-1.8 4-4 4h-47c-2.2 0-4-1.8-4-4v-13c0-2.2 1.8-4 4-4z"/>
<path id="path98" fill="#2e3842" d="M366 495v-7c0-1.6 1.3-3 3-3h12.3c1.2 0 2.3.7 2.8 1.9.8 1.9 2.6 3.1 4.6 3.1H416c1.6 0 3 1.3 3 3v4h2v-4c0-2.7-2.2-5-5-5h-27.3c-1.2 0-2.3-.7-2.8-1.9-.8-1.9-2.6-3.1-4.6-3.1H369c-2.7 0-5 2.2-5 5v7z"/>
</g>
<path id="path102" fill="#b4bcc1" d="M55 57c0 1.6-1.3 3-3 3H5c-1.6 0-3-1.3-3-3v-4H0v4c0 2.7 2.2 5 5 5h47c2.7 0 5-2.2 5-5z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
backend/src/Squidex/wwwroot/images/dashboard-feedback.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

BIN
backend/src/Squidex/wwwroot/images/dashboard-github.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

6
backend/src/Squidex/wwwroot/images/dashboard-github.svg

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 61 61">
<circle id="circle188" cx="30.5" cy="30.5" r="29.5" fill="#d6e2b3" fill-rule="evenodd" stroke="#2e3842" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" clip-rule="evenodd"/>
<path id="path190" fill="#fff" fill-rule="evenodd" d="M25.3 45.7c2.7.3 7.2.4 13.6-.6 4.9-1.3 9.1-4.4 9.1-13 0-3-1.1-5.5-2.8-7.4.3-.7 1.2-3.5-.3-7.3 0 0-2.3-.7-7.6 2.8-2.2-.6-4.6-.9-6.9-.9-2.4 0-4.7.3-6.9.9-5.3-3.6-7.6-2.8-7.6-2.8-1.5 3.8-.6 6.6-.3 7.3-1.8 1.9-2.9 4.4-2.9 7.4 0 10.5 6.4 12.9 12.6 13.6z" clip-rule="evenodd"/>
<path id="path192" fill="#77ba61" d="M41 31.6c-2.3-2.1-6.1-1-10.5-1h-.1c-4.4 0-8.2-1.1-10.5 1-1.4 1.3-1.9 2.8-1.9 4.4 0 6.8 5.4 7.6 12.4 7.6h.1c7 0 12.4-.8 12.4-7.6 0-1.7-.6-3.2-1.9-4.4zm-16.5 8.3c-1.3 0-2.4-1.5-2.4-3.3 0-1.8 1.1-3.3 2.4-3.3 1.3 0 2.4 1.5 2.4 3.3 0 1.8-1.1 3.3-2.4 3.3zm11.8 0c-1.3 0-2.4-1.5-2.4-3.3 0-1.8 1.1-3.3 2.4-3.3 1.3 0 2.4 1.5 2.4 3.3 0 1.8-1.1 3.3-2.4 3.3z"/>
<circle id="circle194" cx="30.5" cy="30.5" r="27.5" fill="none" fill-rule="evenodd" stroke="#e7f7dc" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" clip-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
backend/src/Squidex/wwwroot/images/dashboard-schema.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

10
backend/src/Squidex/wwwroot/images/dashboard-schema.svg

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 86 64">
<path id="path36" fill="#c7e0ff" stroke="#2e3842" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M54.2 63H11.8C5.9 63 1 58.1 1 52.2V28.8C1 22.9 5.9 18 11.8 18h42.4C60.1 18 65 22.9 65 28.8v23.4C65 58.1 60.1 63 54.2 63z"/>
<path id="line42" fill="none" stroke="#7d878e" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M12.1 12h42.6"/>
<path id="line84" fill="none" stroke="#b4bcc1" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M18.1 6h30.6"/>
<path id="path86" fill="none" stroke="#eef6ff" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M54.2 61H11.9c-4.9 0-8.8-4-8.8-8.8V28.9c0-4.9 4-8.8 8.8-8.8h42.3c4.9 0 8.8 4 8.8 8.8v23.3c0 4.8-4 8.8-8.8 8.8z"/>
<path id="path88" fill="#3389ff" d="M54.7 23v14.1c0 2.7-2.2 4.9-4.9 4.9H16.2c-2.7 0-4.9-2.2-4.9-4.9V23h-2v14.1c0 3.8 3.1 6.9 6.9 6.9h33.6c3.8 0 6.9-3.1 6.9-6.9V23z" opacity=".2"/>
<circle id="circle122" cx="74.9" cy="11.1" r="11.1" fill="#3389ff"/>
<path id="line124" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M70 11h10" class="st19"/>
<path id="line126" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M75 16V6" class="st19"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

16
backend/src/Squidex/wwwroot/images/dashboard_feedback.svg

@ -0,0 +1,16 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 54.1 62">
<path id="path16" fill="#f7f6e7" d="M43.5 31.5s-2 10-18 16l11 7v-7l4.7.1 2.3-1.1 1-3z"/>
<path id="path134" fill="#efebbf" stroke="#2e3842" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M49.7 1H12.5C7.3 1 3 5.3 3 10.5v13.3C3 26.7 5.3 29 8.2 29H15v6l5.3-3.7c2.1-1.5 4.7-2.3 7.3-2.3h16c5.2 0 9.5-4.3 9.5-9.5V4.3C53 2.5 51.5 1 49.7 1z"/>
<g id="g140" transform="translate(-461 -444)">
<g id="g138">
<path id="path136" fill="#fcfbeb" d="M509.8 448c.7 0 1.2.5 1.2 1.1v13.8c0 3.7-3.2 6.6-7.1 6.6h-15.3c-2.9 0-5.7.8-8.1 2.4l-2 1.3v-1.7c0-1.1-.9-2-2-2H470c-1.7 0-3-1.2-3-2.8v-12.1c0-3.7 3.2-6.6 7.1-6.6h35.7m0-2h-35.7c-5 0-9.1 3.9-9.1 8.6v12.1c0 2.6 2.3 4.8 5 4.8h6.5v5.5l5.1-3.4c2-1.4 4.5-2.1 7-2.1h15.3c5 0 9.1-3.9 9.1-8.6v-13.8c0-1.7-1.4-3.1-3.2-3.1z"/>
</g>
</g>
<path id="line142" fill="none" stroke="#cec395" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M13.5 10h29" class="st22"/>
<path id="line144" fill="none" stroke="#cec395" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M13 16h18" class="st22"/>
<circle id="circle146" cx="11.5" cy="50.5" r="11.5" fill="#f9c553"/>
<circle id="circle148" cx="15.1" cy="48.1" r="1.5" fill="#fff" class="st14"/>
<circle id="circle150" cx="8" cy="48" r="1.5" fill="#fff" class="st14"/>
<path id="path152" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M6 53s6 5 12 0"/>
<path id="path154" fill="#b4bcc1" d="M43 32v11.8c0 2.3-1.9 4.2-4.2 4.2H36c-.6 0-1 .4-1 1v4.1l-3.7-2.6c-1.9-1.3-4-2.1-6.3-2.4v2c1.8.2 3.6.9 5.1 2l5.3 3.7c.2.1.4.2.6.2.2 0 .3 0 .5-.1.3-.2.5-.5.5-.9v-5h1.8c3.4 0 6.2-2.8 6.2-6.2V32z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

10
backend/src/Squidex/wwwroot/images/dashboard_schema.svg

@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 86 64">
<path id="path36" fill="#c7e0ff" stroke="#2e3842" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M54.2 63H11.8C5.9 63 1 58.1 1 52.2V28.8C1 22.9 5.9 18 11.8 18h42.4C60.1 18 65 22.9 65 28.8v23.4C65 58.1 60.1 63 54.2 63z"/>
<path id="line42" fill="none" stroke="#7d878e" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M12.1 12h42.6"/>
<path id="line84" fill="none" stroke="#b4bcc1" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M18.1 6h30.6"/>
<path id="path86" fill="none" stroke="#eef6ff" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M54.2 61H11.9c-4.9 0-8.8-4-8.8-8.8V28.9c0-4.9 4-8.8 8.8-8.8h42.3c4.9 0 8.8 4 8.8 8.8v23.3c0 4.8-4 8.8-8.8 8.8z"/>
<path id="path88" fill="#3389ff" d="M54.7 23v14.1c0 2.7-2.2 4.9-4.9 4.9H16.2c-2.7 0-4.9-2.2-4.9-4.9V23h-2v14.1c0 3.8 3.1 6.9 6.9 6.9h33.6c3.8 0 6.9-3.1 6.9-6.9V23z" opacity=".2"/>
<circle id="circle122" cx="74.9" cy="11.1" r="11.1" fill="#3389ff"/>
<path id="line124" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M70 11h10" class="st19"/>
<path id="line126" fill="none" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M75 16V6" class="st19"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

5
backend/src/Squidex/wwwroot/images/folder.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" x="0" y="0" version="1.1" xml:space="preserve" viewBox="0 0 38 29">
<path id="path382" fill="#c7e0ff" stroke="#5587c4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M1 7.1V4.6C1 2.6 2.6 1 4.6 1h8.9c1 0 1.9.4 2.6 1.1l4.1 4.1c.7.7 1.6 1.1 2.6 1.1h5.5c4.8 0 8.7 3.9 8.7 8.7v8.4c0 2-1.6 3.6-3.6 3.6H4.6c-2 0-3.6-1.6-3.6-3.6z"/>
<path id="path384" fill="none" stroke="#eef6ff" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M2 7.6V5c0-1.7 1.4-3.1 3.1-3.1h8.5c.9 0 1.4.4 2.1 1l3.8 3.8c.6.6 2.1 1.3 3 1.3h5.2c4.5 0 8.1 3.3 8.1 7.8v8c0 1.7-1.4 3.1-3.1 3.1H5.2c-1.7 0-3.1-1.4-3.1-3.1V7.6z" class="st11"/>
<path id="line386" fill="none" stroke="#eef6ff" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="2" d="M22 7.9H11" class="st11"/>
</svg>

After

Width:  |  Height:  |  Size: 913 B

8
backend/src/Squidex/wwwroot/images/logo.svg

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 494 111">
<path fill="#2d3964" d="M90.902 65.898h15.952c1.21 4.842 4.841 8.143 14.632 8.143 6.931 0 10.232-1.65 10.232-4.951 0-4.07-4.731-4.4-13.862-6.602-17.163-4.07-24.204-7.92-24.204-18.371 0-12.764 10.562-20.135 26.514-20.135 16.832 0 25.083 8.143 27.174 19.363h-15.952c-1.21-3.85-4.511-6.16-11.772-6.16-6.271 0-10.122 1.76-10.122 5.061 0 2.859 2.75 3.41 11.552 5.391C138.318 51.598 148 54.898 148 67c0 13.531-11.992 20.133-26.954 20.133-16.172 0-27.944-7.922-30.144-21.235zM213.235 25.303v75.439h-15.731v-23.07c-4.181 5.721-10.562 9.461-19.033 9.461-15.842 0-27.614-12.982-27.614-31.574 0-18.703 11.992-31.576 27.503-31.576 8.582 0 14.963 3.961 19.144 9.793v-8.473h15.731zm-15.731 30.256c0-9.902-6.711-16.613-15.513-16.613-8.801 0-15.292 6.711-15.292 16.613 0 10.01 6.601 16.611 15.292 16.611 8.802 0 15.513-6.602 15.513-16.611zM274.281 25.303v60.51h-15.732v-8.361c-4.4 6.16-10.342 9.682-18.703 9.682-13.312 0-21.562-9.131-21.562-23.873V25.303h15.732v34.545c0 7.922 2.971 12.322 11.111 12.322 8.691 0 13.422-5.17 13.422-15.072V25.303h15.732zM279.767 42.927H295.5v42.886h-15.732V42.927zM363.037 8.506v77.307h-15.733v-8.141c-4.18 5.721-10.561 9.461-19.031 9.461-15.732 0-27.615-12.982-27.615-31.574 0-18.703 11.992-31.576 27.504-31.576 8.582 0 14.963 3.961 19.143 9.793V8.506h15.732zm-15.733 47.053c0-9.902-6.711-16.613-15.512-16.613s-15.291 6.711-15.291 16.613c0 10.01 6.6 16.611 15.291 16.611 8.801 0 15.512-6.602 15.512-16.611zM430.254 58.309h-51.818c.881 12.541 9.791 20.133 22.004 20.133 8.361 0 15.291-3.521 18.922-10.562h9.461c-5.061 12.873-16.061 19.143-28.713 19.143-17.934 0-31.025-12.762-31.025-31.244 0-18.373 12.873-31.684 30.916-31.684 18.811-.001 31.022 14.3 30.253 34.214zm-51.377-8.582h41.475c-1.98-10.121-9.461-17.053-20.352-17.053-11.113 0-19.033 7.152-21.123 17.053z"/>
<path fill="#2d3964" d="M469.848 85.812l-16.723-24.094-16.502 24.094h-11.002l22.004-31.135-20.793-29.375h11.002l15.512 22.555 15.293-22.555h11.111l-20.904 29.484 22.004 31.025h-11.002z"/>
<path fill="#3389FF" d="M290.666 30.575h2.198a2.637 2.637 0 000-5.273h-10.461a2.637 2.637 0 00-2.636 2.637c0 .805.368 1.516.938 2l-.018.013a2.936 2.936 0 011.071 2.259c0 .9-.411 1.696-1.046 2.237l.018.031a2.622 2.622 0 00-.963 2.022 2.637 2.637 0 002.636 2.636h8.23a2.637 2.637 0 000-5.273h.21a1.65 1.65 0 01-.177-3.289zM21.267 80.827c-.473 0-.971-.031-1.494-.096-3.67-.459-6.484-2.862-7.531-6.428-1.109-3.789.053-8.048 2.826-10.359a2.696 2.696 0 113.451 4.142c-1.138.948-1.611 2.968-1.104 4.7.309 1.051 1.083 2.353 3.025 2.596 1.399.175 2.459-.063 3.246-.724 1.435-1.209 1.869-3.684 1.922-4.596V53.077a2.695 2.695 0 115.392 0v17.051l-.001.098c-.02.558-.297 5.538-3.803 8.525-1.165.991-3.089 2.076-5.929 2.076z"/>
<path fill="#3389FF" d="M31.435 99.967c-.473 0-.971-.031-1.494-.096-3.67-.46-6.486-2.862-7.531-6.428-1.109-3.789.051-8.048 2.826-10.358a2.696 2.696 0 113.451 4.141c-1.138.947-1.612 2.968-1.104 4.701.308 1.049 1.082 2.351 3.025 2.594 1.404.177 2.473-.065 3.259-.735 1.638-1.394 1.887-4.275 1.909-4.586V52.238a2.696 2.696 0 115.39 0v37.031c0 .032 0 .062-.002.097-.019.558-.296 5.538-3.803 8.525-1.162.991-3.086 2.076-5.926 2.076zM65.941 80.827c-2.84 0-4.764-1.085-5.928-2.076-3.507-2.987-3.784-7.968-3.803-8.525-.002-.032-.002-.064-.002-.098V53.077a2.694 2.694 0 115.39 0v16.991c.05.889.482 3.377 1.923 4.591.786.66 1.849.899 3.245.723 1.942-.242 2.718-1.544 3.025-2.595.509-1.732.034-3.752-1.104-4.7a2.695 2.695 0 113.451-4.142c2.774 2.312 3.936 6.57 2.826 10.359-1.045 3.565-3.861 5.969-7.53 6.428-.522.064-1.02.095-1.493.095z"/>
<path fill="#3389FF" d="M55.773 99.967c-2.84 0-4.764-1.085-5.928-2.076-3.507-2.987-3.783-7.968-3.803-8.525-.002-.034-.002-.064-.002-.097V52.238a2.696 2.696 0 115.39 0v36.967c.051.891.482 3.379 1.923 4.592.786.661 1.847.9 3.245.724 1.942-.243 2.718-1.545 3.025-2.594.509-1.733.034-3.754-1.104-4.701a2.696 2.696 0 113.452-4.141c2.774 2.311 3.935 6.569 2.825 10.358-1.045 3.565-3.861 5.968-7.53 6.428-.522.065-1.019.096-1.493.096z"/>
<path fill="#3389FF" d="M64.793 38.868c-.178-1.947 3.787-1.169 3.311-2.94C63.147 17.484 43.646 8.512 43.632 8.506c-.015.006-19.516 8.978-24.472 27.422-.478 1.771 3.487.993 3.31 2.94-.217 2.364-4.765 3.333-4.172 10.121.641 7.341 7.182 14.765 7.418 18.873h5.295v-.153a2.388 2.388 0 014.777 0v.153l5.39-2.695v-.107a2.435 2.435 0 114.87 0v.107l5.375 2.695v-.153c0-1.319 1.069-2.388 2.389-2.388s2.389 1.068 2.389 2.388v.153h5.391c.542-5.791 6.735-11.532 7.376-18.873.59-6.788-3.959-7.757-4.175-10.121z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

10
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs

@ -62,6 +62,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
Assert.Equal(type, result!.Type);
}
[Fact]
public async Task Should_skip_moved_event()
{
var envelope = Envelope.Create<AppEvent>(new AssetMoved());
var result = await sut.CreateEnrichedEventAsync(envelope);
Assert.Null(result);
}
[Fact]
public void Should_not_trigger_precheck_when_event_type_not_correct()
{

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
file = new AssetFile("my-image.png", "image/png", 1024, () => stream);
asset = new AssetGrain(Store, tagService, A.Fake<IActivationLimit>(), A.Dummy<ISemanticLog>());
asset = new AssetGrain(Store, tagService, assetQuery, A.Fake<IActivationLimit>(), A.Dummy<ISemanticLog>());
asset.ActivateAsync(Id).Wait();
A.CallTo(() => contextProvider.Context)

171
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderGrainTests.cs

@ -0,0 +1,171 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetFolderGrainTests : HandlerTestBase<AssetFolderState>
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IActivationLimit limit = A.Fake<IActivationLimit>();
private readonly Guid parentId = Guid.NewGuid();
private readonly Guid assetFolderId = Guid.NewGuid();
private readonly AssetFolderGrain sut;
protected override Guid Id
{
get { return assetFolderId; }
}
public AssetFolderGrainTests()
{
A.CallTo(() => assetQuery.FindAssetFolderAsync(parentId))
.Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() });
sut = new AssetFolderGrain(Store, assetQuery, limit, A.Dummy<ISemanticLog>());
sut.ActivateAsync(Id).Wait();
}
[Fact]
public void Should_set_limit()
{
A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5)))
.MustHaveHappened();
}
[Fact]
public async Task Command_should_throw_exception_if_rule_is_deleted()
{
await ExecuteCreateAsync();
await ExecuteDeleteAsync();
await Assert.ThrowsAsync<DomainException>(ExecuteUpdateAsync);
}
[Fact]
public async Task Create_should_create_events_and_update_state()
{
var command = new CreateAssetFolder { FolderName = "New Name" };
var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command));
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.FolderName, sut.Snapshot.FolderName);
LastEvents
.ShouldHaveSameEvents(
CreateAssetFolderEvent(new AssetFolderCreated
{
FolderName = command.FolderName
})
);
}
[Fact]
public async Task Update_should_create_events_and_update_state()
{
var command = new RenameAssetFolder { FolderName = "New Name" };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command));
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(command.FolderName, sut.Snapshot.FolderName);
LastEvents
.ShouldHaveSameEvents(
CreateAssetFolderEvent(new AssetFolderRenamed
{
FolderName = command.FolderName
})
);
}
[Fact]
public async Task Move_should_create_events_and_update_state()
{
var command = new MoveAssetFolder { ParentId = parentId };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command));
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(parentId, sut.Snapshot.ParentId);
LastEvents
.ShouldHaveSameEvents(
CreateAssetFolderEvent(new AssetFolderMoved { ParentId = parentId })
);
}
[Fact]
public async Task Delete_should_create_events_with_total_file_size()
{
var command = new DeleteAssetFolder();
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command));
result.ShouldBeEquivalent(new EntitySavedResult(1));
Assert.True(sut.Snapshot.IsDeleted);
LastEvents
.ShouldHaveSameEvents(
CreateAssetFolderEvent(new AssetFolderDeleted())
);
}
private Task ExecuteCreateAsync()
{
return sut.ExecuteAsync(CreateAssetFolderCommand(new CreateAssetFolder { FolderName = "My Folder" }));
}
private Task ExecuteUpdateAsync()
{
return sut.ExecuteAsync(CreateAssetFolderCommand(new RenameAssetFolder { FolderName = "My Folder" }));
}
private Task ExecuteDeleteAsync()
{
return sut.ExecuteAsync(CreateAssetFolderCommand(new DeleteAssetFolder()));
}
protected T CreateAssetFolderEvent<T>(T @event) where T : AssetFolderEvent
{
@event.AssetFolderId = assetFolderId;
return CreateEvent(@event);
}
protected T CreateAssetFolderCommand<T>(T command) where T : AssetFolderCommand
{
command.AssetFolderId = assetFolderId;
return CreateCommand(command);
}
}
}

53
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs

@ -27,11 +27,12 @@ namespace Squidex.Domain.Apps.Entities.Assets
public class AssetGrainTests : HandlerTestBase<AssetState>
{
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IActivationLimit limit = A.Fake<IActivationLimit>();
private readonly ImageInfo image = new ImageInfo(2048, 2048);
private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream());
private readonly Guid parentId = Guid.NewGuid();
private readonly Guid assetId = Guid.NewGuid();
private readonly string fileHash = Guid.NewGuid().ToString();
private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream());
private readonly AssetGrain sut;
protected override Guid Id
@ -41,10 +42,13 @@ namespace Squidex.Domain.Apps.Entities.Assets
public AssetGrainTests()
{
A.CallTo(() => assetQuery.FindAssetFolderAsync(parentId))
.Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() });
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored))
.Returns(new Dictionary<string, string>());
sut = new AssetGrain(Store, tagService, limit, A.Dummy<ISemanticLog>());
sut = new AssetGrain(Store, tagService, assetQuery, limit, A.Dummy<ISemanticLog>());
sut.ActivateAsync(Id).Wait();
}
@ -56,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Command_should_throw_exception_if_rule_is_deleted()
public async Task Command_should_throw_exception_if_asset_is_deleted()
{
await ExecuteCreateAsync();
await ExecuteDeleteAsync();
@ -65,16 +69,16 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Create_should_create_events()
public async Task Create_should_create_events_and_update_state()
{
var command = new CreateAsset { File = file, ImageInfo = image, FileHash = fileHash, Tags = new HashSet<string>() };
var command = new CreateAsset { File = file, ImageInfo = image, FileHash = "NewHash", Tags = new HashSet<string>() };
var result = await sut.ExecuteAsync(CreateAssetCommand(command));
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(0, sut.Snapshot.FileVersion);
Assert.Equal(fileHash, sut.Snapshot.FileHash);
Assert.Equal(command.FileHash, sut.Snapshot.FileHash);
LastEvents
.ShouldHaveSameEvents(
@ -82,8 +86,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
IsImage = true,
FileName = file.FileName,
FileHash = fileHash,
FileSize = file.FileSize,
FileHash = command.FileHash,
FileVersion = 0,
MimeType = file.MimeType,
PixelWidth = image.PixelWidth,
@ -95,9 +99,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task Update_should_create_events()
public async Task Update_should_create_events_and_update_state()
{
var command = new UpdateAsset { File = file, ImageInfo = image, FileHash = fileHash };
var command = new UpdateAsset { File = file, ImageInfo = image, FileHash = "NewHash" };
await ExecuteCreateAsync();
@ -106,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(1, sut.Snapshot.FileVersion);
Assert.Equal(fileHash, sut.Snapshot.FileHash);
Assert.Equal(command.FileHash, sut.Snapshot.FileHash);
LastEvents
.ShouldHaveSameEvents(
@ -114,7 +118,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
IsImage = true,
FileSize = file.FileSize,
FileHash = fileHash,
FileHash = command.FileHash,
FileVersion = 1,
MimeType = file.MimeType,
PixelWidth = image.PixelWidth,
@ -124,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task AnnotateName_should_create_events()
public async Task AnnotateName_should_create_events_and_update_state()
{
var command = new AnnotateAsset { FileName = "My New Image.png" };
@ -143,7 +147,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task AnnotateSlug_should_create_events()
public async Task AnnotateSlug_should_create_events_and_update_state()
{
var command = new AnnotateAsset { Slug = "my-new-image.png" };
@ -162,7 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
}
[Fact]
public async Task AnnotateTag_should_create_events()
public async Task AnnotateTag_should_create_events_and_update_state()
{
var command = new AnnotateAsset { Tags = new HashSet<string>() };
@ -178,6 +182,25 @@ namespace Squidex.Domain.Apps.Entities.Assets
);
}
[Fact]
public async Task Move_should_create_events_and_update_state()
{
var command = new MoveAsset { ParentId = parentId };
await ExecuteCreateAsync();
var result = await sut.ExecuteAsync(CreateAssetCommand(command));
result.ShouldBeEquivalent(sut.Snapshot);
Assert.Equal(parentId, sut.Snapshot.ParentId);
LastEvents
.ShouldHaveSameEvents(
CreateAssetEvent(new AssetMoved { ParentId = parentId })
);
}
[Fact]
public async Task Delete_should_create_events_with_total_file_size()
{

167
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetFolderTests.cs

@ -0,0 +1,167 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Validation;
using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets.Guards
{
public class GuardAssetFolderTests
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
[Fact]
public async Task CanCreate_should_throw_exception_when_folder_name_not_defined()
{
var command = new CreateAssetFolder();
A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId))
.Returns(new List<IAssetFolderEntity>());
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanCreate(command, assetQuery),
new ValidationError("Folder name is required.", "FolderName"));
}
[Fact]
public async Task CanCreate_should_throw_exception_when_folder_not_found()
{
var command = new CreateAssetFolder { FolderName = "My Folder", ParentId = Guid.NewGuid() };
A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId))
.Returns(new List<IAssetFolderEntity>());
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanCreate(command, assetQuery),
new ValidationError("Asset folder does not exist.", "ParentId"));
}
[Fact]
public async Task CanCreate_should_not_throw_exception_when_folder_found()
{
var command = new CreateAssetFolder { FolderName = "My Folder", ParentId = Guid.NewGuid() };
A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId))
.Returns(new List<IAssetFolderEntity> { CreateFolder() });
await GuardAssetFolder.CanCreate(command, assetQuery);
}
[Fact]
public async Task CanCreate_should_not_throw_exception_when_added_to_root()
{
var command = new CreateAssetFolder { FolderName = "My Folder" };
await GuardAssetFolder.CanCreate(command, assetQuery);
}
[Fact]
public async Task CanMove_should_throw_exception_when_adding_to_its_own_child()
{
var id = Guid.NewGuid();
var command = new MoveAssetFolder { ParentId = Guid.NewGuid() };
A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId))
.Returns(new List<IAssetFolderEntity>
{
CreateFolder(id),
CreateFolder(command.ParentId)
});
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanMove(command, assetQuery, id, Guid.NewGuid()),
new ValidationError("Cannot add folder to its own child.", "ParentId"));
}
[Fact]
public async Task CanMove_should_throw_exception_when_folder_not_found()
{
var command = new MoveAssetFolder { ParentId = Guid.NewGuid() };
A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId))
.Returns(new List<IAssetFolderEntity>());
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), Guid.NewGuid()),
new ValidationError("Asset folder does not exist.", "ParentId"));
}
[Fact]
public async Task CanMove_should_throw_exception_when_folder_has_not_changed()
{
var command = new MoveAssetFolder { ParentId = Guid.NewGuid() };
await ValidationAssert.ThrowsAsync(() => GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), command.ParentId),
new ValidationError("Asset folder is already part of this folder.", "ParentId"));
}
[Fact]
public async Task CanMove_should_not_throw_exception_when_folder_found()
{
var command = new MoveAssetFolder { ParentId = Guid.NewGuid() };
A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId))
.Returns(new List<IAssetFolderEntity> { CreateFolder() });
await GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), Guid.NewGuid());
}
[Fact]
public async Task CanMove_should_not_throw_exception_when_added_to_root()
{
var command = new MoveAssetFolder();
await GuardAssetFolder.CanMove(command, assetQuery, Guid.NewGuid(), Guid.NewGuid());
}
[Fact]
public void CanRename_should_throw_exception_if_folder_name_is_empty()
{
var command = new RenameAssetFolder();
ValidationAssert.Throws(() => GuardAssetFolder.CanRename(command, "My Folder"),
new ValidationError("Folder name is required.", "FolderName"));
}
[Fact]
public void CanRename_should_throw_exception_if_names_are_the_same()
{
var command = new RenameAssetFolder { FolderName = "My Folder" };
ValidationAssert.Throws(() => GuardAssetFolder.CanRename(command, "My Folder"),
new ValidationError("Asset folder has already this name.", "FolderName"));
}
[Fact]
public void CanRename_should_not_throw_exception_if_names_are_different()
{
var command = new RenameAssetFolder { FolderName = "New Folder Name" };
GuardAssetFolder.CanRename(command, "My Folder");
}
[Fact]
public void CanDelete_should_not_throw_exception()
{
var command = new DeleteAssetFolder();
GuardAssetFolder.CanDelete(command);
}
private IAssetFolderEntity CreateFolder(Guid id = default)
{
var assetFolder = A.Fake<IAssetFolderEntity>();
A.CallTo(() => assetFolder.Id).Returns(id);
return assetFolder;
}
}
}

94
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Guards/GuardAssetTests.cs

@ -5,6 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Validation;
@ -14,6 +18,79 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
{
public class GuardAssetTests
{
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
[Fact]
public async Task CanCreate_should_not_throw_exception_when_folder_found()
{
var command = new CreateAsset { ParentId = Guid.NewGuid() };
A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId))
.Returns(new List<IAssetFolderEntity> { CreateFolder() });
await GuardAsset.CanCreate(command, assetQuery);
}
[Fact]
public async Task CanCreate_should_throw_exception_when_folder_not_found()
{
var command = new CreateAsset { ParentId = Guid.NewGuid() };
A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId))
.Returns(new List<IAssetFolderEntity>());
await ValidationAssert.ThrowsAsync(() => GuardAsset.CanCreate(command, assetQuery),
new ValidationError("Asset folder does not exist.", "ParentId"));
}
[Fact]
public async Task CanCreate_should_not_throw_exception_when_added_to_root()
{
var command = new CreateAsset();
await GuardAsset.CanCreate(command, assetQuery);
}
[Fact]
public async Task CanMove_should_throw_exception_when_folder_not_found()
{
var command = new MoveAsset { ParentId = Guid.NewGuid() };
A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId))
.Returns(new List<IAssetFolderEntity>());
await ValidationAssert.ThrowsAsync(() => GuardAsset.CanMove(command, assetQuery, Guid.NewGuid()),
new ValidationError("Asset folder does not exist.", "ParentId"));
}
[Fact]
public async Task CanMove_should_throw_exception_when_folder_has_not_changed()
{
var command = new MoveAsset { ParentId = Guid.NewGuid() };
await ValidationAssert.ThrowsAsync(() => GuardAsset.CanMove(command, assetQuery, command.ParentId),
new ValidationError("Asset is already part of this folder.", "ParentId"));
}
[Fact]
public async Task CanMove_should_not_throw_exception_when_folder_found()
{
var command = new MoveAsset { ParentId = Guid.NewGuid() };
A.CallTo(() => assetQuery.FindAssetFolderAsync(command.ParentId))
.Returns(new List<IAssetFolderEntity> { CreateFolder() });
await GuardAsset.CanMove(command, assetQuery, Guid.NewGuid());
}
[Fact]
public async Task CanMove_should_not_throw_exception_when_added_to_root()
{
var command = new MoveAsset();
await GuardAsset.CanMove(command, assetQuery, Guid.NewGuid());
}
[Fact]
public void CanAnnotate_should_throw_exception_if_nothing_defined()
{
@ -31,14 +108,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
GuardAsset.CanAnnotate(command, "asset-name", "asset-slug");
}
[Fact]
public void CanCreate_should_not_throw_exception()
{
var command = new CreateAsset();
GuardAsset.CanCreate(command);
}
[Fact]
public void CanUpdate_should_not_throw_exception()
{
@ -54,5 +123,14 @@ namespace Squidex.Domain.Apps.Entities.Assets.Guards
GuardAsset.CanDelete(command);
}
private IAssetFolderEntity CreateFolder(Guid id = default)
{
var assetFolder = A.Fake<IAssetFolderEntity>();
A.CallTo(() => assetFolder.Id).Returns(id);
return assetFolder;
}
}
}

130
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs

@ -22,6 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
{
private readonly IAssetEnricher assetEnricher = A.Fake<IAssetEnricher>();
private readonly IAssetRepository assetRepository = A.Fake<IAssetRepository>();
private readonly IAssetFolderRepository assetFolderRepository = A.Fake<IAssetFolderRepository>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly Context requestContext;
private readonly AssetQueryParser queryParser = A.Fake<AssetQueryParser>();
@ -34,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
A.CallTo(() => queryParser.ParseQuery(requestContext, A<Q>.Ignored))
.Returns(new ClrQuery());
sut = new AssetQueryService(assetEnricher, assetRepository, queryParser);
sut = new AssetQueryService(assetEnricher, assetRepository, assetFolderRepository, queryParser);
}
[Fact]
@ -44,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
var enriched = new AssetEntity();
A.CallTo(() => assetRepository.FindAssetAsync(found.Id, false))
A.CallTo(() => assetRepository.FindAssetAsync(found.Id))
.Returns(found);
A.CallTo(() => assetEnricher.EnrichAsync(found, requestContext))
@ -90,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2), requestContext))
.Returns(new List<IEnrichedAssetEntity> { enriched1, enriched2 });
var result = await sut.QueryAsync(requestContext, Q.Empty.WithIds(ids));
var result = await sut.QueryAsync(requestContext, null, Q.Empty.WithIds(ids));
Assert.Equal(8, result.Total);
@ -106,17 +107,136 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries
var enriched1 = new AssetEntity();
var enriched2 = new AssetEntity();
A.CallTo(() => assetRepository.QueryAsync(appId.Id, A<ClrQuery>.Ignored))
var parentId = Guid.NewGuid();
A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, A<ClrQuery>.Ignored))
.Returns(ResultList.CreateFrom(8, found1, found2));
A.CallTo(() => assetEnricher.EnrichAsync(A<IEnumerable<IAssetEntity>>.That.IsSameSequenceAs(found1, found2), requestContext))
.Returns(new List<IEnrichedAssetEntity> { enriched1, enriched2 });
var result = await sut.QueryAsync(requestContext, Q.Empty);
var result = await sut.QueryAsync(requestContext, parentId, Q.Empty);
Assert.Equal(8, result.Total);
Assert.Equal(new[] { enriched1, enriched2 }, result.ToArray());
}
[Fact]
public async Task Should_load_assets_folders_from_repository()
{
var parentId = Guid.NewGuid();
var assetFolders = ResultList.CreateFrom<IAssetFolderEntity>(10);
A.CallTo(() => assetFolderRepository.QueryAsync(appId.Id, parentId))
.Returns(assetFolders);
var result = await sut.QueryAssetFoldersAsync(requestContext, parentId);
Assert.Same(assetFolders, result);
}
[Fact]
public async Task Should_resolve_folder_path_from_root()
{
var folderId1 = Guid.NewGuid();
var folder1 = CreateFolder(folderId1);
A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId1))
.Returns(folder1);
var result = await sut.FindAssetFolderAsync(folderId1);
Assert.Equal(result, new[] { folder1 });
}
[Fact]
public async Task Should_resolve_folder_path_from_child()
{
var folderId1 = Guid.NewGuid();
var folderId2 = Guid.NewGuid();
var folderId3 = Guid.NewGuid();
var folder1 = CreateFolder(folderId1);
var folder2 = CreateFolder(folderId2, folderId1);
var folder3 = CreateFolder(folderId3, folderId2);
A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId1))
.Returns(folder1);
A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId2))
.Returns(folder2);
A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId3))
.Returns(folder3);
var result = await sut.FindAssetFolderAsync(folderId3);
Assert.Equal(result, new[] { folder1, folder2, folder3 });
}
[Fact]
public async Task Should_not_resolve_folder_path_if_root_not_found()
{
var folderId1 = Guid.NewGuid();
A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId1))
.Returns(Task.FromResult<IAssetFolderEntity?>(null));
var result = await sut.FindAssetFolderAsync(folderId1);
Assert.Empty(result);
}
[Fact]
public async Task Should_not_resolve_folder_path_if_parent_of_child_not_found()
{
var folderId1 = Guid.NewGuid();
var folderId2 = Guid.NewGuid();
var folder1 = CreateFolder(folderId1);
var folder2 = CreateFolder(folderId2, folderId1);
A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId1))
.Returns(Task.FromResult<IAssetFolderEntity?>(null));
A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId2))
.Returns(folder2);
var result = await sut.FindAssetFolderAsync(folderId2);
Assert.Empty(result);
}
[Fact]
public async Task Should_not_resolve_folder_path_if_recursion_detected()
{
var folderId1 = Guid.NewGuid();
var folderId2 = Guid.NewGuid();
var folder1 = CreateFolder(folderId1, folderId2);
var folder2 = CreateFolder(folderId2, folderId1);
A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId1))
.Returns(Task.FromResult<IAssetFolderEntity?>(null));
A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(folderId2))
.Returns(folder2);
var result = await sut.FindAssetFolderAsync(folderId2);
Assert.Empty(result);
}
private IAssetFolderEntity CreateFolder(Guid id, Guid parentId = default)
{
var assetFolder = A.Fake<IAssetFolderEntity>();
A.CallTo(() => assetFolder.Id).Returns(id);
A.CallTo(() => assetFolder.ParentId).Returns(parentId);
return assetFolder;
}
}
}

14
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs

@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var asset = CreateAsset(Guid.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query")))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query")))
.Returns(ResultList.CreateFrom(0, asset));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -228,7 +228,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var asset = CreateAsset(Guid.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query")))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.Matches(x => x.ODataQuery == "?$top=30&$skip=5&$filter=my-query")))
.Returns(ResultList.CreateFrom(10, asset));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -284,7 +284,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}".Replace("<ID>", assetId.ToString());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId)))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId)))
.Returns(ResultList.CreateFrom<IEnrichedAssetEntity>(1));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -331,7 +331,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}".Replace("<ID>", assetId.ToString());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId)))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId)))
.Returns(ResultList.CreateFrom(1, asset));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -1131,7 +1131,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), MatchId(contentId)))
.Returns(ResultList.CreateFrom(1, content));
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), A<Q>.Ignored))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.Ignored))
.Returns(ResultList.CreateFrom(0, assetRef));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query });
@ -1184,10 +1184,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}".Replace("<ID>", assetId2.ToString());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId1)))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId1)))
.Returns(ResultList.CreateFrom(0, asset1));
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), MatchIdQuery(assetId2)))
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, MatchIdQuery(assetId2)))
.Returns(ResultList.CreateFrom(0, asset2));
var result = await sut.QueryAsync(requestContext, new GraphQLQuery { Query = query1 }, new GraphQLQuery { Query = query2 });

12
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherAssetsTests.cs

@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
new[] { document2.Id, image2.Id })
};
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => x.IsNoAssetEnrichment()), A<Q>.That.Matches(x => x.Ids.Count == 4)))
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => x.IsNoAssetEnrichment()), null, A<Q>.That.Matches(x => x.Ids.Count == 4)))
.Returns(ResultList.CreateFrom(4, image1, image2, document1, document2));
var enriched = await sut.EnrichAsync(source, requestContext);
@ -122,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
new[] { document2.Id, image2.Id })
};
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => x.IsNoAssetEnrichment()), A<Q>.That.Matches(x => x.Ids.Count == 4)))
A.CallTo(() => assetQuery.QueryAsync(A<Context>.That.Matches(x => x.IsNoAssetEnrichment()), null, A<Q>.That.Matches(x => x.Ids.Count == 4)))
.Returns(ResultList.CreateFrom(4, image1, image2, document1, document2));
var enriched = await sut.EnrichAsync(source, requestContext);
@ -160,7 +160,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.Null(enriched.ElementAt(0).ReferenceData);
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, A<Q>.Ignored))
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored))
.MustNotHaveHappened();
}
@ -176,7 +176,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.Null(enriched.ElementAt(0).ReferenceData);
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, A<Q>.Ignored))
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored))
.MustNotHaveHappened();
}
@ -192,7 +192,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
Assert.NotNull(enriched.ElementAt(0).ReferenceData);
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, A<Q>.Ignored))
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored))
.MustNotHaveHappened();
}
@ -203,7 +203,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await sut.EnrichAsync(source, requestContext);
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, A<Q>.Ignored))
A.CallTo(() => assetQuery.QueryAsync(A<Context>.Ignored, null, A<Q>.Ignored))
.MustNotHaveHappened();
}

25
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs

@ -15,7 +15,6 @@ using Squidex.Domain.Apps.Entities.Rules.State;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Collections;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
@ -37,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
public sealed class TestAction : RuleAction
{
public Uri Url { get; set; }
public int Value { get; set; }
}
public RuleGrainTests()
@ -209,30 +208,16 @@ namespace Squidex.Domain.Apps.Entities.Rules
private static CreateRule MakeCreateCommand()
{
var newTrigger = new ContentChangedTriggerV2
{
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
};
var newAction = new TestAction
{
Url = new Uri("https://squidex.io/v2")
};
var newTrigger = new ManualTrigger();
var newAction = new TestAction { Value = 123 };
return new CreateRule { Trigger = newTrigger, Action = newAction };
}
private static UpdateRule MakeUpdateCommand()
{
var newTrigger = new ContentChangedTriggerV2
{
Schemas = ReadOnlyCollection.Empty<ContentChangedTriggerSchemaV2>()
};
var newAction = new TestAction
{
Url = new Uri("https://squidex.io/v2")
};
var newTrigger = new ManualTrigger();
var newAction = new TestAction { Value = 123 };
return new UpdateRule { Trigger = newTrigger, Action = newAction, Name = "NewName" };
}

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

Loading…
Cancel
Save