Browse Source

Merge branch 'release/4.x' of github.com:Squidex/squidex into release/4.x

pull/585/head
Sebastian 5 years ago
parent
commit
4b4f4ed54b
  1. 46
      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. 27
      backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs
  6. 5
      backend/i18n/frontend_en.json
  7. 5
      backend/i18n/frontend_it.json
  8. 5
      backend/i18n/frontend_nl.json
  9. 2
      backend/i18n/source/backend_en.json
  10. 5
      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. 5
      backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs
  14. 7
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
  15. 6
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs
  16. 32
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Fields.cs
  17. 10
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs
  18. 12
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  19. 32
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs
  20. 11
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs
  21. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  22. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs
  23. 17
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs
  24. 45
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs
  25. 4
      backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs
  26. 35
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs
  27. 3
      backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs
  28. 1
      backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs
  29. 1
      backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs
  30. 21
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  31. 8
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  32. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs
  33. 4
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs
  34. 2
      backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs
  35. 2
      backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs
  36. 8
      backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs
  37. 2
      backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs
  38. 6
      backend/src/Squidex.Shared/Texts.it.resx
  39. 6
      backend/src/Squidex.Shared/Texts.nl.resx
  40. 6
      backend/src/Squidex.Shared/Texts.resx
  41. 5
      backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs
  42. 5
      backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs
  43. 23
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs
  44. 18
      backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs
  45. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs
  46. 4
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  47. 30
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs
  48. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs
  49. 82
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs
  50. 9
      frontend/app/features/content/pages/content/content-history-page.component.html
  51. 3
      frontend/app/features/content/pages/content/content-page.component.html
  52. 7
      frontend/app/features/content/pages/content/content-page.component.ts
  53. 3
      frontend/app/features/content/pages/contents/contents-page.component.html
  54. 8
      frontend/app/features/content/pages/contents/contents-page.component.ts
  55. 4
      frontend/app/features/content/pages/sidebar/sidebar-page.component.ts
  56. 2
      frontend/app/features/content/shared/due-time-selector.component.ts
  57. 3
      frontend/app/features/content/shared/list/content.component.html
  58. 3
      frontend/app/features/content/shared/references/reference-item.component.html
  59. 3
      frontend/app/features/dashboard/pages/dashboard-config.component.html
  60. 4
      frontend/app/features/rules/pages/rules/actions/generic-action.component.html
  61. 9
      frontend/app/features/rules/pages/rules/rule.component.html
  62. 6
      frontend/app/features/schemas/pages/schema/fields/field.component.html
  63. 3
      frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.html
  64. 2
      frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts
  65. 3
      frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html
  66. 3
      frontend/app/features/schemas/pages/schema/schema-page.component.html
  67. 3
      frontend/app/features/settings/pages/backups/backup.component.html
  68. 3
      frontend/app/features/settings/pages/clients/client.component.html
  69. 3
      frontend/app/features/settings/pages/contributors/contributor.component.html
  70. 4
      frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts
  71. 3
      frontend/app/features/settings/pages/languages/language.component.html
  72. 3
      frontend/app/features/settings/pages/patterns/pattern.component.html
  73. 1
      frontend/app/features/settings/pages/plans/plan.component.html
  74. 3
      frontend/app/features/settings/pages/roles/role.component.html
  75. 24
      frontend/app/features/settings/pages/workflows/workflow.component.html
  76. 5
      frontend/app/framework/angular/forms/confirm-click.directive.ts
  77. 4
      frontend/app/framework/angular/forms/editors/date-time-editor.component.ts
  78. 2
      frontend/app/framework/angular/forms/forms-helper.ts
  79. 2
      frontend/app/framework/angular/forms/indeterminate-value.directive.ts
  80. 2
      frontend/app/framework/angular/forms/transform-input.directive.ts
  81. 14
      frontend/app/framework/angular/modals/dialog-renderer.component.html
  82. 4
      frontend/app/framework/angular/modals/dialog-renderer.component.ts
  83. 2
      frontend/app/framework/angular/template-wrapper.directive.ts
  84. 131
      frontend/app/framework/services/dialog.service.spec.ts
  85. 58
      frontend/app/framework/services/dialog.service.ts
  86. 2
      frontend/app/framework/services/resource-loader.service.ts
  87. 2
      frontend/app/framework/utils/date-time.ts
  88. 10
      frontend/app/framework/utils/rxjs-extensions.ts
  89. 3
      frontend/app/shared/components/assets/asset-dialog.component.html
  90. 3
      frontend/app/shared/components/assets/asset-folder.component.html
  91. 12
      frontend/app/shared/components/assets/asset.component.html
  92. 1
      frontend/app/shared/components/comments/comment.component.html
  93. 2
      frontend/app/shared/components/forms/references-checkboxes.component.ts
  94. 2
      frontend/app/shared/components/forms/references-dropdown.component.ts
  95. 2
      frontend/app/shared/components/forms/references-tags.component.ts
  96. 2
      frontend/app/shared/guards/load-apps.guard.ts
  97. 2
      frontend/app/shared/guards/load-languages.guard.ts
  98. 8
      frontend/app/shared/interceptors/auth.interceptor.ts
  99. 4
      frontend/app/shared/services/assets.service.spec.ts
  100. 4
      frontend/app/shared/services/assets.service.ts

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

@ -5,7 +5,6 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.HandleRules;
@ -13,11 +12,10 @@ using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Extensions.Actions.Comment 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 const string Description = "Send a Comment";
private readonly ICommandBus commandBus; private readonly ICommandBus commandBus;
@ -30,56 +28,48 @@ namespace Squidex.Extensions.Actions.Comment
this.commandBus = commandBus; 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) 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)) if (!string.IsNullOrEmpty(action.Client))
{ {
actor = new RefToken(RefTokenType.Client, action.Client); ruleJob.Actor = new RefToken(RefTokenType.Client, action.Client);
} }
else
var ruleJob = new CommentJob
{ {
AppId = contentEvent.AppId, ruleJob.Actor = contentEvent.Actor;
Actor = actor, }
CommentsId = contentEvent.Id.ToString(),
Text = text ruleJob.CommentsId = contentEvent.Id.ToString();
};
return (Description, ruleJob); 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 (string.IsNullOrWhiteSpace(job.CommentsId))
{ {
return Result.Ignored(); return Result.Ignored();
} }
var command = SimpleMapper.Map(job, new CreateComment()); var command = job;
command.FromRule = true;
await commandBus.PublishAsync(command); await commandBus.PublishAsync(command);
return Result.Success($"Commented: {job.Text}"); return Result.Success($"Commented: {job.Text}");
} }
} }
public sealed class CommentJob
{
public NamedId<Guid> 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>();
}
}
}

27
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.Domain.Apps.Entities.Comments.Commands;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared.Users; using Squidex.Shared.Users;
namespace Squidex.Extensions.Actions.Notification 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 const string Description = "Send a Notification";
private static readonly NamedId<Guid> NoApp = NamedId.Of(Guid.Empty, "none"); private static readonly NamedId<Guid> NoApp = NamedId.Of(Guid.Empty, "none");
@ -36,7 +35,7 @@ namespace Squidex.Extensions.Actions.Notification
this.userResolver = userResolver; 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) if (@event is EnrichedUserEventBase userEvent)
{ {
@ -56,7 +55,7 @@ namespace Squidex.Extensions.Actions.Notification
throw new InvalidOperationException($"Cannot find user by '{action.User}'"); 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)) if (!string.IsNullOrWhiteSpace(action.Url))
{ {
@ -71,32 +70,24 @@ namespace Squidex.Extensions.Actions.Notification
return (Description, ruleJob); 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 (string.IsNullOrWhiteSpace(job.CommentsId))
{ {
return Result.Ignored(); 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); await commandBus.PublishAsync(command);
return Result.Success($"Notified: {job.Text}"); 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; }
}
} }

5
backend/i18n/frontend_en.json

@ -58,6 +58,8 @@
"assets.deleteFolderConfirmTitle": "Delete folder", "assets.deleteFolderConfirmTitle": "Delete folder",
"assets.deleteMetadataConfirmText": "Do you really want to remove this metadata?", "assets.deleteMetadataConfirmText": "Do you really want to remove this metadata?",
"assets.deleteMetadataConfirmTitle": "Remove 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.downloadVersion": "Download this Version",
"assets.dropToUpdate": "Drop to update", "assets.dropToUpdate": "Drop to update",
"assets.duplicateFile": "Asset has already been uploaded.", "assets.duplicateFile": "Asset has already been uploaded.",
@ -286,6 +288,7 @@
"common.queryOperators.ne": "is not equals to", "common.queryOperators.ne": "is not equals to",
"common.queryOperators.startsWith": "starts with", "common.queryOperators.startsWith": "starts with",
"common.refresh": "Refresh", "common.refresh": "Refresh",
"common.remember": "Remember my decision",
"common.rename": "Rename", "common.rename": "Rename",
"common.requiredHint": "required", "common.requiredHint": "required",
"common.reset": "Reset", "common.reset": "Reset",
@ -354,6 +357,8 @@
"contents.deleteConfirmTitle": "Delete content", "contents.deleteConfirmTitle": "Delete content",
"contents.deleteFailed": "Failed to delete content. Please reload.", "contents.deleteFailed": "Failed to delete content. Please reload.",
"contents.deleteManyConfirmText": "Do you really want to delete the selected content items?", "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.deleteVersionConfirmText": "Do you really want to delete this version?",
"contents.deleteVersionFailed": "Failed to delete version. Please reload.", "contents.deleteVersionFailed": "Failed to delete version. Please reload.",
"contents.draftNew": "New Draft", "contents.draftNew": "New Draft",

5
backend/i18n/frontend_it.json

@ -58,6 +58,8 @@
"assets.deleteFolderConfirmTitle": "Elimina la cartella", "assets.deleteFolderConfirmTitle": "Elimina la cartella",
"assets.deleteMetadataConfirmText": "Sei sicuro di voler rimuovere questi metadati?", "assets.deleteMetadataConfirmText": "Sei sicuro di voler rimuovere questi metadati?",
"assets.deleteMetadataConfirmTitle": "Rimuovi 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.downloadVersion": "Scarica questa versione",
"assets.dropToUpdate": "Trascina il file per aggiornare", "assets.dropToUpdate": "Trascina il file per aggiornare",
"assets.duplicateFile": "La risorsa è già stata caricata.", "assets.duplicateFile": "La risorsa è già stata caricata.",
@ -286,6 +288,7 @@
"common.queryOperators.ne": "è uguale a", "common.queryOperators.ne": "è uguale a",
"common.queryOperators.startsWith": "inizia con", "common.queryOperators.startsWith": "inizia con",
"common.refresh": "Aggiorna", "common.refresh": "Aggiorna",
"common.remember": "Remember my decision",
"common.rename": "Rinomina", "common.rename": "Rinomina",
"common.requiredHint": "obbligatorio", "common.requiredHint": "obbligatorio",
"common.reset": "Reimposta", "common.reset": "Reimposta",
@ -354,6 +357,8 @@
"contents.deleteConfirmTitle": "Elimina il contenuto", "contents.deleteConfirmTitle": "Elimina il contenuto",
"contents.deleteFailed": "Non è stato possibile eliminare il contenuto. Per favore ricarica.", "contents.deleteFailed": "Non è stato possibile eliminare il contenuto. Per favore ricarica.",
"contents.deleteManyConfirmText": "Sei sicuro di voler eliminare gli elementi del contenuto selezionati?", "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.deleteVersionConfirmText": "Do you really want to delete this version?",
"contents.deleteVersionFailed": "Non è stato possibile eliminare la versione. Per favore ricarica.", "contents.deleteVersionFailed": "Non è stato possibile eliminare la versione. Per favore ricarica.",
"contents.draftNew": "Nuova bozza", "contents.draftNew": "Nuova bozza",

5
backend/i18n/frontend_nl.json

@ -58,6 +58,8 @@
"assets.deleteFolderConfirmTitle": "Map verwijderen", "assets.deleteFolderConfirmTitle": "Map verwijderen",
"assets.deleteMetadataConfirmText": "Wil je deze metadata echt verwijderen?", "assets.deleteMetadataConfirmText": "Wil je deze metadata echt verwijderen?",
"assets.deleteMetadataConfirmTitle": "Metadata 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.downloadVersion": "Download deze versie",
"assets.dropToUpdate": "Zet neer om te updaten", "assets.dropToUpdate": "Zet neer om te updaten",
"assets.duplicateFile": "Asset is al geüpload.", "assets.duplicateFile": "Asset is al geüpload.",
@ -286,6 +288,7 @@
"common.queryOperators.ne": "is not equals to", "common.queryOperators.ne": "is not equals to",
"common.queryOperators.startsWith": "starts with", "common.queryOperators.startsWith": "starts with",
"common.refresh": "Vernieuwen", "common.refresh": "Vernieuwen",
"common.remember": "Remember my decision",
"common.rename": "Hernoemen", "common.rename": "Hernoemen",
"common.requiredHint": "verplicht", "common.requiredHint": "verplicht",
"common.reset": "Reset", "common.reset": "Reset",
@ -354,6 +357,8 @@
"contents.deleteConfirmTitle": "Inhoud verwijderen", "contents.deleteConfirmTitle": "Inhoud verwijderen",
"contents.deleteFailed": "Verwijderen van inhoud is mislukt. Laad opnieuw.", "contents.deleteFailed": "Verwijderen van inhoud is mislukt. Laad opnieuw.",
"contents.deleteManyConfirmText": "Weet je zeker dat je de geselecteerde inhoudsitems wilt verwijderen?", "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.deleteVersionConfirmText": "Wil je deze versie echt verwijderen?",
"contents.deleteVersionFailed": "Verwijderen van versie is mislukt. Laad opnieuw.", "contents.deleteVersionFailed": "Verwijderen van versie is mislukt. Laad opnieuw.",
"contents.draftNew": "Nieuw concept", "contents.draftNew": "Nieuw concept",

2
backend/i18n/source/backend_en.json

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

5
backend/i18n/source/frontend_en.json

@ -58,6 +58,8 @@
"assets.deleteFolderConfirmTitle": "Delete folder", "assets.deleteFolderConfirmTitle": "Delete folder",
"assets.deleteMetadataConfirmText": "Do you really want to remove this metadata?", "assets.deleteMetadataConfirmText": "Do you really want to remove this metadata?",
"assets.deleteMetadataConfirmTitle": "Remove 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.downloadVersion": "Download this Version",
"assets.dropToUpdate": "Drop to update", "assets.dropToUpdate": "Drop to update",
"assets.duplicateFile": "Asset has already been uploaded.", "assets.duplicateFile": "Asset has already been uploaded.",
@ -286,6 +288,7 @@
"common.queryOperators.ne": "is not equals to", "common.queryOperators.ne": "is not equals to",
"common.queryOperators.startsWith": "starts with", "common.queryOperators.startsWith": "starts with",
"common.refresh": "Refresh", "common.refresh": "Refresh",
"common.remember": "Remember my decision",
"common.rename": "Rename", "common.rename": "Rename",
"common.requiredHint": "required", "common.requiredHint": "required",
"common.reset": "Reset", "common.reset": "Reset",
@ -354,6 +357,8 @@
"contents.deleteConfirmTitle": "Delete content", "contents.deleteConfirmTitle": "Delete content",
"contents.deleteFailed": "Failed to delete content. Please reload.", "contents.deleteFailed": "Failed to delete content. Please reload.",
"contents.deleteManyConfirmText": "Do you really want to delete the selected content items?", "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.deleteVersionConfirmText": "Do you really want to delete this version?",
"contents.deleteVersionFailed": "Failed to delete version. Please reload.", "contents.deleteVersionFailed": "Failed to delete version. Please reload.",
"contents.draftNew": "New Draft", "contents.draftNew": "New Draft",

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

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

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>(); var typed = @event.To<AppEvent>();
if (typed.Payload.FromRule)
{
return result;
}
var actionType = rule.Action.GetType(); var actionType = rule.Action.GetType();
if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler))

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Jint; using Jint;
@ -12,6 +13,7 @@ using Jint.Native;
using Jint.Native.Object; using Jint.Native.Object;
using Jint.Runtime; using Jint.Runtime;
using Jint.Runtime.Descriptors; using Jint.Runtime.Descriptors;
using Orleans;
using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -116,6 +118,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
var propertyName = property.AsString(); 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))); 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. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Jint; using Jint;
@ -131,6 +132,11 @@ namespace Squidex.Domain.Apps.Core.Scripting.ContentWrapper
var propertyName = property.AsString(); var propertyName = property.AsString();
if (propertyName.Equals("toJSON", StringComparison.OrdinalIgnoreCase))
{
return PropertyDescriptor.Undefined;
}
return valueProperties?.GetOrDefault(propertyName) ?? 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;
}
}
}

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

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

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

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson.Serialization;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.Assets.Repositories;
@ -27,8 +26,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{ {
public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository
{ {
private static readonly Lazy<string> IdField = new Lazy<string>(GetIdField);
public MongoAssetRepository(IMongoDatabase database) public MongoAssetRepository(IMongoDatabase database)
: base(database) : base(database)
{ {
@ -105,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(BuildFilter(appId, ids)).Only(x => x.Id) await Collection.Find(BuildFilter(appId, ids)).Only(x => x.Id)
.ToListAsync(); .ToListAsync();
return assetEntities.Select(x => Guid.Parse(x[IdField.Value].AsString)).ToList(); return assetEntities.Select(x => Guid.Parse(x[Fields.AssetId].AsString)).ToList();
} }
} }
@ -117,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.ParentId == parentId).Only(x => x.Id) await Collection.Find(x => x.IndexedAppId == appId && !x.IsDeleted && x.ParentId == parentId).Only(x => x.Id)
.ToListAsync(); .ToListAsync();
return assetEntities.Select(x => Guid.Parse(x[IdField.Value].AsString)).ToList(); return assetEntities.Select(x => Guid.Parse(x[Fields.AssetId].AsString)).ToList();
} }
} }
@ -176,10 +173,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
Filter.In(x => x.Id, ids), Filter.In(x => x.Id, ids),
Filter.Ne(x => x.IsDeleted, true)); 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 QueryContentsByIds queryContentsById;
private readonly QueryContentsByQuery queryContentsByQuery; private readonly QueryContentsByQuery queryContentsByQuery;
private readonly QueryIdsAsync queryIdsAsync; private readonly QueryIdsAsync queryIdsAsync;
private readonly QueryReferrersAsync queryReferrersAsync;
private readonly QueryScheduledContents queryScheduledItems; private readonly QueryScheduledContents queryScheduledItems;
public MongoContentCollectionAll(IMongoDatabase database, IAppProvider appProvider, ITextIndex indexer, DataConverter converter) 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); queryContentsById = new QueryContentsByIds(converter, appProvider);
queryContentsByQuery = new QueryContentsByQuery(converter, indexer); queryContentsByQuery = new QueryContentsByQuery(converter, indexer);
queryIdsAsync = new QueryIdsAsync(appProvider); queryIdsAsync = new QueryIdsAsync(appProvider);
queryReferrersAsync = new QueryReferrersAsync();
queryScheduledItems = new QueryScheduledContents(); queryScheduledItems = new QueryScheduledContents();
} }
@ -58,6 +60,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await queryContentsById.PrepareAsync(collection, ct); await queryContentsById.PrepareAsync(collection, ct);
await queryContentsByQuery.PrepareAsync(collection, ct); await queryContentsByQuery.PrepareAsync(collection, ct);
await queryIdsAsync.PrepareAsync(collection, ct); await queryIdsAsync.PrepareAsync(collection, ct);
await queryReferrersAsync.PrepareAsync(collection, ct);
await queryScheduledItems.PrepareAsync(collection, ct); await queryScheduledItems.PrepareAsync(collection, ct);
} }
@ -125,6 +128,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
} }
} }
public async Task<bool> HasReferrersAsync(Guid contentId)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
return await queryReferrersAsync.DoAsync(contentId);
}
}
public Task ResetScheduledAsync(Guid id) public Task ResetScheduledAsync(Guid id)
{ {
return Collection.UpdateOneAsync(x => x.Id == id, Update.Unset(x => x.ScheduleJob).Unset(x => x.ScheduledAt)); return Collection.UpdateOneAsync(x => x.Id == id, Update.Unset(x => x.ScheduleJob).Unset(x => x.ScheduledAt));

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

@ -129,6 +129,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
return collectionAll.QueryIdsAsync(appId, schemaId, filterNode); return collectionAll.QueryIdsAsync(appId, schemaId, filterNode);
} }
public Task<bool> HasReferrersAsync(Guid contentId)
{
return collectionAll.HasReferrersAsync(contentId);
}
public IEnumerable<IMongoCollection<MongoContentEntity>> GetInternalCollections() public IEnumerable<IMongoCollection<MongoContentEntity>> GetInternalCollections()
{ {
yield return collectionAll.GetInternalCollection(); 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;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations 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 SortDefinitionBuilder<MongoContentEntity> Sort = Builders<MongoContentEntity>.Sort;
protected static readonly UpdateDefinitionBuilder<MongoContentEntity> Update = Builders<MongoContentEntity>.Update; 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 FilterDefinitionBuilder<MongoContentEntity> Filter = Builders<MongoContentEntity>.Filter;
protected static readonly IndexKeysDefinitionBuilder<MongoContentEntity> Index = Builders<MongoContentEntity>.IndexKeys; protected static readonly IndexKeysDefinitionBuilder<MongoContentEntity> Index = Builders<MongoContentEntity>.IndexKeys;
protected static readonly ProjectionDefinitionBuilder<MongoContentEntity> Projection = Builders<MongoContentEntity>.Projection; protected static readonly ProjectionDefinitionBuilder<MongoContentEntity> Projection = Builders<MongoContentEntity>.Projection;

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

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson.Serialization;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries; using Squidex.Infrastructure.MongoDb.Queries;
@ -21,8 +20,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
internal sealed class QueryIdsAsync : OperationBase internal sealed class QueryIdsAsync : OperationBase
{ {
private static readonly List<(Guid SchemaId, Guid Id)> EmptyIds = new List<(Guid SchemaId, Guid Id)>(); private static readonly List<(Guid SchemaId, Guid Id)> EmptyIds = new List<(Guid SchemaId, Guid Id)>();
private static readonly Lazy<string> IdField = new Lazy<string>(GetIdField);
private static readonly Lazy<string> SchemaIdField = new Lazy<string>(GetSchemaIdField);
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
public QueryIdsAsync(IAppProvider appProvider) public QueryIdsAsync(IAppProvider appProvider)
@ -52,7 +49,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId) await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId)
.ToListAsync(); .ToListAsync();
return contentEntities.Select(x => (Guid.Parse(x[SchemaIdField.Value].AsString), Guid.Parse(x[IdField.Value].AsString))).ToList(); return contentEntities.Select(x => (Guid.Parse(x[Fields.SchemaId].AsString), Guid.Parse(x[Fields.Id].AsString))).ToList();
} }
public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> DoAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode) public async Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> DoAsync(Guid appId, Guid schemaId, FilterNode<ClrValue> filterNode)
@ -70,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId) await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId)
.ToListAsync(); .ToListAsync();
return contentEntities.Select(x => (Guid.Parse(x[SchemaIdField.Value].AsString), Guid.Parse(x[IdField.Value].AsString))).ToList(); return contentEntities.Select(x => (Guid.Parse(x[Fields.SchemaId].AsString), Guid.Parse(x[Fields.Id].AsString))).ToList();
} }
public static FilterDefinition<MongoContentEntity> BuildFilter(FilterNode<ClrValue>? filterNode, Guid schemaId) public static FilterDefinition<MongoContentEntity> BuildFilter(FilterNode<ClrValue>? filterNode, Guid schemaId)
@ -88,15 +85,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
return Filter.And(filters); 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;
}
} }
} }

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

@ -0,0 +1,45 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
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.IsDeleted));
return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct);
}
public async Task<bool> DoAsync(Guid id)
{
var filter =
Filter.And(
Filter.AnyEq(x => x.ReferencedIds, id),
Filter.Ne(x => x.IsDeleted, true),
Filter.Ne(x => x.Id, id));
var hasReferrerAsync =
await Collection.Find(filter).Only(x => x.Id)
.AnyAsync();
return hasReferrerAsync;
}
}
}

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

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

35
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.Commands;
using Squidex.Domain.Apps.Entities.Assets.Guards; using Squidex.Domain.Apps.Entities.Assets.Guards;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -21,23 +22,29 @@ using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Translations;
using IAssetTagService = Squidex.Domain.Apps.Core.Tags.ITagService;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public class AssetDomainObject : LogSnapshotDomainObject<AssetState> public class AssetDomainObject : LogSnapshotDomainObject<AssetState>
{ {
private readonly ITagService tagService; private readonly IContentRepository contentRepository;
private readonly IAssetTagService assetTags;
private readonly IAssetQueryService assetQuery; private readonly IAssetQueryService assetQuery;
public AssetDomainObject(IStore<Guid> store, ITagService tagService, IAssetQueryService assetQuery, ISemanticLog log) public AssetDomainObject(IStore<Guid> store, ISemanticLog log,
IAssetTagService assetTags,
IAssetQueryService assetQuery,
IContentRepository contentRepository)
: base(store, log) : base(store, log)
{ {
Guard.NotNull(tagService, nameof(tagService)); Guard.NotNull(assetTags, nameof(assetTags));
Guard.NotNull(assetQuery, nameof(assetQuery)); Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(contentRepository, nameof(contentRepository));
this.tagService = tagService; this.assetTags = assetTags;
this.assetQuery = assetQuery; this.assetQuery = assetQuery;
this.contentRepository = contentRepository;
} }
public override Task<object?> ExecuteAsync(IAggregateCommand command) public override Task<object?> ExecuteAsync(IAggregateCommand command)
@ -91,7 +98,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
GuardAsset.CanDelete(c); GuardAsset.CanDelete(c);
await tagService.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, null, Snapshot.Tags); if (c.CheckReferrers)
{
var hasReferrer = await contentRepository.HasReferrersAsync(c.AssetId);
if (hasReferrer)
{
throw new DomainException(T.Get("assets.referenced"));
}
}
await assetTags.NormalizeTagsAsync(Snapshot.AppId.Id, TagGroups.Assets, null, Snapshot.Tags);
Delete(c); Delete(c);
}); });
@ -107,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
return null; 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); return new HashSet<string>(normalized.Values);
} }
@ -116,10 +133,10 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
var @event = SimpleMapper.Map(command, new AssetCreated var @event = SimpleMapper.Map(command, new AssetCreated
{ {
MimeType = command.File.MimeType,
FileName = command.File.FileName, FileName = command.File.FileName,
FileSize = command.File.FileSize, FileSize = command.File.FileSize,
FileVersion = 0, FileVersion = 0,
MimeType = command.File.MimeType,
Slug = command.File.FileName.ToAssetSlug() Slug = command.File.FileName.ToAssetSlug()
}); });
@ -132,9 +149,9 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
var @event = SimpleMapper.Map(command, new AssetUpdated var @event = SimpleMapper.Map(command, new AssetUpdated
{ {
MimeType = command.File.MimeType,
FileVersion = Snapshot.FileVersion + 1, FileVersion = Snapshot.FileVersion + 1,
FileSize = command.File.FileSize, FileSize = command.File.FileSize,
MimeType = command.File.MimeType
}); });
RaiseEvent(@event); 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; private readonly IAssetQueryService assetQuery;
public AssetFolderDomainObject(IStore<Guid> store, IAssetQueryService assetQuery, ISemanticLog log) public AssetFolderDomainObject(ISemanticLog log, IStore<Guid> store,
IAssetQueryService assetQuery)
: base(store, log) : base(store, log)
{ {
Guard.NotNull(assetQuery, nameof(assetQuery)); 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 sealed class DeleteAsset : AssetCommand
{ {
public bool CheckReferrers { get; set; }
} }
} }

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

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

@ -16,9 +16,11 @@ using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation; using Squidex.Infrastructure.Validation;
#pragma warning disable IDE0016 // Use 'throw' expression #pragma warning disable IDE0016 // Use 'throw' expression
@ -43,7 +45,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
private ContentCommand command; private ContentCommand command;
private ValidationContext validationContext; 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.appProvider = appProvider;
this.factories = factories; this.factories = factories;

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

@ -28,6 +28,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories
Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, HashSet<Guid> ids, SearchScope scope); Task<IReadOnlyList<(Guid SchemaId, Guid Id)>> QueryIdsAsync(Guid appId, HashSet<Guid> ids, SearchScope scope);
Task<bool> HasReferrersAsync(Guid contentId);
Task<IContentEntity?> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, SearchScope scope); Task<IContentEntity?> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, SearchScope scope);
Task ResetScheduledAsync(Guid contentId); Task ResetScheduledAsync(Guid contentId);

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

@ -27,14 +27,14 @@ namespace Squidex.Domain.Apps.Entities.Rules
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly IRuleEnqueuer ruleEnqueuer; private readonly IRuleEnqueuer ruleEnqueuer;
public RuleDomainObject(IStore<Guid> store, ISemanticLog log, IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer) public RuleDomainObject(IStore<Guid> store, ISemanticLog log,
IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer)
: base(store, log) : base(store, log)
{ {
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(ruleEnqueuer, nameof(ruleEnqueuer)); Guard.NotNull(ruleEnqueuer, nameof(ruleEnqueuer));
this.appProvider = appProvider; this.appProvider = appProvider;
this.ruleEnqueuer = ruleEnqueuer; this.ruleEnqueuer = ruleEnqueuer;
} }

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

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities
public ClaimsPrincipal User { get; set; } public ClaimsPrincipal User { get; set; }
public bool FromRule { get; set; }
public long ExpectedVersion { get; set; } = EtagVersion.Auto; 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 abstract class SquidexEvent : IEvent
{ {
public RefToken Actor { get; set; } public RefToken Actor { get; set; }
public bool FromRule { get; set; }
} }
} }

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

@ -15,10 +15,10 @@ namespace Squidex.Infrastructure.EventSourcing
{ {
public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore public partial class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore
{ {
private static readonly FieldDefinition<MongoEventCommit, BsonTimestamp> TimestampField = Fields.Build(x => x.Timestamp); private static readonly FieldDefinition<MongoEventCommit, BsonTimestamp> TimestampField = FieldBuilder.Build(x => x.Timestamp);
private static readonly FieldDefinition<MongoEventCommit, long> EventsCountField = Fields.Build(x => x.EventsCount); private static readonly FieldDefinition<MongoEventCommit, long> EventsCountField = FieldBuilder.Build(x => x.EventsCount);
private static readonly FieldDefinition<MongoEventCommit, long> EventStreamOffsetField = Fields.Build(x => x.EventStreamOffset); private static readonly FieldDefinition<MongoEventCommit, long> EventStreamOffsetField = FieldBuilder.Build(x => x.EventStreamOffset);
private static readonly FieldDefinition<MongoEventCommit, string> EventStreamField = Fields.Build(x => x.EventStream); private static readonly FieldDefinition<MongoEventCommit, string> EventStreamField = FieldBuilder.Build(x => x.EventStream);
private readonly IEventNotifier notifier; private readonly IEventNotifier notifier;
public IMongoCollection<BsonDocument> RawCollection 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 ReplaceOptions UpsertReplace = new ReplaceOptions { IsUpsert = true };
protected static readonly SortDefinitionBuilder<TEntity> Sort = Builders<TEntity>.Sort; protected static readonly SortDefinitionBuilder<TEntity> Sort = Builders<TEntity>.Sort;
protected static readonly UpdateDefinitionBuilder<TEntity> Update = Builders<TEntity>.Update; 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 FilterDefinitionBuilder<TEntity> Filter = Builders<TEntity>.Filter;
protected static readonly IndexKeysDefinitionBuilder<TEntity> Index = Builders<TEntity>.IndexKeys; protected static readonly IndexKeysDefinitionBuilder<TEntity> Index = Builders<TEntity>.IndexKeys;
protected static readonly ProjectionDefinitionBuilder<TEntity> Projection = Builders<TEntity>.Projection; protected static readonly ProjectionDefinitionBuilder<TEntity> Projection = Builders<TEntity>.Projection;

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

@ -184,6 +184,9 @@
<data name="assets.maxSizeReached" xml:space="preserve"> <data name="assets.maxSizeReached" xml:space="preserve">
<value>Hai raggiunto la dimensione massima consentito per le risorse.</value> <value>Hai raggiunto la dimensione massima consentito per le risorse.</value>
</data> </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"> <data name="backups.alreadyRunning" xml:space="preserve">
<value>E' in esecuzione una altro processo di backup.</value> <value>E' in esecuzione una altro processo di backup.</value>
</data> </data>
@ -493,6 +496,9 @@
<data name="contents.listReferences" xml:space="preserve"> <data name="contents.listReferences" xml:space="preserve">
<value>{count} Collegamenti(s)</value> <value>{count} Collegamenti(s)</value>
</data> </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"> <data name="contents.singletonNotChangeable" xml:space="preserve">
<value>Il contenuto singleton non può essere aggiornato</value> <value>Il contenuto singleton non può essere aggiornato</value>
</data> </data>

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

@ -184,6 +184,9 @@
<data name="assets.maxSizeReached" xml:space="preserve"> <data name="assets.maxSizeReached" xml:space="preserve">
<value>Je hebt jouw maximale assetgrootte bereikt.</value> <value>Je hebt jouw maximale assetgrootte bereikt.</value>
</data> </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"> <data name="backups.alreadyRunning" xml:space="preserve">
<value>Er wordt al een ander back-upproces uitgevoerd.</value> <value>Er wordt al een ander back-upproces uitgevoerd.</value>
</data> </data>
@ -493,6 +496,9 @@
<data name="contents.listReferences" xml:space="preserve"> <data name="contents.listReferences" xml:space="preserve">
<value>{count} referentie (s)</value> <value>{count} referentie (s)</value>
</data> </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"> <data name="contents.singletonNotChangeable" xml:space="preserve">
<value>Singleton-inhoud kan niet worden bijgewerkt.</value> <value>Singleton-inhoud kan niet worden bijgewerkt.</value>
</data> </data>

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

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

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

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

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

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

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

@ -705,6 +705,29 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
Assert.Equal("[1,2,3]", result?.Replace(" ", string.Empty)); 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] [Theory]
[Expressions( [Expressions(
null, null,

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

@ -173,6 +173,24 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules
.MustHaveHappened(); .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] [Fact]
public async Task Should_not_create_job_if_not_triggered_with_precheck() 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]*") } { 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); sut.Setup(Id);
} }

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.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
@ -30,6 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>(); private readonly IAssetFileStore assetFileStore = A.Fake<IAssetFileStore>();
private readonly IAssetMetadataSource assetMetadataSource = A.Fake<IAssetMetadataSource>(); private readonly IAssetMetadataSource assetMetadataSource = A.Fake<IAssetMetadataSource>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly IContextProvider contextProvider = A.Fake<IContextProvider>(); private readonly IContextProvider contextProvider = A.Fake<IContextProvider>();
private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>(); private readonly IGrainFactory grainFactory = A.Fake<IGrainFactory>();
private readonly IServiceProvider serviceProvider = A.Fake<IServiceProvider>(); private readonly IServiceProvider serviceProvider = A.Fake<IServiceProvider>();
@ -53,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
file = new NoopAssetFile(); 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))) A.CallTo(() => serviceProvider.GetService(typeof(AssetDomainObject)))
.Returns(assetDomainObject); .Returns(assetDomainObject);

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

@ -14,6 +14,7 @@ using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Assets; using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure; using Squidex.Infrastructure;
@ -26,6 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
public class AssetDomainObjectTests : HandlerTestBase<AssetState> public class AssetDomainObjectTests : HandlerTestBase<AssetState>
{ {
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly ITagService tagService = A.Fake<ITagService>(); private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>(); private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly Guid parentId = Guid.NewGuid(); private readonly Guid parentId = Guid.NewGuid();
@ -46,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>._, A<HashSet<string>>._)) 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)!)); .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); sut.Setup(Id);
} }
@ -246,6 +248,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(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(Id))
.Returns(true);
await PublishAsync(command);
}
private Task ExecuteCreateAsync() private Task ExecuteCreateAsync()
{ {
return PublishAsync(new CreateAsset { File = file }); return PublishAsync(new CreateAsset { File = file });

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

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

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

@ -17,6 +17,7 @@ using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent; using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Commands; 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.Contents.State;
using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Entities.TestHelpers;
@ -35,6 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
private readonly IAppEntity app; private readonly IAppEntity app;
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IContentWorkflow contentWorkflow = A.Fake<IContentWorkflow>(x => x.Wrapping(new DefaultContentWorkflow())); 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 ISchemaEntity schema;
private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>(); private readonly IScriptEngine scriptEngine = A.Fake<IScriptEngine>();
@ -105,9 +107,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
patched = patch.MergeInto(data); 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); sut.Setup(Id);
} }
@ -125,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var command = new CreateContent { Data = data }; var command = new CreateContent { Data = data };
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -148,7 +152,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var command = new CreateContent { Data = data, Publish = true }; var command = new CreateContent { Data = data, Publish = true };
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -171,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
{ {
var command = new CreateContent { Data = invalidData }; var command = new CreateContent { Data = invalidData };
await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(CreateContentCommand(command))); await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(command));
} }
[Fact] [Fact]
@ -181,7 +185,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -205,7 +209,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecutePublishAsync(); await ExecutePublishAsync();
await ExecuteCreateDraftAsync(); await ExecuteCreateDraftAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -227,7 +231,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -244,7 +248,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(CreateContentCommand(command))); await Assert.ThrowsAsync<ValidationException>(() => PublishAsync(command));
} }
[Fact] [Fact]
@ -254,7 +258,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -278,7 +282,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecutePublishAsync(); await ExecutePublishAsync();
await ExecuteCreateDraftAsync(); await ExecuteCreateDraftAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -300,7 +304,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -317,7 +321,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -339,7 +343,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -362,7 +366,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
await ExecutePublishAsync(); await ExecutePublishAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -386,7 +390,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecutePublishAsync(); await ExecutePublishAsync();
await ExecuteCreateDraftAsync(); await ExecuteCreateDraftAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -410,7 +414,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -441,7 +445,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>._, Status.Draft, Status.Archived, User)) A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>._, Status.Draft, Status.Archived, User))
.Returns(true); .Returns(true);
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -469,7 +473,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>._, Status.Draft, Status.Published, User)) A.CallTo(() => contentWorkflow.CanMoveToAsync(A<IContentEntity>._, Status.Draft, Status.Published, User))
.Returns(false); .Returns(false);
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -491,7 +495,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(new EntitySavedResult(1)); result.ShouldBeEquivalent(new EntitySavedResult(1));
@ -506,6 +510,32 @@ namespace Squidex.Domain.Apps.Entities.Contents
.MustHaveHappened(); .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(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(Id))
.Returns(true);
await PublishAsync(command);
}
[Fact] [Fact]
public async Task CreateDraft_should_create_events_and_update_new_state() public async Task CreateDraft_should_create_events_and_update_new_state()
{ {
@ -514,7 +544,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecuteCreateAsync(); await ExecuteCreateAsync();
await ExecutePublishAsync(); await ExecutePublishAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(sut.Snapshot); result.ShouldBeEquivalent(sut.Snapshot);
@ -535,7 +565,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
await ExecutePublishAsync(); await ExecutePublishAsync();
await ExecuteCreateDraftAsync(); await ExecuteCreateDraftAsync();
var result = await PublishAsync(CreateContentCommand(command)); var result = await PublishAsync(command);
result.ShouldBeEquivalent(new EntitySavedResult(3)); result.ShouldBeEquivalent(new EntitySavedResult(3));
@ -549,22 +579,22 @@ namespace Squidex.Domain.Apps.Entities.Contents
private Task ExecuteCreateAsync() private Task ExecuteCreateAsync()
{ {
return PublishAsync(CreateContentCommand(new CreateContent { Data = data })); return PublishAsync(new CreateContent { Data = data });
} }
private Task ExecuteUpdateAsync() private Task ExecuteUpdateAsync()
{ {
return PublishAsync(CreateContentCommand(new UpdateContent { Data = otherData })); return PublishAsync(new UpdateContent { Data = otherData });
} }
private Task ExecuteCreateDraftAsync() private Task ExecuteCreateDraftAsync()
{ {
return PublishAsync(CreateContentCommand(new CreateContentDraft())); return PublishAsync(new CreateContentDraft());
} }
private Task ExecuteChangeStatusAsync(Status status, Instant? dueTime = null) 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() private Task ExecuteDeleteAsync()

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

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

@ -36,7 +36,8 @@
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete()" (sqxConfirmClick)="delete()"
confirmTitle="i18n:contents.deleteConfirmTitle" confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText"> confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }} {{ 'common.delete' | sqxTranslate }}
</a> </a>
</div> </div>

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

@ -11,7 +11,7 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ApiUrlConfig, AppLanguageDto, AppsState, AuthService, AutoSaveKey, AutoSaveService, CanComponentDeactivate, ContentDto, ContentsState, DialogService, EditContentForm, fadeAnimation, FieldForm, FieldSection, LanguagesState, ModalModel, ResourceOwner, RootFieldDto, SchemaDetailsDto, SchemasState, TempService, valueAll$, Version } from '@app/shared'; import { ApiUrlConfig, AppLanguageDto, AppsState, AuthService, AutoSaveKey, AutoSaveService, CanComponentDeactivate, ContentDto, ContentsState, DialogService, EditContentForm, fadeAnimation, FieldForm, FieldSection, LanguagesState, ModalModel, ResourceOwner, RootFieldDto, SchemaDetailsDto, SchemasState, TempService, valueAll$, Version } from '@app/shared';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { debounceTime, filter, onErrorResumeNext, tap } from 'rxjs/operators'; import { debounceTime, filter, tap } from 'rxjs/operators';
@Component({ @Component({
selector: 'sqx-content-page', selector: 'sqx-content-page',
@ -192,10 +192,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
const content = this.content; const content = this.content;
if (content) { if (content) {
this.contentsState.deleteMany([content]).pipe(onErrorResumeNext()) this.contentsState.deleteMany([content]);
.subscribe(() => {
this.back();
});
} }
} }

3
frontend/app/features/content/pages/contents/contents-page.component.html

@ -55,7 +55,8 @@
<button type="button" class="btn btn-danger" *ngIf="selectionCanDelete" <button type="button" class="btn btn-danger" *ngIf="selectionCanDelete"
(sqxConfirmClick)="deleteSelected()" (sqxConfirmClick)="deleteSelected()"
confirmTitle="i18n:contents.deleteConfirmTitle" confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteManyConfirmText"> confirmText="i18n:contents.deleteManyConfirmText"
confirmRememberKey="deleteContents">
{{ 'common.delete' | sqxTranslate }} {{ 'common.delete' | sqxTranslate }}
</button> </button>
</div> </div>

8
frontend/app/features/content/pages/contents/contents-page.component.ts

@ -75,7 +75,7 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.own( this.own(
this.route.params.pipe( this.route.params.pipe(
switchMap(x => this.schemasState.selectedSchema), distinctUntilChanged()) switchMap(() => this.schemasState.selectedSchema), distinctUntilChanged())
.subscribe(schema => { .subscribe(schema => {
this.resetSelection(); this.resetSelection();
@ -202,8 +202,10 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.selectionCount++; this.selectionCount++;
for (const action in this.nextStatuses) { for (const action in this.nextStatuses) {
if (!content.statusUpdates.find(x => x.status === action)) { if (this.nextStatuses.hasOwnProperty(action)) {
delete this.nextStatuses[action]; if (!content.statusUpdates.find(x => x.status === action)) {
delete this.nextStatuses[action];
}
} }
} }

4
frontend/app/features/content/pages/sidebar/sidebar-page.component.ts

@ -18,9 +18,9 @@ import { combineLatest } from 'rxjs';
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SidebarPageComponent extends ResourceOwner implements AfterViewInit { export class SidebarPageComponent extends ResourceOwner implements AfterViewInit {
private isInitialized = false; private readonly context: any;
private context: any;
private content: any; private content: any;
private isInitialized = false;
@ViewChild('iframe', { static: false }) @ViewChild('iframe', { static: false })
public iframe: ElementRef<HTMLIFrameElement>; public iframe: ElementRef<HTMLIFrameElement>;

2
frontend/app/features/content/shared/due-time-selector.component.ts

@ -17,7 +17,7 @@ const OPTION_IMMEDIATELY = 'Immediately';
templateUrl: './due-time-selector.component.html' templateUrl: './due-time-selector.component.html'
}) })
export class DueTimeSelectorComponent { export class DueTimeSelectorComponent {
private disabled: boolean; private readonly disabled: boolean;
private dueTimeResult: Subject<string | null>; private dueTimeResult: Subject<string | null>;
public dueTimeDialog = new DialogModel(); public dueTimeDialog = new DialogModel();

3
frontend/app/features/content/shared/list/content.component.html

@ -41,7 +41,8 @@
<a class="dropdown-item dropdown-item-delete" <a class="dropdown-item dropdown-item-delete"
(sqxConfirmClick)="delete.emit()" (sqxConfirmClick)="delete.emit()"
confirmTitle="i18n:contents.deleteConfirmTitle" confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText"> confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }} {{ 'common.delete' | sqxTranslate }}
</a> </a>
</div> </div>

3
frontend/app/features/content/shared/references/reference-item.component.html

@ -29,7 +29,8 @@
<button type="button" class="btn btn-text-secondary" <button type="button" class="btn btn-text-secondary"
(sqxConfirmClick)="delete.emit()" (sqxConfirmClick)="delete.emit()"
confirmTitle="i18n:contents.removeConfirmTitle" confirmTitle="i18n:contents.removeConfirmTitle"
confirmText="i18n:contents.removeConfirmText"> confirmText="i18n:contents.removeConfirmText"
confirmRememberKey="removeReference">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
</div> </div>

3
frontend/app/features/dashboard/pages/dashboard-config.component.html

@ -26,7 +26,8 @@
<a class="dropdown-item dropdown-item-delete" (beforeClick)="dropdownModal.hide()" <a class="dropdown-item dropdown-item-delete" (beforeClick)="dropdownModal.hide()"
(sqxConfirmClick)="resetConfig()" (sqxConfirmClick)="resetConfig()"
confirmTitle="i18n:dashboard.resetConfigConfirmTitle" confirmTitle="i18n:dashboard.resetConfigConfirmTitle"
confirmText="i18n:dashboard.resetConfigConfirmText"> confirmText="i18n:dashboard.resetConfigConfirmText"
confirmRememberKey="resetConfig">
{{ 'common.reset' | sqxTranslate }} {{ 'common.reset' | sqxTranslate }}
</a> </a>
</div> </div>

4
frontend/app/features/rules/pages/rules/actions/generic-action.component.html

@ -13,8 +13,8 @@
</ng-container> </ng-container>
<ng-container *ngSwitchCase="'Checkbox'"> <ng-container *ngSwitchCase="'Checkbox'">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" [formControlName]="property.name" /> <input class="form-check-input" type="checkbox" id="{{property.name}}" [formControlName]="property.name" />
<label class="form-check-label" [for]="property.name"> <label class="form-check-label" for="{{property.name}}">
{{property.display}} {{property.display}}
</label> </label>
</div> </div>

9
frontend/app/features/rules/pages/rules/rule.component.html

@ -20,14 +20,16 @@
<a class="dropdown-item" *ngIf="rule.canRun" <a class="dropdown-item" *ngIf="rule.canRun"
(sqxConfirmClick)="run()" (sqxConfirmClick)="run()"
confirmTitle="i18n:rules.runRuleConfirmTitle" confirmTitle="i18n:rules.runRuleConfirmTitle"
confirmText="i18n:rules.runRuleConfirmText"> confirmText="i18n:rules.runRuleConfirmText"
confirmRememberKey="runRule">
{{ 'rules.run' | sqxTranslate }} {{ 'rules.run' | sqxTranslate }}
</a> </a>
<a class="dropdown-item dropdown-item-delete" *ngIf="rule.canDelete" <a class="dropdown-item dropdown-item-delete" *ngIf="rule.canDelete"
(sqxConfirmClick)="delete()" (sqxConfirmClick)="delete()"
confirmTitle="i18n:rules.deleteConfirmTitle" confirmTitle="i18n:rules.deleteConfirmTitle"
confirmText="i18n:rules.deleteConfirmText"> confirmText="i18n:rules.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }} {{ 'common.delete' | sqxTranslate }}
</a> </a>
</div> </div>
@ -58,7 +60,8 @@
<button class="btn btn-secondary" [disabled]="!rule.canTrigger" <button class="btn btn-secondary" [disabled]="!rule.canTrigger"
(sqxConfirmClick)="trigger()" (sqxConfirmClick)="trigger()"
confirmTitle="i18n:rules.triggerConfirmTitle" confirmTitle="i18n:rules.triggerConfirmTitle"
confirmText="i18n:rules.triggerConfirmText"> confirmText="i18n:rules.triggerConfirmText"
confirmRememberKey="triggerRule">
<i class="icon-play-line"></i> <i class="icon-play-line"></i>
</button> </button>
</ng-container> </ng-container>

6
frontend/app/features/schemas/pages/schema/fields/field.component.html

@ -69,7 +69,8 @@
<a class="dropdown-item" <a class="dropdown-item"
(sqxConfirmClick)="lockField()" (sqxConfirmClick)="lockField()"
confirmTitle="i18n:schemas.field.lockConfirmText" confirmTitle="i18n:schemas.field.lockConfirmText"
confirmText="i18n:schemas.field.lockConfirmText"> confirmText="i18n:schemas.field.lockConfirmText"
confirmRememberKey="lockField">
{{ 'schemas.field.lock' | sqxTranslate }} {{ 'schemas.field.lock' | sqxTranslate }}
</a> </a>
</ng-container> </ng-container>
@ -80,7 +81,8 @@
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!field.canDelete" <a class="dropdown-item dropdown-item-delete" [class.disabled]="!field.canDelete"
(sqxConfirmClick)="deleteField()" (sqxConfirmClick)="deleteField()"
confirmTitle="i18n:schemas.field.deleteConfirmTitle" confirmTitle="i18n:schemas.field.deleteConfirmTitle"
confirmText="i18n:schemas.field.deleteConfirmText"> confirmText="i18n:schemas.field.deleteConfirmText"
confirmRememberKey="deleteField">
{{ 'common.delete' | sqxTranslate }} {{ 'common.delete' | sqxTranslate }}
</a> </a>
</ng-container> </ng-container>

3
frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.html

@ -29,7 +29,8 @@
<button type="button" class="btn btn-text-danger" [disabled]="!isEditable" <button type="button" class="btn btn-text-danger" [disabled]="!isEditable"
(sqxConfirmClick)="editForm.remove(i)" (sqxConfirmClick)="editForm.remove(i)"
confirmTitle="i18n:schemas.deleteUrlConfirmTitle" confirmTitle="i18n:schemas.deleteUrlConfirmTitle"
confirmText="i18n:schemas.deleteUrlConfirmText"> confirmText="i18n:schemas.deleteUrlConfirmText"
confirmRememberKey="removePreviewUrl">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>

2
frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts

@ -48,7 +48,7 @@ export class SchemaPreviewUrlsFormComponent implements OnChanges {
if (value) { if (value) {
this.schemasState.configurePreviewUrls(this.schema, value) this.schemasState.configurePreviewUrls(this.schema, value)
.subscribe(update => { .subscribe(() => {
this.editForm.submitCompleted({ noReset: true }); this.editForm.submitCompleted({ noReset: true });
}, error => { }, error => {
this.editForm.submitFailed(error); this.editForm.submitFailed(error);

3
frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html

@ -39,7 +39,8 @@
<button type="button" class="btn btn-text-danger" [disabled]="!isEditable" <button type="button" class="btn btn-text-danger" [disabled]="!isEditable"
(sqxConfirmClick)="editForm.remove(i)" (sqxConfirmClick)="editForm.remove(i)"
confirmTitle="i18n:schemas.deleteRuleConfirmTitle" confirmTitle="i18n:schemas.deleteRuleConfirmTitle"
confirmText="i18n:schemas.deleteRuleConfirmText"> confirmText="i18n:schemas.deleteRuleConfirmText"
confirmRememberKey="deleteFieldRule">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>

3
frontend/app/features/schemas/pages/schema/schema-page.component.html

@ -40,7 +40,8 @@
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!schema.canDelete" <a class="dropdown-item dropdown-item-delete" [class.disabled]="!schema.canDelete"
(sqxConfirmClick)="deleteSchema()" (sqxConfirmClick)="deleteSchema()"
confirmTitle="i18n:schemas.deleteConfirmTitle" confirmTitle="i18n:schemas.deleteConfirmTitle"
confirmText="i18n:schemas.deleteConfirmText"> confirmText="i18n:schemas.deleteConfirmText"
confirmRememberKey="deleteSchema">
{{ 'common.delete' | sqxTranslate }} {{ 'common.delete' | sqxTranslate }}
</a> </a>
</ng-container> </ng-container>

3
frontend/app/features/settings/pages/backups/backup.component.html

@ -40,7 +40,8 @@
<button type="button" class="btn btn-text-danger mt-1" [disabled]="!backup.canDelete" <button type="button" class="btn btn-text-danger mt-1" [disabled]="!backup.canDelete"
(sqxConfirmClick)="delete()" (sqxConfirmClick)="delete()"
confirmTitle="i18n:backups.deleteConfirmTitle" confirmTitle="i18n:backups.deleteConfirmTitle"
confirmText="i18n:backups.deleteConfirmText"> confirmText="i18n:backups.deleteConfirmText"
confirmRememberKey="deleteBackup">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>

3
frontend/app/features/settings/pages/clients/client.component.html

@ -12,7 +12,8 @@
<button type="button" class="btn btn-text-danger" [disabled]="!client.canRevoke" <button type="button" class="btn btn-text-danger" [disabled]="!client.canRevoke"
(sqxConfirmClick)="revoke()" (sqxConfirmClick)="revoke()"
confirmTitle="i18n:clients.deleteConfirmTitle" confirmTitle="i18n:clients.deleteConfirmTitle"
confirmText="i18n:clients.deleteConfirmText"> confirmText="i18n:clients.deleteConfirmText"
confirmRememberKey="revokeClient">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>

3
frontend/app/features/settings/pages/contributors/contributor.component.html

@ -14,7 +14,8 @@
<button type="button" class="btn btn-text-danger" [disabled]="!contributor.canRevoke" <button type="button" class="btn btn-text-danger" [disabled]="!contributor.canRevoke"
(sqxConfirmClick)="remove()" (sqxConfirmClick)="remove()"
confirmTitle="i18n:contributors.deleteConfirmTitle" confirmTitle="i18n:contributors.deleteConfirmTitle"
confirmText="i18n:contributors.deleteConfirmText"> confirmText="i18n:contributors.deleteConfirmText"
confirmRememberKey="removeContributor">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</td> </td>

4
frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts

@ -8,7 +8,7 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { ContributorsState, ErrorDto, ImportContributorsForm, RoleDto } from '@app/shared'; import { ContributorsState, ErrorDto, ImportContributorsForm, RoleDto } from '@app/shared';
import { empty, of } from 'rxjs'; import { EMPTY, of } from 'rxjs';
import { catchError, mergeMap, tap } from 'rxjs/operators'; import { catchError, mergeMap, tap } from 'rxjs/operators';
type ImportStatus = { type ImportStatus = {
@ -79,7 +79,7 @@ export class ImportContributorsDialogComponent {
status.result = 'Failed'; status.result = 'Failed';
} }
return empty(); return EMPTY;
}) })
), 1) ), 1)
).subscribe(); ).subscribe();

3
frontend/app/features/settings/pages/languages/language.component.html

@ -16,7 +16,8 @@
<button type="button" class="btn btn-text-danger" [disabled]="!language.canDelete" <button type="button" class="btn btn-text-danger" [disabled]="!language.canDelete"
(sqxConfirmClick)="remove()" (sqxConfirmClick)="remove()"
confirmTitle="i18n:languages.deleteConfirmTitle" confirmTitle="i18n:languages.deleteConfirmTitle"
confirmText="i18n:languages.deleteConfirmText"> confirmText="i18n:languages.deleteConfirmText"
confirmRememberKey="removeLanguage">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>

3
frontend/app/features/settings/pages/patterns/pattern.component.html

@ -27,7 +27,8 @@
<button type="button" class="btn btn-text-danger" [disabled]="!isDeletable" <button type="button" class="btn btn-text-danger" [disabled]="!isDeletable"
(sqxConfirmClick)="delete()" (sqxConfirmClick)="delete()"
confirmTitle="i18n:patterns.deleteConfirmTitle" confirmTitle="i18n:patterns.deleteConfirmTitle"
confirmText="i18n:patterns.deleteConfirmText"> confirmText="i18n:patterns.deleteConfirmText"
confirmRememberKey="deletePattern">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>

1
frontend/app/features/settings/pages/plans/plan.component.html

@ -27,6 +27,7 @@
<button *ngIf="!planInfo.isSelected" class="btn btn-block btn-success" [disabled]="plansState.isDisabled | async" <button *ngIf="!planInfo.isSelected" class="btn btn-block btn-success" [disabled]="plansState.isDisabled | async"
(sqxConfirmClick)="changeMonthly()" (sqxConfirmClick)="changeMonthly()"
confirmRememberKey="changePlan"
confirmTitle="i18n:plans.changeConfirmTitle" confirmTitle="i18n:plans.changeConfirmTitle"
[confirmText]="planInfo.plan.confirmText" [confirmText]="planInfo.plan.confirmText"
[confirmRequired]="planInfo.plan.confirmText"> [confirmRequired]="planInfo.plan.confirmText">

3
frontend/app/features/settings/pages/roles/role.component.html

@ -19,7 +19,8 @@
<button type="button" class="btn btn-text-danger" [disabled]="!role.canDelete" <button type="button" class="btn btn-text-danger" [disabled]="!role.canDelete"
(sqxConfirmClick)="delete()" (sqxConfirmClick)="delete()"
confirmTitle="i18n:roles.deleteConfirmTitle" confirmTitle="i18n:roles.deleteConfirmTitle"
confirmText="i18n:roles.deleteConfirmText"> confirmText="i18n:roles.deleteConfirmText"
confirmRememberKey="deleteRole">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>

24
frontend/app/features/settings/pages/workflows/workflow.component.html

@ -5,7 +5,11 @@
<span class="workflow-name">{{workflow.displayName}}</span> <span class="workflow-name">{{workflow.displayName}}</span>
</div> </div>
<div class="col col-tags"> <div class="col col-tags">
<sqx-tag-editor [converter]="schemasSource.converter | async" [ngModel]="workflow.schemaIds" styleGray="true" styleBlank="true" singleLine="true" readonly="true"> <sqx-tag-editor [converter]="schemasSource.converter | async" [ngModel]="workflow.schemaIds"
styleGray="true"
styleBlank="true"
singleLine="true"
readonly="true">
</sqx-tag-editor> </sqx-tag-editor>
</div> </div>
<div class="col-options"> <div class="col-options">
@ -17,7 +21,8 @@
<button type="button" class="btn btn-text-danger" [disabled]="!workflow.canDelete" <button type="button" class="btn btn-text-danger" [disabled]="!workflow.canDelete"
(sqxConfirmClick)="remove()" (sqxConfirmClick)="remove()"
confirmTitle="i18n:workflows.deleteConfirmTitle" confirmTitle="i18n:workflows.deleteConfirmTitle"
confirmText="i18n:workflows.deleteConfirmText"> confirmText="i18n:workflows.deleteConfirmText"
confirmRememberKey="deleteWorkflow">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>
@ -62,7 +67,10 @@
<label class="col-form-label" for="{{workflow.id}}_schemas">{{ 'common.schemas' | sqxTranslate }}</label> <label class="col-form-label" for="{{workflow.id}}_schemas">{{ 'common.schemas' | sqxTranslate }}</label>
<div class="col"> <div class="col">
<sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}" [converter]="schemasSource.converter | async" [ngModel]="workflow.schemaIds" (ngModelChange)="changeSchemaIds($event)" [suggestions]="(schemasSource.converter | async)?.suggestions"> <sqx-tag-editor placeholder="{{ 'common.tagAddSchema' | sqxTranslate }}" [converter]="schemasSource.converter | async"
[ngModel]="workflow.schemaIds"
(ngModelChange)="changeSchemaIds($event)"
[suggestions]="(schemasSource.converter | async)?.suggestions">
</sqx-tag-editor> </sqx-tag-editor>
<sqx-form-hint> <sqx-form-hint>
@ -71,7 +79,15 @@
</div> </div>
</div> </div>
<sqx-workflow-step *ngFor="let step of workflow.steps; trackBy: trackByStep" [step]="step" [workflow]="workflow" [disabled]="!workflow.canUpdate" (makeInitial)="setInitial(step)" (rename)="renameStep(step, $event)" (remove)="removeStep(step)" (transitionAdd)="addTransiton(step, $event)" (transitionRemove)="removeTransition(step, $event)" (transitionUpdate)="updateTransition($event)" (update)="updateStep(step, $event)" [roles]="roles"> <sqx-workflow-step *ngFor="let step of workflow.steps; trackBy: trackByStep" [step]="step" [workflow]="workflow"
[disabled]="!workflow.canUpdate"
(makeInitial)="setInitial(step)"
(rename)="renameStep(step, $event)"
(remove)="removeStep(step)"
(transitionAdd)="addTransiton(step, $event)"
(transitionRemove)="removeTransition(step, $event)"
(transitionUpdate)="updateTransition($event)"
(update)="updateStep(step, $event)" [roles]="roles">
</sqx-workflow-step> </sqx-workflow-step>
<button class="btn btn-success" (click)="addStep()" *ngIf="workflow.canUpdate"> <button class="btn btn-success" (click)="addStep()" *ngIf="workflow.canUpdate">

5
frontend/app/framework/angular/forms/confirm-click.directive.ts

@ -22,6 +22,9 @@ export class ConfirmClickDirective {
@Input() @Input()
public confirmText: string; public confirmText: string;
@Input()
public confirmRememberKey: string;
@Input() @Input()
public confirmRequired = true; public confirmRequired = true;
@ -48,7 +51,7 @@ export class ConfirmClickDirective {
this.beforeClick.emit(); this.beforeClick.emit();
this.dialogs.confirm(this.confirmTitle, this.confirmText).pipe(take(1)) this.dialogs.confirm(this.confirmTitle, this.confirmText, this.confirmRememberKey).pipe(take(1))
.subscribe(confirmed => { .subscribe(confirmed => {
if (confirmed) { if (confirmed) {
for (const observer of observers) { for (const observer of observers) {

4
frontend/app/framework/angular/forms/editors/date-time-editor.component.ts

@ -29,10 +29,10 @@ const NO_EMIT = { emitEvent: false };
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class DateTimeEditorComponent extends StatefulControlComponent<{}, string | null> implements OnInit, AfterViewInit, FocusComponent { export class DateTimeEditorComponent extends StatefulControlComponent<{}, string | null> implements OnInit, AfterViewInit, FocusComponent {
private readonly hideDateButtonsSettings: boolean;
private readonly hideDateTimeModeButtonSetting: boolean;
private picker: any; private picker: any;
private dateTime: DateTime | null; private dateTime: DateTime | null;
private hideDateButtonsSettings: boolean;
private hideDateTimeModeButtonSetting: boolean;
private suppressEvents = false; private suppressEvents = false;
@Input() @Input()

2
frontend/app/framework/angular/forms/forms-helper.ts

@ -33,7 +33,7 @@ export function value$<T = any>(form: AbstractControl): Observable<T> {
} }
export function valueAll$<T = any>(form: AbstractControl): Observable<T> { export function valueAll$<T = any>(form: AbstractControl): Observable<T> {
return form.valueChanges.pipe(map(_ => getRawValue(form)), startWith(getRawValue(form)), distinctUntilChanged()); return form.valueChanges.pipe(map(() => getRawValue(form)), startWith(getRawValue(form)), distinctUntilChanged());
} }
export function hasValue$(form: AbstractControl): Observable<boolean> { export function hasValue$(form: AbstractControl): Observable<boolean> {

2
frontend/app/framework/angular/forms/indeterminate-value.directive.ts

@ -20,7 +20,7 @@ export const SQX_INDETERMINATE_VALUE_CONTROL_VALUE_ACCESSOR: any = {
] ]
}) })
export class IndeterminateValueDirective implements ControlValueAccessor { export class IndeterminateValueDirective implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (_: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ }; private callTouched = () => { /* NOOP */ };
constructor( constructor(

2
frontend/app/framework/angular/forms/transform-input.directive.ts

@ -29,7 +29,7 @@ export const SQX_TRANSFORM_INPUT_VALUE_ACCESSOR: any = {
] ]
}) })
export class TransformInputDirective implements ControlValueAccessor { export class TransformInputDirective implements ControlValueAccessor {
private callChange = (v: any) => { /* NOOP */ }; private callChange = (_: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ }; private callTouched = () => { /* NOOP */ };
private transformer: Transform; private transformer: Transform;

14
frontend/app/framework/angular/modals/dialog-renderer.component.html

@ -1,13 +1,21 @@
<ng-content></ng-content> <ng-content></ng-content>
<ng-container *sqxModal="dialogView"> <ng-container *sqxModal="dialogView">
<sqx-modal-dialog showClose="false" (close)="cancel()"> <sqx-modal-dialog showClose="false" (close)="cancel()" *ngIf="snapshot.dialogRequest; let request">
<ng-container title> <ng-container title>
{{snapshot.dialogRequest?.title | sqxTranslate}} {{request.title | sqxTranslate}}
</ng-container> </ng-container>
<ng-container content> <ng-container content>
<span [innerHTML]="snapshot.dialogRequest?.text | sqxTranslate | sqxMarkdown"></span> <span [innerHTML]="request.text | sqxTranslate | sqxMarkdown"></span>
<div class="form-check mt-4" *ngIf="request.canRemember">
<input class="form-check-input" type="checkbox" id="remember" [(ngModel)]="request.remember">
<label class="form-check-label" for="remember">
{{ 'common.remember' | sqxTranslate}}
</label>
</div>
</ng-container> </ng-container>
<ng-container footer> <ng-container footer>

4
frontend/app/framework/angular/modals/dialog-renderer.component.ts

@ -95,9 +95,7 @@ export class DialogRendererComponent extends StatefulComponent<State> implements
private finishRequest(value: boolean) { private finishRequest(value: boolean) {
this.next(s => { this.next(s => {
if (s.dialogRequest) { s.dialogRequest?.complete(value);
s.dialogRequest.complete(value);
}
return { ...s, dialogRequest: null }; return { ...s, dialogRequest: null };
}); });

2
frontend/app/framework/angular/template-wrapper.directive.ts

@ -26,7 +26,7 @@ export class TemplateWrapperDirective implements OnDestroy, OnInit, OnChanges {
public view: EmbeddedViewRef<any>; public view: EmbeddedViewRef<any>;
public constructor( public constructor(
private viewContainer: ViewContainerRef private readonly viewContainer: ViewContainerRef
) { ) {
} }

131
frontend/app/framework/services/dialog.service.spec.ts

@ -5,18 +5,25 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogRequest, DialogService, DialogServiceFactory, Notification, Tooltip } from './dialog.service'; import { DialogRequest, DialogService, DialogServiceFactory, Notification, Tooltip } from './dialog.service';
import { LocalStoreService } from './local-store.service';
describe('DialogService', () => { describe('DialogService', () => {
let localStore: IMock<LocalStoreService>;
beforeEach(() => {
localStore = Mock.ofType<LocalStoreService>();
});
it('should instantiate from factory', () => { it('should instantiate from factory', () => {
const dialogService = DialogServiceFactory(); const dialogService = DialogServiceFactory(localStore.object);
expect(dialogService).toBeDefined(); expect(dialogService).toBeDefined();
}); });
it('should instantiate', () => { it('should instantiate', () => {
const dialogService = new DialogService(); const dialogService = new DialogService(localStore.object);
expect(dialogService).toBeDefined(); expect(dialogService).toBeDefined();
}); });
@ -37,33 +44,115 @@ describe('DialogService', () => {
expect(notification.messageType).toBe('info'); expect(notification.messageType).toBe('info');
}); });
it('should create dialog request', () => { [true, false].map(confirmed => {
const dialog = new DialogRequest('MyTitle', 'MyText'); it(`should confirm dialog with ${confirmed}`, () => {
const dialogService = new DialogService(localStore.object);
let isCompleted = false;
let isNext: boolean;
dialogService.dialogs.subscribe(dialog => {
dialog.complete(confirmed);
});
expect(dialog.title).toBe('MyTitle'); dialogService.confirm('MyTitle', 'MyText').subscribe(result => {
expect(dialog.text).toBe('MyText'); isNext = result;
}, undefined, () => {
isCompleted = true;
});
expect(isCompleted).toBeTruthy();
expect(isNext!).toEqual(confirmed);
localStore.verify(x => x.setInt(It.isAnyString(), It.isAnyNumber()), Times.never());
});
}); });
it('should confirm dialog', () => { [true, false].map(confirmed => {
const dialog = new DialogRequest('MyTitle', 'MyText'); it(`should confirm dialog with '${confirmed}' but not remember`, () => {
const dialogService = new DialogService(localStore.object);
let isCompleted = false; let isCompleted = false;
let isNext = false; let isNext: boolean;
dialog.closed.subscribe(result => { dialogService.dialogs.subscribe(dialog => {
isNext = result; dialog.complete(confirmed);
}, undefined, () => { });
isCompleted = true;
dialogService.confirm('MyTitle', 'MyText').subscribe(result => {
isNext = result;
}, undefined, () => {
isCompleted = true;
});
expect(isCompleted).toBeTruthy();
expect(isNext!).toEqual(confirmed);
localStore.verify(x => x.setInt(It.isAnyString(), It.isAnyNumber()), Times.never());
}); });
});
[
{ confirmed: true, saved: 1 },
{ confirmed: false, saved: 2 }
].map(({ confirmed, saved }) => {
it(`should confirm dialog with '${confirmed}' and remember when remembered`, () => {
const dialogService = new DialogService(localStore.object);
let isCompleted = false;
let isNext: boolean;
dialog.complete(true); dialogService.dialogs.subscribe(dialog => {
dialog.remember = true;
dialog.complete(confirmed);
});
expect(isCompleted).toBeTruthy(); dialogService.confirm('MyTitle', 'MyText', 'MyKey').subscribe(result => {
expect(isNext).toBeTruthy(); isNext = result;
}, undefined, () => {
isCompleted = true;
});
expect(isCompleted).toBeTruthy();
expect(isNext!).toEqual(confirmed);
localStore.verify(x => x.setInt(`dialogs.confirm.MyKey`, saved), Times.once());
});
});
[
{ confirmed: true, saved: 1 },
{ confirmed: false, saved: 2 }
].map(({ confirmed, saved }) => {
it(`should confirm dialog with '${confirmed}' from local store when saved`, () => {
const dialogService = new DialogService(localStore.object);
localStore.setup(x => x.getInt(`dialogs.confirm.MyKey`))
.returns(() => saved);
let requestCount = 0;
let isCompleted = false;
let isNext: boolean;
dialogService.dialogs.subscribe(_ => {
requestCount++;
});
dialogService.confirm('MyTitle', 'MyText', 'MyKey').subscribe(result => {
isNext = result;
}, undefined, () => {
isCompleted = true;
});
expect(isCompleted).toBeTruthy();
expect(isNext!).toEqual(confirmed);
expect(requestCount).toEqual(0);
});
}); });
it('should publish tooltip', () => { it('should publish tooltip', () => {
const dialogService = new DialogService(); const dialogService = new DialogService(localStore.object);
const tooltip = new Tooltip('target', 'text', 'left'); const tooltip = new Tooltip('target', 'text', 'left');
@ -79,7 +168,7 @@ describe('DialogService', () => {
}); });
it('should publish notification', () => { it('should publish notification', () => {
const dialogService = new DialogService(); const dialogService = new DialogService(localStore.object);
const notification = Notification.error('Message'); const notification = Notification.error('Message');
@ -95,7 +184,7 @@ describe('DialogService', () => {
}); });
it('should publish dialog request', () => { it('should publish dialog request', () => {
const dialogService = new DialogService(); const dialogService = new DialogService(localStore.object);
let pushedDialog: DialogRequest; let pushedDialog: DialogRequest;
@ -105,6 +194,6 @@ describe('DialogService', () => {
dialogService.confirm('MyTitle', 'MyText'); dialogService.confirm('MyTitle', 'MyText');
expect(pushedDialog!).toEqual(new DialogRequest('MyTitle', 'MyText')); expect(pushedDialog!).toBeDefined();
}); });
}); });

58
frontend/app/framework/services/dialog.service.ts

@ -6,29 +6,56 @@
*/ */
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, Subject, throwError } from 'rxjs'; import { Observable, ReplaySubject, Subject, throwError } from 'rxjs';
import { ErrorDto } from './../utils/error'; import { ErrorDto } from './../utils/error';
import { Types } from './../utils/types'; import { Types } from './../utils/types';
import { LocalStoreService } from './local-store.service';
export const DialogServiceFactory = () => { export const DialogServiceFactory = (localStore: LocalStoreService) => {
return new DialogService(); return new DialogService(localStore);
}; };
export class DialogRequest { export class DialogRequest {
private readonly resultStream$ = new Subject<boolean>(); private readonly resultStream$ = new ReplaySubject<boolean>();
public get closed(): Observable<boolean> { public get result(): Observable<boolean> {
return this.resultStream$; return this.resultStream$;
} }
public get isCompleted() {
return this.resultStream$.isStopped;
}
public get canRemember() {
return !!this.rememberKey;
}
public remember: boolean;
constructor( constructor(
public readonly title: string, public readonly title: string,
public readonly text: string public readonly text: string,
private readonly rememberKey: string | undefined,
private readonly localStore: LocalStoreService
) { ) {
if (rememberKey) {
this.rememberKey = `dialogs.confirm.${rememberKey}`;
const isConfirmed = this.localStore.getInt(this.rememberKey);
if (isConfirmed > 0) {
this.resultStream$.next(isConfirmed === 1);
this.resultStream$.complete();
}
}
} }
public complete(value: boolean) { public complete(confirmed: boolean) {
this.resultStream$.next(value); if (this.rememberKey && this.remember) {
this.localStore.setInt(this.rememberKey, confirmed ? 1 : 2);
}
this.resultStream$.next(confirmed);
this.resultStream$.complete(); this.resultStream$.complete();
} }
} }
@ -77,6 +104,11 @@ export class DialogService {
return this.notificationsStream$; return this.notificationsStream$;
} }
constructor(
private readonly localStore: LocalStoreService
) {
}
public notifyError(error: string | ErrorDto) { public notifyError(error: string | ErrorDto) {
if (Types.is(error, ErrorDto)) { if (Types.is(error, ErrorDto)) {
this.notify(Notification.error(error)); this.notify(Notification.error(error));
@ -99,11 +131,15 @@ export class DialogService {
this.tooltipStream$.next(tooltip); this.tooltipStream$.next(tooltip);
} }
public confirm(title: string, text: string): Observable<boolean> { public confirm(title: string, text: string, rememberKey?: string): Observable<boolean> {
const request = new DialogRequest(title, text); const request = new DialogRequest(title, text, rememberKey, this.localStore);
if (request.isCompleted) {
return request.result;
}
this.requestStream$.next(request); this.requestStream$.next(request);
return request.closed; return request.result;
} }
} }

2
frontend/app/framework/services/resource-loader.service.ts

@ -22,7 +22,7 @@ export class ResourceLoaderService {
let result = this.cache[key]; let result = this.cache[key];
if (!result) { if (!result) {
result = new Promise((resolve, reject) => { result = new Promise(resolve => {
const style = this.renderer.createElement('link'); const style = this.renderer.createElement('link');
this.renderer.listen(style, 'load', () => resolve()); this.renderer.listen(style, 'load', () => resolve());

2
frontend/app/framework/utils/date-time.ts

@ -95,7 +95,7 @@ export class DateTime {
if (value.length === DATE_FORMAT.length) { if (value.length === DATE_FORMAT.length) {
date = parse(value, DATE_FORMAT, new Date()); date = parse(value, DATE_FORMAT, new Date());
} else { } else {
date = date = parseISO(value); date = parseISO(value);
} }
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {

10
frontend/app/framework/utils/rxjs-extensions.ts

@ -7,7 +7,7 @@
// tslint:disable: only-arrow-functions // tslint:disable: only-arrow-functions
import { empty, Observable } from 'rxjs'; import { EMPTY, Observable, throwError } from 'rxjs';
import { catchError, distinctUntilChanged, filter, map, onErrorResumeNext, publishReplay, refCount, switchMap } from 'rxjs/operators'; import { catchError, distinctUntilChanged, filter, map, onErrorResumeNext, publishReplay, refCount, switchMap } from 'rxjs/operators';
import { DialogService } from './../services/dialog.service'; import { DialogService } from './../services/dialog.service';
import { Version, versioned, Versioned } from './version'; import { Version, versioned, Versioned } from './version';
@ -20,7 +20,7 @@ export function mapVersioned<T = any, R = any>(project: (value: T, version: Vers
}; };
} }
type Options = { silent?: boolean }; type Options = { silent?: boolean, throw?: boolean };
export function shareSubscribed<T>(dialogs: DialogService, options?: Options) { export function shareSubscribed<T>(dialogs: DialogService, options?: Options) {
return shareMapSubscribed<T, T>(dialogs, x => x, options); return shareMapSubscribed<T, T>(dialogs, x => x, options);
@ -36,7 +36,11 @@ export function shareMapSubscribed<T, R = T>(dialogs: DialogService, project: (v
dialogs.notifyError(error); dialogs.notifyError(error);
} }
return empty(); if (options?.throw) {
return throwError(error);
}
return EMPTY;
})) }))
.subscribe(); .subscribe();

3
frontend/app/shared/components/assets/asset-dialog.component.html

@ -94,7 +94,8 @@
<button type="button" class="btn btn-text-danger" [disabled]="!isEditable" <button type="button" class="btn btn-text-danger" [disabled]="!isEditable"
(sqxConfirmClick)="annotateForm.removeMetadata(i)" (sqxConfirmClick)="annotateForm.removeMetadata(i)"
confirmTitle="i18n:assets.deleteMetadataConfirmTitle" confirmTitle="i18n:assets.deleteMetadataConfirmTitle"
confirmText="i18n:assets.deleteMetadataConfirmText"> confirmText="i18n:assets.deleteMetadataConfirmText"
confirmRememberKey="removeAssetMetadata">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
</div> </div>

3
frontend/app/shared/components/assets/asset-folder.component.html

@ -26,7 +26,8 @@
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!canDelete" <a class="dropdown-item dropdown-item-delete" [class.disabled]="!canDelete"
(sqxConfirmClick)="emitDelete()" (sqxConfirmClick)="emitDelete()"
confirmTitle="i18n:assets.deleteFolderConfirmTitle" confirmTitle="i18n:assets.deleteFolderConfirmTitle"
confirmText="i18n:assets.deleteFolderConfirmText"> confirmText="i18n:assets.deleteFolderConfirmText"
confirmRememberKey="deleteAssetFolder">
{{ 'common.delete' | sqxTranslate }} {{ 'common.delete' | sqxTranslate }}
</a> </a>
</div> </div>

12
frontend/app/shared/components/assets/asset.component.html

@ -33,14 +33,16 @@
<a class="file-delete ml-2" *ngIf="!removeMode && asset.canDelete" <a class="file-delete ml-2" *ngIf="!removeMode && asset.canDelete"
(sqxConfirmClick)="delete.emit()" (sqxConfirmClick)="delete.emit()"
confirmTitle="i18n:assets.deleteConfirmTitle" confirmTitle="i18n:assets.deleteConfirmTitle"
confirmText="i18n:assets.deleteConfirmText"> confirmText="i18n:assets.deleteConfirmText"
confirmRememberKey="deleteAsset">
<i class="icon-delete"></i> <i class="icon-delete"></i>
</a> </a>
<a class="file-delete ml-2" *ngIf="removeMode" <a class="file-delete ml-2" *ngIf="removeMode"
(sqxConfirmClick)="remove.emit()" (sqxConfirmClick)="remove.emit()"
confirmTitle="i18n:assets.removeConfirmTitle" confirmTitle="i18n:assets.removeConfirmTitle"
confirmText="i18n:assets.removeConfirmText"> confirmText="i18n:assets.removeConfirmText"
confirmRememberKey="removeAsset">
<i class="icon-close"></i> <i class="icon-close"></i>
</a> </a>
</ng-container> </ng-container>
@ -134,14 +136,16 @@
<button type="button" class="btn btn-text-danger" *ngIf="!removeMode && asset.canDelete" <button type="button" class="btn btn-text-danger" *ngIf="!removeMode && asset.canDelete"
(sqxConfirmClick)="delete.emit()" (sqxConfirmClick)="delete.emit()"
confirmTitle="i18n:assets.deleteConfirmTitle" confirmTitle="i18n:assets.deleteConfirmTitle"
confirmText="i18n:assets.deleteConfirmText"> confirmText="i18n:assets.deleteConfirmText"
confirmRememberKey="deleteAsset">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>
<button type="button" class="btn btn-text-secondary" *ngIf="removeMode" <button type="button" class="btn btn-text-secondary" *ngIf="removeMode"
(sqxConfirmClick)="remove.emit()" (sqxConfirmClick)="remove.emit()"
confirmTitle="i18n:assets.removeConfirmTitle" confirmTitle="i18n:assets.removeConfirmTitle"
confirmText="i18n:assets.removeConfirmText"> confirmText="i18n:assets.removeConfirmText"
confirmRememberKey="removeAsset">
<i class="icon-close"></i> <i class="icon-close"></i>
</button> </button>
</td> </td>

1
frontend/app/shared/components/comments/comment.component.html

@ -49,6 +49,7 @@
(sqxConfirmClick)="delete()" (sqxConfirmClick)="delete()"
confirmTitle="i18n:comments.deleteConfirmTitle" confirmTitle="i18n:comments.deleteConfirmTitle"
confirmText="i18n:comments.deleteConfirmText" confirmText="i18n:comments.deleteConfirmText"
confirmRememberKey="deleteComment"
[confirmRequired]="confirmDelete"> [confirmRequired]="confirmDelete">
<i class="icon-bin2"></i> <i class="icon-bin2"></i>
</button> </button>

2
frontend/app/shared/components/forms/references-checkboxes.component.ts

@ -31,7 +31,7 @@ const NO_EMIT = { emitEvent: false };
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ReferencesCheckboxesComponent extends StatefulControlComponent<State, ReadonlyArray<string>> implements OnChanges { export class ReferencesCheckboxesComponent extends StatefulControlComponent<State, ReadonlyArray<string>> implements OnChanges {
private itemCount: number; private readonly itemCount: number;
private contentItems: ReadonlyArray<ContentDto> | null = null; private contentItems: ReadonlyArray<ContentDto> | null = null;
@Input() @Input()

2
frontend/app/shared/components/forms/references-dropdown.component.ts

@ -38,9 +38,9 @@ const NO_EMIT = { emitEvent: false };
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ReferencesDropdownComponent extends StatefulControlComponent<State, ReadonlyArray<string> | string> implements OnChanges { export class ReferencesDropdownComponent extends StatefulControlComponent<State, ReadonlyArray<string> | string> implements OnChanges {
private readonly itemCount: number;
private languageField: LanguageDto; private languageField: LanguageDto;
private selectedId: string | undefined; private selectedId: string | undefined;
private itemCount: number;
@Input() @Input()
public schemaId: string; public schemaId: string;

2
frontend/app/shared/components/forms/references-tags.component.ts

@ -31,7 +31,7 @@ const NO_EMIT = { emitEvent: false };
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class ReferencesTagsComponent extends StatefulControlComponent<State, ReadonlyArray<string>> implements OnChanges { export class ReferencesTagsComponent extends StatefulControlComponent<State, ReadonlyArray<string>> implements OnChanges {
private itemCount: number; private readonly itemCount: number;
private contentItems: ReadonlyArray<ContentDto> | null = null; private contentItems: ReadonlyArray<ContentDto> | null = null;
@Input() @Input()

2
frontend/app/shared/guards/load-apps.guard.ts

@ -19,6 +19,6 @@ export class LoadAppsGuard implements CanActivate {
} }
public canActivate(): Observable<boolean> { public canActivate(): Observable<boolean> {
return this.appsState.load().pipe(map(a => true)); return this.appsState.load().pipe(map(_ => true));
} }
} }

2
frontend/app/shared/guards/load-languages.guard.ts

@ -19,6 +19,6 @@ export class LoadLanguagesGuard implements CanActivate {
} }
public canActivate(): Observable<boolean> { public canActivate(): Observable<boolean> {
return this.languagesState.load().pipe(map(a => true)); return this.languagesState.load().pipe(map(_ => true));
} }
} }

8
frontend/app/shared/interceptors/auth.interceptor.ts

@ -9,13 +9,13 @@ import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { ApiUrlConfig, ErrorDto } from '@app/framework'; import { ApiUrlConfig, ErrorDto } from '@app/framework';
import { empty, Observable, throwError } from 'rxjs'; import { EMPTY, Observable, throwError } from 'rxjs';
import { catchError, switchMap, take } from 'rxjs/operators'; import { catchError, switchMap, take } from 'rxjs/operators';
import { AuthService, Profile } from './../services/auth.service'; import { AuthService, Profile } from './../services/auth.service';
@Injectable() @Injectable()
export class AuthInterceptor implements HttpInterceptor { export class AuthInterceptor implements HttpInterceptor {
private baseUrl: string; private readonly baseUrl: string;
constructor(apiUrlConfig: ApiUrlConfig, constructor(apiUrlConfig: ApiUrlConfig,
private readonly authService: AuthService, private readonly authService: AuthService,
@ -52,7 +52,7 @@ export class AuthInterceptor implements HttpInterceptor {
catchError(() => { catchError(() => {
this.authService.logoutRedirect(); this.authService.logoutRedirect();
return empty(); return EMPTY;
}), }),
switchMap(u => this.makeRequest(req, next, u))); switchMap(u => this.makeRequest(req, next, u)));
} else if (error.status === 401 || error.status === 403) { } else if (error.status === 401 || error.status === 403) {
@ -63,7 +63,7 @@ export class AuthInterceptor implements HttpInterceptor {
this.router.navigate(['/forbidden'], { replaceUrl: true }); this.router.navigate(['/forbidden'], { replaceUrl: true });
} }
return empty(); return EMPTY;
} else { } else {
return throwError(new ErrorDto(403, 'i18n:common.errorNoPermission')); return throwError(new ErrorDto(403, 'i18n:common.errorNoPermission'));
} }

4
frontend/app/shared/services/assets.service.spec.ts

@ -376,9 +376,9 @@ describe('AssetsService', () => {
} }
}; };
assetsService.deleteAssetItem('my-app', resource, version).subscribe(); assetsService.deleteAssetItem('my-app', resource, true, version).subscribe();
const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/123'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/assets/123?checkReferrers=true');
expect(req.request.method).toEqual('DELETE'); expect(req.request.method).toEqual('DELETE');
expect(req.request.headers.get('If-Match')).toEqual(version.value); expect(req.request.headers.get('If-Match')).toEqual(version.value);

4
frontend/app/shared/services/assets.service.ts

@ -394,10 +394,10 @@ export class AssetsService {
pretifyError('i18n:assets.moveFailed')); pretifyError('i18n:assets.moveFailed'));
} }
public deleteAssetItem(appName: string, asset: Resource, version: Version): Observable<Versioned<any>> { public deleteAssetItem(appName: string, asset: Resource, checkReferrers: boolean, version: Version): Observable<Versioned<any>> {
const link = asset._links['delete']; const link = asset._links['delete'];
const url = this.apiUrl.buildUrl(link.href); const url = this.apiUrl.buildUrl(link.href) + `?checkReferrers=${checkReferrers}`;
return HTTP.requestVersioned(this.http, link.method, url, version).pipe( return HTTP.requestVersioned(this.http, link.method, url, version).pipe(
tap(() => { tap(() => {

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

Loading…
Cancel
Save