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.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
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.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Extensions.Actions.Comment
{
public sealed class CommentActionHandler : RuleActionHandler<CommentAction, CommentJob>
public sealed class CommentActionHandler : RuleActionHandler<CommentAction, CreateComment>
{
private const string Description = "Send a Comment";
private readonly ICommandBus commandBus;
@ -30,56 +28,48 @@ namespace Squidex.Extensions.Actions.Comment
this.commandBus = commandBus;
}
protected override async Task<(string Description, CommentJob Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action)
protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, CommentAction action)
{
if (@event is EnrichedContentEvent contentEvent)
{
var text = await FormatAsync(action.Text, @event);
var ruleJob = new CreateComment
{
AppId = contentEvent.AppId,
};
var actor = contentEvent.Actor;
ruleJob.Text = await FormatAsync(action.Text, @event);
if (!string.IsNullOrEmpty(action.Client))
{
actor = new RefToken(RefTokenType.Client, action.Client);
ruleJob.Actor = new RefToken(RefTokenType.Client, action.Client);
}
var ruleJob = new CommentJob
else
{
AppId = contentEvent.AppId,
Actor = actor,
CommentsId = contentEvent.Id.ToString(),
Text = text
};
ruleJob.Actor = contentEvent.Actor;
}
ruleJob.CommentsId = contentEvent.Id.ToString();
return (Description, ruleJob);
}
return ("Ignore", new CommentJob());
return ("Ignore", new CreateComment());
}
protected override async Task<Result> ExecuteJobAsync(CommentJob job, CancellationToken ct = default)
protected override async Task<Result> ExecuteJobAsync(CreateComment job, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(job.CommentsId))
{
return Result.Ignored();
}
var command = SimpleMapper.Map(job, new CreateComment());
var command = job;
command.FromRule = true;
await commandBus.PublishAsync(command);
return Result.Success($"Commented: {job.Text}");
}
}
public sealed class CommentJob
{
public NamedId<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.Infrastructure;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Reflection;
using Squidex.Shared.Users;
namespace Squidex.Extensions.Actions.Notification
{
public sealed class NotificationActionHandler : RuleActionHandler<NotificationAction, NotificationJob>
public sealed class NotificationActionHandler : RuleActionHandler<NotificationAction, CreateComment>
{
private const string Description = "Send a Notification";
private static readonly NamedId<Guid> NoApp = NamedId.Of(Guid.Empty, "none");
@ -36,7 +35,7 @@ namespace Squidex.Extensions.Actions.Notification
this.userResolver = userResolver;
}
protected override async Task<(string Description, NotificationJob Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action)
protected override async Task<(string Description, CreateComment Data)> CreateJobAsync(EnrichedEvent @event, NotificationAction action)
{
if (@event is EnrichedUserEventBase userEvent)
{
@ -56,7 +55,7 @@ namespace Squidex.Extensions.Actions.Notification
throw new InvalidOperationException($"Cannot find user by '{action.User}'");
}
var ruleJob = new NotificationJob { Actor = actor, CommentsId = user.Id, Text = text };
var ruleJob = new CreateComment { Actor = actor, CommentsId = user.Id, Text = text };
if (!string.IsNullOrWhiteSpace(action.Url))
{
@ -71,32 +70,24 @@ namespace Squidex.Extensions.Actions.Notification
return (Description, ruleJob);
}
return ("Ignore", new NotificationJob());
return ("Ignore", new CreateComment());
}
protected override async Task<Result> ExecuteJobAsync(NotificationJob job, CancellationToken ct = default)
protected override async Task<Result> ExecuteJobAsync(CreateComment job, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(job.CommentsId))
{
return Result.Ignored();
}
var command = SimpleMapper.Map(job, new CreateComment { AppId = NoApp });
var command = job;
command.AppId = NoApp;
command.FromRule = true;
await commandBus.PublishAsync(command);
return Result.Success($"Notified: {job.Text}");
}
}
public sealed class NotificationJob
{
public RefToken Actor { get; set; }
public string CommentsId { get; set; }
public string Text { get; set; }
public Uri Url { get; set; }
}
}

5
backend/i18n/frontend_en.json

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

5
backend/i18n/frontend_it.json

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

5
backend/i18n/frontend_nl.json

@ -58,6 +58,8 @@
"assets.deleteFolderConfirmTitle": "Map verwijderen",
"assets.deleteMetadataConfirmText": "Wil je deze metadata echt verwijderen?",
"assets.deleteMetadataConfirmTitle": "Metadata verwijderen",
"assets.deleteReferrerConfirmText": "The asset is referenced by a content item.\n\nDo you really want to delete the asset?",
"assets.deleteReferrerConfirmTitle": "Delete asset",
"assets.downloadVersion": "Download deze versie",
"assets.dropToUpdate": "Zet neer om te updaten",
"assets.duplicateFile": "Asset is al geüpload.",
@ -286,6 +288,7 @@
"common.queryOperators.ne": "is not equals to",
"common.queryOperators.startsWith": "starts with",
"common.refresh": "Vernieuwen",
"common.remember": "Remember my decision",
"common.rename": "Hernoemen",
"common.requiredHint": "verplicht",
"common.reset": "Reset",
@ -354,6 +357,8 @@
"contents.deleteConfirmTitle": "Inhoud verwijderen",
"contents.deleteFailed": "Verwijderen van inhoud is mislukt. Laad opnieuw.",
"contents.deleteManyConfirmText": "Weet je zeker dat je de geselecteerde inhoudsitems wilt verwijderen?",
"contents.deleteReferrerConfirmText": "The content is referenced by another content item.\n\nDo you really want to delete the content?",
"contents.deleteReferrerConfirmTitle": "Delete content",
"contents.deleteVersionConfirmText": "Wil je deze versie echt verwijderen?",
"contents.deleteVersionFailed": "Verwijderen van versie is mislukt. Laad opnieuw.",
"contents.draftNew": "Nieuw concept",

2
backend/i18n/source/backend_en.json

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

5
backend/i18n/source/frontend_en.json

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -10,7 +10,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
@ -22,8 +21,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed partial class MongoAssetFolderRepository : MongoRepositoryBase<MongoAssetFolderEntity>, IAssetFolderRepository
{
private static readonly Lazy<string> IdField = new Lazy<string>(GetIdField);
public MongoAssetFolderRepository(IMongoDatabase database)
: base(database)
{
@ -67,7 +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)
.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;
}
}
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.Threading;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
@ -27,8 +26,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository
{
private static readonly Lazy<string> IdField = new Lazy<string>(GetIdField);
public MongoAssetRepository(IMongoDatabase database)
: base(database)
{
@ -105,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
await Collection.Find(BuildFilter(appId, ids)).Only(x => x.Id)
.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)
.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.Ne(x => x.IsDeleted, true));
}
private static string GetIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoAssetEntity)).GetMemberMap(nameof(MongoAssetEntity.Id)).ElementName;
}
}
}

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

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

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

@ -30,6 +30,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
private readonly QueryContentsByIds queryContentsById;
private readonly QueryContentsByQuery queryContentsByQuery;
private readonly QueryIdsAsync queryIdsAsync;
private readonly QueryReferrersAsync queryReferrersAsync;
private readonly QueryScheduledContents queryScheduledItems;
public MongoContentCollectionAll(IMongoDatabase database, IAppProvider appProvider, ITextIndex indexer, DataConverter converter)
@ -39,6 +40,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
queryContentsById = new QueryContentsByIds(converter, appProvider);
queryContentsByQuery = new QueryContentsByQuery(converter, indexer);
queryIdsAsync = new QueryIdsAsync(appProvider);
queryReferrersAsync = new QueryReferrersAsync();
queryScheduledItems = new QueryScheduledContents();
}
@ -58,6 +60,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await queryContentsById.PrepareAsync(collection, ct);
await queryContentsByQuery.PrepareAsync(collection, ct);
await queryIdsAsync.PrepareAsync(collection, ct);
await queryReferrersAsync.PrepareAsync(collection, ct);
await queryScheduledItems.PrepareAsync(collection, ct);
}
@ -125,6 +128,14 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
public async Task<bool> HasReferrersAsync(Guid contentId)
{
using (Profiler.TraceMethod<MongoContentRepository>())
{
return await queryReferrersAsync.DoAsync(contentId);
}
}
public Task ResetScheduledAsync(Guid id)
{
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);
}
public Task<bool> HasReferrersAsync(Guid contentId)
{
return collectionAll.HasReferrersAsync(contentId);
}
public IEnumerable<IMongoCollection<MongoContentEntity>> GetInternalCollections()
{
yield return collectionAll.GetInternalCollection();

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

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

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.Threading;
using System.Threading.Tasks;
using MongoDB.Bson.Serialization;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.MongoDb.Queries;
@ -21,8 +20,6 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
internal sealed class QueryIdsAsync : OperationBase
{
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;
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)
.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)
@ -70,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
await Collection.Find(filter).Only(x => x.Id, x => x.IndexedSchemaId)
.ToListAsync();
return contentEntities.Select(x => (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)
@ -88,15 +85,5 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents.Operations
return Filter.And(filters);
}
private static string GetIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.Id)).ElementName;
}
private static string GetSchemaIdField()
{
return BsonClassMap.LookupClassMap(typeof(MongoContentEntity)).GetMemberMap(nameof(MongoContentEntity.IndexedSchemaId)).ElementName;
}
}
}

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

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

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

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

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

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

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

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

@ -12,6 +12,7 @@ using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Guards;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Contents.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Contents;
@ -28,15 +29,21 @@ namespace Squidex.Domain.Apps.Entities.Contents
public class ContentDomainObject : LogSnapshotDomainObject<ContentState>
{
private readonly IContentWorkflow contentWorkflow;
private readonly IContentRepository contentRepository;
private readonly ContentOperationContext context;
public ContentDomainObject(IStore<Guid> store, IContentWorkflow contentWorkflow, ContentOperationContext context, ISemanticLog log)
public ContentDomainObject(IStore<Guid> store, ISemanticLog log,
IContentWorkflow contentWorkflow,
IContentRepository contentRepository,
ContentOperationContext context)
: base(store, log)
{
Guard.NotNull(context, nameof(context));
Guard.NotNull(contentRepository, nameof(contentRepository));
Guard.NotNull(contentWorkflow, nameof(contentWorkflow));
Guard.NotNull(context, nameof(context));
this.contentWorkflow = contentWorkflow;
this.contentRepository = contentRepository;
this.context = context;
}
@ -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);
});

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.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation;
#pragma warning disable IDE0016 // Use 'throw' expression
@ -43,7 +45,11 @@ namespace Squidex.Domain.Apps.Entities.Contents
private ContentCommand command;
private ValidationContext validationContext;
public ContentOperationContext(IAppProvider appProvider, IEnumerable<IValidatorsFactory> factories, IScriptEngine scriptEngine, ISemanticLog log)
public ContentOperationContext(
IAppProvider appProvider,
IEnumerable<IValidatorsFactory> factories,
IScriptEngine scriptEngine,
ISemanticLog log)
{
this.appProvider = appProvider;
this.factories = factories;

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<bool> HasReferrersAsync(Guid contentId);
Task<IContentEntity?> FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, SearchScope scope);
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 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)
{
Guard.NotNull(appProvider, nameof(appProvider));
Guard.NotNull(ruleEnqueuer, nameof(ruleEnqueuer));
this.appProvider = appProvider;
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 bool FromRule { get; set; }
public long ExpectedVersion { get; set; } = EtagVersion.Auto;
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -284,6 +284,7 @@ namespace Squidex.Areas.Api.Controllers.Assets
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="id">The id of the asset to delete.</param>
/// <param name="checkReferrers">True to check referrers of this asset.</param>
/// <returns>
/// 204 => Asset deleted.
/// 404 => Asset or app not found.
@ -292,9 +293,9 @@ namespace Squidex.Areas.Api.Controllers.Assets
[Route("apps/{app}/assets/{id}/")]
[ApiPermissionOrAnonymous(Permissions.AppAssetsDelete)]
[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();
}

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="name">The name of the schema.</param>
/// <param name="id">The id of the content item to delete.</param>
/// <param name="checkReferrers">True to check referrers of this content.</param>
/// <returns>
/// 204 => Content deleted.
/// 404 => Content, schema or app not found.
@ -562,9 +563,9 @@ namespace Squidex.Areas.Api.Controllers.Contents
[Route("content/{app}/{name}/{id}/")]
[ApiPermissionOrAnonymous(Permissions.AppContentsDelete)]
[ApiCosts(1)]
public async Task<IActionResult> DeleteContent(string app, string name, 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);

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));
}
[Theory]
[Expressions(
"$CONTENT_DATA",
"${CONTENT_DATA}",
"${JSON.stringify(event.data)}",
null
)]
public async Task Should_return_json_string_when_data(string script)
{
var @event = new EnrichedContentEvent
{
Data =
new NamedContentData()
.AddField("city",
new ContentFieldData()
.AddJsonValue(JsonValue.Object().Add("name", "Berlin")))
};
var result = await sut.FormatAsync(script, @event);
Assert.Equal("{\"city\":{\"iv\":{\"name\":\"Berlin\"}}}", result);
}
[Theory]
[Expressions(
null,

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

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

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

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

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

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

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

@ -14,6 +14,7 @@ using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.Tags;
using Squidex.Domain.Apps.Entities.Assets.Commands;
using Squidex.Domain.Apps.Entities.Assets.State;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Infrastructure;
@ -26,6 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetDomainObjectTests : HandlerTestBase<AssetState>
{
private readonly IContentRepository contentRepository = A.Fake<IContentRepository>();
private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IAssetQueryService assetQuery = A.Fake<IAssetQueryService>();
private readonly 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>>._))
.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);
}
@ -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()
{
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))
.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);
}

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

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

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

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

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

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 { 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 { debounceTime, filter, onErrorResumeNext, tap } from 'rxjs/operators';
import { debounceTime, filter, tap } from 'rxjs/operators';
@Component({
selector: 'sqx-content-page',
@ -192,10 +192,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD
const content = this.content;
if (content) {
this.contentsState.deleteMany([content]).pipe(onErrorResumeNext())
.subscribe(() => {
this.back();
});
this.contentsState.deleteMany([content]);
}
}

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

@ -55,7 +55,8 @@
<button type="button" class="btn btn-danger" *ngIf="selectionCanDelete"
(sqxConfirmClick)="deleteSelected()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteManyConfirmText">
confirmText="i18n:contents.deleteManyConfirmText"
confirmRememberKey="deleteContents">
{{ 'common.delete' | sqxTranslate }}
</button>
</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.route.params.pipe(
switchMap(x => this.schemasState.selectedSchema), distinctUntilChanged())
switchMap(() => this.schemasState.selectedSchema), distinctUntilChanged())
.subscribe(schema => {
this.resetSelection();
@ -202,8 +202,10 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit {
this.selectionCount++;
for (const action in this.nextStatuses) {
if (!content.statusUpdates.find(x => x.status === action)) {
delete this.nextStatuses[action];
if (this.nextStatuses.hasOwnProperty(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
})
export class SidebarPageComponent extends ResourceOwner implements AfterViewInit {
private isInitialized = false;
private context: any;
private readonly context: any;
private content: any;
private isInitialized = false;
@ViewChild('iframe', { static: false })
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'
})
export class DueTimeSelectorComponent {
private disabled: boolean;
private readonly disabled: boolean;
private dueTimeResult: Subject<string | null>;
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"
(sqxConfirmClick)="delete.emit()"
confirmTitle="i18n:contents.deleteConfirmTitle"
confirmText="i18n:contents.deleteConfirmText">
confirmText="i18n:contents.deleteConfirmText"
confirmRememberKey="deleteContent">
{{ 'common.delete' | sqxTranslate }}
</a>
</div>

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

@ -29,7 +29,8 @@
<button type="button" class="btn btn-text-secondary"
(sqxConfirmClick)="delete.emit()"
confirmTitle="i18n:contents.removeConfirmTitle"
confirmText="i18n:contents.removeConfirmText">
confirmText="i18n:contents.removeConfirmText"
confirmRememberKey="removeReference">
<i class="icon-close"></i>
</button>
</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()"
(sqxConfirmClick)="resetConfig()"
confirmTitle="i18n:dashboard.resetConfigConfirmTitle"
confirmText="i18n:dashboard.resetConfigConfirmText">
confirmText="i18n:dashboard.resetConfigConfirmText"
confirmRememberKey="resetConfig">
{{ 'common.reset' | sqxTranslate }}
</a>
</div>

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

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

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

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

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

@ -69,7 +69,8 @@
<a class="dropdown-item"
(sqxConfirmClick)="lockField()"
confirmTitle="i18n:schemas.field.lockConfirmText"
confirmText="i18n:schemas.field.lockConfirmText">
confirmText="i18n:schemas.field.lockConfirmText"
confirmRememberKey="lockField">
{{ 'schemas.field.lock' | sqxTranslate }}
</a>
</ng-container>
@ -80,7 +81,8 @@
<a class="dropdown-item dropdown-item-delete" [class.disabled]="!field.canDelete"
(sqxConfirmClick)="deleteField()"
confirmTitle="i18n:schemas.field.deleteConfirmTitle"
confirmText="i18n:schemas.field.deleteConfirmText">
confirmText="i18n:schemas.field.deleteConfirmText"
confirmRememberKey="deleteField">
{{ 'common.delete' | sqxTranslate }}
</a>
</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"
(sqxConfirmClick)="editForm.remove(i)"
confirmTitle="i18n:schemas.deleteUrlConfirmTitle"
confirmText="i18n:schemas.deleteUrlConfirmText">
confirmText="i18n:schemas.deleteUrlConfirmText"
confirmRememberKey="removePreviewUrl">
<i class="icon-bin2"></i>
</button>
</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) {
this.schemasState.configurePreviewUrls(this.schema, value)
.subscribe(update => {
.subscribe(() => {
this.editForm.submitCompleted({ noReset: true });
}, 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"
(sqxConfirmClick)="editForm.remove(i)"
confirmTitle="i18n:schemas.deleteRuleConfirmTitle"
confirmText="i18n:schemas.deleteRuleConfirmText">
confirmText="i18n:schemas.deleteRuleConfirmText"
confirmRememberKey="deleteFieldRule">
<i class="icon-bin2"></i>
</button>
</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"
(sqxConfirmClick)="deleteSchema()"
confirmTitle="i18n:schemas.deleteConfirmTitle"
confirmText="i18n:schemas.deleteConfirmText">
confirmText="i18n:schemas.deleteConfirmText"
confirmRememberKey="deleteSchema">
{{ 'common.delete' | sqxTranslate }}
</a>
</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"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:backups.deleteConfirmTitle"
confirmText="i18n:backups.deleteConfirmText">
confirmText="i18n:backups.deleteConfirmText"
confirmRememberKey="deleteBackup">
<i class="icon-bin2"></i>
</button>
</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"
(sqxConfirmClick)="revoke()"
confirmTitle="i18n:clients.deleteConfirmTitle"
confirmText="i18n:clients.deleteConfirmText">
confirmText="i18n:clients.deleteConfirmText"
confirmRememberKey="revokeClient">
<i class="icon-bin2"></i>
</button>
</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"
(sqxConfirmClick)="remove()"
confirmTitle="i18n:contributors.deleteConfirmTitle"
confirmText="i18n:contributors.deleteConfirmText">
confirmText="i18n:contributors.deleteConfirmText"
confirmRememberKey="removeContributor">
<i class="icon-bin2"></i>
</button>
</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 { FormBuilder } from '@angular/forms';
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';
type ImportStatus = {
@ -79,7 +79,7 @@ export class ImportContributorsDialogComponent {
status.result = 'Failed';
}
return empty();
return EMPTY;
})
), 1)
).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"
(sqxConfirmClick)="remove()"
confirmTitle="i18n:languages.deleteConfirmTitle"
confirmText="i18n:languages.deleteConfirmText">
confirmText="i18n:languages.deleteConfirmText"
confirmRememberKey="removeLanguage">
<i class="icon-bin2"></i>
</button>
</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"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:patterns.deleteConfirmTitle"
confirmText="i18n:patterns.deleteConfirmText">
confirmText="i18n:patterns.deleteConfirmText"
confirmRememberKey="deletePattern">
<i class="icon-bin2"></i>
</button>
</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"
(sqxConfirmClick)="changeMonthly()"
confirmRememberKey="changePlan"
confirmTitle="i18n:plans.changeConfirmTitle"
[confirmText]="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"
(sqxConfirmClick)="delete()"
confirmTitle="i18n:roles.deleteConfirmTitle"
confirmText="i18n:roles.deleteConfirmText">
confirmText="i18n:roles.deleteConfirmText"
confirmRememberKey="deleteRole">
<i class="icon-bin2"></i>
</button>
</div>

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

@ -5,7 +5,11 @@
<span class="workflow-name">{{workflow.displayName}}</span>
</div>
<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>
</div>
<div class="col-options">
@ -17,7 +21,8 @@
<button type="button" class="btn btn-text-danger" [disabled]="!workflow.canDelete"
(sqxConfirmClick)="remove()"
confirmTitle="i18n:workflows.deleteConfirmTitle"
confirmText="i18n:workflows.deleteConfirmText">
confirmText="i18n:workflows.deleteConfirmText"
confirmRememberKey="deleteWorkflow">
<i class="icon-bin2"></i>
</button>
</div>
@ -62,7 +67,10 @@
<label class="col-form-label" for="{{workflow.id}}_schemas">{{ 'common.schemas' | sqxTranslate }}</label>
<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-form-hint>
@ -71,7 +79,15 @@
</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>
<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()
public confirmText: string;
@Input()
public confirmRememberKey: string;
@Input()
public confirmRequired = true;
@ -48,7 +51,7 @@ export class ConfirmClickDirective {
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 => {
if (confirmed) {
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
})
export class DateTimeEditorComponent extends StatefulControlComponent<{}, string | null> implements OnInit, AfterViewInit, FocusComponent {
private readonly hideDateButtonsSettings: boolean;
private readonly hideDateTimeModeButtonSetting: boolean;
private picker: any;
private dateTime: DateTime | null;
private hideDateButtonsSettings: boolean;
private hideDateTimeModeButtonSetting: boolean;
private suppressEvents = false;
@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> {
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> {

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 {
private callChange = (v: any) => { /* NOOP */ };
private callChange = (_: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
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 {
private callChange = (v: any) => { /* NOOP */ };
private callChange = (_: any) => { /* NOOP */ };
private callTouched = () => { /* NOOP */ };
private transformer: Transform;

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

@ -1,13 +1,21 @@
<ng-content></ng-content>
<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>
{{snapshot.dialogRequest?.title | sqxTranslate}}
{{request.title | sqxTranslate}}
</ng-container>
<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 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) {
this.next(s => {
if (s.dialogRequest) {
s.dialogRequest.complete(value);
}
s.dialogRequest?.complete(value);
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 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.
*/
import { IMock, It, Mock, Times } from 'typemoq';
import { DialogRequest, DialogService, DialogServiceFactory, Notification, Tooltip } from './dialog.service';
import { LocalStoreService } from './local-store.service';
describe('DialogService', () => {
let localStore: IMock<LocalStoreService>;
beforeEach(() => {
localStore = Mock.ofType<LocalStoreService>();
});
it('should instantiate from factory', () => {
const dialogService = DialogServiceFactory();
const dialogService = DialogServiceFactory(localStore.object);
expect(dialogService).toBeDefined();
});
it('should instantiate', () => {
const dialogService = new DialogService();
const dialogService = new DialogService(localStore.object);
expect(dialogService).toBeDefined();
});
@ -37,33 +44,115 @@ describe('DialogService', () => {
expect(notification.messageType).toBe('info');
});
it('should create dialog request', () => {
const dialog = new DialogRequest('MyTitle', 'MyText');
[true, false].map(confirmed => {
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');
expect(dialog.text).toBe('MyText');
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());
});
});
it('should confirm dialog', () => {
const dialog = new DialogRequest('MyTitle', 'MyText');
[true, false].map(confirmed => {
it(`should confirm dialog with '${confirmed}' but not remember`, () => {
const dialogService = new DialogService(localStore.object);
let isCompleted = false;
let isNext = false;
let isCompleted = false;
let isNext: boolean;
dialog.closed.subscribe(result => {
isNext = result;
}, undefined, () => {
isCompleted = true;
dialogService.dialogs.subscribe(dialog => {
dialog.complete(confirmed);
});
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();
expect(isNext).toBeTruthy();
dialogService.confirm('MyTitle', 'MyText', 'MyKey').subscribe(result => {
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', () => {
const dialogService = new DialogService();
const dialogService = new DialogService(localStore.object);
const tooltip = new Tooltip('target', 'text', 'left');
@ -79,7 +168,7 @@ describe('DialogService', () => {
});
it('should publish notification', () => {
const dialogService = new DialogService();
const dialogService = new DialogService(localStore.object);
const notification = Notification.error('Message');
@ -95,7 +184,7 @@ describe('DialogService', () => {
});
it('should publish dialog request', () => {
const dialogService = new DialogService();
const dialogService = new DialogService(localStore.object);
let pushedDialog: DialogRequest;
@ -105,6 +194,6 @@ describe('DialogService', () => {
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 { Observable, Subject, throwError } from 'rxjs';
import { Observable, ReplaySubject, Subject, throwError } from 'rxjs';
import { ErrorDto } from './../utils/error';
import { Types } from './../utils/types';
import { LocalStoreService } from './local-store.service';
export const DialogServiceFactory = () => {
return new DialogService();
export const DialogServiceFactory = (localStore: LocalStoreService) => {
return new DialogService(localStore);
};
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$;
}
public get isCompleted() {
return this.resultStream$.isStopped;
}
public get canRemember() {
return !!this.rememberKey;
}
public remember: boolean;
constructor(
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) {
this.resultStream$.next(value);
public complete(confirmed: boolean) {
if (this.rememberKey && this.remember) {
this.localStore.setInt(this.rememberKey, confirmed ? 1 : 2);
}
this.resultStream$.next(confirmed);
this.resultStream$.complete();
}
}
@ -77,6 +104,11 @@ export class DialogService {
return this.notificationsStream$;
}
constructor(
private readonly localStore: LocalStoreService
) {
}
public notifyError(error: string | ErrorDto) {
if (Types.is(error, ErrorDto)) {
this.notify(Notification.error(error));
@ -99,11 +131,15 @@ export class DialogService {
this.tooltipStream$.next(tooltip);
}
public confirm(title: string, text: string): Observable<boolean> {
const request = new DialogRequest(title, text);
public confirm(title: string, text: string, rememberKey?: string): Observable<boolean> {
const request = new DialogRequest(title, text, rememberKey, this.localStore);
if (request.isCompleted) {
return request.result;
}
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];
if (!result) {
result = new Promise((resolve, reject) => {
result = new Promise(resolve => {
const style = this.renderer.createElement('link');
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) {
date = parse(value, DATE_FORMAT, new Date());
} else {
date = date = parseISO(value);
date = parseISO(value);
}
if (isNaN(date.getTime())) {

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

@ -7,7 +7,7 @@
// 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 { DialogService } from './../services/dialog.service';
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) {
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);
}
return empty();
if (options?.throw) {
return throwError(error);
}
return EMPTY;
}))
.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"
(sqxConfirmClick)="annotateForm.removeMetadata(i)"
confirmTitle="i18n:assets.deleteMetadataConfirmTitle"
confirmText="i18n:assets.deleteMetadataConfirmText">
confirmText="i18n:assets.deleteMetadataConfirmText"
confirmRememberKey="removeAssetMetadata">
<i class="icon-bin2"></i>
</button>
</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"
(sqxConfirmClick)="emitDelete()"
confirmTitle="i18n:assets.deleteFolderConfirmTitle"
confirmText="i18n:assets.deleteFolderConfirmText">
confirmText="i18n:assets.deleteFolderConfirmText"
confirmRememberKey="deleteAssetFolder">
{{ 'common.delete' | sqxTranslate }}
</a>
</div>

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

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

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

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

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

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

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

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

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

@ -19,6 +19,6 @@ export class LoadAppsGuard implements CanActivate {
}
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> {
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 { Router } from '@angular/router';
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 { AuthService, Profile } from './../services/auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private baseUrl: string;
private readonly baseUrl: string;
constructor(apiUrlConfig: ApiUrlConfig,
private readonly authService: AuthService,
@ -52,7 +52,7 @@ export class AuthInterceptor implements HttpInterceptor {
catchError(() => {
this.authService.logoutRedirect();
return empty();
return EMPTY;
}),
switchMap(u => this.makeRequest(req, next, u)));
} else if (error.status === 401 || error.status === 403) {
@ -63,7 +63,7 @@ export class AuthInterceptor implements HttpInterceptor {
this.router.navigate(['/forbidden'], { replaceUrl: true });
}
return empty();
return EMPTY;
} else {
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.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'));
}
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 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(
tap(() => {

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

Loading…
Cancel
Save