Browse Source

Merge branch 'release/4.x'

# Conflicts:
#	backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs
#	backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs
#	backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs
#	backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs
#	backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
#	backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs
#	backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs
#	backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs
#	backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs
#	backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs
#	backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs
#	backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs
#	backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
#	backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
#	backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs
#	backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs
#	backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
#	backend/src/Squidex.Web/Services/UrlGenerator.cs
#	backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
#	backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
#	backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs
#	backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs
pull/590/head
Sebastian 5 years ago
parent
commit
4c6b6bab61
  1. 47
      backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs
  2. 42
      backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs
  3. 87
      backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs
  4. 21
      backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs
  5. 29
      backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs
  6. 10
      backend/i18n/frontend_en.json
  7. 10
      backend/i18n/frontend_it.json
  8. 10
      backend/i18n/frontend_nl.json
  9. 2
      backend/i18n/source/backend_en.json
  10. 10
      backend/i18n/source/frontend_en.json
  11. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs
  12. 6
      backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs
  13. 4
      backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs
  14. 107
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs
  15. 23
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs
  16. 25
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs
  17. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  18. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs
  19. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
  20. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs
  21. 32
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Fields.cs
  22. 11
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs
  23. 13
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  24. 32
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs
  25. 11
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs
  26. 9
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  27. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs
  28. 20
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs
  29. 47
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs
  30. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs
  31. 2
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppSettingsSearchSource.cs
  32. 11
      backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs
  33. 12
      backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs
  34. 36
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs
  35. 3
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs
  36. 1
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs
  37. 4
      backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs
  38. 9
      backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs
  39. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs
  40. 21
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  41. 6
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  42. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResolvers.cs
  43. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ResolveAssets.cs
  44. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  45. 2
      backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs
  46. 17
      backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs
  47. 6
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs
  48. 48
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  49. 15
      backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs
  50. 2
      backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs
  51. 2
      backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs
  52. 8
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs
  53. 2
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs
  54. 2
      backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs
  55. 6
      backend/src/Squidex.Shared/Texts.it.resx
  56. 6
      backend/src/Squidex.Shared/Texts.nl.resx
  57. 6
      backend/src/Squidex.Shared/Texts.resx
  58. 14
      backend/src/Squidex.Web/Services/UrlGenerator.cs
  59. 8
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs
  60. 5
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  61. 5
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  62. 2
      backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs
  63. 10
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs
  64. 10
      backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs
  65. 2
      backend/src/Squidex/Config/Orleans/OrleansServices.cs
  66. 28
      backend/src/Squidex/wwwroot/scripts/context-editor.html
  67. 4
      backend/src/Squidex/wwwroot/scripts/editor-combined.html
  68. 47
      backend/src/Squidex/wwwroot/scripts/editor-context.html
  69. 3
      backend/src/Squidex/wwwroot/scripts/editor-json-schema.html
  70. 11
      backend/src/Squidex/wwwroot/scripts/editor-log.html
  71. 146
      backend/src/Squidex/wwwroot/scripts/editor-sdk.js
  72. 4
      backend/src/Squidex/wwwroot/scripts/editor-simple.html
  73. 49
      backend/src/Squidex/wwwroot/scripts/sidebar-content.html
  74. 49
      backend/src/Squidex/wwwroot/scripts/sidebar-context.html
  75. 118
      backend/src/Squidex/wwwroot/scripts/sidebar-search.html
  76. 4
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs
  77. 98
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs
  78. 2
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs
  79. 18
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
  80. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs
  81. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs
  82. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  83. 30
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs
  84. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs
  85. 82
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs
  86. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs
  87. 28
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLQueriesTests.cs
  88. 7
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestAsset.cs
  89. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs
  90. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs
  91. 13
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs
  92. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs
  93. 5
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs
  94. 60
      backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs
  95. 1
      frontend/app/features/content/declarations.ts
  96. 15
      frontend/app/features/content/module.ts
  97. 10
      frontend/app/features/content/pages/comments/comments-page.component.html
  98. 11
      frontend/app/features/content/pages/content/content-field.component.ts
  99. 9
      frontend/app/features/content/pages/content/content-history-page.component.html
  100. 7
      frontend/app/features/content/pages/content/content-page.component.html

47
backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs

@ -12,11 +12,10 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Extensions.Actions.Comment
{
public sealed class CommentActionHandler : RuleActionHandler<CommentAction, CommentJob>
public sealed class CommentActionHandler : RuleActionHandler<CommentAction, CreateComment>
{
private const string Description = "Send a Comment";
private readonly ICommandBus commandBus;
@ -29,56 +28,48 @@ namespace Squidex.Extensions.Actions.Comment
this.commandBus = commandBus;
}
protected override async Task<(string Description, CommentJob Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action)
protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action)
{
if (@event is EnrichedContentEvent contentEvent)
{
var text = await FormatAsync(action.Text, @event);
var ruleJob = new CreateComment
{
AppId = contentEvent.AppId,
};
var actor = contentEvent.Actor;
ruleJob.Text = await FormatAsync(action.Text, @event);
if (!string.IsNullOrEmpty(action.Client))
{
actor = new RefToken(RefTokenType.Client, action.Client);
ruleJob.Actor = new RefToken(RefTokenType.Client, action.Client);
}
var ruleJob = new CommentJob
else
{
AppId = contentEvent.AppId,
Actor = actor,
CommentsId = contentEvent.Id.ToString(),
Text = text
};
ruleJob.Actor = contentEvent.Actor;
}
ruleJob.CommentsId = contentEvent.Id.ToString();
return (Description, ruleJob);
}
return ("Ignore", new CommentJob());
return ("Ignore", new CreateComment());
}
protected override async Task<Result> ExecuteJobAsync(CommentJob job, CancellationToken ct = default)
protected override async Task<Result> ExecuteJobAsync(CreateComment job, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(job.CommentsId))
if (job.CommentsId == default)
{
return Result.Ignored();
}
var command = SimpleMapper.Map(job, new CreateComment());
var command = job;
command.FromRule = true;
await commandBus.PublishAsync(command);
return Result.Success($"Commented: {job.Text}");
}
}
public sealed class CommentJob
{
public NamedId<DomainId> AppId { get; set; }
public RefToken Actor { get; set; }
public string CommentsId { get; set; }
public string Text { get; set; }
}
}

42
backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs

@ -0,0 +1,42 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Infrastructure.Validation;
namespace Squidex.Extensions.Actions.CreateContent
{
[RuleAction(
Title = "CreateContent",
IconImage = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 28 28'><path d='M21.875 28H6.125A6.087 6.087 0 010 21.875V6.125A6.087 6.087 0 016.125 0h15.75A6.087 6.087 0 0128 6.125v15.75A6.088 6.088 0 0121.875 28zM6.125 1.75A4.333 4.333 0 001.75 6.125v15.75a4.333 4.333 0 004.375 4.375h15.75a4.333 4.333 0 004.375-4.375V6.125a4.333 4.333 0 00-4.375-4.375H6.125z'/><path d='M13.125 12.25H7.35c-1.575 0-2.888-1.313-2.888-2.888V7.349c0-1.575 1.313-2.888 2.888-2.888h5.775c1.575 0 2.887 1.313 2.887 2.888v2.013c0 1.575-1.312 2.888-2.887 2.888zM7.35 6.212c-.613 0-1.138.525-1.138 1.138v2.012A1.16 1.16 0 007.35 10.5h5.775a1.16 1.16 0 001.138-1.138V7.349a1.16 1.16 0 00-1.138-1.138H7.35zM22.662 16.713H5.337c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h17.237c.525 0 .875.35.875.875s-.35.875-.787.875zM15.138 21.262h-9.8c-.525 0-.875-.35-.875-.875s.35-.875.875-.875h9.713c.525 0 .875.35.875.875s-.35.875-.787.875z'/></svg>",
IconColor = "#3389ff",
Display = "Create content",
Description = "Create a a new content item for any schema.")]
public sealed class CreateContentAction : RuleAction
{
[LocalizedRequired]
[Display(Name = "Data", Description = "The content data.")]
[DataType(DataType.MultilineText)]
[Formattable]
public string Data { get; set; }
[LocalizedRequired]
[Display(Name = "Schema", Description = "The name of the schema.")]
[DataType(DataType.Text)]
public string Schema { get; set; }
[Display(Name = "Client", Description = "An optional client name.")]
[DataType(DataType.Text)]
public string Client { get; set; }
[Display(Name = "Publish", Description = "Publish the content.")]
[DataType(DataType.Text)]
public bool Publish { get; set; }
}
}

87
backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs

@ -0,0 +1,87 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Entities;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Json;
using Command = Squidex.Domain.Apps.Entities.Contents.Commands.CreateContent;
namespace Squidex.Extensions.Actions.CreateContent
{
public sealed class CreateContentActionHandler : RuleActionHandler<CreateContentAction, Command>
{
private const string Description = "Create a content";
private readonly ICommandBus commandBus;
private readonly IAppProvider appProvider;
private readonly IJsonSerializer jsonSerializer;
public CreateContentActionHandler(RuleEventFormatter formatter, IAppProvider appProvider, ICommandBus commandBus, IJsonSerializer jsonSerializer)
: base(formatter)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(commandBus, nameof(commandBus));
Guard.NotNull(jsonSerializer, nameof(jsonSerializer));
this.appProvider = appProvider;
this.commandBus = commandBus;
this.jsonSerializer = jsonSerializer;
}
protected override async Task<(string Description, Command Data)> CreateJobAsync(EnrichedEvent @event, CreateContentAction action)
{
var ruleJob = new Command
{
AppId = @event.AppId,
};
var schema = await appProvider.GetSchemaAsync(@event.AppId.Id, action.Schema, true);
if (schema == null)
{
throw new InvalidOperationException($"Cannot find schema '{action.Schema}'");
}
ruleJob.SchemaId = schema.NamedId();
var json = await FormatAsync(action.Data, @event);
ruleJob.Data = jsonSerializer.Deserialize<NamedContentData>(json);
if (!string.IsNullOrEmpty(action.Client))
{
ruleJob.Actor = new RefToken(RefTokenType.Client, action.Client);
}
else if (@event is EnrichedUserEventBase userEvent)
{
ruleJob.Actor = userEvent.Actor;
}
ruleJob.Publish = action.Publish;
return (Description, ruleJob);
}
protected override async Task<Result> ExecuteJobAsync(Command job, CancellationToken ct = default)
{
var command = job;
command.FromRule = true;
await commandBus.PublishAsync(command);
return Result.Success($"Created to: {job.SchemaId.Name}");
}
}
}

21
backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs

@ -0,0 +1,21 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.Plugins;
namespace Squidex.Extensions.Actions.CreateContent
{
public sealed class CreateContentPlugin : IPlugin
{
public void ConfigureServices(IServiceCollection services, IConfiguration config)
{
services.AddRuleAction<CreateContentAction, CreateContentActionHandler>();
}
}
}

29
backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs

@ -13,12 +13,11 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared.Users;
namespace Squidex.Extensions.Actions.Notification
{
public sealed class NotificationActionHandler : RuleActionHandler<NotificationAction, NotificationJob>
public sealed class NotificationActionHandler : RuleActionHandler<NotificationAction, CreateComment>
{
private const string Description = "Send a Notification";
private static readonly NamedId<DomainId> NoApp = NamedId.Of(DomainId.Empty, "none");
@ -36,7 +35,7 @@ namespace Squidex.Extensions.Actions.Notification
this.userResolver = userResolver;
}
protected override async Task<(string Description, NotificationJob Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action)
protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action)
{
if (@event is EnrichedUserEventBase userEvent)
{
@ -56,7 +55,7 @@ namespace Squidex.Extensions.Actions.Notification
throw new InvalidOperationException($"Cannot find user by '{action.User}'");
}
var ruleJob = new NotificationJob { Actor = actor, CommentsId = user.Id, Text = text };
var ruleJob = new CreateComment { Actor = actor, CommentsId = user.Id, Text = text };
if (!string.IsNullOrWhiteSpace(action.Url))
{
@ -71,32 +70,24 @@ namespace Squidex.Extensions.Actions.Notification
return (Description, ruleJob);
}
return ("Ignore", new NotificationJob());
return ("Ignore", new CreateComment());
}
protected override async Task<Result> ExecuteJobAsync(NotificationJob job, CancellationToken ct = default)
protected override async Task<Result> ExecuteJobAsync(CreateComment job, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(job.CommentsId))
if (job.CommentsId == default)
{
return Result.Ignored();
}
var command = SimpleMapper.Map(job, new CreateComment { AppId = NoApp });
var command = job;
command.AppId = NoApp;
command.FromRule = true;
await commandBus.PublishAsync(command);
return Result.Success($"Notified: {job.Text}");
}
}
public sealed class NotificationJob
{
public RefToken Actor { get; set; }
public string CommentsId { get; set; }
public string Text { get; set; }
public Uri Url { get; set; }
}
}

10
backend/i18n/frontend_en.json

@ -58,6 +58,8 @@
"assets.deleteFolderConfirmTitle": "Delete folder",
"assets.deleteMetadataConfirmText": "Do you really want to remove this metadata?",
"assets.deleteMetadataConfirmTitle": "Remove metadata",
"assets.deleteReferrerConfirmText": "The asset is referenced by a content item.\n\nDo you really want to delete the asset?",
"assets.deleteReferrerConfirmTitle": "Delete asset",
"assets.downloadVersion": "Download this Version",
"assets.dropToUpdate": "Drop to update",
"assets.duplicateFile": "Asset has already been uploaded.",
@ -286,6 +288,7 @@
"common.queryOperators.ne": "is not equals to",
"common.queryOperators.startsWith": "starts with",
"common.refresh": "Refresh",
"common.remember": "Remember my decision",
"common.rename": "Rename",
"common.requiredHint": "required",
"common.reset": "Reset",
@ -301,6 +304,7 @@
"common.searchResults": "Search Results",
"common.separateByLine": "Separate by line",
"common.settings": "Settings",
"common.sidebar": "Sidebar Extension",
"common.sidebarTour": "The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.",
"common.slug": "Slug",
"common.stars.max": "Must not have more more than 15 stars",
@ -353,6 +357,8 @@
"contents.deleteConfirmTitle": "Delete content",
"contents.deleteFailed": "Failed to delete content. Please reload.",
"contents.deleteManyConfirmText": "Do you really want to delete the selected content items?",
"contents.deleteReferrerConfirmText": "The content is referenced by another content item.\n\nDo you really want to delete the content?",
"contents.deleteReferrerConfirmTitle": "Delete content",
"contents.deleteVersionConfirmText": "Do you really want to delete this version?",
"contents.deleteVersionFailed": "Failed to delete version. Please reload.",
"contents.draftNew": "New Draft",
@ -622,6 +628,10 @@
"schemas.addNestedField": "Add Nested Field",
"schemas.changeCategoryFailed": "Failed to change category. Please reload.",
"schemas.clone": "Clone Schema",
"schemas.contentSidebarUrl": "Content Sidebar Extension",
"schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.",
"schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.contextMenuTour": "Open the context menu to delete the schema or to create some scripts for content changes.",
"schemas.create": "Create Schema",
"schemas.createCategory": "Create new category...",

10
backend/i18n/frontend_it.json

@ -58,6 +58,8 @@
"assets.deleteFolderConfirmTitle": "Elimina la cartella",
"assets.deleteMetadataConfirmText": "Sei sicuro di voler rimuovere questi metadati?",
"assets.deleteMetadataConfirmTitle": "Rimuovi metadati",
"assets.deleteReferrerConfirmText": "The asset is referenced by a content item.\n\nDo you really want to delete the asset?",
"assets.deleteReferrerConfirmTitle": "Delete asset",
"assets.downloadVersion": "Scarica questa versione",
"assets.dropToUpdate": "Trascina il file per aggiornare",
"assets.duplicateFile": "La risorsa è già stata caricata.",
@ -286,6 +288,7 @@
"common.queryOperators.ne": "è uguale a",
"common.queryOperators.startsWith": "inizia con",
"common.refresh": "Aggiorna",
"common.remember": "Remember my decision",
"common.rename": "Rinomina",
"common.requiredHint": "obbligatorio",
"common.reset": "Reimposta",
@ -301,6 +304,7 @@
"common.searchResults": "Risultati di ricerca",
"common.separateByLine": "Separato dalla linea",
"common.settings": "Impostazioni",
"common.sidebar": "Sidebar Extension",
"common.sidebarTour": "La barra di navigazione laterale contiene specifici utili collegamenti per il contesto. Qui puoi visualizzare la cronologia dei cambiamenti di questo schema.",
"common.slug": "Slug",
"common.stars.max": "Non deve avere più di 15 stelle",
@ -353,6 +357,8 @@
"contents.deleteConfirmTitle": "Elimina il contenuto",
"contents.deleteFailed": "Non è stato possibile eliminare il contenuto. Per favore ricarica.",
"contents.deleteManyConfirmText": "Sei sicuro di voler eliminare gli elementi del contenuto selezionati?",
"contents.deleteReferrerConfirmText": "The content is referenced by another content item.\n\nDo you really want to delete the content?",
"contents.deleteReferrerConfirmTitle": "Delete content",
"contents.deleteVersionConfirmText": "Do you really want to delete this version?",
"contents.deleteVersionFailed": "Non è stato possibile eliminare la versione. Per favore ricarica.",
"contents.draftNew": "Nuova bozza",
@ -622,6 +628,10 @@
"schemas.addNestedField": "Aggiungi un campo annidato",
"schemas.changeCategoryFailed": "Non è stato possibile cambiare la categoria. Per favore ricarica.",
"schemas.clone": "Clona lo Schema",
"schemas.contentSidebarUrl": "Content Sidebar Extension",
"schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.",
"schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.contextMenuTour": "Apri il menu per cancellare lo schema o per inserire alcuni script che modificano il contenuto.",
"schemas.create": "Crea uno Schema",
"schemas.createCategory": "Crea una nuova categoria...",

10
backend/i18n/frontend_nl.json

@ -58,6 +58,8 @@
"assets.deleteFolderConfirmTitle": "Map verwijderen",
"assets.deleteMetadataConfirmText": "Wil je deze metadata echt verwijderen?",
"assets.deleteMetadataConfirmTitle": "Metadata verwijderen",
"assets.deleteReferrerConfirmText": "The asset is referenced by a content item.\n\nDo you really want to delete the asset?",
"assets.deleteReferrerConfirmTitle": "Delete asset",
"assets.downloadVersion": "Download deze versie",
"assets.dropToUpdate": "Zet neer om te updaten",
"assets.duplicateFile": "Asset is al geüpload.",
@ -286,6 +288,7 @@
"common.queryOperators.ne": "is not equals to",
"common.queryOperators.startsWith": "starts with",
"common.refresh": "Vernieuwen",
"common.remember": "Remember my decision",
"common.rename": "Hernoemen",
"common.requiredHint": "verplicht",
"common.reset": "Reset",
@ -301,6 +304,7 @@
"common.searchResults": "Zoekresultaten",
"common.separateByLine": "Scheiden op regel",
"common.settings": "Instellingen",
"common.sidebar": "Sidebar Extension",
"common.sidebarTour": "De zijbalknavigatie bevat nuttige contextspecifieke links. Hier kun je de geschiedenis bekijken hoe dit schema in de loop van de tijd is veranderd.",
"common.slug": "Slug",
"common.stars.max": "Mag niet meer dan 15 sterren hebben",
@ -353,6 +357,8 @@
"contents.deleteConfirmTitle": "Inhoud verwijderen",
"contents.deleteFailed": "Verwijderen van inhoud is mislukt. Laad opnieuw.",
"contents.deleteManyConfirmText": "Weet je zeker dat je de geselecteerde inhoudsitems wilt verwijderen?",
"contents.deleteReferrerConfirmText": "The content is referenced by another content item.\n\nDo you really want to delete the content?",
"contents.deleteReferrerConfirmTitle": "Delete content",
"contents.deleteVersionConfirmText": "Wil je deze versie echt verwijderen?",
"contents.deleteVersionFailed": "Verwijderen van versie is mislukt. Laad opnieuw.",
"contents.draftNew": "Nieuw concept",
@ -622,6 +628,10 @@
"schemas.addNestedField": "Voeg genest veld toe",
"schemas.changeCategoryFailed": "Kan categorie niet wijzigen. Laad opnieuw.",
"schemas.clone": "Clone Schema",
"schemas.contentSidebarUrl": "Content Sidebar Extension",
"schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.",
"schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.contextMenuTour": "Open het contextmenu om het schema te verwijderen of om scripts te maken voor wijzigingen in de inhoud.",
"schemas.create": "Schema maken",
"schemas.createCategory": "Nieuwe categorie maken ...",

2
backend/i18n/source/backend_en.json

@ -33,6 +33,7 @@
"assets.folderNotFound": "Asset folder does not exist.",
"assets.folderRecursion": "Cannot add folder to its own child.",
"assets.maxSizeReached": "You have reached your max asset size.",
"assets.referenced": "Assets is referenced by a content and cannot be deleted.",
"backups.alreadyRunning": "Another backup process is already running.",
"backups.maxReached": "You cannot have more than {max} backups.",
"backups.restoreRunning": "A restore operation is already running.",
@ -136,6 +137,7 @@
"contents.invalidNumber": "Invalid json type, expected number.",
"contents.invalidString": "Invalid json type, expected string.",
"contents.listReferences": "{count} Reference(s)",
"contents.referenced": "Content is referenced by another content and cannot be deleted.",
"contents.singletonNotChangeable": "Singleton content cannot be updated.",
"contents.singletonNotCreatable": "Singleton content cannot be created.",
"contents.singletonNotDeletable": "Singleton content cannot be deleted.",

10
backend/i18n/source/frontend_en.json

@ -58,6 +58,8 @@
"assets.deleteFolderConfirmTitle": "Delete folder",
"assets.deleteMetadataConfirmText": "Do you really want to remove this metadata?",
"assets.deleteMetadataConfirmTitle": "Remove metadata",
"assets.deleteReferrerConfirmText": "The asset is referenced by a content item.\n\nDo you really want to delete the asset?",
"assets.deleteReferrerConfirmTitle": "Delete asset",
"assets.downloadVersion": "Download this Version",
"assets.dropToUpdate": "Drop to update",
"assets.duplicateFile": "Asset has already been uploaded.",
@ -286,6 +288,7 @@
"common.queryOperators.ne": "is not equals to",
"common.queryOperators.startsWith": "starts with",
"common.refresh": "Refresh",
"common.remember": "Remember my decision",
"common.rename": "Rename",
"common.requiredHint": "required",
"common.reset": "Reset",
@ -301,6 +304,7 @@
"common.searchResults": "Search Results",
"common.separateByLine": "Separate by line",
"common.settings": "Settings",
"common.sidebar": "Sidebar Extension",
"common.sidebarTour": "The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.",
"common.slug": "Slug",
"common.stars.max": "Must not have more more than 15 stars",
@ -353,6 +357,8 @@
"contents.deleteConfirmTitle": "Delete content",
"contents.deleteFailed": "Failed to delete content. Please reload.",
"contents.deleteManyConfirmText": "Do you really want to delete the selected content items?",
"contents.deleteReferrerConfirmText": "The content is referenced by another content item.\n\nDo you really want to delete the content?",
"contents.deleteReferrerConfirmTitle": "Delete content",
"contents.deleteVersionConfirmText": "Do you really want to delete this version?",
"contents.deleteVersionFailed": "Failed to delete version. Please reload.",
"contents.draftNew": "New Draft",
@ -622,6 +628,10 @@
"schemas.addNestedField": "Add Nested Field",
"schemas.changeCategoryFailed": "Failed to change category. Please reload.",
"schemas.clone": "Clone Schema",
"schemas.contentSidebarUrl": "Content Sidebar Extension",
"schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.",
"schemas.contentsSidebarUrl": "Contents Sidebar Extension",
"schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.",
"schemas.contextMenuTour": "Open the context menu to delete the schema or to create some scripts for content changes.",
"schemas.create": "Create Schema",
"schemas.createCategory": "Create new category...",

6
backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs

@ -7,6 +7,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
@ -88,5 +89,10 @@ namespace Squidex.Domain.Apps.Core.Contents
{
return this.DictionaryHashCode();
}
public override string ToString()
{
return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value.ToJsonString()}"))}}}";
}
}
}

6
backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs

@ -7,6 +7,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Contents
@ -68,5 +69,10 @@ namespace Squidex.Domain.Apps.Core.Contents
{
return base.Equals(other);
}
public override string ToString()
{
return $"{{{string.Join(", ", this.Select(x => $"\"{x.Key}\":{x.Value}"))}}}";
}
}
}

4
backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs

@ -15,6 +15,10 @@ namespace Squidex.Domain.Apps.Core.Schemas
{
public ReadOnlyCollection<string>? Tags { get; set; }
public string? ContentsSidebarUrl { get; set; }
public string? ContentSidebarUrl { get; set; }
public bool DeepEquals(SchemaProperties properties)
{
return SimpleEquals.IsEquals(this, properties);

107
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs

@ -10,6 +10,7 @@ using Fluid.Values;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Templates;
using Squidex.Infrastructure;
using Squidex.Text;
namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
{
@ -28,21 +29,34 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
{
TemplateContext.GlobalFilters.AddFilter("contentUrl", ContentUrl);
TemplateContext.GlobalFilters.AddFilter("assetContentUrl", AssetContentUrl);
TemplateContext.GlobalFilters.AddFilter("assetContentAppUrl", AssetContentAppUrl);
TemplateContext.GlobalFilters.AddFilter("assetContentSlugUrl", AssetContentSlugUrl);
}
private FluidValue ContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context)
{
if (input is ObjectValue objectValue)
var value = input.ToObjectValue();
switch (value)
{
if (context.GetValue("event")?.ToObjectValue() is EnrichedContentEvent contentEvent)
{
if (objectValue.ToObjectValue() is DomainId id && id != DomainId.Empty)
case DomainId id:
{
if (context.GetValue("event")?.ToObjectValue() is EnrichedContentEvent contentEvent)
{
var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, id.ToString());
return new StringValue(result);
}
break;
}
case EnrichedContentEvent contentEvent:
{
var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, id);
var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id);
return new StringValue(result);
}
}
}
return NilValue.Empty;
@ -50,17 +64,86 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
private FluidValue AssetContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context)
{
if (input is ObjectValue objectValue)
var value = input.ToObjectValue();
switch (value)
{
case DomainId id:
{
if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent)
{
var result = urlGenerator.AssetContent(assetEvent.AppId, id.ToString());
return new StringValue(result);
}
break;
}
case EnrichedAssetEvent assetEvent:
{
var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString());
return new StringValue(result);
}
}
return NilValue.Empty;
}
private FluidValue AssetContentAppUrl(FluidValue input, FilterArguments arguments, TemplateContext context)
{
var value = input.ToObjectValue();
switch (value)
{
if (context.GetValue("event")?.ToObjectValue() is EnrichedEvent @event)
{
if (objectValue.ToObjectValue() is DomainId id && id != DomainId.Empty)
case DomainId id:
{
if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent)
{
var result = urlGenerator.AssetContent(assetEvent.AppId, id.ToString());
return new StringValue(result);
}
break;
}
case EnrichedAssetEvent assetEvent:
{
var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString());
return new StringValue(result);
}
}
return NilValue.Empty;
}
private FluidValue AssetContentSlugUrl(FluidValue input, FilterArguments arguments, TemplateContext context)
{
var value = input.ToObjectValue();
switch (value)
{
case string s:
{
if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent)
{
var result = urlGenerator.AssetContent(assetEvent.AppId, s.Slugify());
return new StringValue(result);
}
break;
}
case EnrichedAssetEvent assetEvent:
{
var result = urlGenerator.AssetContent(@event.AppId, id);
var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify());
return new StringValue(result);
}
}
}
return NilValue.Empty;

23
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs

@ -9,6 +9,7 @@ using Jint.Native;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Infrastructure;
using Squidex.Text;
namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
{
@ -50,7 +51,27 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions
{
if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent)
{
return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id);
return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString());
}
return JsValue.Null;
}));
context.Engine.SetValue("assetContentAppUrl", new EventDelegate(() =>
{
if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent)
{
return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString());
}
return JsValue.Null;
}));
context.Engine.SetValue("assetContentSlugUrl", new EventDelegate(() =>
{
if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent)
{
return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify());
}
return JsValue.Null;

25
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs

@ -11,6 +11,7 @@ using System.Globalization;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Infrastructure;
using Squidex.Shared.Users;
using Squidex.Text;
namespace Squidex.Domain.Apps.Core.HandleRules
{
@ -28,6 +29,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules
AddPattern("APP_ID", AppId);
AddPattern("APP_NAME", AppName);
AddPattern("ASSET_CONTENT_URL", AssetContentUrl);
AddPattern("ASSET_CONTENT_APP_URL", AssetContentAppUrl);
AddPattern("ASSET_CONTENT_SLUG_URL", AssetContentSlugUrl);
AddPattern("CONTENT_ACTION", ContentAction);
AddPattern("CONTENT_URL", ContentUrl);
AddPattern("MENTIONED_ID", MentionedId);
@ -118,7 +121,27 @@ namespace Squidex.Domain.Apps.Core.HandleRules
{
if (@event is EnrichedAssetEvent assetEvent)
{
return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id);
return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString());
}
return null;
}
private string? AssetContentAppUrl(EnrichedEvent @event)
{
if (@event is EnrichedAssetEvent assetEvent)
{
return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString());
}
return null;
}
private string? AssetContentSlugUrl(EnrichedEvent @event)
{
if (@event is EnrichedAssetEvent assetEvent)
{
return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify());
}
return null;

5
backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs

@ -89,6 +89,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules
var typed = @event.To<AppEvent>();
if (typed.Payload.FromRule)
{
return result;
}
var actionType = rule.Action.GetType();
if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler))

6
backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs

@ -16,15 +16,13 @@ namespace Squidex.Domain.Apps.Core
string? AssetSource(NamedId<DomainId> appId, DomainId assetId, long fileVersion);
string? AssetThumbnail(NamedId<DomainId> appId, DomainId assetId, AssetType assetType);
string? AssetThumbnail(NamedId<DomainId> appId, string idOrSlug, AssetType assetType);
string AppSettingsUI(NamedId<DomainId> appId);
string AssetsUI(NamedId<DomainId> appId);
string AssetsUI(NamedId<DomainId> appId, string? query = null);
string AssetContent(NamedId<DomainId> appId, DomainId assetId);
string AssetContent(NamedId<DomainId> appId, string idOrSlug);
string BackupsUI(NamedId<DomainId> appId);

6
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Jint;
@ -116,6 +117,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
var propertyName = property.AsString();
if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase))
{
return PropertyDescriptor.Undefined;
}
return fieldProperties.GetOrAdd(propertyName, this, (k, c) => new ContentDataProperty(c, new ContentFieldObject(c, new ContentFieldData(), false)));
}

6
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using Jint;
@ -128,6 +129,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
var propertyName = property.AsString();
if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase))
{
return PropertyDescriptor.Undefined;
}
return valueProperties?.GetOrDefault(propertyName) ?? PropertyDescriptor.Undefined;
}

32
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Fields.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using MongoDB.Bson.Serialization;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
internal static class Fields
{
private static readonly Lazy<string> AssetIdField = new Lazy<string>(GetAssetIdField);
private static readonly Lazy<string> AssetFolderIdField = new Lazy<string>(GetAssetFolderIdField);
public static string AssetId => AssetIdField.Value;
public static string AssetFolderId => AssetFolderIdField.Value;
private static string GetAssetIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoAssetEntity)).GetMemberMap(nameof(MongoAssetEntity.Id)).ElementName;
}
private static string GetAssetFolderIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoAssetFolderEntity)).GetMemberMap(nameof(MongoAssetFolderEntity.Id)).ElementName;
}
}
}

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

@ -5,12 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
@ -22,8 +20,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed partial class MongoAssetFolderRepository : MongoRepositoryBase<MongoAssetFolderEntity>, IAssetFolderRepository
{
private static readonly Lazy<string> IdField = new Lazy<string>(GetIdField);
public MongoAssetFolderRepository(IMongoDatabase database)
: base(database)
{
@ -67,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.ParentId == parentId).Only(x => x.Id)
.ToListAsync();
return assetFolderEntities.Select(x => DomainId.Create(x[IdField.Value].AsString)).ToList();
return assetFolderEntities.Select(x => DomainId.Create(x[Fields.AssetFolderId].AsString)).ToList();
}
}
@ -84,10 +80,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
return assetFolderEntity;
}
}
private static string GetIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoAssetFolderEntity)).GetMemberMap(nameof(MongoAssetFolderEntity.DocumentId)).ElementName;
}
}
}

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

@ -5,12 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
@ -27,8 +25,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository
{
private static readonly Lazy<string> IdField = new Lazy<string>(GetIdField);
public MongoAssetRepository(IMongoDatabase database)
: base(database)
{
@ -105,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(BuildFilter(appId, ids)).Only(x => x.Id)
.ToListAsync();
return assetEntities.Select(x => DomainId.Create(x[IdField.Value].AsString)).ToList();
return assetEntities.Select(x => DomainId.Create(x[Fields.AssetId].AsString)).ToList();
}
}
@ -117,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.ParentId == parentId).Only(x => x.DocumentId)
.ToListAsync();
return assetEntities.Select(x => DomainId.Create(x[IdField.Value].AsString)).ToList();
return assetEntities.Select(x => DomainId.Create(x[Fields.AssetId].AsString)).ToList();
}
}
@ -191,10 +187,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
Filter.In(x => x.DocumentId, documentIds),
Filter.Ne(x => x.IsDeleted, true));
}
private static string GetIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoAssetEntity)).GetMemberMap(nameof(MongoAssetEntity.Id)).ElementName;
}
}
}

32
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using MongoDB.Bson.Serialization;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
{
internal static class Fields
{
private static readonly Lazy<string> IdField = new Lazy<string>(GetIdField);
private static readonly Lazy<string> SchemaIdField = new Lazy<string>(GetSchemaIdField);
public static string Id => IdField.Value;
public static string SchemaId => SchemaIdField.Value;
private static string GetIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.Id)).ElementName;
}
private static string GetSchemaIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.IndexedSchemaId)).ElementName;
}
}
}

11
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs

@ -30,6 +30,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
private readonly QueryContentsByIds queryContentsById;
private readonly QueryContentsByQuery queryContentsByQuery;
private readonly QueryIdsAsync queryIdsAsync;
private readonly QueryReferrersAsync queryReferrersAsync;
private readonly QueryScheduledContents queryScheduledItems;
public MongoContentCollectionAll(IMongoDatabase database, IAppProvider appProvider, ITextIndex indexer, DataConverter converter)
@ -39,6 +40,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
queryContentsById = new QueryContentsByIds(converter, appProvider);
queryContentsByQuery = new QueryContentsByQuery(converter, indexer);
queryIdsAsync = new QueryIdsAsync(appProvider);
queryReferrersAsync = new QueryReferrersAsync();
queryScheduledItems = new QueryScheduledContents();
}
@ -58,6 +60,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await queryContentsById.PrepareAsync(collection, ct);
await queryContentsByQuery.PrepareAsync(collection, ct);
await queryIdsAsync.PrepareAsync(collection, ct);
await queryReferrersAsync.PrepareAsync(collection, ct);
await queryScheduledItems.PrepareAsync(collection, ct);
}
@ -125,6 +128,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
public async Task<bool> HasReferrersAsync(DomainId appId, DomainId contentId)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
return await queryReferrersAsync.DoAsync(appId, contentId);
}
}
public Task ResetScheduledAsync(DomainId documentId)
{
return Collection.UpdateOneAsync(x => x.DocumentId == documentId, Update.Unset(x => x.ScheduleJob).Unset(x => x.ScheduledAt));

9
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -114,9 +114,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
public Task ResetScheduledAsync(DomainId id)
public Task ResetScheduledAsync(DomainId documentId)
{
return collectionAll.ResetScheduledAsync(id);
return collectionAll.ResetScheduledAsync(documentId);
}
public Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback)
@ -129,6 +129,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return collectionAll.QueryIdsAsync(appId, schemaId, filterNode);
}
public Task<bool> HasReferrersAsync(DomainId appId, DomainId contentId)
{
return collectionAll.HasReferrersAsync(appId, contentId);
}
public IEnumerable<IMongoCollection<MongoContentEntity>> GetInternalCollections()
{
yield return collectionAll.GetInternalCollection();

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs

@ -8,7 +8,6 @@
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
@ -16,7 +15,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
protected static readonly SortDefinitionBuilder<MongoContentEntity> Sort = Builders<MongoContentEntity>.Sort;
protected static readonly UpdateDefinitionBuilder<MongoContentEntity> Update = Builders<MongoContentEntity>.Update;
protected static readonly FieldDefinitionBuilder<MongoContentEntity> Fields = FieldDefinitionBuilder<MongoContentEntity>.Instance;
protected static readonly FilterDefinitionBuilder<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter;
protected static readonly IndexKeysDefinitionBuilder<MongoContentEntity> Index = Builders<MongoContentEntity>.IndexKeys;
protected static readonly ProjectionDefinitionBuilder<MongoContentEntity> Projection = Builders<MongoContentEntity>.Projection;

20
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs

@ -5,12 +5,10 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
@ -21,9 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class QueryIdsAsync : OperationBase
{
private static readonly IReadOnlyList<(DomainId SchemaId, DomainId Id)> EmptyIds = new List<(DomainId SchemaId, DomainId Id)>();
private static readonly Lazy<string> IdField = new Lazy<string>(GetIdField);
private static readonly Lazy<string> SchemaIdField = new Lazy<string>(GetSchemaIdField);
private static readonly List<(DomainId SchemaId, DomainId Id)> EmptyIds = new List<(DomainId SchemaId, DomainId Id)>();
private readonly IAppProvider appProvider;
public QueryIdsAsync(IAppProvider appProvider)
@ -55,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId)
.ToListAsync();
return contentEntities.Select(x => (DomainId.Create(x[SchemaIdField.Value].AsString), DomainId.Create(x[IdField.Value].AsString))).ToList();
return contentEntities.Select(x => (DomainId.Create(x[Fields.SchemaId].AsString), DomainId.Create(x[Fields.Id].AsString))).ToList();
}
public async Task<IReadOnlyList<(DomainId SchemaId, DomainId Id)>> DoAsync(DomainId appId, DomainId schemaId, FilterNode<ClrValue> filterNode)
@ -73,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId)
.ToListAsync();
return contentEntities.Select(x => (DomainId.Create(x[SchemaIdField.Value].AsString), DomainId.Create(x[IdField.Value].AsString))).ToList();
return contentEntities.Select(x => (DomainId.Create(x[Fields.SchemaId].AsString), DomainId.Create(x[Fields.Id].AsString))).ToList();
}
public static FilterDefinition<MongoContentEntity> BuildFilter(FilterNode<ClrValue>? filterNode, DomainId appId, DomainId schemaId)
@ -92,15 +88,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
return Filter.And(filters);
}
private static string GetIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.Id)).ElementName;
}
private static string GetSchemaIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.IndexedSchemaId)).ElementName;
}
}
}

47
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs

@ -0,0 +1,47 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
{
internal sealed class QueryReferrersAsync : OperationBase
{
protected override Task PrepareAsync(CancellationToken ct = default)
{
var index =
new CreateIndexModel<MongoContentEntity>(Index
.Ascending(x => x.ReferencedIds)
.Ascending(x => x.IndexedAppId)
.Ascending(x => x.IsDeleted));
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct);
}
public async Task<bool> DoAsync(DomainId appId, DomainId contentId)
{
var currentId = DomainId.Combine(appId, contentId);
var filter =
Filter.And(
Filter.AnyEq(x => x.ReferencedIds, appId),
Filter.Eq(x => x.IndexedAppId, appId),
Filter.Ne(x => x.IsDeleted, true),
Filter.Ne(x => x.Id, currentId));
var hasReferrerAsync =
await Collection.Find(filter).Only(x => x.Id)
.AnyAsync();
return hasReferrerAsync;
}
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs

@ -32,10 +32,8 @@ namespace Squidex.Domain.Apps.Entities.Apps
private readonly IAppPlanBillingManager appPlansBillingManager;
private readonly IUserResolver userResolver;
public AppDomainObject(
public AppDomainObject(IStore<DomainId> store, ISemanticLog log,
InitialPatterns initialPatterns,
IStore<DomainId> store,
ISemanticLog log,
IAppPlansProvider appPlansProvider,
IAppPlanBillingManager appPlansBillingManager,
IUserResolver userResolver)

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

@ -48,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
}
Search("Assets", Permissions.AppAssetsRead,
urlGenerator.AssetsUI, SearchResultType.Asset);
a => urlGenerator.AssetsUI(a), SearchResultType.Asset);
Search("Backups", Permissions.AppBackupsRead,
urlGenerator.BackupsUI, SearchResultType.Setting);

11
backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs

@ -17,20 +17,25 @@ namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics
{
public sealed class OrleansAppsHealthCheck : IHealthCheck
{
private readonly IAppsByNameIndexGrain index;
private readonly IGrainFactory grainFactory;
public OrleansAppsHealthCheck(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
index = grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id);
this.grainFactory = grainFactory;
}
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
await index.CountAsync();
await GetGrain().CountAsync();
return HealthCheckResult.Healthy("Orleans must establish communication.");
}
private IAppsByNameIndexGrain GetGrain()
{
return grainFactory.GetGrain<IAppsByNameIndexGrain>(SingleGrain.Id);
}
}
}

12
backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs

@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IAppPlansProvider appPlansProvider;
private readonly IApiUsageTracker apiUsageTracker;
private readonly IUsageNotifierGrain usageLimitNotifier;
private readonly IGrainFactory grainFactory;
public UsageGate(IAppPlansProvider appPlansProvider, IApiUsageTracker apiUsageTracker, IGrainFactory grainFactory)
{
@ -34,8 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
this.appPlansProvider = appPlansProvider;
this.apiUsageTracker = apiUsageTracker;
usageLimitNotifier = grainFactory.GetGrain<IUsageNotifierGrain>(SingleGrain.Id);
this.grainFactory = grainFactory;
}
public virtual async Task<bool> IsBlockedAsync(IAppEntity app, string? clientId, DateTime today)
@ -72,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
Users = users
};
usageLimitNotifier.NotifyAsync(notification).Forget();
GetGrain().NotifyAsync(notification).Forget();
TrackNotified(appId);
}
@ -83,6 +82,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans
return isBlocked;
}
private IUsageNotifierGrain GetGrain()
{
return grainFactory.GetGrain<IUsageNotifierGrain>(SingleGrain.Id);
}
private bool HasNotifiedBefore(DomainId appId)
{
return memoryCache.Get<bool>(appId);

36
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs

@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Core.Tags;
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.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
@ -20,23 +21,30 @@ using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Translations;
using IAssetTagService = Squidex.Domain.Apps.Core.Tags.ITagService;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetDomainObject : LogSnapshotDomainObject<AssetState>
{
private readonly ITagService tagService;
private readonly IContentRepository contentRepository;
private readonly IAssetTagService assetTags;
private readonly IAssetQueryService assetQuery;
public AssetDomainObject(IStore<DomainId> store, ITagService tagService, IAssetQueryService assetQuery, ISemanticLog log)
public AssetDomainObject(IStore<DomainId> store, ISemanticLog log,
IAssetTagService assetTags,
IAssetQueryService assetQuery,
IContentRepository contentRepository)
: base(store, log)
{
Guard.NotNull(tagService, nameof(tagService));
Guard.NotNull(assetTags, nameof(assetTags));
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(contentRepository, nameof(contentRepository));
this.tagService = tagService;
this.assetTags = assetTags;
this.assetQuery = assetQuery;
this.contentRepository = contentRepository;
}
protected override bool IsDeleted()
@ -105,7 +113,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
GuardAsset.CanDelete(c);
await tagService.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, null, Snapshot.Tags);
if (c.CheckReferrers)
{
var hasReferrer = await contentRepository.HasReferrersAsync(Snapshot.AppId.Id, c.AssetId);
if (hasReferrer)
{
throw new DomainException(T.Get("assets.referenced"));
}
}
await assetTags.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, null, Snapshot.Tags);
Delete(c);
});
@ -121,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
return null;
}
var normalized = await tagService.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags);
var normalized = await assetTags.NormalizeTagsAsync(appId, TagGroups.Assets, tags, Snapshot.Tags);
return new HashSet<string>(normalized.Values);
}
@ -130,10 +148,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var @event = SimpleMapper.Map(command, new AssetCreated
{
MimeType = command.File.MimeType,
FileName = command.File.FileName,
FileSize = command.File.FileSize,
FileVersion = 0,
MimeType = command.File.MimeType,
Slug = command.File.FileName.ToAssetSlug()
});
@ -146,9 +164,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
var @event = SimpleMapper.Map(command, new AssetUpdated
{
MimeType = command.File.MimeType,
FileVersion = Snapshot.FileVersion + 1,
FileSize = command.File.FileSize,
MimeType = command.File.MimeType
});
RaiseEvent(@event);

3
backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs

@ -26,7 +26,8 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
private readonly IAssetQueryService assetQuery;
public AssetFolderDomainObject(IStore<DomainId> store, IAssetQueryService assetQuery, ISemanticLog log)
public AssetFolderDomainObject(IStore<DomainId> store, ISemanticLog log,
IAssetQueryService assetQuery)
: base(store, log)
{
Guard.NotNull(assetQuery, nameof(assetQuery));

1
backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs

@ -9,5 +9,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.Commands
{
public sealed class DeleteAsset : AssetCommand
{
public bool CheckReferrers { get; set; }
}
}

4
backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs

@ -88,14 +88,14 @@ namespace Squidex.Domain.Apps.Entities.Assets
foreach (var assetFolderId in childAssetFolders)
{
await PublishAsync(new DeleteAssetFolder { AssetFolderId = assetFolderId });
await PublishAsync(new DeleteAssetFolder { AppId = appId, AssetFolderId = assetFolderId });
}
var childAssets = await assetRepository.QueryChildIdsAsync(appId.Id, folderDeleted.AssetFolderId);
foreach (var assetId in childAssets)
{
await PublishAsync(new DeleteAsset { AssetId = assetId });
await PublishAsync(new DeleteAsset { AppId = appId, AssetId = assetId });
}
}
}

9
backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs

@ -52,13 +52,16 @@ namespace Squidex.Domain.Apps.Entities.Comments
private async Task ExecuteCommandAsync(CommandContext context, CommentsCommand commentsCommand)
{
var grain = grainFactory.GetGrain<ICommentsGrain>(commentsCommand.CommentsId.ToString());
var result = await grain.ExecuteAsync(commentsCommand.AsJ());
var result = await GetGrain(commentsCommand).ExecuteAsync(commentsCommand.AsJ());
context.Complete(result.Value);
}
private ICommentsGrain GetGrain(CommentsCommand commentsCommand)
{
return grainFactory.GetGrain<ICommentsGrain>(commentsCommand.CommentsId.ToString());
}
private static bool IsMention(CreateComment createComment)
{
return createComment.IsMention;

1
backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs

@ -9,5 +9,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.Commands
{
public sealed class DeleteContent : ContentCommand
{
public bool CheckReferrers { get; set; }
}
}

21
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs

@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Guards;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
@ -28,15 +29,21 @@ namespace Squidex.Domain.Apps.Entities.Contents
public class ContentDomainObject : LogSnapshotDomainObject<ContentState>
{
private readonly IContentWorkflow contentWorkflow;
private readonly IContentRepository contentRepository;
private readonly ContentOperationContext context;
public ContentDomainObject(IStore<DomainId> store, IContentWorkflow contentWorkflow, ContentOperationContext context, ISemanticLog log)
public ContentDomainObject(IStore<DomainId> store, ISemanticLog log,
IContentWorkflow contentWorkflow,
IContentRepository contentRepository,
ContentOperationContext context)
: base(store, log)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(contentRepository, nameof(contentRepository));
Guard.NotNull(contentWorkflow, nameof(contentWorkflow));
Guard.NotNull(context, nameof(context));
this.contentWorkflow = contentWorkflow;
this.contentRepository = contentRepository;
this.context = context;
}
@ -238,6 +245,16 @@ namespace Squidex.Domain.Apps.Entities.Contents
});
}
if (c.CheckReferrers)
{
var hasReferrer = await contentRepository.HasReferrersAsync(Snapshot.AppId.Id, c.AggregateId);
if (hasReferrer)
{
throw new DomainException(T.Get("contents.referenced"));
}
}
Delete(c);
});

6
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -43,7 +43,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
private ContentCommand command;
private ValidationContext validationContext;
public ContentOperationContext(IAppProvider appProvider, IEnumerable<IValidatorsFactory> factories, IScriptEngine scriptEngine, ISemanticLog log)
public ContentOperationContext(
IAppProvider appProvider,
IEnumerable<IValidatorsFactory> factories,
IScriptEngine scriptEngine,
ISemanticLog log)
{
this.appProvider = appProvider;
this.factories = factories;

4
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/AssetResolvers.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
{
public static readonly IFieldResolver Url = Resolve((asset, _, context) =>
{
return context.UrlGenerator.AssetContent(asset.AppId, asset.Id);
return context.UrlGenerator.AssetContent(asset.AppId, asset.Id.ToString());
});
public static readonly IFieldResolver SourceUrl = Resolve((asset, _, context) =>
@ -28,7 +28,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types
public static readonly IFieldResolver ThumbnailUrl = Resolve((asset, _, context) =>
{
return context.UrlGenerator.AssetThumbnail(asset.AppId, asset.Id, asset.Type);
return context.UrlGenerator.AssetThumbnail(asset.AppId, asset.Id.ToString(), asset.Type);
});
public static readonly IFieldResolver FileHash = Resolve(x => x.FileHash);

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

@ -92,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps
{
var url = urlGenerator.AssetContent(
referencedAsset.AppId,
referencedAsset.Id);
referencedAsset.Id.ToString());
array = JsonValue.Array(url, referencedAsset.FileName);
}

4
backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs

@ -30,7 +30,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
Task<IContentEntity?> FindContentAsync(IAppEntity app, ISchemaEntity schema, DomainId id, SearchScope scope);
Task ResetScheduledAsync(DomainId id);
Task<bool> HasReferrersAsync(DomainId appId, DomainId contentId);
Task ResetScheduledAsync(DomainId documentId);
Task QueryScheduledWithoutDataAsync(Instant now, Func<IContentEntity, Task> callback);
}

2
backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs

@ -14,6 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Rules
{
public interface IRuleEnqueuer
{
Task Enqueue(Rule rule, DomainId ruleId, Envelope<IEvent> @event);
Task EnqueueAsync(Rule rule, DomainId ruleId, Envelope<IEvent> @event);
}
}

17
backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs

@ -46,14 +46,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes
}
}
private async Task<IRuleEntity?> GetRuleAsync(DomainId appId, DomainId id)
{
using (Profiler.TraceMethod<RulesIndex>())
{
return await GetRuleCoreAsync(DomainId.Combine(appId, id));
}
}
private async Task<List<DomainId>> GetRuleIdsAsync(DomainId appId)
{
using (Profiler.TraceMethod<RulesIndex>())
@ -95,15 +87,6 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes
}
}
private async Task<IRuleEntity> GetRuleInternalAsync(DomainId appId, DomainId id)
{
var key = DomainId.Combine(appId, id).ToString();
var rule = await grainFactory.GetGrain<IRuleGrain>(key).GetStateAsync();
return rule.Value;
}
private IRulesByAppIndexGrain Index(DomainId appId)
{
return grainFactory.GetGrain<IRulesByAppIndexGrain>(appId.ToString());

6
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs

@ -26,14 +26,14 @@ namespace Squidex.Domain.Apps.Entities.Rules
private readonly IAppProvider appProvider;
private readonly IRuleEnqueuer ruleEnqueuer;
public RuleDomainObject(IStore<DomainId> store, ISemanticLog log, IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer)
public RuleDomainObject(IStore<DomainId> store, ISemanticLog log,
IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer)
: base(store, log)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(ruleEnqueuer, nameof(ruleEnqueuer));
this.appProvider = appProvider;
this.ruleEnqueuer = ruleEnqueuer;
}
@ -114,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
var @event = SimpleMapper.Map(command, new RuleManuallyTriggered { RuleId = Snapshot.Id, AppId = Snapshot.AppId });
await ruleEnqueuer.Enqueue(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event));
await ruleEnqueuer.EnqueueAsync(Snapshot.RuleDef, Snapshot.UniqueId, Envelope.Create(@event));
return null;
}

48
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -50,46 +50,46 @@ namespace Squidex.Domain.Apps.Entities.Rules
this.localCache = localCache;
}
public async Task Enqueue(Rule rule, DomainId ruleId, Envelope<IEvent> @event)
public async Task EnqueueAsync(Rule rule, DomainId ruleId, Envelope<IEvent> @event)
{
Guard.NotNull(rule, nameof(rule));
Guard.NotNull(@event, nameof(@event));
using (localCache.StartContext())
{
var jobs = await ruleService.CreateJobsAsync(rule, ruleId, @event);
var jobs = await ruleService.CreateJobsAsync(rule, ruleId, @event);
foreach (var (job, ex) in jobs)
foreach (var (job, ex) in jobs)
{
if (ex != null)
{
if (ex != null)
{
await ruleEventRepository.EnqueueAsync(job, null);
await ruleEventRepository.EnqueueAsync(job, null);
await ruleEventRepository.UpdateAsync(job, new RuleJobUpdate
{
JobResult = RuleJobResult.Failed,
ExecutionResult = RuleResult.Failed,
ExecutionDump = ex.ToString(),
Finished = job.Created
});
}
else
await ruleEventRepository.UpdateAsync(job, new RuleJobUpdate
{
await ruleEventRepository.EnqueueAsync(job, job.Created);
}
JobResult = RuleJobResult.Failed,
ExecutionResult = RuleResult.Failed,
ExecutionDump = ex.ToString(),
Finished = job.Created
});
}
else
{
await ruleEventRepository.EnqueueAsync(job, job.Created);
}
}
}
public async Task On(Envelope<IEvent> @event)
{
if (@event.Payload is AppEvent appEvent)
using (localCache.StartContext())
{
var rules = await GetRulesAsync(appEvent.AppId.Id);
foreach (var ruleEntity in rules)
if (@event.Payload is AppEvent appEvent)
{
await Enqueue(ruleEntity.RuleDef, ruleEntity.Id, @event);
var rules = await GetRulesAsync(appEvent.AppId.Id);
foreach (var ruleEntity in rules)
{
await EnqueueAsync(ruleEntity.RuleDef, ruleEntity.Id, @event);
}
}
}
}

15
backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs

@ -17,13 +17,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{
public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware
{
private readonly IUsageTrackerGrain usageTrackerGrain;
private readonly IGrainFactory grainFactory;
public UsageTrackerCommandMiddleware(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
usageTrackerGrain = grainFactory.GetGrain<IUsageTrackerGrain>(SingleGrain.Id);
this.grainFactory = grainFactory;
}
public async Task HandleAsync(CommandContext context, NextDelegate next)
@ -31,13 +31,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
switch (context.Command)
{
case DeleteRule deleteRule:
await usageTrackerGrain.RemoveTargetAsync(deleteRule.RuleId);
await GetGrain().RemoveTargetAsync(deleteRule.RuleId);
break;
case CreateRule createRule:
{
if (createRule.Trigger is UsageTrigger usage)
{
await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays);
await GetGrain().AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays);
}
break;
@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{
if (ruleUpdated.Trigger is UsageTrigger usage)
{
await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays);
await GetGrain().UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays);
}
break;
@ -56,5 +56,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
await next(context);
}
private IUsageTrackerGrain GetGrain()
{
return grainFactory.GetGrain<IUsageTrackerGrain>(SingleGrain.Id);
}
}
}

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

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities
public ClaimsPrincipal User { get; set; }
public bool FromRule { get; set; }
public long ExpectedVersion { get; set; } = EtagVersion.Auto;
}
}

2
backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs

@ -13,5 +13,7 @@ namespace Squidex.Domain.Apps.Events
public abstract class SquidexEvent : IEvent
{
public RefToken Actor { get; set; }
public bool FromRule { get; set; }
}
}

8
backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs

@ -16,10 +16,10 @@ namespace Squidex.Infrastructure.EventSourcing
{
public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore
{
private static readonly FieldDefinition<MongoEventCommit, BsonTimestamp> TimestampField = Fields.Build(x => x.Timestamp);
private static readonly FieldDefinition<MongoEventCommit, long> EventsCountField = Fields.Build(x => x.EventsCount);
private static readonly FieldDefinition<MongoEventCommit, long> EventStreamOffsetField = Fields.Build(x => x.EventStreamOffset);
private static readonly FieldDefinition<MongoEventCommit, string> EventStreamField = Fields.Build(x => x.EventStream);
private static readonly FieldDefinition<MongoEventCommit, BsonTimestamp> TimestampField = FieldBuilder.Build(x => x.Timestamp);
private static readonly FieldDefinition<MongoEventCommit, long> EventsCountField = FieldBuilder.Build(x => x.EventsCount);
private static readonly FieldDefinition<MongoEventCommit, long> EventStreamOffsetField = FieldBuilder.Build(x => x.EventStreamOffset);
private static readonly FieldDefinition<MongoEventCommit, string> EventStreamField = FieldBuilder.Build(x => x.EventStream);
private readonly IEventNotifier notifier;
public IMongoCollection<BsonDocument> RawCollection

2
backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs

@ -23,7 +23,7 @@ namespace Squidex.Infrastructure.MongoDb
protected static readonly ReplaceOptions UpsertReplace = new ReplaceOptions { IsUpsert = true };
protected static readonly SortDefinitionBuilder<TEntity> Sort = Builders<TEntity>.Sort;
protected static readonly UpdateDefinitionBuilder<TEntity> Update = Builders<TEntity>.Update;
protected static readonly FieldDefinitionBuilder<TEntity> Fields = FieldDefinitionBuilder<TEntity>.Instance;
protected static readonly FieldDefinitionBuilder<TEntity> FieldBuilder = FieldDefinitionBuilder<TEntity>.Instance;
protected static readonly FilterDefinitionBuilder<TEntity> Filter = Builders<TEntity>.Filter;
protected static readonly IndexKeysDefinitionBuilder<TEntity> Index = Builders<TEntity>.IndexKeys;
protected static readonly ProjectionDefinitionBuilder<TEntity> Projection = Builders<TEntity>.Projection;

2
backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs

@ -7,13 +7,11 @@
using System.Threading.Tasks;
using Orleans;
using Orleans.Concurrency;
namespace Squidex.Infrastructure.Orleans
{
public interface IBackgroundGrain : IGrainWithStringKey
{
[OneWay]
Task ActivateAsync();
}
}

6
backend/src/Squidex.Shared/Texts.it.resx

@ -184,6 +184,9 @@
<data name="assets.maxSizeReached" xml:space="preserve">
<value>Hai raggiunto la dimensione massima consentito per le risorse.</value>
</data>
<data name="assets.referenced" xml:space="preserve">
<value>Assets is referenced by a content and cannot be deleted.</value>
</data>
<data name="backups.alreadyRunning" xml:space="preserve">
<value>E' in esecuzione una altro processo di backup.</value>
</data>
@ -493,6 +496,9 @@
<data name="contents.listReferences" xml:space="preserve">
<value>{count} Collegamenti(s)</value>
</data>
<data name="contents.referenced" xml:space="preserve">
<value>Content is referenced by another content and cannot be deleted.</value>
</data>
<data name="contents.singletonNotChangeable" xml:space="preserve">
<value>Il contenuto singleton non può essere aggiornato</value>
</data>

6
backend/src/Squidex.Shared/Texts.nl.resx

@ -184,6 +184,9 @@
<data name="assets.maxSizeReached" xml:space="preserve">
<value>Je hebt jouw maximale assetgrootte bereikt.</value>
</data>
<data name="assets.referenced" xml:space="preserve">
<value>Assets is referenced by a content and cannot be deleted.</value>
</data>
<data name="backups.alreadyRunning" xml:space="preserve">
<value>Er wordt al een ander back-upproces uitgevoerd.</value>
</data>
@ -493,6 +496,9 @@
<data name="contents.listReferences" xml:space="preserve">
<value>{count} referentie (s)</value>
</data>
<data name="contents.referenced" xml:space="preserve">
<value>Content is referenced by another content and cannot be deleted.</value>
</data>
<data name="contents.singletonNotChangeable" xml:space="preserve">
<value>Singleton-inhoud kan niet worden bijgewerkt.</value>
</data>

6
backend/src/Squidex.Shared/Texts.resx

@ -184,6 +184,9 @@
<data name="assets.maxSizeReached" xml:space="preserve">
<value>You have reached your max asset size.</value>
</data>
<data name="assets.referenced" xml:space="preserve">
<value>Assets is referenced by a content and cannot be deleted.</value>
</data>
<data name="backups.alreadyRunning" xml:space="preserve">
<value>Another backup process is already running.</value>
</data>
@ -493,6 +496,9 @@
<data name="contents.listReferences" xml:space="preserve">
<value>{count} Reference(s)</value>
</data>
<data name="contents.referenced" xml:space="preserve">
<value>Content is referenced by another content and cannot be deleted.</value>
</data>
<data name="contents.singletonNotChangeable" xml:space="preserve">
<value>Singleton content cannot be updated.</value>
</data>

14
backend/src/Squidex.Web/Services/UrlGenerator.cs

@ -32,14 +32,14 @@ namespace Squidex.Web.Services
CanGenerateAssetSourceUrl = allowAssetSourceUrl;
}
public string? AssetThumbnail(NamedId<DomainId> appId, DomainId assetId, AssetType assetType)
public string? AssetThumbnail(NamedId<DomainId> appId, string idOrSlug, AssetType assetType)
{
if (assetType != AssetType.Image)
{
return null;
}
return urlsOptions.BuildUrl($"api/assets/{appId.Name}/{assetId}?width=100&mode=Max");
return urlsOptions.BuildUrl($"api/assets/{appId.Name}/{idOrSlug}?width=100&mode=Max");
}
public string AppSettingsUI(NamedId<DomainId> appId)
@ -52,19 +52,19 @@ namespace Squidex.Web.Services
return urlsOptions.BuildUrl($"api/assets/{appId.Name}/{assetId}");
}
public string? AssetSource(NamedId<DomainId> appId, DomainId assetId, long fileVersion)
public string AssetContent(NamedId<DomainId> appId, string idOrSlug)
{
return assetFileStore.GeneratePublicUrl(appId.Id, assetId, fileVersion);
return urlsOptions.BuildUrl($"assets/{appId.Name}/{idOrSlug}");
}
public string AssetsUI(NamedId<DomainId> appId)
public string? AssetSource(NamedId<DomainId> appId, DomainId assetId, long fileVersion)
{
return urlsOptions.BuildUrl($"app/{appId.Name}/assets", false);
return assetFileStore.GeneratePublicUrl(appId.Id, assetId, fileVersion);
}
public string AssetsUI(NamedId<DomainId> appId, string? query = null)
{
return urlsOptions.BuildUrl($"app/{appId.Name}/assets?query={query}", false);
return urlsOptions.BuildUrl($"app/{appId.Name}/assets", false) + query != null ? $"?query={query}" : string.Empty;
}
public string BackupsUI(NamedId<DomainId> appId)

8
backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs

@ -73,13 +73,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
[AllowAnonymous]
public async Task<IActionResult> GetAssetContentBySlug(string app, string idOrSlug, [FromQuery] AssetContentQueryDto queries, string? more = null)
{
IAssetEntity? asset;
var asset = await assetRepository.FindAssetAsync(AppId, idOrSlug);
if (Guid.TryParse(idOrSlug, out var guid))
{
asset = await assetRepository.FindAssetAsync(AppId, guid);
}
else
if (asset == null)
{
asset = await assetRepository.FindAssetBySlugAsync(AppId, idOrSlug);
}

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

@ -290,6 +290,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset to delete.</param>
/// <param name="checkReferrers">True to check referrers of this asset.</param>
/// <returns>
/// 204 => Asset deleted.
/// 404 => Asset or app not found.
@ -298,9 +299,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("apps/{app}/assets/{id}/")]
[ApiPermissionOrAnonymous(Permissions.AppAssetsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteAsset(string app, string id)
public async Task<IActionResult> DeleteAsset(string app, string id, [FromQuery] bool checkReferrers = false)
{
await CommandBus.PublishAsync(new DeleteAsset { AssetId = id });
await CommandBus.PublishAsync(new DeleteAsset { AssetId = id, CheckReferrers = checkReferrers });
return NoContent();
}

5
backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs

@ -586,6 +586,7 @@ namespace Squidex.Areas.Api.Controllers.Contents
/// <param name="app">The name of the app.</param>
/// <param name="name">The name of the schema.</param>
/// <param name="id">The id of the content item to delete.</param>
/// <param name="checkReferrers">True to check referrers of this content.</param>
/// <returns>
/// 204 => Content deleted.
/// 404 => Content, schema or app not found.
@ -597,9 +598,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[Route("content/{app}/{name}/{id}/")]
[ApiPermissionOrAnonymous(Permissions.AppContentsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string name, string id)
public async Task<IActionResult> DeleteContent(string app, string name, string id, [FromQuery] bool checkReferrers = false)
{
var command = new DeleteContent { ContentId = id };
var command = new DeleteContent { ContentId = id, CheckReferrers = checkReferrers };
await CommandBus.PublishAsync(command);

2
backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs

@ -16,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.News.Service
{
public sealed class FeaturesService
{
private const int FeatureVersion = 9;
private const int FeatureVersion = 11;
private readonly QueryContext flatten = QueryContext.Default.Flatten();
private readonly IContentsClient<NewsEntity, FeatureDto> client;

10
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs

@ -24,6 +24,16 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
[LocalizedStringLength(1000)]
public string? Hints { get; set; }
/// <summary>
/// The url to a the sidebar plugin for content lists.
/// </summary>
public string? ContentsSidebarUrl { get; set; }
/// <summary>
/// The url to a the sidebar plugin for content items.
/// </summary>
public string? ContentSidebarUrl { get; set; }
/// <summary>
/// Tags for automation processes.
/// </summary>

10
backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs

@ -27,6 +27,16 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models
[LocalizedStringLength(1000)]
public string? Hints { get; set; }
/// <summary>
/// The url to a the sidebar plugin for content lists.
/// </summary>
public string? ContentsSidebarUrl { get; set; }
/// <summary>
/// The url to a the sidebar plugin for content items.
/// </summary>
public string? ContentSidebarUrl { get; set; }
/// <summary>
/// Tags for automation processes.
/// </summary>

2
backend/src/Squidex/Config/Orleans/OrleansServices.cs

@ -59,10 +59,10 @@ namespace Squidex.Config.Orleans
options.HostSelf = false;
});
builder.AddIncomingGrainCallFilter<LoggingFilter>();
builder.AddIncomingGrainCallFilter<ExceptionWrapperFilter>();
builder.AddIncomingGrainCallFilter<ActivationLimiterFilter>();
builder.AddIncomingGrainCallFilter<LocalCacheFilter>();
builder.AddIncomingGrainCallFilter<LoggingFilter>();
builder.AddIncomingGrainCallFilter<StateFilter>();
var orleansPortSilo = config.GetOptionalValue("orleans:siloPort", 11111);

28
backend/src/Squidex/wwwroot/scripts/context-editor.html

@ -1,28 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
</head>
<body>
<textarea style="width: 100%; box-sizing: border-box; height: 100px;" name="content" id="editor"></textarea>
<script>
var element = document.getElementById('editor');
// When the field is instantiated it notified the UI that it has been loaded.
var field = new SquidexFormField();
field.onInit(function (context) {
if (context) {
element.innerHTML = JSON.stringify(context, null, 2);
}
});
</script>
</body>
</html>

4
backend/src/Squidex/wwwroot/scripts/combined-editor.html → backend/src/Squidex/wwwroot/scripts/editor-combined.html

@ -14,7 +14,9 @@
<script>
var element = document.getElementById('editor');
// When the field is instantiated it notified the UI that it has been loaded.
// When the field is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
var field = new SquidexFormField();
// Handle the value change event and set the text to the editor.

47
backend/src/Squidex/wwwroot/scripts/editor-context.html

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<style>
textarea {
box-sizing: border-box;
resize: none;
overflow: hidden;
width: 100%;
}
</style>
</head>
<body>
<script>
function grow(element) {
element.style.height = "5px";
element.style.height = (element.scrollHeight)+"px";
}
</script>
<textarea oninput="grow(this)" name="content" id="editor"></textarea>
<script>
var element = document.getElementById('editor');
// When the field is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
var field = new SquidexFormField();
// Init is called once with a context that contains the app name, schema name and authentication information.
field.onInit(function (context) {
element.innerHTML = JSON.stringify(context, null, 2);
grow(element);
});
</script>
</body>
</html>

3
backend/src/Squidex/wwwroot/scripts/editor-json-schema.html

@ -29,6 +29,9 @@
<script>
const Form = JSONSchemaForm.default;
// When the field is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
const field = new SquidexFormField();
function Editor() {

11
backend/src/Squidex/wwwroot/scripts/simple-log.html → backend/src/Squidex/wwwroot/scripts/editor-log.html

@ -16,13 +16,16 @@
var button = document.getElementById('button');
// When the field is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
var field = new SquidexFormField();
function logState(message) {
console.log(`${message}. Value: <${JSON.stringify(field.getValue(), 2)}>, Form Value: <${JSON.stringify(field.getFormValue())}>`);
}
logState('Init');
logState('Setup');
if (button) {
button.addEventListener('click', function () {
@ -34,6 +37,10 @@
});
}
field.onInit(function () {
logState('Init');
});
// Handle the value change event and set the text to the editor.
field.onValueChanged(function (value) {
logState(`Value changed: <${JSON.stringify(value, 2)}>`);

146
backend/src/Squidex/wwwroot/scripts/editor-sdk.js

@ -1,3 +1,123 @@
function measureAndNotifyParent() {
var height = 0;
document.body.style.margin = '0';
document.body.style.padding = '0';
window.parent.postMessage({ type: 'started' }, '*');
function notifySize() {
var newHeight = document.body.offsetHeight;
if (height !== newHeight) {
height = newHeight;
if (window.parent) {
window.parent.postMessage({ type: 'resize', height: height }, '*');
}
}
window.parent.postMessage({ type: 'resize', height: height }, '*');
}
notifySize();
return setInterval(function () {
notifySize();
}, 50);
}
function SquidexPlugin() {
var initHandler;
var initCalled = false;
var contentHandler;
var content;
var context;
var timer;
function raiseContentChanged() {
if (contentHandler && content) {
contentHandler(content);
}
}
function raiseInit() {
if (initHandler && !initCalled && context) {
initHandler(context);
initCalled = true;
}
}
function eventListener(event) {
if (event.source !== window) {
var type = event.data.type;
if (type === 'contentChanged') {
content = event.data.content;
raiseContentChanged();
} else if (type === 'init') {
context = event.data.context;
raiseInit();
}
}
}
window.addEventListener('message', eventListener, false);
timer = measureAndNotifyParent();
var editor = {
/**
* Get the current value.
*/
getContext: function () {
return context;
},
/*
* Notifies the parent to navigate to the path.
*/
navigate: function (url) {
if (window.parent) {
window.parent.postMessage({ type: 'navigate', url: url }, '*');
}
},
/**
* Register the init handler.
*/
onInit: function (callback) {
initHandler = callback;
raiseInit();
},
/**
* Register the content changed handler.
*/
onContentChanged: function (callback) {
contentHandler = callback;
raiseContentChanged();
},
/**
* Clean the editor SDK.
*/
clean: function () {
if (timer) {
window.removeEventListener('message', eventListener);
timer();
}
}
};
return editor;
}
function SquidexFormField() {
var initHandler;
@ -10,7 +130,6 @@ function SquidexFormField() {
var formValue;
var context;
var timer;
var height = document.body.offsetHeight;
function raiseDisabled() {
if (disabledHandler) {
@ -63,23 +182,9 @@ function SquidexFormField() {
}
}
document.body.style.margin = '0';
document.body.style.padding = '0';
window.addEventListener('message', eventListener, false);
window.parent.postMessage({ type: 'started' }, '*');
window.parent.postMessage({ type: 'resize', height: height }, '*');
timer = setInterval(function () {
var newHeight = document.body.offsetHeight;
if (height !== newHeight) {
height = newHeight;
window.parent.postMessage({ type: 'resize', height: height }, '*');
}
}, 500);
timer = measureAndNotifyParent();
var editor = {
/**
@ -112,6 +217,15 @@ function SquidexFormField() {
}
},
/*
* Notifies the parent to navigate to the path.
*/
navigate: function (url) {
if (window.parent) {
window.parent.postMessage({ type: 'navigate', url: url }, '*');
}
},
/**
* Notifies the control container that the value has been changed.
*/

4
backend/src/Squidex/wwwroot/scripts/simple-editor.html → backend/src/Squidex/wwwroot/scripts/editor-simple.html

@ -27,7 +27,9 @@
console.error(error);
})
.then(editor => {
// When the field is instantiated it notified the UI that it has been loaded.
// When the field is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
var field = new SquidexFormField();
// Handle the value change event and set the text to the editor.

49
backend/src/Squidex/wwwroot/scripts/sidebar-content.html

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<style>
textarea {
box-sizing: border-box;
border: 0;
border-radius: 0;
resize: none;
overflow: hidden;
width: 100%;
}
</style>
</head>
<body>
<script>
function grow(element) {
element.style.height = "5px";
element.style.height = (element.scrollHeight)+"px";
}
</script>
<textarea oninput="grow(this)" name="content" id="editor"></textarea>
<script>
var element = document.getElementById('editor');
// When the plugin is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
var plugin = new SquidexPlugin();
// The content is only available when it is used as a sidebar plugin for single content items.
plugin.onContentChanged(function (content) {
element.innerHTML = JSON.stringify(content, null, 2);
grow(element);
});
</script>
</body>
</html>

49
backend/src/Squidex/wwwroot/scripts/sidebar-context.html

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<style>
textarea {
box-sizing: border-box;
border: 0;
border-radius: 0;
resize: none;
overflow: hidden;
width: 100%;
}
</style>
</head>
<body>
<script>
function grow(element) {
element.style.height = "5px";
element.style.height = (element.scrollHeight)+"px";
}
</script>
<textarea oninput="grow(this)" name="content" id="editor"></textarea>
<script>
var element = document.getElementById('editor');
// When the field is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
var plugin = new SquidexPlugin();
// Init is called once with a context that contains the app name, schema name and authentication information.
plugin.onInit(function (context) {
element.innerHTML = JSON.stringify(context, null, 2);
grow(element);
});
</script>
</body>
</html>

118
backend/src/Squidex/wwwroot/scripts/sidebar-search.html

@ -0,0 +1,118 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- Load the editor sdk from the local folder or https://cloud.squidex.io/scripts/editor-sdk.js -->
<script src="editor-sdk.js"></script>
<script src="https://cdn.jsdelivr.net/npm/algoliasearch@4.0.0/dist/algoliasearch-lite.umd.js" integrity="sha256-MfeKq2Aw9VAkaE9Caes2NOxQf6vUa8Av0JqcUXUGkd0=" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.0.0/dist/instantsearch.production.min.js" integrity="sha256-6S7q0JJs/Kx4kb/fv0oMjS855QTz5Rc2hh9AkIUjUsk=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.3.1/themes/algolia-min.css" integrity="sha256-HB49n/BZjuqiCtQQf49OdZn63XuKFaxcIHWf0HNKte8=" crossorigin="anonymous">
<style>
.container {
min-height: 400px;
}
.ais-Hits {
margin-top: 1rem;
}
.ais-Hits-item {
margin-right: 0;
margin-top: 5px;
width: 100%;
}
.button-click {
margin-top: 5px;
color: #3389ff;
cursor: pointer;
display: inline-block;
}
.button-click:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<script>
function grow(element) {
element.style.height = "5px";
element.style.height = (element.scrollHeight)+"px";
}
</script>
<div class="container">
<div id="searchbox"></div>
<div id="hits"></div>
</div>
<script>
var element = document.getElementById('editor');
// When the field is instantiated it notifies the UI that it has been loaded.
//
// Furthermore it sends the current size to the parent.
var plugin = new SquidexPlugin();
// Init is called once with a context that contains the app name, schema name and authentication information.
plugin.onInit(function (context) {
var searchClient = algoliasearch('CFNTEE51PJ', 'afa3a7605277b85348c6fa160fb5cecc');
var search = instantsearch({ indexName: 'test', searchClient });
document.addEventListener('click', event => {
if (event.target.matches('.button-click')) {
var id = event.target.getAttribute('data-object-id');
// We cannot directly navigate to in the iframe because it would only change the URL of the iframe.
plugin.navigate(`/app/${context.appName}/content/${context.schemaName}`);
// plugin.navigate(`/app/${context.appName}/content/${context.schemaName}/${id}`);
}
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: '#searchbox',
}),
instantsearch.widgets.hits({
container: '#hits',
templates: {
item(hit, bindEvent) {
return `
<div>
<div class="hit-name">
${instantsearch.highlight({
attribute: 'firstname',
hit,
})}
${instantsearch.highlight({
attribute: 'lastname',
hit,
})}
<div>
<a data-object-id="${hit.objectID}" class="button-click">EDIT</a>
</div>
</div>
</div>
`;
}
},
})
]);
search.start();
});
</script>
</body>
</html>

4
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/ConvertContent/ValueConvertersTests.cs

@ -27,8 +27,8 @@ namespace Squidex.Domain.Apps.Core.Operations.ConvertContent
public ValueConvertersTests()
{
A.CallTo(() => urlGenerator.AssetContent(appId, A<DomainId>._))
.ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<DomainId>(1)}");
A.CallTo(() => urlGenerator.AssetContent(appId, A<string>._))
.ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<string>(1)}");
}
[Fact]

98
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs

@ -67,9 +67,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId))
.Returns("content-url");
A.CallTo(() => urlGenerator.AssetContent(appId, assetId))
A.CallTo(() => urlGenerator.AssetContent(appId, assetId.ToString()))
.Returns("asset-content-url");
A.CallTo(() => urlGenerator.AssetContent(appId, "file-name"))
.Returns("asset-content-slug-url");
A.CallTo(() => user.Id)
.Returns("user123");
@ -301,6 +304,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
"Download at ${assetContentUrl()}",
"Download at {{event.id | assetContentUrl}}"
)]
[InlineData("Liquid(Download at {{event | assetContentUrl}})")]
public async Task Should_format_asset_content_url_from_event(string script)
{
var @event = new EnrichedAssetEvent { Id = assetId, AppId = appId };
@ -317,6 +321,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
"Download at ${assetContentUrl()}",
"Download at {{event.id | assetContentUrl | default: 'null'}}"
)]
[InlineData("Liquid(Download at {{event | assetContentUrl | default: 'null'}})")]
public async Task Should_return_null_when_asset_content_url_not_found(string script)
{
var @event = new EnrichedContentEvent();
@ -326,6 +331,74 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Equal("Download at null", result);
}
[Theory]
[Expressions(
"Download at $ASSET_CONTENT_APP_URL",
null,
"Download at ${assetContentAppUrl()}",
"Download at {{event.id | assetContentAppUrl | default: 'null'}}"
)]
[InlineData("Liquid(Download at {{event | assetContentAppUrl | default: 'null'}})")]
public async Task Should_format_asset_content_app_url_from_event(string script)
{
var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Download at asset-content-url", result);
}
[Theory]
[Expressions(
"Download at $ASSET_CONTENT_APP_URL",
null,
"Download at ${assetContentAppUrl()}",
"Download at {{event.id | assetContentAppUrl | default: 'null'}}"
)]
[InlineData("Liquid(Download at {{event | assetContentAppUrl | default: 'null'}})")]
public async Task Should_return_null_when_asset_content_app_url_not_found(string script)
{
var @event = new EnrichedContentEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Download at null", result);
}
[Theory]
[Expressions(
"Download at $ASSET_CONTENT_SLUG_URL",
null,
"Download at ${assetContentSlugUrl()}",
"Download at {{event.fileName | assetContentSlugUrl | default: 'null'}}"
)]
[InlineData("Liquid(Download at {{event | assetContentSlugUrl | default: 'null'}})")]
public async Task Should_format_asset_content_slug_url_from_event(string script)
{
var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" };
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Download at asset-content-slug-url", result);
}
[Theory]
[Expressions(
"Download at $ASSET_CONTENT_SLUG_URL",
null,
"Download at ${assetContentSlugUrl()}",
"Download at {{event.id | assetContentSlugUrl | default: 'null'}}"
)]
[InlineData("Liquid(Download at {{event | assetContentSlugUrl | default: 'null'}})")]
public async Task Should_return_null_when_asset_content_slug_url_not_found(string script)
{
var @event = new EnrichedContentEvent();
var result = await sut.FormatAsync(script, @event);
Assert.Equal("Download at null", result);
}
[Theory]
[Expressions(
"Go to $CONTENT_URL",
@ -629,6 +702,29 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Equal("[1,2,3]", result?.Replace(" ", string.Empty));
}
[Theory]
[Expressions(
"$CONTENT_DATA",
"${CONTENT_DATA}",
"${JSON.stringify(event.data)}",
null
)]
public async Task Should_return_json_string_when_data(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("{\"city\":{\"iv\":{\"name\":\"Berlin\"}}}", result);
}
[Theory]
[Expressions(
null,

2
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs

@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
A.CallTo(() => urlGenerator.ContentUI(appId, schemaId, contentId))
.Returns("content-url");
A.CallTo(() => urlGenerator.AssetContent(appId, assetId))
A.CallTo(() => urlGenerator.AssetContent(appId, assetId.ToString()))
.Returns("asset-content-url");
A.CallTo(() => user.Id)

18
backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs

@ -173,6 +173,24 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.MustHaveHappened();
}
[Fact]
public async Task Should_not_create_job_if_event_created_by_rule()
{
var rule = ValidRule();
var @event = Envelope.Create(new ContentCreated { FromRule = true });
var jobs = await sut.CreateJobsAsync(rule, ruleId, @event);
Assert.Empty(jobs);
A.CallTo(() => ruleTriggerHandler.Trigger(@event.Payload, rule.Trigger, ruleId))
.MustNotHaveHappened();
A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A<Envelope<AppEvent>>._))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_create_job_if_not_triggered_with_precheck()
{

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs

@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
{ patternId2, new AppPattern("Numbers", "[0-9]*") }
};
sut = new AppDomainObject(initialPatterns, Store, A.Dummy<ISemanticLog>(), appPlansProvider, appPlansBillingManager, userResolver);
sut = new AppDomainObject(Store, A.Dummy<ISemanticLog>(), initialPatterns, appPlansProvider, appPlansBillingManager, userResolver);
sut.Setup(Id);
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppSettingsSearchSourceTests.cs

@ -155,7 +155,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
var ctx = ContextWithPermission(permission.Id);
A.CallTo(() => urlGenerator.AssetsUI(appId))
A.CallTo(() => urlGenerator.AssetsUI(appId, A<string?>._))
.Returns("assets-url");
var result = await sut.SearchAsync("assets", ctx);

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

@ -15,6 +15,7 @@ using Orleans;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
@ -31,6 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>();
private readonly IAssetMetadataSource assetMetadataSource = A.Fake<IAssetMetadataSource>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly IContextProvider contextProvider = A.Fake<IContextProvider>();
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly IServiceProvider serviceProvider = A.Fake<IServiceProvider>();
@ -54,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
file = new NoopAssetFile();
var assetDomainObject = new AssetDomainObject(Store, tagService, assetQuery, A.Dummy<ISemanticLog>());
var assetDomainObject = new AssetDomainObject(Store, A.Dummy<ISemanticLog>(), tagService, assetQuery, contentRepository);
A.CallTo(() => serviceProvider.GetService(typeof(AssetDomainObject)))
.Returns(assetDomainObject);

30
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs

@ -13,6 +13,7 @@ using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
@ -25,6 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetDomainObjectTests : HandlerTestBase<AssetState>
{
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly DomainId parentId = DomainId.NewGuid();
@ -45,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>._, A<HashSet<string>>._))
.ReturnsLazily(x => Task.FromResult(x.GetArgument<HashSet<string>>(2)?.ToDictionary(x => x)!));
sut = new AssetDomainObject(Store, tagService, assetQuery, A.Dummy<ISemanticLog>());
sut = new AssetDomainObject(Store, A.Dummy<ISemanticLog>(), tagService, assetQuery, contentRepository);
sut.Setup(Id);
}
@ -245,6 +247,32 @@ namespace Squidex.Domain.Apps.Entities.Assets
);
}
[Fact]
public async Task Delete_should_throw_exception_if_referenced_by_other_item()
{
var command = new DeleteAsset { CheckReferrers = true };
await ExecuteCreateAsync();
A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id))
.Returns(true);
await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command));
}
[Fact]
public async Task Delete_should_not_throw_exception_if_referenced_by_other_item_but_forced()
{
var command = new DeleteAsset();
await ExecuteCreateAsync();
A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id))
.Returns(true);
await PublishAsync(command);
}
private Task ExecuteCreateAsync()
{
return PublishAsync(new CreateAsset { File = file });

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

@ -36,7 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId))
.Returns(new List<IAssetFolderEntity> { A.Fake<IAssetFolderEntity>() });
sut = new AssetFolderDomainObject(Store, assetQuery, A.Dummy<ISemanticLog>());
sut = new AssetFolderDomainObject(Store, A.Dummy<ISemanticLog>(), assetQuery);
sut.Setup(Id);
}

82
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs

@ -16,6 +16,7 @@ using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers;
@ -34,6 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly IAppEntity app;
private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(x => x.Wrapping(new DefaultContentWorkflow()));
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly ISchemaEntity schema;
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
@ -104,9 +106,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
patched = patch.MergeInto(data);
var context = new ContentOperationContext(appProvider, Enumerable.Repeat(new DefaultValidatorsFactory(), 1), scriptEngine, A.Fake<ISemanticLog>());
var validators = Enumerable.Repeat(new DefaultValidatorsFactory(), 1);
sut = new ContentDomainObject(Store, contentWorkflow, context, A.Dummy<ISemanticLog>());
var context = new ContentOperationContext(appProvider, validators, scriptEngine, A.Fake<ISemanticLog>());
sut = new ContentDomainObject(Store, A.Dummy<ISemanticLog>(), contentWorkflow, contentRepository, context);
sut.Setup(Id);
}
@ -124,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var command = new CreateContent { Data = data };
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -147,7 +151,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var command = new CreateContent { Data = data, Publish = true };
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -170,7 +174,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{
var command = new CreateContent { Data = invalidData };
await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(CreateContentCommand(command)));
await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(command));
}
[Fact]
@ -224,7 +228,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -248,7 +252,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecutePublishAsync();
await ExecuteCreateDraftAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -270,7 +274,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -287,7 +291,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync();
await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(CreateContentCommand(command)));
await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(command));
}
[Fact]
@ -297,7 +301,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -321,7 +325,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecutePublishAsync();
await ExecuteCreateDraftAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -343,7 +347,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -360,7 +364,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -382,7 +386,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -405,7 +409,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync();
await ExecutePublishAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -429,7 +433,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecutePublishAsync();
await ExecuteCreateDraftAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -453,7 +457,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -484,7 +488,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>._, Status.Draft, Status.Archived, User))
.Returns(true);
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -512,7 +516,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>._, Status.Draft, Status.Published, User))
.Returns(false);
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -534,7 +538,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(new EntitySavedResult(1));
@ -549,6 +553,32 @@ namespace Squidex.Domain.Apps.Entities.Contents
.MustHaveHappened();
}
[Fact]
public async Task Delete_should_throw_exception_if_referenced_by_other_item()
{
var command = new DeleteContent { CheckReferrers = true };
await ExecuteCreateAsync();
A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id))
.Returns(true);
await Assert.ThrowsAsync<DomainException>(() => PublishAsync(command));
}
[Fact]
public async Task Delete_should_not_throw_exception_if_referenced_by_other_item_but_forced()
{
var command = new DeleteContent();
await ExecuteCreateAsync();
A.CallTo(() => contentRepository.HasReferrersAsync(AppId, Id))
.Returns(true);
await PublishAsync(command);
}
[Fact]
public async Task CreateDraft_should_create_events_and_update_new_state()
{
@ -557,7 +587,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync();
await ExecutePublishAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot);
@ -578,7 +608,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecutePublishAsync();
await ExecuteCreateDraftAsync();
var result = await PublishAsync(CreateContentCommand(command));
var result = await PublishAsync(command);
result.ShouldBeEquivalent(new EntitySavedResult(3));
@ -592,22 +622,22 @@ namespace Squidex.Domain.Apps.Entities.Contents
private Task ExecuteCreateAsync()
{
return PublishAsync(CreateContentCommand(new CreateContent { Data = data }));
return PublishAsync(new CreateContent { Data = data });
}
private Task ExecuteUpdateAsync()
{
return PublishAsync(CreateContentCommand(new UpdateContent { Data = otherData }));
return PublishAsync(new UpdateContent { Data = otherData });
}
private Task ExecuteCreateDraftAsync()
{
return PublishAsync(CreateContentCommand(new CreateContentDraft()));
return PublishAsync(new CreateContentDraft());
}
private Task ExecuteChangeStatusAsync(Status status, Instant? dueTime = null)
{
return PublishAsync(CreateContentCommand(new ChangeContentStatus { Status = status, DueTime = dueTime }));
return PublishAsync(new ChangeContentStatus { Status = status, DueTime = dueTime });
}
private Task ExecuteDeleteAsync()

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs

@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public GraphQLMutationTests()
{
content = TestContent.Create(schemaId, contentId, schemaRefId1.Id, schemaRefId2.Id, null);
content = TestContent.Create(appId, schemaId, contentId, schemaRefId1.Id, schemaRefId2.Id, null);
A.CallTo(() => commandBus.PublishAsync(A<ICommand>.Ignored))
.Returns(commandContext);

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

@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}".Replace("<FIELDS>", TestAsset.AllFields);
var asset = TestAsset.Create(DomainId.NewGuid());
var asset = TestAsset.Create(appId, DomainId.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.HasOData("?$top=30&$skip=5&$filter=my-query")))
.Returns(ResultList.CreateFrom(0, asset));
@ -80,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}".Replace("<FIELDS>", TestAsset.AllFields);
var asset = TestAsset.Create(DomainId.NewGuid());
var asset = TestAsset.Create(appId, DomainId.NewGuid());
A.CallTo(() => assetQuery.QueryAsync(MatchsAssetContext(), null, A<Q>.That.HasOData("?$top=30&$skip=5&$filter=my-query")))
.Returns(ResultList.CreateFrom(10, asset));
@ -137,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public async Task Should_return_single_asset_when_finding_asset()
{
var assetId = DomainId.NewGuid();
var asset = TestAsset.Create(assetId);
var asset = TestAsset.Create(appId, assetId);
var query = @"
query {
@ -195,7 +195,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}";
var content = TestContent.Create(schemaId, DomainId.NewGuid(), DomainId.Empty, DomainId.Empty);
var content = TestContent.Create(appId, schemaId, DomainId.NewGuid(), DomainId.Empty, DomainId.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A<Q>.That.HasOData("?$top=30&$skip=5")))
.Returns(ResultList.CreateFrom(0, content));
@ -273,7 +273,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}".Replace("<FIELDS>", TestContent.AllFields);
var content = TestContent.Create(schemaId, DomainId.NewGuid(), DomainId.Empty, DomainId.Empty);
var content = TestContent.Create(appId, schemaId, DomainId.NewGuid(), DomainId.Empty, DomainId.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A<Q>.That.HasOData("?$top=30&$skip=5")))
.Returns(ResultList.CreateFrom(0, content));
@ -307,7 +307,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}".Replace("<FIELDS>", TestContent.AllFields);
var content = TestContent.Create(schemaId, DomainId.NewGuid(), DomainId.Empty, DomainId.Empty);
var content = TestContent.Create(appId, schemaId, DomainId.NewGuid(), DomainId.Empty, DomainId.Empty);
A.CallTo(() => contentQuery.QueryAsync(MatchsContentContext(), schemaId.Id.ToString(), A<Q>.That.HasOData("?$top=30&$skip=5")))
.Returns(ResultList.CreateFrom(10, content));
@ -364,7 +364,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public async Task Should_return_single_content_when_finding_content()
{
var contentId = DomainId.NewGuid();
var content = TestContent.Create(schemaId, contentId, DomainId.Empty, DomainId.Empty);
var content = TestContent.Create(appId, schemaId, contentId, DomainId.Empty, DomainId.Empty);
var query = @"
query {
@ -396,7 +396,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var contentRef = TestContent.CreateRef(schemaRefId1, contentRefId, "ref1-field", "ref1");
var contentId = DomainId.NewGuid();
var content = TestContent.Create(schemaId, contentId, contentRefId, DomainId.Empty);
var content = TestContent.Create(appId, schemaId, contentId, contentRefId, DomainId.Empty);
var query = @"
query {
@ -466,7 +466,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var contentRef = TestContent.CreateRef(schemaRefId1, contentRefId, "ref1-field", "ref1");
var contentId = DomainId.NewGuid();
var content = TestContent.Create(schemaId, contentId, contentRefId, DomainId.Empty);
var content = TestContent.Create(appId, schemaId, contentId, contentRefId, DomainId.Empty);
var query = @"
query {
@ -539,10 +539,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public async Task Should_also_fetch_referenced_assets_when_field_is_included_in_query()
{
var assetRefId = DomainId.NewGuid();
var assetRef = TestAsset.Create(assetRefId);
var assetRef = TestAsset.Create(appId, assetRefId);
var contentId = DomainId.NewGuid();
var content = TestContent.Create(schemaId, contentId, DomainId.Empty, assetRefId);
var content = TestContent.Create(appId, schemaId, contentId, DomainId.Empty, assetRefId);
var query = @"
query {
@ -598,8 +598,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var assetId1 = DomainId.NewGuid();
var assetId2 = DomainId.NewGuid();
var asset1 = TestAsset.Create(assetId1);
var asset2 = TestAsset.Create(assetId2);
var asset1 = TestAsset.Create(appId, assetId1);
var asset2 = TestAsset.Create(appId, assetId2);
var query1 = @"
query {
@ -653,7 +653,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
public async Task Should_not_return_data_when_field_not_part_of_content()
{
var contentId = DomainId.NewGuid();
var content = TestContent.Create(schemaId, contentId, DomainId.Empty, DomainId.Empty, new NamedContentData());
var content = TestContent.Create(appId, schemaId, contentId, DomainId.Empty, DomainId.Empty, new NamedContentData());
var query = @"
query {

7
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestAsset.cs

@ -42,13 +42,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
metadata
slug";
public static IEnrichedAssetEntity Create(DomainId id)
public static IEnrichedAssetEntity Create(NamedId<DomainId> appId, DomainId id)
{
var now = SystemClock.Instance.GetCurrentInstant();
var asset = new AssetEntity
{
Id = id,
AppId = appId,
Version = 1,
Created = now,
CreatedBy = new RefToken(RefTokenType.Subject, "user1"),
@ -86,8 +87,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
createdBy = asset.CreatedBy.ToString(),
lastModified = asset.LastModified,
lastModifiedBy = asset.LastModifiedBy.ToString(),
url = $"assets/{asset.Id}",
thumbnailUrl = $"assets/{asset.Id}?width=100",
url = $"assets/{asset.AppId.Name}/{asset.Id}",
thumbnailUrl = $"assets/{asset.AppId.Name}/{asset.Id}?width=100",
sourceUrl = $"assets/source/{asset.Id}",
mimeType = asset.MimeType,
fileName = asset.FileName,

3
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/TestContent.cs

@ -68,7 +68,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}";
public static IEnrichedContentEntity Create(NamedId<DomainId> schemaId, DomainId id, DomainId refId, DomainId assetId, NamedContentData? data = null)
public static IEnrichedContentEntity Create(NamedId<DomainId> appId, NamedId<DomainId> schemaId, DomainId id, DomainId refId, DomainId assetId, NamedContentData? data = null)
{
var now = SystemClock.Instance.GetCurrentInstant();
@ -131,6 +131,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var content = new ContentEntity
{
Id = id,
AppId = appId,
Version = 1,
Created = now,
CreatedBy = new RefToken(RefTokenType.Subject, "user1"),

4
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ResolveAssetsTests.cs

@ -53,8 +53,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
})
.SetFieldsInLists("asset1", "asset2");
A.CallTo(() => urlGenerator.AssetContent(appId, A<DomainId>._))
.ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<DomainId>(1)}");
A.CallTo(() => urlGenerator.AssetContent(appId, A<string>._))
.ReturnsLazily(ctx => $"url/to/{ctx.GetArgument<string>(1)}");
schemaProvider = x =>
{

13
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs

@ -16,9 +16,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData
{
public bool CanGenerateAssetSourceUrl { get; } = true;
public string? AssetThumbnail(NamedId<DomainId> appId, DomainId assetId, AssetType assetType)
public string? AssetThumbnail(NamedId<DomainId> appId, string idOrSlug, AssetType assetType)
{
return $"assets/{assetId}?width=100";
return $"assets/{appId.Name}/{idOrSlug}?width=100";
}
public string? AssetSource(NamedId<DomainId> appId, DomainId assetId, long fileVersion)
@ -26,9 +26,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData
return $"assets/source/{assetId}";
}
public string AssetContent(NamedId<DomainId> appId, DomainId assetId)
public string AssetContent(NamedId<DomainId> appId, string idOrSlug)
{
return $"assets/{assetId}";
return $"assets/{appId.Name}/{idOrSlug}";
}
public string ContentUI(NamedId<DomainId> appId, NamedId<DomainId> schemaId, DomainId contentId)
@ -41,11 +41,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData
throw new NotSupportedException();
}
public string AssetsUI(NamedId<DomainId> appId)
{
throw new NotSupportedException();
}
public string AssetsUI(NamedId<DomainId> appId, string? query = null)
{
throw new NotSupportedException();

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs

@ -164,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.Rules
Assert.Null(result);
A.CallTo(() => ruleEnqueuer.Enqueue(sut.Snapshot.RuleDef, sut.Snapshot.Id,
A.CallTo(() => ruleEnqueuer.EnqueueAsync(sut.Snapshot.RuleDef, sut.UniqueId,
A<Envelope<IEvent>>.That.Matches(x => x.Payload is RuleManuallyTriggered)))
.MustHaveHappened();
}

5
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs

@ -86,13 +86,10 @@ namespace Squidex.Domain.Apps.Entities.Rules
A.CallTo(() => ruleService.CreateJobsAsync(rule.RuleDef, rule.Id, @event, true))
.Returns(new List<(RuleJob, Exception?)> { (job, null) });
await sut.Enqueue(rule.RuleDef, rule.Id, @event);
await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event);
A.CallTo(() => ruleEventRepository.EnqueueAsync(job, now, default))
.MustHaveHappened();
A.CallTo(() => localCache.StartContext())
.MustHaveHappened();
}
[Fact]

60
backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs

@ -0,0 +1,60 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading;
using System.Threading.Tasks;
using Orleans;
using Orleans.TestingHost;
using Xunit;
namespace Squidex.Infrastructure.Orleans
{
[Trait("Category", "Dependencies")]
public class AsyncLocalTests
{
public interface IAsyncLocalGrain : IGrainWithStringKey
{
public Task<int> GetValueAsync();
}
public class AsyncLocalGrain : Grain, IAsyncLocalGrain
{
private readonly AsyncLocal<int> temp = new AsyncLocal<int>();
public Task<int> GetValueAsync()
{
temp.Value++;
return Task.FromResult(temp.Value);
}
}
[Fact]
public async Task Should_use_async_local()
{
var cluster =
new TestClusterBuilder(1)
.Build();
await cluster.DeployAsync();
var grain = cluster.GrainFactory.GetGrain<IAsyncLocalGrain>(SingleGrain.Id);
var result1 = await grain.GetValueAsync();
var result2 = await grain.GetValueAsync();
await cluster.KillSiloAsync(cluster.Silos[0]);
await cluster.StartAdditionalSiloAsync();
var result3 = await grain.GetValueAsync();
Assert.Equal(1, result1);
Assert.Equal(1, result2);
Assert.Equal(1, result3);
}
}
}

1
frontend/app/features/content/declarations.ts

@ -16,6 +16,7 @@ export * from './pages/contents/contents-filters-page.component';
export * from './pages/contents/contents-page.component';
export * from './pages/contents/custom-view-editor.component';
export * from './pages/schemas/schemas-page.component';
export * from './pages/sidebar/sidebar-page.component';
export * from './shared/content-status.component';
export * from './shared/due-time-selector.component';
export * from './shared/forms/array-editor.component';

15
frontend/app/features/content/module.ts

@ -10,7 +10,7 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule, UnsetContentGuard } from '@app/shared';
import { ArrayEditorComponent, ArrayItemComponent, ArraySectionComponent, AssetsEditorComponent, CommentsPageComponent, ContentComponent, ContentCreatorComponent, ContentEventComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentPageComponent, ContentSectionComponent, ContentSelectorComponent, ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, StockPhotoEditorComponent } from './declarations';
import { ArrayEditorComponent, ArrayItemComponent, ArraySectionComponent, AssetsEditorComponent, CommentsPageComponent, ContentComponent, ContentCreatorComponent, ContentEventComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentPageComponent, ContentSectionComponent, ContentSelectorComponent, ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations';
const routes: Routes = [
{
@ -28,12 +28,16 @@ const routes: Routes = [
{
path: '',
component: ContentsPageComponent,
canActivate: [SchemaMustNotBeSingletonGuard],
canActivate: [SchemaMustNotBeSingletonGuard, UnsetContentGuard],
canDeactivate: [CanDeactivateGuard],
children: [
{
path: 'filters',
component: ContentsFiltersPageComponent
},
{
path: 'sidebar',
component: SidebarPageComponent
}
]
},
@ -59,7 +63,11 @@ const routes: Routes = [
{
path: 'comments',
component: CommentsPageComponent
}
},
{
path: 'sidebar',
component: SidebarPageComponent
}
]
}
]
@ -105,6 +113,7 @@ const routes: Routes = [
ReferenceItemComponent,
ReferencesEditorComponent,
SchemasPageComponent,
SidebarPageComponent,
StockPhotoEditorComponent
]
})

10
frontend/app/features/content/pages/comments/comments-page.component.html

@ -1 +1,9 @@
<sqx-comments [commentsId]="commentsId | async"></sqx-comments>
<sqx-panel desiredWidth="20rem" isBlank="true" [isLazyLoaded]="false" grid="true">
<ng-container title>
{{ 'comments.title' | sqxTranslate }}
</ng-container>
<ng-container content>
<sqx-comments [commentsId]="commentsId | async"></sqx-comments>
</ng-container>
</sqx-panel>

11
frontend/app/features/content/pages/content/content-field.component.ts

@ -7,8 +7,8 @@
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AppLanguageDto, AppsState, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, Settings, StringFieldPropertiesDto, TranslationsService, Types, value$ } from '@app/shared';
import { Observable } from 'rxjs';
import { combineLatest } from 'rxjs/operators';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'sqx-content-field',
@ -78,9 +78,10 @@ export class ContentFieldComponent implements OnChanges {
if ((changes['formModel'] || changes['formModelCompare']) && this.formModelCompare) {
this.isDifferent =
value$(this.formModel.form).pipe(
combineLatest(value$(this.formModelCompare!.form),
(lhs, rhs) => !Types.equals(lhs, rhs, true)));
combineLatest([
value$(this.formModel.form),
value$(this.formModelCompare!.form)
]).pipe(map(([lhs, rhs]) => !Types.equals(lhs, rhs, true)));
}
}

9
frontend/app/features/content/pages/content/content-history-page.component.html

@ -36,7 +36,8 @@
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDraftDelete"
(sqxConfirmClick)="deleteDraft()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteVersionConfirmText">
confirmText="i18n:contents.deleteVersionConfirmText"
confirmRememberKey="deleteDraft">
{{ 'contents.versionDelete' | sqxTranslate }}
</a>
@ -45,7 +46,8 @@
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText">
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</div>
@ -85,7 +87,8 @@
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!content.canDelete"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText">
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</div>

7
frontend/app/features/content/pages/content/content-page.component.html

@ -36,7 +36,8 @@
<a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText">
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</div>
@ -104,6 +105,10 @@
<a class="panel-link" routerLink="comments" routerLinkActive="active" queryParamsHandling="preserve" title="i18n:common.comments" titlePosition="left">
<i class="icon-comments"></i>
</a>
<a class="panel-link" routerLink="sidebar" routerLinkActive="active" queryParamsHandling="preserve" title="i18n:common.sidebar" titlePosition="left" *ngIf="schema.properties.contentSidebarUrl">
<i class="icon-plugin"></i>
</a>
<sqx-onboarding-tooltip helpId="history" [for]="linkHistory" position="left-top" after="120000">
{{ 'common.sidebarTour' | sqxTranslate }}

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

Loading…
Cancel
Save