From 3971b82416403c478754000f854ea2bc50bb8228 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 25 Sep 2020 16:11:18 +0200 Subject: [PATCH 01/11] Fix is editing. --- frontend/app/features/settings/pages/roles/role.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/features/settings/pages/roles/role.component.ts b/frontend/app/features/settings/pages/roles/role.component.ts index 823a19df9..30bbaa136 100644 --- a/frontend/app/features/settings/pages/roles/role.component.ts +++ b/frontend/app/features/settings/pages/roles/role.component.ts @@ -59,7 +59,7 @@ export class RoleComponent implements OnChanges { public properties: {}; public propertiesSimple = SIMPLE_PROPERTIES; - public isEditing = true; + public isEditing = false; public isEditable = false; public addPermissionForm = new AddPermissionForm(this.formBuilder); From a7d6654b9b64abc572b27f1d5f5b7eb12c5b58ad Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 26 Sep 2020 11:47:46 +0200 Subject: [PATCH 02/11] Disable button for array editor when max item reached. --- .../Extensions/EventFluidExtensions.cs | 105 ++++++++++++++++-- .../Extensions/EventJintExtension.cs | 21 ++++ .../PredefinedPatternsFormatter.cs | 23 ++++ .../IUrlGenerator.cs | 2 + .../src/Squidex.Web/Services/UrlGenerator.cs | 5 + .../RuleEventFormatterCompareTests.cs | 76 +++++++++++++ .../Contents/TestData/FakeUrlGenerator.cs | 5 + .../shared/forms/array-editor.component.html | 82 +++++++------- .../shared/forms/array-editor.component.ts | 24 +++- 9 files changed, 289 insertions(+), 54 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs index 099b67053..11e567a6b 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventFluidExtensions.cs @@ -11,6 +11,7 @@ using Fluid.Values; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Templates; using Squidex.Infrastructure; +using Squidex.Text; namespace Squidex.Domain.Apps.Core.HandleRules.Extensions { @@ -29,21 +30,34 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions { TemplateContext.GlobalFilters.AddFilter("contentUrl", ContentUrl); TemplateContext.GlobalFilters.AddFilter("assetContentUrl", AssetContentUrl); + TemplateContext.GlobalFilters.AddFilter("assetContentAppUrl", AssetContentAppUrl); + TemplateContext.GlobalFilters.AddFilter("assetContentSlugUrl", AssetContentSlugUrl); } private FluidValue ContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context) { - if (input is ObjectValue objectValue) + var value = input.ToObjectValue(); + + switch (value) { - if (context.GetValue("event")?.ToObjectValue() is EnrichedContentEvent contentEvent) - { - if (objectValue.ToObjectValue() is Guid guid && guid != Guid.Empty) + case Guid guid when guid != Guid.Empty: { - var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, guid); + if (context.GetValue("event")?.ToObjectValue() is EnrichedContentEvent contentEvent) + { + var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, guid); + + return new StringValue(result); + } + + break; + } + + case EnrichedContentEvent contentEvent: + { + var result = urlGenerator.ContentUI(contentEvent.AppId, contentEvent.SchemaId, contentEvent.Id); return new StringValue(result); } - } } return NilValue.Empty; @@ -51,14 +65,81 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions private FluidValue AssetContentUrl(FluidValue input, FilterArguments arguments, TemplateContext context) { - if (input is ObjectValue objectValue) + var value = input.ToObjectValue(); + + switch (value) { - if (objectValue.ToObjectValue() is Guid guid && guid != Guid.Empty) - { - var result = urlGenerator.AssetContent(guid); + case Guid guid when guid != Guid.Empty: + { + var result = urlGenerator.AssetContent(guid); + + return new StringValue(result); + } + + case EnrichedAssetEvent assetEvent: + { + var result = urlGenerator.AssetContent(assetEvent.Id); + + return new StringValue(result); + } + } + + return NilValue.Empty; + } + + private FluidValue AssetContentAppUrl(FluidValue input, FilterArguments arguments, TemplateContext context) + { + var value = input.ToObjectValue(); + + switch (value) + { + case Guid guid when guid != Guid.Empty: + { + if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent) + { + var result = urlGenerator.AssetContent(assetEvent.AppId, guid.ToString()); - return new StringValue(result); - } + return new StringValue(result); + } + + break; + } + + case EnrichedAssetEvent assetEvent: + { + var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); + + return new StringValue(result); + } + } + + return NilValue.Empty; + } + + private FluidValue AssetContentSlugUrl(FluidValue input, FilterArguments arguments, TemplateContext context) + { + var value = input.ToObjectValue(); + + switch (value) + { + case string s: + { + if (context.GetValue("event")?.ToObjectValue() is EnrichedAssetEvent assetEvent) + { + var result = urlGenerator.AssetContent(assetEvent.AppId, s.Slugify()); + + return new StringValue(result); + } + + break; + } + + case EnrichedAssetEvent assetEvent: + { + var result = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); + + return new StringValue(result); + } } return NilValue.Empty; diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs index 14d91d048..9fc63df23 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Extensions/EventJintExtension.cs @@ -9,6 +9,7 @@ using Jint.Native; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure; +using Squidex.Text; namespace Squidex.Domain.Apps.Core.HandleRules.Extensions { @@ -55,6 +56,26 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Extensions return JsValue.Null; })); + + context.Engine.SetValue("assetContentAppUrl", new EventDelegate(() => + { + if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); + } + + return JsValue.Null; + })); + + context.Engine.SetValue("assetContentSlugUrl", new EventDelegate(() => + { + if (context.TryGetValue("event", out var temp) && temp is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); + } + + return JsValue.Null; + })); } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs index f2877da5f..1c50966c6 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/PredefinedPatternsFormatter.cs @@ -11,6 +11,7 @@ using System.Globalization; using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; using Squidex.Infrastructure; using Squidex.Shared.Users; +using Squidex.Text; namespace Squidex.Domain.Apps.Core.HandleRules { @@ -28,6 +29,8 @@ namespace Squidex.Domain.Apps.Core.HandleRules AddPattern("APP_ID", AppId); AddPattern("APP_NAME", AppName); AddPattern("ASSET_CONTENT_URL", AssetContentUrl); + AddPattern("ASSET_CONTENT_APP_URL", AssetContentAppUrl); + AddPattern("ASSET_CONTENT_SLUG_URL", AssetContentSlugUrl); AddPattern("CONTENT_ACTION", ContentAction); AddPattern("CONTENT_URL", ContentUrl); AddPattern("MENTIONED_ID", MentionedId); @@ -124,6 +127,26 @@ namespace Squidex.Domain.Apps.Core.HandleRules return null; } + private string? AssetContentAppUrl(EnrichedEvent @event) + { + if (@event is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString()); + } + + return null; + } + + private string? AssetContentSlugUrl(EnrichedEvent @event) + { + if (@event is EnrichedAssetEvent assetEvent) + { + return urlGenerator.AssetContent(assetEvent.AppId, assetEvent.FileName.Slugify()); + } + + return null; + } + private string? ContentUrl(EnrichedEvent @event) { if (@event is EnrichedContentEvent contentEvent) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs index 1e987251d..4b96939f0 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/IUrlGenerator.cs @@ -27,6 +27,8 @@ namespace Squidex.Domain.Apps.Core string AssetContent(Guid assetId); + string AssetContent(NamedId appId, string idOrSlug); + string BackupsUI(NamedId appId); string ClientsUI(NamedId appId); diff --git a/backend/src/Squidex.Web/Services/UrlGenerator.cs b/backend/src/Squidex.Web/Services/UrlGenerator.cs index 4ae668c10..135eee58b 100644 --- a/backend/src/Squidex.Web/Services/UrlGenerator.cs +++ b/backend/src/Squidex.Web/Services/UrlGenerator.cs @@ -53,6 +53,11 @@ namespace Squidex.Web.Services return urlsOptions.BuildUrl($"api/assets/{assetId}"); } + public string AssetContent(NamedId appId, string idOrSlug) + { + return urlsOptions.BuildUrl($"assets/{appId.Name}/{idOrSlug}"); + } + public string? AssetSource(Guid assetId, long fileVersion) { return assetFileStore.GeneratePublicUrl(assetId, fileVersion); diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs index 7936c857e..2ad6092c7 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs @@ -70,6 +70,12 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => urlGenerator.AssetContent(assetId)) .Returns("asset-content-url"); + A.CallTo(() => urlGenerator.AssetContent(appId, assetId.ToString())) + .Returns("asset-content-app-url"); + + A.CallTo(() => urlGenerator.AssetContent(appId, "file-name")) + .Returns("asset-content-slug-url"); + A.CallTo(() => user.Id) .Returns("user123"); @@ -301,6 +307,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules "Download at ${assetContentUrl()}", "Download at {{event.id | assetContentUrl}}" )] + [InlineData("Liquid(Download at {{event | assetContentUrl}})")] public async Task Should_format_asset_content_url_from_event(string script) { var @event = new EnrichedAssetEvent { Id = assetId }; @@ -317,6 +324,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules "Download at ${assetContentUrl()}", "Download at {{event.id | assetContentUrl | default: 'null'}}" )] + [InlineData("Liquid(Download at {{event | assetContentUrl | default: 'null'}})")] public async Task Should_return_null_when_asset_content_url_not_found(string script) { var @event = new EnrichedContentEvent(); @@ -326,6 +334,74 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Equal("Download at null", result); } + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_APP_URL", + null, + "Download at ${assetContentAppUrl()}", + "Download at {{event.id | assetContentAppUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentAppUrl | default: 'null'}})")] + public async Task Should_format_asset_content_app_url_from_event(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" }; + + var result = await sut.FormatAsync(script, @event); + + Assert.Equal("Download at asset-content-app-url", result); + } + + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_APP_URL", + null, + "Download at ${assetContentAppUrl()}", + "Download at {{event.id | assetContentAppUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentAppUrl | default: 'null'}})")] + public async Task Should_return_null_when_asset_content_app_url_not_found(string script) + { + var @event = new EnrichedContentEvent(); + + var result = await sut.FormatAsync(script, @event); + + Assert.Equal("Download at null", result); + } + + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_SLUG_URL", + null, + "Download at ${assetContentSlugUrl()}", + "Download at {{event.fileName | assetContentSlugUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentSlugUrl | default: 'null'}})")] + public async Task Should_format_asset_content_slug_url_from_event(string script) + { + var @event = new EnrichedAssetEvent { AppId = appId, Id = assetId, FileName = "File Name" }; + + var result = await sut.FormatAsync(script, @event); + + Assert.Equal("Download at asset-content-slug-url", result); + } + + [Theory] + [Expressions( + "Download at $ASSET_CONTENT_SLUG_URL", + null, + "Download at ${assetContentSlugUrl()}", + "Download at {{event.id | assetContentSlugUrl | default: 'null'}}" + )] + [InlineData("Liquid(Download at {{event | assetContentSlugUrl | default: 'null'}})")] + public async Task Should_return_null_when_asset_content_slug_url_not_found(string script) + { + var @event = new EnrichedContentEvent(); + + var result = await sut.FormatAsync(script, @event); + + Assert.Equal("Download at null", result); + } + [Theory] [Expressions( "Go to $CONTENT_URL", diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs index ef8d64e00..cad98469d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/TestData/FakeUrlGenerator.cs @@ -31,6 +31,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.TestData return $"assets/{assetId}"; } + public string AssetContent(NamedId appId, string idOrSlug) + { + return $"assets/{appId.Name}/{idOrSlug}"; + } + public string ContentUI(NamedId appId, NamedId schemaId, Guid contentId) { return $"contents/{schemaId.Name}/{contentId}"; diff --git a/frontend/app/features/content/shared/forms/array-editor.component.html b/frontend/app/features/content/shared/forms/array-editor.component.html index 864b63810..9c760a428 100644 --- a/frontend/app/features/content/shared/forms/array-editor.component.html +++ b/frontend/app/features/content/shared/forms/array-editor.component.html @@ -1,43 +1,47 @@ -
-
- - - + +
+
+ + + +
-
-
-
- +
+
+ +
+ +
+ + +
+ -
- - -
-
- - - {{ 'contents.arrayNoFields' | sqxTranslate }} - \ No newline at end of file + + + {{ 'contents.arrayNoFields' | sqxTranslate }} + + \ No newline at end of file diff --git a/frontend/app/features/content/shared/forms/array-editor.component.ts b/frontend/app/features/content/shared/forms/array-editor.component.ts index f81c6b3af..1e59e0f22 100644 --- a/frontend/app/features/content/shared/forms/array-editor.component.ts +++ b/frontend/app/features/content/shared/forms/array-editor.component.ts @@ -6,8 +6,8 @@ */ import { CdkDragDrop } from '@angular/cdk/drag-drop'; -import { ChangeDetectionStrategy, Component, Input, QueryList, ViewChildren } from '@angular/core'; -import { AppLanguageDto, EditContentForm, FieldArrayForm, FieldArrayItemForm, sorted } from '@app/shared'; +import { ChangeDetectionStrategy, Component, Input, OnChanges, QueryList, SimpleChanges, ViewChildren } from '@angular/core'; +import { AppLanguageDto, ArrayFieldPropertiesDto, EditContentForm, FieldArrayForm, FieldArrayItemForm, sorted } from '@app/shared'; import { ArrayItemComponent } from './array-item.component'; @Component({ @@ -16,7 +16,7 @@ import { ArrayItemComponent } from './array-item.component'; templateUrl: './array-editor.component.html', changeDetection: ChangeDetectionStrategy.OnPush }) -export class ArrayEditorComponent { +export class ArrayEditorComponent implements OnChanges { @Input() public form: EditContentForm; @@ -38,10 +38,28 @@ export class ArrayEditorComponent { @ViewChildren(ArrayItemComponent) public children: QueryList; + public maxItems: number; + public get field() { return this.formModel.field; } + public get hasFields() { + return this.field.nested.length > 0; + } + + public get canAdd() { + return this.formModel.items.length < this.maxItems; + } + + public ngOnChanges(changes: SimpleChanges) { + if (changes['formModel']) { + const properties = this.field.properties as ArrayFieldPropertiesDto; + + this.maxItems = properties.maxItems || Number.MAX_VALUE; + } + } + public itemRemove(index: number) { this.formModel.removeItemAt(index); } From 542fca6d7612d8065b94e071f03652f82ee1478b Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Sun, 27 Sep 2020 11:47:23 +0200 Subject: [PATCH 03/11] Sidebar plugin. (#581) * Sidebar plugin. --- backend/i18n/frontend_en.json | 5 + backend/i18n/frontend_it.json | 5 + backend/i18n/frontend_nl.json | 5 + backend/i18n/source/frontend_en.json | 5 + .../Schemas/SchemaProperties.cs | 4 + .../Schemas/Models/SchemaPropertiesDto.cs | 10 + .../Schemas/Models/UpdateSchemaDto.cs | 10 + .../wwwroot/scripts/context-editor.html | 28 - ...bined-editor.html => editor-combined.html} | 4 +- .../wwwroot/scripts/editor-context.html | 47 ++ .../wwwroot/scripts/editor-json-schema.html | 3 + .../{simple-log.html => editor-log.html} | 11 +- .../src/Squidex/wwwroot/scripts/editor-sdk.js | 146 ++++- ...{simple-editor.html => editor-simple.html} | 4 +- .../wwwroot/scripts/sidebar-context.html | 49 ++ .../wwwroot/scripts/sidebar-search.html | 118 ++++ frontend/app/features/content/declarations.ts | 1 + frontend/app/features/content/module.ts | 15 +- .../comments/comments-page.component.html | 10 +- .../pages/content/content-field.component.ts | 11 +- .../pages/content/content-page.component.html | 4 + .../pages/content/content-page.component.ts | 11 +- .../contents/contents-page.component.html | 4 + .../pages/contents/contents-page.component.ts | 4 +- .../pages/sidebar/sidebar-page.component.html | 15 + .../pages/sidebar/sidebar-page.component.scss | 5 + .../pages/sidebar/sidebar-page.component.ts | 113 ++++ .../pages/rules/rules-page.component.html | 8 +- .../common/schema-edit-form.component.html | 20 + .../fields/forms/field-form-ui.component.html | 2 +- .../forms/editors/iframe-editor.component.ts | 14 +- .../comments/comments.component.html | 49 +- .../guards/content-must-exist.guard.spec.ts | 2 +- .../shared/services/schemas.service.spec.ts | 26 +- .../app/shared/services/schemas.service.ts | 19 +- frontend/app/shared/state/schemas.forms.ts | 2 + frontend/app/theme/_panels.scss | 2 + .../app/theme/icomoon/demo-files/demo.css | 2 +- frontend/app/theme/icomoon/demo.html | 556 +++++++++--------- frontend/app/theme/icomoon/fonts/icomoon.eot | Bin 31928 -> 32412 bytes frontend/app/theme/icomoon/fonts/icomoon.svg | 1 + frontend/app/theme/icomoon/fonts/icomoon.ttf | Bin 31764 -> 32248 bytes frontend/app/theme/icomoon/fonts/icomoon.woff | Bin 31840 -> 32324 bytes frontend/app/theme/icomoon/selection.json | 2 +- frontend/app/theme/icomoon/style.css | 161 ++--- 45 files changed, 1058 insertions(+), 455 deletions(-) delete mode 100644 backend/src/Squidex/wwwroot/scripts/context-editor.html rename backend/src/Squidex/wwwroot/scripts/{combined-editor.html => editor-combined.html} (88%) create mode 100644 backend/src/Squidex/wwwroot/scripts/editor-context.html rename backend/src/Squidex/wwwroot/scripts/{simple-log.html => editor-log.html} (82%) rename backend/src/Squidex/wwwroot/scripts/{simple-editor.html => editor-simple.html} (91%) create mode 100644 backend/src/Squidex/wwwroot/scripts/sidebar-context.html create mode 100644 backend/src/Squidex/wwwroot/scripts/sidebar-search.html create mode 100644 frontend/app/features/content/pages/sidebar/sidebar-page.component.html create mode 100644 frontend/app/features/content/pages/sidebar/sidebar-page.component.scss create mode 100644 frontend/app/features/content/pages/sidebar/sidebar-page.component.ts diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index d923512e9..81e1a1628 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -301,6 +301,7 @@ "common.searchResults": "Search Results", "common.separateByLine": "Separate by line", "common.settings": "Settings", + "common.sidebar": "Sidebar Extension", "common.sidebarTour": "The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.", "common.slug": "Slug", "common.stars.max": "Must not have more more than 15 stars", @@ -622,6 +623,10 @@ "schemas.addNestedField": "Add Nested Field", "schemas.changeCategoryFailed": "Failed to change category. Please reload.", "schemas.clone": "Clone Schema", + "schemas.contentSidebarUrl": "Content Sidebar Extension", + "schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.", + "schemas.contentsSidebarUrl": "Contents Sidebar Extension", + "schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.", "schemas.contextMenuTour": "Open the context menu to delete the schema or to create some scripts for content changes.", "schemas.create": "Create Schema", "schemas.createCategory": "Create new category...", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 0fcf9432b..f5f87d70d 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -301,6 +301,7 @@ "common.searchResults": "Risultati di ricerca", "common.separateByLine": "Separato dalla linea", "common.settings": "Impostazioni", + "common.sidebar": "Sidebar Extension", "common.sidebarTour": "La barra di navigazione laterale contiene specifici utili collegamenti per il contesto. Qui puoi visualizzare la cronologia dei cambiamenti di questo schema.", "common.slug": "Slug", "common.stars.max": "Non deve avere più di 15 stelle", @@ -622,6 +623,10 @@ "schemas.addNestedField": "Aggiungi un campo annidato", "schemas.changeCategoryFailed": "Non è stato possibile cambiare la categoria. Per favore ricarica.", "schemas.clone": "Clona lo Schema", + "schemas.contentSidebarUrl": "Content Sidebar Extension", + "schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.", + "schemas.contentsSidebarUrl": "Contents Sidebar Extension", + "schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.", "schemas.contextMenuTour": "Apri il menu per cancellare lo schema o per inserire alcuni script che modificano il contenuto.", "schemas.create": "Crea uno Schema", "schemas.createCategory": "Crea una nuova categoria...", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index a537dbf5e..5c67eb6ab 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -301,6 +301,7 @@ "common.searchResults": "Zoekresultaten", "common.separateByLine": "Scheiden op regel", "common.settings": "Instellingen", + "common.sidebar": "Sidebar Extension", "common.sidebarTour": "De zijbalknavigatie bevat nuttige contextspecifieke links. Hier kun je de geschiedenis bekijken hoe dit schema in de loop van de tijd is veranderd.", "common.slug": "Slug", "common.stars.max": "Mag niet meer dan 15 sterren hebben", @@ -622,6 +623,10 @@ "schemas.addNestedField": "Voeg genest veld toe", "schemas.changeCategoryFailed": "Kan categorie niet wijzigen. Laad opnieuw.", "schemas.clone": "Clone Schema", + "schemas.contentSidebarUrl": "Content Sidebar Extension", + "schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.", + "schemas.contentsSidebarUrl": "Contents Sidebar Extension", + "schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.", "schemas.contextMenuTour": "Open het contextmenu om het schema te verwijderen of om scripts te maken voor wijzigingen in de inhoud.", "schemas.create": "Schema maken", "schemas.createCategory": "Nieuwe categorie maken ...", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index d923512e9..81e1a1628 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -301,6 +301,7 @@ "common.searchResults": "Search Results", "common.separateByLine": "Separate by line", "common.settings": "Settings", + "common.sidebar": "Sidebar Extension", "common.sidebarTour": "The sidebar navigation contains useful context specific links. Here you can view the history how this schema has changed over time.", "common.slug": "Slug", "common.stars.max": "Must not have more more than 15 stars", @@ -622,6 +623,10 @@ "schemas.addNestedField": "Add Nested Field", "schemas.changeCategoryFailed": "Failed to change category. Please reload.", "schemas.clone": "Clone Schema", + "schemas.contentSidebarUrl": "Content Sidebar Extension", + "schemas.contentSidebarUrlHint": "URL to the plugin for the sidebar in the details view.", + "schemas.contentsSidebarUrl": "Contents Sidebar Extension", + "schemas.contentsSidebarUrlHint": "URL to the plugin for the sidebar in the list view.", "schemas.contextMenuTour": "Open the context menu to delete the schema or to create some scripts for content changes.", "schemas.create": "Create Schema", "schemas.createCategory": "Create new category...", diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs index bb2e8a27c..fa06a7f61 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Model/Schemas/SchemaProperties.cs @@ -15,6 +15,10 @@ namespace Squidex.Domain.Apps.Core.Schemas { public ReadOnlyCollection? Tags { get; set; } + public string? ContentsSidebarUrl { get; set; } + + public string? ContentSidebarUrl { get; set; } + public bool DeepEquals(SchemaProperties properties) { return SimpleEquals.IsEquals(this, properties); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs index cc629a5e5..c8630b2df 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/SchemaPropertiesDto.cs @@ -24,6 +24,16 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models [LocalizedStringLength(1000)] public string? Hints { get; set; } + /// + /// The url to a the sidebar plugin for content lists. + /// + public string? ContentsSidebarUrl { get; set; } + + /// + /// The url to a the sidebar plugin for content items. + /// + public string? ContentSidebarUrl { get; set; } + /// /// Tags for automation processes. /// diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs index 576fc669f..fec58f16c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/Models/UpdateSchemaDto.cs @@ -27,6 +27,16 @@ namespace Squidex.Areas.Api.Controllers.Schemas.Models [LocalizedStringLength(1000)] public string? Hints { get; set; } + /// + /// The url to a the sidebar plugin for content lists. + /// + public string? ContentsSidebarUrl { get; set; } + + /// + /// The url to a the sidebar plugin for content items. + /// + public string? ContentSidebarUrl { get; set; } + /// /// Tags for automation processes. /// diff --git a/backend/src/Squidex/wwwroot/scripts/context-editor.html b/backend/src/Squidex/wwwroot/scripts/context-editor.html deleted file mode 100644 index e44344ae8..000000000 --- a/backend/src/Squidex/wwwroot/scripts/context-editor.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/scripts/combined-editor.html b/backend/src/Squidex/wwwroot/scripts/editor-combined.html similarity index 88% rename from backend/src/Squidex/wwwroot/scripts/combined-editor.html rename to backend/src/Squidex/wwwroot/scripts/editor-combined.html index 556b6ebb8..5d2abe715 100644 --- a/backend/src/Squidex/wwwroot/scripts/combined-editor.html +++ b/backend/src/Squidex/wwwroot/scripts/editor-combined.html @@ -14,7 +14,9 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/scripts/editor-json-schema.html b/backend/src/Squidex/wwwroot/scripts/editor-json-schema.html index 281af4e8b..2054ad8cd 100644 --- a/backend/src/Squidex/wwwroot/scripts/editor-json-schema.html +++ b/backend/src/Squidex/wwwroot/scripts/editor-json-schema.html @@ -29,6 +29,9 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/backend/src/Squidex/wwwroot/scripts/sidebar-search.html b/backend/src/Squidex/wwwroot/scripts/sidebar-search.html new file mode 100644 index 000000000..8ba7ff6a3 --- /dev/null +++ b/backend/src/Squidex/wwwroot/scripts/sidebar-search.html @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + + \ No newline at end of file diff --git a/frontend/app/features/content/declarations.ts b/frontend/app/features/content/declarations.ts index 2245c5082..7b1c41dcf 100644 --- a/frontend/app/features/content/declarations.ts +++ b/frontend/app/features/content/declarations.ts @@ -16,6 +16,7 @@ export * from './pages/contents/contents-filters-page.component'; export * from './pages/contents/contents-page.component'; export * from './pages/contents/custom-view-editor.component'; export * from './pages/schemas/schemas-page.component'; +export * from './pages/sidebar/sidebar-page.component'; export * from './shared/content-status.component'; export * from './shared/due-time-selector.component'; export * from './shared/forms/array-editor.component'; diff --git a/frontend/app/features/content/module.ts b/frontend/app/features/content/module.ts index a16823683..4848f56f9 100644 --- a/frontend/app/features/content/module.ts +++ b/frontend/app/features/content/module.ts @@ -10,7 +10,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CanDeactivateGuard, ContentMustExistGuard, LoadLanguagesGuard, SchemaMustExistPublishedGuard, SchemaMustNotBeSingletonGuard, SqxFrameworkModule, SqxSharedModule, UnsetContentGuard } from '@app/shared'; -import { ArrayEditorComponent, ArrayItemComponent, ArraySectionComponent, AssetsEditorComponent, CommentsPageComponent, ContentComponent, ContentCreatorComponent, ContentEventComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentPageComponent, ContentSectionComponent, ContentSelectorComponent, ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, StockPhotoEditorComponent } from './declarations'; +import { ArrayEditorComponent, ArrayItemComponent, ArraySectionComponent, AssetsEditorComponent, CommentsPageComponent, ContentComponent, ContentCreatorComponent, ContentEventComponent, ContentFieldComponent, ContentHistoryPageComponent, ContentListCellDirective, ContentListFieldComponent, ContentListHeaderComponent, ContentListWidthPipe, ContentPageComponent, ContentSectionComponent, ContentSelectorComponent, ContentSelectorItemComponent, ContentsFiltersPageComponent, ContentsPageComponent, ContentStatusComponent, ContentValueComponent, ContentValueEditorComponent, CustomViewEditorComponent, DueTimeSelectorComponent, FieldEditorComponent, FieldLanguagesComponent, PreviewButtonComponent, ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, SidebarPageComponent, StockPhotoEditorComponent } from './declarations'; const routes: Routes = [ { @@ -28,12 +28,16 @@ const routes: Routes = [ { path: '', component: ContentsPageComponent, - canActivate: [SchemaMustNotBeSingletonGuard], + canActivate: [SchemaMustNotBeSingletonGuard, UnsetContentGuard], canDeactivate: [CanDeactivateGuard], children: [ { path: 'filters', component: ContentsFiltersPageComponent + }, + { + path: 'sidebar', + component: SidebarPageComponent } ] }, @@ -59,7 +63,11 @@ const routes: Routes = [ { path: 'comments', component: CommentsPageComponent - } + }, + { + path: 'sidebar', + component: SidebarPageComponent + } ] } ] @@ -105,6 +113,7 @@ const routes: Routes = [ ReferenceItemComponent, ReferencesEditorComponent, SchemasPageComponent, + SidebarPageComponent, StockPhotoEditorComponent ] }) diff --git a/frontend/app/features/content/pages/comments/comments-page.component.html b/frontend/app/features/content/pages/comments/comments-page.component.html index 5a63794b5..d3d658289 100644 --- a/frontend/app/features/content/pages/comments/comments-page.component.html +++ b/frontend/app/features/content/pages/comments/comments-page.component.html @@ -1 +1,9 @@ - \ No newline at end of file + + + {{ 'comments.title' | sqxTranslate }} + + + + + + \ No newline at end of file diff --git a/frontend/app/features/content/pages/content/content-field.component.ts b/frontend/app/features/content/pages/content/content-field.component.ts index 2857e5b69..05181c54c 100644 --- a/frontend/app/features/content/pages/content/content-field.component.ts +++ b/frontend/app/features/content/pages/content/content-field.component.ts @@ -7,8 +7,8 @@ import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; import { AppLanguageDto, AppsState, EditContentForm, FieldForm, invalid$, LocalStoreService, SchemaDto, Settings, StringFieldPropertiesDto, TranslationsService, Types, value$ } from '@app/shared'; -import { Observable } from 'rxjs'; -import { combineLatest } from 'rxjs/operators'; +import { combineLatest, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; @Component({ selector: 'sqx-content-field', @@ -78,9 +78,10 @@ export class ContentFieldComponent implements OnChanges { if ((changes['formModel'] || changes['formModelCompare']) && this.formModelCompare) { this.isDifferent = - value$(this.formModel.form).pipe( - combineLatest(value$(this.formModelCompare!.form), - (lhs, rhs) => !Types.equals(lhs, rhs, true))); + combineLatest([ + value$(this.formModel.form), + value$(this.formModelCompare!.form) + ]).pipe(map(([lhs, rhs]) => !Types.equals(lhs, rhs, true))); } } diff --git a/frontend/app/features/content/pages/content/content-page.component.html b/frontend/app/features/content/pages/content/content-page.component.html index 03e956b27..8a36ce99f 100644 --- a/frontend/app/features/content/pages/content/content-page.component.html +++ b/frontend/app/features/content/pages/content/content-page.component.html @@ -104,6 +104,10 @@ + + + + {{ 'common.sidebarTour' | sqxTranslate }} diff --git a/frontend/app/features/content/pages/content/content-page.component.ts b/frontend/app/features/content/pages/content/content-page.component.ts index a88859e67..67f279d76 100644 --- a/frontend/app/features/content/pages/content/content-page.component.ts +++ b/frontend/app/features/content/pages/content/content-page.component.ts @@ -9,7 +9,7 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { ApiUrlConfig, AppLanguageDto, AuthService, AutoSaveKey, AutoSaveService, CanComponentDeactivate, ContentDto, ContentsState, DialogService, EditContentForm, fadeAnimation, FieldForm, FieldSection, LanguagesState, ModalModel, ResourceOwner, RootFieldDto, SchemaDetailsDto, SchemasState, TempService, valueAll$, Version } from '@app/shared'; +import { ApiUrlConfig, AppLanguageDto, AppsState, AuthService, AutoSaveKey, AutoSaveService, CanComponentDeactivate, ContentDto, ContentsState, DialogService, EditContentForm, fadeAnimation, FieldForm, FieldSection, LanguagesState, ModalModel, ResourceOwner, RootFieldDto, SchemaDetailsDto, SchemasState, TempService, valueAll$, Version } from '@app/shared'; import { Observable, of } from 'rxjs'; import { debounceTime, filter, onErrorResumeNext, tap } from 'rxjs/operators'; @@ -39,7 +39,7 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD public language: AppLanguageDto; public languages: ReadonlyArray; - constructor(apiUrl: ApiUrlConfig, authService: AuthService, + constructor(apiUrl: ApiUrlConfig, authService: AuthService, appsState: AppsState, public readonly contentsState: ContentsState, private readonly autoSaveService: AutoSaveService, private readonly dialogs: DialogService, @@ -51,7 +51,12 @@ export class ContentPageComponent extends ResourceOwner implements CanComponentD ) { super(); - this.formContext = { user: authService.user, apiUrl: apiUrl.buildUrl('api') }; + this.formContext = { + apiUrl: apiUrl.buildUrl('api'), + appId: appsState.snapshot.selectedApp!.id, + appName: appsState.snapshot.selectedApp!.name, + user: authService.user + }; } public ngOnInit() { diff --git a/frontend/app/features/content/pages/contents/contents-page.component.html b/frontend/app/features/content/pages/contents/contents-page.component.html index 8dc4ecce3..b2d0f14c8 100644 --- a/frontend/app/features/content/pages/contents/contents-page.component.html +++ b/frontend/app/features/content/pages/contents/contents-page.component.html @@ -130,6 +130,10 @@ + + + +
diff --git a/frontend/app/features/content/pages/contents/contents-page.component.ts b/frontend/app/features/content/pages/contents/contents-page.component.ts index f69c41cef..256580a45 100644 --- a/frontend/app/features/content/pages/contents/contents-page.component.ts +++ b/frontend/app/features/content/pages/contents/contents-page.component.ts @@ -65,11 +65,11 @@ export class ContentsPageComponent extends ResourceOwner implements OnInit { public ngOnInit() { this.own( - combineLatest( + combineLatest([ this.schemasState.selectedSchema, this.languagesState.languages, this.contentsState.statuses - ).subscribe(([schema, languages, statuses]) => { + ]).subscribe(([schema, languages, statuses]) => { this.queryModel = queryModelFromSchema(schema, languages.map(x => x.language), statuses); })); diff --git a/frontend/app/features/content/pages/sidebar/sidebar-page.component.html b/frontend/app/features/content/pages/sidebar/sidebar-page.component.html new file mode 100644 index 000000000..f8a1476f9 --- /dev/null +++ b/frontend/app/features/content/pages/sidebar/sidebar-page.component.html @@ -0,0 +1,15 @@ + + + {{ 'common.sidebar' | sqxTranslate }} + + + +
+ +
+
+
+ + + + diff --git a/frontend/app/features/content/pages/sidebar/sidebar-page.component.scss b/frontend/app/features/content/pages/sidebar/sidebar-page.component.scss new file mode 100644 index 000000000..ae2678456 --- /dev/null +++ b/frontend/app/features/content/pages/sidebar/sidebar-page.component.scss @@ -0,0 +1,5 @@ +iframe { + background: 0; + border: 0; + overflow: hidden; +} \ No newline at end of file diff --git a/frontend/app/features/content/pages/sidebar/sidebar-page.component.ts b/frontend/app/features/content/pages/sidebar/sidebar-page.component.ts new file mode 100644 index 000000000..9e02dd1e0 --- /dev/null +++ b/frontend/app/features/content/pages/sidebar/sidebar-page.component.ts @@ -0,0 +1,113 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Renderer2, ViewChild } from '@angular/core'; +import { Router } from '@angular/router'; +import { ApiUrlConfig, ResourceOwner, Types } from '@app/framework/internal'; +import { AppsState, AuthService, ContentsState, SchemasState } from '@app/shared'; +import { combineLatest } from 'rxjs'; + +@Component({ + selector: 'sqx-sidebar-page', + styleUrls: ['./sidebar-page.component.scss'], + templateUrl: './sidebar-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class SidebarPageComponent extends ResourceOwner implements AfterViewInit { + private isInitialized = false; + private context: any; + private content: any; + + @ViewChild('iframe', { static: false }) + public iframe: ElementRef; + + constructor(apiUrl: ApiUrlConfig, authService: AuthService, appsState: AppsState, + private readonly contentsState: ContentsState, + private readonly schemasState: SchemasState, + private readonly renderer: Renderer2, + private readonly router: Router + ) { + super(); + + this.context = { + apiUrl: apiUrl.buildUrl('api'), + appId: appsState.snapshot.selectedApp!.id, + appName: appsState.snapshot.selectedApp!.name, + user: authService.user + }; + } + + public ngAfterViewInit() { + this.own( + combineLatest([ + this.schemasState.selectedSchema, + this.contentsState.selectedContent + ]).subscribe(([schema, content]) => { + const url = + content ? + schema.properties.contentSidebarUrl : + schema.properties.contentsSidebarUrl; + + this.context['schemaName'] = schema.name; + this.context['schemaId'] = schema.id; + + this.iframe.nativeElement.src = url || ''; + })); + + this.own( + this.contentsState.selectedContent + .subscribe(content => { + this.content = content; + + this.sendContent(); + })); + + this.own( + this.renderer.listen('window', 'message', (event: MessageEvent) => { + if (event.source === this.iframe.nativeElement.contentWindow) { + const { type } = event.data; + + if (type === 'started') { + this.isInitialized = true; + + this.sendInit(); + this.sendContent(); + } else if (type === 'resize') { + const { height } = event.data; + + this.iframe.nativeElement.height = height + 'px'; + } else if (type === 'navigate') { + const { url } = event.data; + + this.router.navigateByUrl(url); + } + } + })); + } + + private sendInit() { + this.sendMessage('init', { context: this.context }); + } + + private sendContent() { + this.sendMessage('contentChanged', { content: this.content }); + } + + private sendMessage(type: string, payload: any) { + if (!this.iframe) { + return; + } + + const iframe = this.iframe.nativeElement; + + if (this.isInitialized && iframe.contentWindow && Types.isFunction(iframe.contentWindow.postMessage)) { + const message = { type, ...payload }; + + iframe.contentWindow.postMessage(message, '*'); + } + } +} \ No newline at end of file diff --git a/frontend/app/features/rules/pages/rules/rules-page.component.html b/frontend/app/features/rules/pages/rules/rules-page.component.html index 6a86d80f2..d7a77c019 100644 --- a/frontend/app/features/rules/pages/rules/rules-page.component.html +++ b/frontend/app/features/rules/pages/rules/rules-page.component.html @@ -71,11 +71,9 @@ +
+ + + + + + + {{ 'schemas.contentsSidebarUrl' | sqxTranslate }} +
+ +
+ + + + + + + {{ 'schemas.contentSidebarUrlHint' | sqxTranslate }} +
+
diff --git a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html index e8e41106b..5b6edff4c 100644 --- a/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/forms/field-form-ui.component.html @@ -3,7 +3,7 @@
- + {{ 'schemas.field.editorUrlHint' | sqxTranslate }} diff --git a/frontend/app/framework/angular/forms/editors/iframe-editor.component.ts b/frontend/app/framework/angular/forms/editors/iframe-editor.component.ts index 510e04b1d..f0c9e7ea2 100644 --- a/frontend/app/framework/angular/forms/editors/iframe-editor.component.ts +++ b/frontend/app/framework/angular/forms/editors/iframe-editor.component.ts @@ -7,6 +7,7 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, forwardRef, Input, OnChanges, Renderer2, SimpleChanges, ViewChild } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Router } from '@angular/router'; import { StatefulControlComponent, Types } from '@app/framework/internal'; export const SQX_IFRAME_EDITOR_CONTROL_VALUE_ACCESSOR: any = { @@ -40,7 +41,8 @@ export class IFrameEditorComponent extends StatefulControlComponent im public url: string; constructor(changeDetector: ChangeDetectorRef, - private readonly renderer: Renderer2 + private readonly renderer: Renderer2, + private readonly router: Router ) { super(changeDetector, {}); } @@ -72,7 +74,7 @@ export class IFrameEditorComponent extends StatefulControlComponent im if (type === 'started') { this.isInitialized = true; - this.sendMessage('init', { context: this.context || {} }); + this.sendInit(); this.sendFormValue(); this.sendDisabled(); this.sendValue(); @@ -80,6 +82,10 @@ export class IFrameEditorComponent extends StatefulControlComponent im const { height } = event.data; this.iframe.nativeElement.height = height + 'px'; + } else if (type === 'navigate') { + const { url } = event.data; + + this.router.navigateByUrl(url); } else if (type === 'valueChanged') { const { value } = event.data; @@ -107,6 +113,10 @@ export class IFrameEditorComponent extends StatefulControlComponent im this.sendDisabled(); } + private sendInit() { + this.sendMessage('init', { context: this.context || {} }); + } + private sendValue() { this.sendMessage('valueChanged', { value: this.value }); } diff --git a/frontend/app/shared/components/comments/comments.component.html b/frontend/app/shared/components/comments/comments.component.html index 7599d054c..6dd0ba859 100644 --- a/frontend/app/shared/components/comments/comments.component.html +++ b/frontend/app/shared/components/comments/comments.component.html @@ -1,27 +1,28 @@ - - - {{ 'comments.title' | sqxTranslate }} - - - - - -
-
- - -
-
- - -
-
-
- + +
+
+ + +
+
+ + +
diff --git a/frontend/app/shared/guards/content-must-exist.guard.spec.ts b/frontend/app/shared/guards/content-must-exist.guard.spec.ts index 9dd51e1d3..a7e99b0dc 100644 --- a/frontend/app/shared/guards/content-must-exist.guard.spec.ts +++ b/frontend/app/shared/guards/content-must-exist.guard.spec.ts @@ -18,8 +18,8 @@ describe('ContentMustExistGuard', () => { } }; - let contentsState: IMock; let router: IMock; + let contentsState: IMock; let contentGuard: ContentMustExistGuard; beforeEach(() => { diff --git a/frontend/app/shared/services/schemas.service.spec.ts b/frontend/app/shared/services/schemas.service.spec.ts index 0ac097ec3..b2084aef5 100644 --- a/frontend/app/shared/services/schemas.service.spec.ts +++ b/frontend/app/shared/services/schemas.service.spec.ts @@ -613,8 +613,12 @@ describe('SchemasService', () => { lastModifiedBy: `modifier${id}`, properties: { label: `label${id}${suffix}`, - hints: `hints${id}${suffix}`, - tags: [`tags${id}${suffix}`] + contentsSidebarUrl: `url/to/contents/${id}${suffix}`, + contentSidebarUrl: `url/to/content/${id}${suffix}`, + tags: [ + `tags${id}${suffix}` + ], + hints: `hints${id}${suffix}` }, version: `${id}`, _links: { @@ -637,6 +641,8 @@ describe('SchemasService', () => { version: `${id}`, properties: { label: `label${id}${suffix}`, + contentsSidebarUrl: `url/to/contents/${id}${suffix}`, + contentSidebarUrl: `url/to/content/${id}${suffix}`, tags: [ `tags${id}${suffix}` ], @@ -811,6 +817,18 @@ describe('SchemasService', () => { } }); +function createSchemaProperties(id: number, suffix = '') { + return new SchemaPropertiesDto( + `label${id}${suffix}`, + `hints${id}${suffix}`, + `url/to/contents/${id}${suffix}`, + `url/to/content/${id}${suffix}`, + [ + `tags${id}${suffix}` + ] + ); +} + export function createSchema(id: number, suffix = '') { const links: ResourceLinks = { update: { method: 'PUT', href: `/schemas/${id}` } @@ -820,7 +838,7 @@ export function createSchema(id: number, suffix = '') { `schema-id${id}`, `schema-name${id}${suffix}`, `category${id}${suffix}`, - new SchemaPropertiesDto(`label${id}${suffix}`, `hints${id}${suffix}`, [`tags${id}${suffix}`]), + createSchemaProperties(id, suffix), id % 2 === 0, id % 3 === 0, DateTime.parseISO(`${id % 1000 + 2000}-12-12T10:10:00Z`), `creator${id}`, @@ -837,7 +855,7 @@ export function createSchemaDetails(id: number, suffix = '') { `schema-id${id}`, `schema-name${id}${suffix}`, `category${id}${suffix}`, - new SchemaPropertiesDto(`label${id}${suffix}`, `hints${id}${suffix}`, [`tags${id}${suffix}`]), + createSchemaProperties(id, suffix), id % 2 === 0, id % 3 === 0, DateTime.parseISO(`${id % 1000 + 2000}-12-12T10:10:00Z`), `creator${id}`, diff --git a/frontend/app/shared/services/schemas.service.ts b/frontend/app/shared/services/schemas.service.ts index 712d4b837..1b39a7fa1 100644 --- a/frontend/app/shared/services/schemas.service.ts +++ b/frontend/app/shared/services/schemas.service.ts @@ -323,6 +323,8 @@ export class SchemaPropertiesDto { constructor( public readonly label?: string, public readonly hints?: string, + public readonly contentsSidebarUrl?: string, + public readonly contentSidebarUrl?: string, public readonly tags?: ReadonlyArray ) { } @@ -362,6 +364,8 @@ export interface SynchronizeSchemaDto { export interface UpdateSchemaDto { readonly label?: string; readonly hints?: string; + readonly contentsSidebarUrl?: string; + readonly contentSidebarUrl?: string; readonly tags?: ReadonlyArray; } @@ -698,7 +702,7 @@ function parseSchemas(response: any) { item.id, item.name, item.category, - new SchemaPropertiesDto(item.properties.label, item.properties.hints, item.properties.tags), + parseProperties(item.properties), item.isSingleton, item.isPublished, DateTime.parseISO(item.created), item.createdBy, @@ -713,13 +717,11 @@ function parseSchemas(response: any) { function parseSchemaWithDetails(response: any) { const fields = response.fields.map((item: any) => parseField(item)); - const properties = new SchemaPropertiesDto(response.properties.label, response.properties.hints, response.properties.tags); - return new SchemaDetailsDto(response._links, response.id, response.name, response.category, - properties, + parseProperties(response.properties), response.isSingleton, response.isPublished, DateTime.parseISO(response.created), response.createdBy, @@ -733,6 +735,15 @@ function parseSchemaWithDetails(response: any) { response.previewUrls || {}); } +function parseProperties(response: any) { + return new SchemaPropertiesDto( + response.label, + response.hints, + response.contentsSidebarUrl, + response.contentSidebarUrl, + response.tags); +} + export function parseField(item: any) { const propertiesDto = createProperties( diff --git a/frontend/app/shared/state/schemas.forms.ts b/frontend/app/shared/state/schemas.forms.ts index c40f4af14..882d083d9 100644 --- a/frontend/app/shared/state/schemas.forms.ts +++ b/frontend/app/shared/state/schemas.forms.ts @@ -232,6 +232,8 @@ export class EditSchemaForm extends Form
-

Font Name: icomoon (Glyphs: 136)

+

Font Name: icomoon (Glyphs: 137)

-

Grid Size: 24

+

Grid Size: 14

- - icon-enter + + icon-plugin
- - + +
liga: @@ -29,12 +29,12 @@
- - icon-zoom_out + + icon-angle-double-right
- - + +
liga: @@ -43,12 +43,12 @@
- - icon-zoom_in + + icon-angle-double-left
- - + +
liga: @@ -57,12 +57,12 @@
- - icon-flip + + icon-filter-filled
- - + +
liga: @@ -71,12 +71,12 @@
- - icon-rotate_right + + icon-clone
- - + +
liga: @@ -85,12 +85,12 @@
- - icon-rotate_left + + icon-control-Tags
- - + +
liga: @@ -99,12 +99,12 @@
- - icon-create_new_folder + + icon-control-Checkboxes
- - + +
liga: @@ -113,12 +113,12 @@
- - icon-folder + + icon-control-List
- - + +
liga: @@ -127,12 +127,12 @@
- - icon-help2 + + icon-control-Html
- - + +
liga: @@ -141,12 +141,12 @@
- - icon-trigger-Manual + + icon-single-content
- - + +
liga: @@ -155,12 +155,12 @@
- - icon-play-line + + icon-search-Content
- - + +
liga: @@ -169,12 +169,12 @@
- - icon-corner-down-right + + icon-multiple-content
- - + +
liga: @@ -183,12 +183,12 @@
- - icon-info-outline + + icon-type-Array
- - + +
liga: @@ -197,12 +197,12 @@
- - icon-upload-2 + + icon-exclamation
- - + +
liga: @@ -211,12 +211,12 @@
- - icon-translate + + icon-orleans
- - + +
liga: @@ -225,12 +225,12 @@
- - icon-arrow_back + + icon-document-lock
- - + +
liga: @@ -239,12 +239,12 @@
- - icon-external-link + + icon-document-unpublish
- - + +
liga: @@ -253,12 +253,12 @@
- - icon-minus-square + + icon-angle-down
- - + +
liga: @@ -267,12 +267,12 @@
- - icon-plus-square + + icon-angle-left
- - + +
liga: @@ -281,12 +281,12 @@
- - icon-drag2 + + icon-angle-right
- - + +
liga: @@ -295,12 +295,12 @@
- - icon-comments + + icon-angle-up
- - + +
liga: @@ -309,12 +309,12 @@
- - icon-backup + + icon-api
- - + +
liga: @@ -323,12 +323,12 @@
- - icon-support + + icon-assets
- - + +
liga: @@ -337,12 +337,12 @@
- - icon-control-RichText + + icon-search-Asset
- - + +
liga: @@ -351,211 +351,225 @@
- - icon-download + + icon-bug
- - + +
liga:
-
-
-

Grid Size: 14

-
+
- - icon-angle-double-right + + icon-caret-down
- - + +
liga:
-
+
- - icon-angle-double-left + + icon-caret-left
- - + +
liga:
-
+
- - icon-filter-filled + + icon-caret-right
- - + +
liga:
-
+
- - icon-clone + + icon-caret-up
- - + +
liga:
-
+
- - icon-control-Tags + + icon-contents
- - + +
liga:
-
+
- - icon-control-Checkboxes -
+ + icon-trigger-ContentChanged +
- - + +
liga:
-
+
- - icon-control-List + + icon-control-Date
- - + +
liga:
-
+
- - icon-control-Html + + icon-control-DateTime
- - + +
liga:
-
+
- - icon-single-content + + icon-control-Markdown
- - + +
liga:
-
+
- - icon-search-Content + + icon-grid
- - + +
liga:
-
+
- - icon-multiple-content + + icon-list1
- - + +
liga:
-
+
- - icon-type-Array + + icon-user-o
- - + +
liga:
-
+
- - icon-exclamation + + icon-rules
- - + +
liga:
+
+
+ + icon-search-Rule +
+
+ + +
+
+ liga: + +
+
+
+
+

Grid Size: 24

- - icon-orleans + + icon-enter
- - + +
liga: @@ -564,12 +578,12 @@
- - icon-document-lock + + icon-zoom_out
- - + +
liga: @@ -578,12 +592,12 @@
- - icon-document-unpublish + + icon-zoom_in
- - + +
liga: @@ -592,12 +606,12 @@
- - icon-angle-down + + icon-flip
- - + +
liga: @@ -606,12 +620,12 @@
- - icon-angle-left + + icon-rotate_right
- - + +
liga: @@ -620,12 +634,12 @@
- - icon-angle-right + + icon-rotate_left
- - + +
liga: @@ -634,12 +648,12 @@
- - icon-angle-up + + icon-create_new_folder
- - + +
liga: @@ -648,12 +662,12 @@
- - icon-api + + icon-folder
- - + +
liga: @@ -662,12 +676,12 @@
- - icon-assets + + icon-help2
- - + +
liga: @@ -676,12 +690,12 @@
- - icon-search-Asset + + icon-trigger-Manual
- - + +
liga: @@ -690,12 +704,12 @@
- - icon-bug + + icon-play-line
- - + +
liga: @@ -704,12 +718,12 @@
- - icon-caret-down + + icon-corner-down-right
- - + +
liga: @@ -718,12 +732,12 @@
- - icon-caret-left + + icon-info-outline
- - + +
liga: @@ -732,12 +746,12 @@
- - icon-caret-right + + icon-upload-2
- - + +
liga: @@ -746,12 +760,12 @@
- - icon-caret-up + + icon-translate
- - + +
liga: @@ -760,12 +774,12 @@
- - icon-contents + + icon-arrow_back
- - + +
liga: @@ -774,12 +788,12 @@
- - icon-trigger-ContentChanged + + icon-external-link
- - + +
liga: @@ -788,12 +802,12 @@
- - icon-control-Date + + icon-minus-square
- - + +
liga: @@ -802,12 +816,12 @@
- - icon-control-DateTime + + icon-plus-square
- - + +
liga: @@ -816,12 +830,12 @@
- - icon-control-Markdown + + icon-drag2
- - + +
liga: @@ -830,12 +844,12 @@
- - icon-grid + + icon-comments
- - + +
liga: @@ -844,12 +858,12 @@
- - icon-list1 + + icon-backup
- - + +
liga: @@ -858,12 +872,12 @@
- - icon-user-o + + icon-support
- - + +
liga: @@ -872,12 +886,12 @@
- - icon-rules + + icon-control-RichText
- - + +
liga: @@ -886,12 +900,12 @@
- - icon-search-Rule + + icon-download
- - + +
liga: diff --git a/frontend/app/theme/icomoon/fonts/icomoon.eot b/frontend/app/theme/icomoon/fonts/icomoon.eot index b29ef24de3414892fae20389c2be2c2d548bcbcc..413597637c99e6d917940b39ff6e4430bb07b5c5 100644 GIT binary patch delta 765 zcmXw%-%C_M6vyYx%>CuPt8RPUdui9}-n+l3m37@8cfGsiTNptQ5>`;;qLP8DhN9+= zmZC%oBUvN@Un2Vp_N9j?p%-^(wjp8eDXU#wWVAdz*K#rPfYsj+!r8Hjq0-9CtM3RcsO*ejximM4HsKmV ziz&v3#!Kaq7Z;>ItiHth%aapR(}=JRTKeSn5h3ugIt)+XQS~Q;xvkdwEgxz+{&A-g z`CBJpQbiN!GgDf;&4rY8T9*d`}V>T?L{T0n<_!&?HuNlwUnY5poyGS>4V3%;9 z7vgS;#o6(^;4V{E8*@# zSQTBfebKUqlRSZeAgDP_w*-oJDGxmTDmJl)iztfEXzv0h2Cj+A2NIerN}a~$;bV|R zDw6K!NxfB)O|w6d=m4s4ys&E|BuVjui7nCB#K|0~VoY0=e|7^fG@7p`h)v?(;tkyPXBbbst j6sc|BH9^}c%opwAJOYNT!WHCqafGWc7kxF~!fW^kF!ht| delta 265 zcmbR9mvP5WMz$R_3=ASQ6WPpIj&p2CpXg97xSxT6VGj@|BXDUGVz2=y}nS8 z8v_I51O|puk&M*D6ls?TQwD~zB|vp%89)I}Ce~gc{|=C^l95|d!NaB!$G}hyS#H)NPGFjN`<&5+7XtSDeu%oxeQP~`(uppciCn>y2r^EZ&61C(zo$S*EoUwbX-{zIE3n4TAIzF!*1ER(*0<9Iy3%~u9)7LX=} r+r^v@VDx04N^{1Z%`KHGOqCwSS-Dwt0c9D1xM%aNnnR2LLc>eV diff --git a/frontend/app/theme/icomoon/fonts/icomoon.svg b/frontend/app/theme/icomoon/fonts/icomoon.svg index 4091bbac1..dbfc77c9e 100644 --- a/frontend/app/theme/icomoon/fonts/icomoon.svg +++ b/frontend/app/theme/icomoon/fonts/icomoon.svg @@ -140,6 +140,7 @@ + diff --git a/frontend/app/theme/icomoon/fonts/icomoon.ttf b/frontend/app/theme/icomoon/fonts/icomoon.ttf index f50d96a39a24fed80a3e13cae3c8352af1679f3b..4fb2ed015b89aac3bb6f689f012894c6c33c8c40 100644 GIT binary patch delta 784 zcmXw1TWb?R6rQs)d+TnRwAp5ZNw?X}4NXbXTsGZC3qD9i6s1a4P;98R6_O^>QrlRG zik6~Olvu=P>mM+$q6J?HzV<-~ULFLY&lQTr>g-B~GjrxU=ljk#5A$LdcK4ux03jry zdkCZS+4HH4yW91iR^HJ)K2a&v+T-mhgdj+9d8#y9quE383eCb)dGSUmRS!}8459g* z>B-Xdz{=KDgccsszBx?`BEvqV*q#@euFNgC&qN+j{2d`CSFTPRFRjeK{(;aP#Wtyw z7HVi6E+e#*rh?E+sWSQMl( z=HTb8M&xfBhj9~C(HE*SIKC5TsDm)zgasP({0C{p0)-3f2GOL0FK_W*ifzVzcs&&MXWAjVx@6-C~!~IYb5t zmW2JUgf|*~)$~RyE>?5}#)6>aRlmU#W}9%pk#9l=#t@NZuhyFaDh9TLEd&y(B#8am z`k%)j2}HmHFOzl_cmYG}!t5m+{5>;-uep!@LK3n618T30>>T9g=6nHG o-`F-&f%x@;Nf52}|Bqwf+YF0r?M*761SM delta 303 zcmezIn{mny#(D-u1_lOhh6V;^1_S?KeItG$-u*z4JwTk0oSRs1JN|YG0|TQBke`#D zSX=<41%UhxAkC4UQ<+GO_jo`FDVP zm5khy3LZ9};) zeFMkwcz&C&4BRXrEeyAdIUm62$tx<%8GANAs8C_r%u%h#DBfk|X4M6h1qDYBV>p8t SNF@W)XA+_>m2z7~SewL4?dssrqeD9x zR_C98trQn&@bWS>hFxsH`Ke-Mf^tu&W`*JLN%_~z^z0n{5*Dzh!*qr5ySfVZ;7ZvA0QS4roHeFoiDA`~kn6 zx9Oc35@yPQesCbkyzFVlNZUD_jay*Gb6Go|PB|Ra#}n~H%G}pR#I(N_(A_*83eavO zvUVozr{PS}M-%8vI4}qa*5U~|UJ%(bWp>ciBphY3{c<3eNxLB~!&EufXXgijRtDb6 z2jq6Mb%68}tVp|`h-lV=0oxa=czD?p8tnuv7YLdn;kJnY0{JYpU<^@J_32%`K+V9n z@cEDxP$aoW-}rME6p2W zV~Gb*5y_@2x!~_PnD02$Zh{d|WXbA&Nd$p#R|%EyA`V-JM~tw_;p4!0$}ue$3>v%Y zilh+Y;6m4lSg)>Ysu}}<<2YG_6(bnT`Ji(&BuW?>&t}iz&Yx2Q_)^=EA4nneARyFS qAEt+Nuh+GOJghZNEkrqjhf%aKvt)p0YoJ=N3sv`*SZh43zJh-axRbsB delta 349 zcmX@|hw;G=Mv-!VH#Y`G1|XBW1sZ0Zk-CwbX- z{zID&Fg`Eb++G^UER(*0<9Iy3%~u9)7NA-NhTFxQ4`B4 Date: Sun, 27 Sep 2020 19:20:44 +0200 Subject: [PATCH 04/11] Feature/sidebar plugin (#582) * Another sidebar plugin. --- .../wwwroot/scripts/sidebar-content.html | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 backend/src/Squidex/wwwroot/scripts/sidebar-content.html diff --git a/backend/src/Squidex/wwwroot/scripts/sidebar-content.html b/backend/src/Squidex/wwwroot/scripts/sidebar-content.html new file mode 100644 index 000000000..ece7ad296 --- /dev/null +++ b/backend/src/Squidex/wwwroot/scripts/sidebar-content.html @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 7c3a6dbb3c6e13d07cdbe61b0fb0c6bcd8caf4f4 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 30 Sep 2020 12:08:18 +0200 Subject: [PATCH 05/11] Create content action. (#584) --- .../Actions/Comment/CommentActionHandler.cs | 46 ++++------ .../CreateContent/CreateContentAction.cs | 42 +++++++++ .../CreateContentActionHandler.cs | 87 +++++++++++++++++++ .../CreateContent/CreateContentPlugin.cs | 21 +++++ .../Notification/NotificationActionHandler.cs | 27 ++---- .../Contents/ContentFieldData.cs | 6 ++ .../Contents/NamedContentData.cs | 6 ++ .../HandleRules/RuleService.cs | 5 ++ .../ContentWrapper/ContentDataObject.cs | 7 ++ .../ContentWrapper/ContentFieldObject.cs | 6 ++ .../SquidexCommand.cs | 2 + .../SquidexEvent.cs | 2 + .../RuleEventFormatterCompareTests.cs | 23 +++++ .../HandleRules/RuleServiceTests.cs | 18 ++++ .../actions/generic-action.component.html | 4 +- 15 files changed, 254 insertions(+), 48 deletions(-) create mode 100644 backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs create mode 100644 backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs create mode 100644 backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs diff --git a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs index c2b89fb54..cc3141a46 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs +++ b/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 + public sealed class CommentActionHandler : RuleActionHandler { 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 ExecuteJobAsync(CommentJob job, CancellationToken ct = default) + protected override async Task 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 AppId { get; set; } - - public RefToken Actor { get; set; } - - public string CommentsId { get; set; } - - public string Text { get; set; } - } } diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentAction.cs new file mode 100644 index 000000000..2689a30aa --- /dev/null +++ b/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 = "", + 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; } + } +} diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs new file mode 100644 index 000000000..3e87610d1 --- /dev/null +++ b/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 + { + 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(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 ExecuteJobAsync(Command job, CancellationToken ct = default) + { + var command = job; + + command.FromRule = true; + + await commandBus.PublishAsync(command); + + return Result.Success($"Created to: {job.SchemaId.Name}"); + } + } +} diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentPlugin.cs new file mode 100644 index 000000000..644917108 --- /dev/null +++ b/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(); + } + } +} diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs index 0093b2335..f1f3f7655 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs +++ b/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 + public sealed class NotificationActionHandler : RuleActionHandler { private const string Description = "Send a Notification"; private static readonly NamedId 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 ExecuteJobAsync(NotificationJob job, CancellationToken ct = default) + protected override async Task 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; } - } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs index 656e81abe..e0408b1df 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs +++ b/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()}"))}}}"; + } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs b/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs index 84ed62235..9eec25ad2 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Model/Contents/NamedContentData.cs +++ b/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}"))}}}"; + } } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index f35b4feb9..1aeb74cd8 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -89,6 +89,11 @@ namespace Squidex.Domain.Apps.Core.HandleRules var typed = @event.To(); + if (typed.Payload.FromRule) + { + return result; + } + var actionType = rule.Action.GetType(); if (!ruleTriggerHandlers.TryGetValue(rule.Trigger.GetType(), out var triggerHandler)) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs index f57eaca91..813cc2c1a 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs +++ b/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))); } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs index f69a49fd9..351a73d61 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentFieldObject.cs +++ b/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; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs index e5dbccb20..c1c4d484d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/SquidexCommand.cs +++ b/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; } } diff --git a/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs b/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs index 8d42986bf..93cc78a14 100644 --- a/backend/src/Squidex.Domain.Apps.Events/SquidexEvent.cs +++ b/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; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs index 2ad6092c7..6afa79c12 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterCompareTests.cs +++ b/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, diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index 95ac00416..9bf2e961c 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/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>._)) + .MustNotHaveHappened(); + } + [Fact] public async Task Should_not_create_job_if_not_triggered_with_precheck() { diff --git a/frontend/app/features/rules/pages/rules/actions/generic-action.component.html b/frontend/app/features/rules/pages/rules/actions/generic-action.component.html index 038c5d184..a3fca239d 100644 --- a/frontend/app/features/rules/pages/rules/actions/generic-action.component.html +++ b/frontend/app/features/rules/pages/rules/actions/generic-action.component.html @@ -13,8 +13,8 @@
- -
From e3d5029922139a0fbee8077591c0dc642b7b18b2 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Wed, 30 Sep 2020 12:08:33 +0200 Subject: [PATCH 06/11] Check references when deleting. (#583) * Check references when deleting. * Tests fixed * Performance optimizations. --- backend/i18n/frontend_en.json | 5 + backend/i18n/frontend_it.json | 5 + backend/i18n/frontend_nl.json | 5 + backend/i18n/source/backend_en.json | 2 + backend/i18n/source/frontend_en.json | 5 + .../Assets/Fields.cs | 32 +++++ .../Assets/MongoAssetFolderRepository.cs | 10 +- .../Assets/MongoAssetRepository.cs | 12 +- .../Contents/Fields.cs | 32 +++++ .../Contents/MongoContentCollectionAll.cs | 11 ++ .../Contents/MongoContentRepository.cs | 5 + .../Contents/Operations/OperationBase.cs | 2 - .../Contents/Operations/QueryIdsAsync.cs | 17 +-- .../Operations/QueryReferrersAsync.cs | 45 ++++++ .../Apps/AppDomainObject.cs | 4 +- .../Assets/AssetDomainObject.cs | 35 +++-- .../Assets/AssetFolderDomainObject.cs | 3 +- .../Assets/Commands/DeleteAsset.cs | 1 + .../Contents/Commands/DeleteContent.cs | 1 + .../Contents/ContentDomainObject.cs | 21 ++- .../Contents/ContentOperationContext.cs | 8 +- .../Repositories/IContentRepository.cs | 2 + .../Rules/RuleDomainObject.cs | 4 +- .../EventSourcing/MongoEventStore.cs | 8 +- .../MongoDb/MongoRepositoryBase.cs | 2 +- backend/src/Squidex.Shared/Texts.it.resx | 6 + backend/src/Squidex.Shared/Texts.nl.resx | 6 + backend/src/Squidex.Shared/Texts.resx | 6 + .../Controllers/Assets/AssetsController.cs | 5 +- .../Contents/ContentsController.cs | 5 +- .../Apps/AppDomainObjectTests.cs | 2 +- .../Assets/AssetCommandMiddlewareTests.cs | 4 +- .../Assets/AssetDomainObjectTests.cs | 30 +++- .../Assets/AssetFolderDomainObjectTests.cs | 2 +- .../Contents/ContentDomainObjectTests.cs | 82 +++++++---- .../content-history-page.component.html | 9 +- .../pages/content/content-page.component.html | 3 +- .../pages/content/content-page.component.ts | 7 +- .../contents/contents-page.component.html | 3 +- .../pages/contents/contents-page.component.ts | 8 +- .../pages/sidebar/sidebar-page.component.ts | 4 +- .../shared/due-time-selector.component.ts | 2 +- .../shared/list/content.component.html | 3 +- .../references/reference-item.component.html | 3 +- .../pages/dashboard-config.component.html | 3 +- .../rules/pages/rules/rule.component.html | 9 +- .../pages/schema/fields/field.component.html | 6 +- .../schema-preview-urls-form.component.html | 3 +- .../schema-preview-urls-form.component.ts | 2 +- .../schema-field-rules-form.component.html | 3 +- .../pages/schema/schema-page.component.html | 3 +- .../pages/backups/backup.component.html | 3 +- .../pages/clients/client.component.html | 3 +- .../contributors/contributor.component.html | 3 +- .../import-contributors-dialog.component.ts | 4 +- .../pages/languages/language.component.html | 3 +- .../pages/patterns/pattern.component.html | 3 +- .../settings/pages/plans/plan.component.html | 1 + .../settings/pages/roles/role.component.html | 3 +- .../pages/workflows/workflow.component.html | 24 +++- .../angular/forms/confirm-click.directive.ts | 5 +- .../editors/date-time-editor.component.ts | 4 +- .../framework/angular/forms/forms-helper.ts | 2 +- .../forms/indeterminate-value.directive.ts | 2 +- .../forms/transform-input.directive.ts | 2 +- .../modals/dialog-renderer.component.html | 14 +- .../modals/dialog-renderer.component.ts | 4 +- .../angular/template-wrapper.directive.ts | 2 +- .../framework/services/dialog.service.spec.ts | 131 +++++++++++++++--- .../app/framework/services/dialog.service.ts | 58 ++++++-- .../services/resource-loader.service.ts | 2 +- frontend/app/framework/utils/date-time.ts | 2 +- .../app/framework/utils/rxjs-extensions.ts | 10 +- .../assets/asset-dialog.component.html | 3 +- .../assets/asset-folder.component.html | 3 +- .../components/assets/asset.component.html | 12 +- .../comments/comment.component.html | 1 + .../forms/references-checkboxes.component.ts | 2 +- .../forms/references-dropdown.component.ts | 2 +- .../forms/references-tags.component.ts | 2 +- frontend/app/shared/guards/load-apps.guard.ts | 2 +- .../app/shared/guards/load-languages.guard.ts | 2 +- .../shared/interceptors/auth.interceptor.ts | 8 +- .../shared/services/assets.service.spec.ts | 4 +- .../app/shared/services/assets.service.ts | 4 +- .../shared/services/contents.service.spec.ts | 4 +- .../app/shared/services/contents.service.ts | 4 +- .../shared/services/users-provider.service.ts | 2 +- .../app/shared/state/assets.state.spec.ts | 34 ++++- frontend/app/shared/state/assets.state.ts | 35 +++-- .../shared/state/contents.forms-helpers.ts | 2 +- .../shared/state/contents.forms.visitors.ts | 2 +- frontend/app/shared/state/contents.state.ts | 115 +++++++++++---- .../shared/state/contributors.state.spec.ts | 6 +- .../app/shared/state/rule-events.state.ts | 4 +- frontend/app/shared/state/schemas.state.ts | 4 +- .../shell/pages/home/home-page.component.ts | 2 +- 97 files changed, 788 insertions(+), 252 deletions(-) create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Fields.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index 81e1a1628..fe11289b7 100644 --- a/backend/i18n/frontend_en.json +++ b/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", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index f5f87d70d..ccc8d5834 100644 --- a/backend/i18n/frontend_it.json +++ b/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", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index 5c67eb6ab..57770b3be 100644 --- a/backend/i18n/frontend_nl.json +++ b/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", diff --git a/backend/i18n/source/backend_en.json b/backend/i18n/source/backend_en.json index cd11ee5b7..4d6b3dd2c 100644 --- a/backend/i18n/source/backend_en.json +++ b/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.", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index 81e1a1628..fe11289b7 100644 --- a/backend/i18n/source/frontend_en.json +++ b/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", diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Fields.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/Fields.cs new file mode 100644 index 000000000..4e1c0dd2e --- /dev/null +++ b/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 AssetIdField = new Lazy(GetAssetIdField); + private static readonly Lazy AssetFolderIdField = new Lazy(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; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs index ecad09811..904b95ab3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetFolderRepository.cs +++ b/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, IAssetFolderRepository { - private static readonly Lazy IdField = new Lazy(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; - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs index 0eddf64c9..ef3e24648 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs +++ b/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, IAssetRepository { - private static readonly Lazy IdField = new Lazy(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; - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Fields.cs new file mode 100644 index 000000000..7e7e988c8 --- /dev/null +++ b/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 IdField = new Lazy(GetIdField); + private static readonly Lazy SchemaIdField = new Lazy(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; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs index a07454389..76b165123 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentCollectionAll.cs +++ b/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 HasReferrersAsync(Guid contentId) + { + using (Profiler.TraceMethod()) + { + 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)); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs index 6198674f0..f69b64052 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs +++ b/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 HasReferrersAsync(Guid contentId) + { + return collectionAll.HasReferrersAsync(contentId); + } + public IEnumerable> GetInternalCollections() { yield return collectionAll.GetInternalCollection(); diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs index 90f40738c..edac8aac1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/OperationBase.cs +++ b/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 Sort = Builders.Sort; protected static readonly UpdateDefinitionBuilder Update = Builders.Update; - protected static readonly FieldDefinitionBuilder Fields = FieldDefinitionBuilder.Instance; protected static readonly FilterDefinitionBuilder Filter = Builders.Filter; protected static readonly IndexKeysDefinitionBuilder Index = Builders.IndexKeys; protected static readonly ProjectionDefinitionBuilder Projection = Builders.Projection; diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs index 2b0a8e773..c6089c964 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryIdsAsync.cs +++ b/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 IdField = new Lazy(GetIdField); - private static readonly Lazy SchemaIdField = new Lazy(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> DoAsync(Guid appId, Guid schemaId, FilterNode 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 BuildFilter(FilterNode? 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; - } } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs new file mode 100644 index 000000000..7456f4b53 --- /dev/null +++ b/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(Index + .Ascending(x => x.ReferencedIds) + .Ascending(x => x.IsDeleted)); + + return Collection.Indexes.CreateOneAsync(index, cancellationToken: ct); + } + + public async Task 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; + } + } +} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs index b9d7da90d..898a7db95 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs +++ b/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 store, ISemanticLog log, InitialPatterns initialPatterns, - IStore store, - ISemanticLog log, IAppPlansProvider appPlansProvider, IAppPlanBillingManager appPlansBillingManager, IUserResolver userResolver) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs index 504b8efe9..587b08b4b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs +++ b/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 { - private readonly ITagService tagService; + private readonly IContentRepository contentRepository; + private readonly IAssetTagService assetTags; private readonly IAssetQueryService assetQuery; - public AssetDomainObject(IStore store, ITagService tagService, IAssetQueryService assetQuery, ISemanticLog log) + public AssetDomainObject(IStore 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 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(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); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs index 33aeaff18..50d211f7d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetFolderDomainObject.cs +++ b/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 store, IAssetQueryService assetQuery, ISemanticLog log) + public AssetFolderDomainObject(ISemanticLog log, IStore store, + IAssetQueryService assetQuery) : base(store, log) { Guard.NotNull(assetQuery, nameof(assetQuery)); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs index 4848be209..c06b7ac1d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Commands/DeleteAsset.cs +++ b/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; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs index 28a0bd836..4d7c41f5f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Commands/DeleteContent.cs +++ b/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; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs index 9cd116038..98b125055 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs +++ b/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 { private readonly IContentWorkflow contentWorkflow; + private readonly IContentRepository contentRepository; private readonly ContentOperationContext context; - public ContentDomainObject(IStore store, IContentWorkflow contentWorkflow, ContentOperationContext context, ISemanticLog log) + public ContentDomainObject(IStore 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); }); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs index 5ee6bfd46..5908d4174 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs +++ b/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 factories, IScriptEngine scriptEngine, ISemanticLog log) + public ContentOperationContext( + IAppProvider appProvider, + IEnumerable factories, + IScriptEngine scriptEngine, + ISemanticLog log) { this.appProvider = appProvider; this.factories = factories; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs index 2339163e4..e0f8ff9b9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Repositories/IContentRepository.cs @@ -28,6 +28,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.Repositories Task> QueryIdsAsync(Guid appId, HashSet ids, SearchScope scope); + Task HasReferrersAsync(Guid contentId); + Task FindContentAsync(IAppEntity app, ISchemaEntity schema, Guid id, SearchScope scope); Task ResetScheduledAsync(Guid contentId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs index b5310a537..f6c23fb31 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs +++ b/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 store, ISemanticLog log, IAppProvider appProvider, IRuleEnqueuer ruleEnqueuer) + public RuleDomainObject(IStore 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; } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index fde1d0a57..8398c0771 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -15,10 +15,10 @@ namespace Squidex.Infrastructure.EventSourcing { public partial class MongoEventStore : MongoRepositoryBase, IEventStore { - private static readonly FieldDefinition TimestampField = Fields.Build(x => x.Timestamp); - private static readonly FieldDefinition EventsCountField = Fields.Build(x => x.EventsCount); - private static readonly FieldDefinition EventStreamOffsetField = Fields.Build(x => x.EventStreamOffset); - private static readonly FieldDefinition EventStreamField = Fields.Build(x => x.EventStream); + private static readonly FieldDefinition TimestampField = FieldBuilder.Build(x => x.Timestamp); + private static readonly FieldDefinition EventsCountField = FieldBuilder.Build(x => x.EventsCount); + private static readonly FieldDefinition EventStreamOffsetField = FieldBuilder.Build(x => x.EventStreamOffset); + private static readonly FieldDefinition EventStreamField = FieldBuilder.Build(x => x.EventStream); private readonly IEventNotifier notifier; public IMongoCollection RawCollection diff --git a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs index 8ca268545..9d40c7de2 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/MongoDb/MongoRepositoryBase.cs +++ b/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 Sort = Builders.Sort; protected static readonly UpdateDefinitionBuilder Update = Builders.Update; - protected static readonly FieldDefinitionBuilder Fields = FieldDefinitionBuilder.Instance; + protected static readonly FieldDefinitionBuilder FieldBuilder = FieldDefinitionBuilder.Instance; protected static readonly FilterDefinitionBuilder Filter = Builders.Filter; protected static readonly IndexKeysDefinitionBuilder Index = Builders.IndexKeys; protected static readonly ProjectionDefinitionBuilder Projection = Builders.Projection; diff --git a/backend/src/Squidex.Shared/Texts.it.resx b/backend/src/Squidex.Shared/Texts.it.resx index 08b32a3b5..3f0baa6ef 100644 --- a/backend/src/Squidex.Shared/Texts.it.resx +++ b/backend/src/Squidex.Shared/Texts.it.resx @@ -184,6 +184,9 @@ Hai raggiunto la dimensione massima consentito per le risorse. + + Assets is referenced by a content and cannot be deleted. + E' in esecuzione una altro processo di backup. @@ -493,6 +496,9 @@ {count} Collegamenti(s) + + Content is referenced by another content and cannot be deleted. + Il contenuto singleton non può essere aggiornato diff --git a/backend/src/Squidex.Shared/Texts.nl.resx b/backend/src/Squidex.Shared/Texts.nl.resx index 0b5bfc1f0..0c6056d44 100644 --- a/backend/src/Squidex.Shared/Texts.nl.resx +++ b/backend/src/Squidex.Shared/Texts.nl.resx @@ -184,6 +184,9 @@ Je hebt jouw maximale assetgrootte bereikt. + + Assets is referenced by a content and cannot be deleted. + Er wordt al een ander back-upproces uitgevoerd. @@ -493,6 +496,9 @@ {count} referentie (s) + + Content is referenced by another content and cannot be deleted. + Singleton-inhoud kan niet worden bijgewerkt. diff --git a/backend/src/Squidex.Shared/Texts.resx b/backend/src/Squidex.Shared/Texts.resx index 9755c2e6d..3d7c7e09f 100644 --- a/backend/src/Squidex.Shared/Texts.resx +++ b/backend/src/Squidex.Shared/Texts.resx @@ -184,6 +184,9 @@ You have reached your max asset size. + + Assets is referenced by a content and cannot be deleted. + Another backup process is already running. @@ -493,6 +496,9 @@ {count} Reference(s) + + Content is referenced by another content and cannot be deleted. + Singleton content cannot be updated. diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 7c6ffdc2c..33173ec61 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -284,6 +284,7 @@ namespace Squidex.Areas.Api.Controllers.Assets /// /// The name of the app. /// The id of the asset to delete. + /// True to check referrers of this asset. /// /// 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 DeleteAsset(string app, Guid id) + public async Task 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(); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 0c40ae356..24cd6be7d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -551,6 +551,7 @@ namespace Squidex.Areas.Api.Controllers.Contents /// The name of the app. /// The name of the schema. /// The id of the content item to delete. + /// True to check referrers of this content. /// /// 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 DeleteContent(string app, string name, Guid id) + public async Task 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); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs index 80e4c20d0..a5dd8c148 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppDomainObjectTests.cs +++ b/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(), appPlansProvider, appPlansBillingManager, userResolver); + sut = new AppDomainObject(Store, A.Dummy(), initialPatterns, appPlansProvider, appPlansBillingManager, userResolver); sut.Setup(Id); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index 054258307..bbc9cfc9c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/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(); private readonly IAssetMetadataSource assetMetadataSource = A.Fake(); private readonly IAssetQueryService assetQuery = A.Fake(); + private readonly IContentRepository contentRepository = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly IGrainFactory grainFactory = A.Fake(); private readonly IServiceProvider serviceProvider = A.Fake(); @@ -53,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { file = new NoopAssetFile(); - var assetDomainObject = new AssetDomainObject(Store, tagService, assetQuery, A.Dummy()); + var assetDomainObject = new AssetDomainObject(Store, A.Dummy(), tagService, assetQuery, contentRepository); A.CallTo(() => serviceProvider.GetService(typeof(AssetDomainObject))) .Returns(assetDomainObject); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs index 0f8a5846b..bda178baa 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetDomainObjectTests.cs +++ b/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 { + private readonly IContentRepository contentRepository = A.Fake(); private readonly ITagService tagService = A.Fake(); private readonly IAssetQueryService assetQuery = A.Fake(); private readonly Guid parentId = Guid.NewGuid(); @@ -46,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A>._, A>._)) .ReturnsLazily(x => Task.FromResult(x.GetArgument>(2)?.ToDictionary(x => x)!)); - sut = new AssetDomainObject(Store, tagService, assetQuery, A.Dummy()); + sut = new AssetDomainObject(Store, A.Dummy(), 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(() => 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 }); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs index fd681cf60..834e248d9 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetFolderDomainObjectTests.cs +++ b/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 { A.Fake() }); - sut = new AssetFolderDomainObject(Store, assetQuery, A.Dummy()); + sut = new AssetFolderDomainObject(A.Dummy(), Store, assetQuery); sut.Setup(Id); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs index 05e72aef9..7c569611e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentDomainObjectTests.cs +++ b/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(); private readonly IContentWorkflow contentWorkflow = A.Fake(x => x.Wrapping(new DefaultContentWorkflow())); + private readonly IContentRepository contentRepository = A.Fake(); private readonly ISchemaEntity schema; private readonly IScriptEngine scriptEngine = A.Fake(); @@ -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()); + var validators = Enumerable.Repeat(new DefaultValidatorsFactory(), 1); - sut = new ContentDomainObject(Store, contentWorkflow, context, A.Dummy()); + var context = new ContentOperationContext(appProvider, validators, scriptEngine, A.Fake()); + + sut = new ContentDomainObject(Store, A.Dummy(), 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(() => PublishAsync(CreateContentCommand(command))); + await Assert.ThrowsAsync(() => 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(() => PublishAsync(CreateContentCommand(command))); + await Assert.ThrowsAsync(() => 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._, 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._, 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(() => 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() diff --git a/frontend/app/features/content/pages/content/content-history-page.component.html b/frontend/app/features/content/pages/content/content-history-page.component.html index 954456e14..9b009c7f1 100644 --- a/frontend/app/features/content/pages/content/content-history-page.component.html +++ b/frontend/app/features/content/pages/content/content-history-page.component.html @@ -36,7 +36,8 @@ + confirmText="i18n:contents.deleteVersionConfirmText" + confirmRememberKey="deleteDraft"> {{ 'contents.versionDelete' | sqxTranslate }} @@ -45,7 +46,8 @@ + confirmText="i18n:contents.deleteConfirmText" + confirmRememberKey="deleteContent"> {{ 'common.delete' | sqxTranslate }}
@@ -85,7 +87,8 @@ + confirmText="i18n:contents.deleteConfirmText" + confirmRememberKey="deleteContent"> {{ 'common.delete' | sqxTranslate }}
diff --git a/frontend/app/features/content/pages/content/content-page.component.html b/frontend/app/features/content/pages/content/content-page.component.html index 8a36ce99f..98f4b25ef 100644 --- a/frontend/app/features/content/pages/content/content-page.component.html +++ b/frontend/app/features/content/pages/content/content-page.component.html @@ -36,7 +36,8 @@ + confirmText="i18n:contents.deleteConfirmText" + confirmRememberKey="deleteContent"> {{ 'common.delete' | sqxTranslate }}
diff --git a/frontend/app/features/content/pages/content/content-page.component.ts b/frontend/app/features/content/pages/content/content-page.component.ts index 67f279d76..7947d9bd8 100644 --- a/frontend/app/features/content/pages/content/content-page.component.ts +++ b/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]); } } diff --git a/frontend/app/features/content/pages/contents/contents-page.component.html b/frontend/app/features/content/pages/contents/contents-page.component.html index b2d0f14c8..40f51bffb 100644 --- a/frontend/app/features/content/pages/contents/contents-page.component.html +++ b/frontend/app/features/content/pages/contents/contents-page.component.html @@ -55,7 +55,8 @@
diff --git a/frontend/app/features/content/pages/contents/contents-page.component.ts b/frontend/app/features/content/pages/contents/contents-page.component.ts index 256580a45..ee362526d 100644 --- a/frontend/app/features/content/pages/contents/contents-page.component.ts +++ b/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]; + } } } diff --git a/frontend/app/features/content/pages/sidebar/sidebar-page.component.ts b/frontend/app/features/content/pages/sidebar/sidebar-page.component.ts index 9e02dd1e0..e54fee3d8 100644 --- a/frontend/app/features/content/pages/sidebar/sidebar-page.component.ts +++ b/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; diff --git a/frontend/app/features/content/shared/due-time-selector.component.ts b/frontend/app/features/content/shared/due-time-selector.component.ts index 0c1abfa79..0aaeac08a 100644 --- a/frontend/app/features/content/shared/due-time-selector.component.ts +++ b/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; public dueTimeDialog = new DialogModel(); diff --git a/frontend/app/features/content/shared/list/content.component.html b/frontend/app/features/content/shared/list/content.component.html index cdef0e7cd..816bb890b 100644 --- a/frontend/app/features/content/shared/list/content.component.html +++ b/frontend/app/features/content/shared/list/content.component.html @@ -41,7 +41,8 @@ + confirmText="i18n:contents.deleteConfirmText" + confirmRememberKey="deleteContent"> {{ 'common.delete' | sqxTranslate }}
diff --git a/frontend/app/features/content/shared/references/reference-item.component.html b/frontend/app/features/content/shared/references/reference-item.component.html index 83a1667e8..03e68b105 100644 --- a/frontend/app/features/content/shared/references/reference-item.component.html +++ b/frontend/app/features/content/shared/references/reference-item.component.html @@ -29,7 +29,8 @@
diff --git a/frontend/app/features/dashboard/pages/dashboard-config.component.html b/frontend/app/features/dashboard/pages/dashboard-config.component.html index 98f186417..d3784d209 100644 --- a/frontend/app/features/dashboard/pages/dashboard-config.component.html +++ b/frontend/app/features/dashboard/pages/dashboard-config.component.html @@ -26,7 +26,8 @@ + confirmText="i18n:dashboard.resetConfigConfirmText" + confirmRememberKey="resetConfig"> {{ 'common.reset' | sqxTranslate }}
diff --git a/frontend/app/features/rules/pages/rules/rule.component.html b/frontend/app/features/rules/pages/rules/rule.component.html index af003078c..c8ce33832 100644 --- a/frontend/app/features/rules/pages/rules/rule.component.html +++ b/frontend/app/features/rules/pages/rules/rule.component.html @@ -20,14 +20,16 @@ + confirmText="i18n:rules.runRuleConfirmText" + confirmRememberKey="runRule"> {{ 'rules.run' | sqxTranslate }}
+ confirmText="i18n:rules.deleteConfirmText" + confirmRememberKey="deleteContent"> {{ 'common.delete' | sqxTranslate }}
@@ -58,7 +60,8 @@ diff --git a/frontend/app/features/schemas/pages/schema/fields/field.component.html b/frontend/app/features/schemas/pages/schema/fields/field.component.html index 12c15e90e..cddad0626 100644 --- a/frontend/app/features/schemas/pages/schema/fields/field.component.html +++ b/frontend/app/features/schemas/pages/schema/fields/field.component.html @@ -69,7 +69,8 @@ + confirmText="i18n:schemas.field.lockConfirmText" + confirmRememberKey="lockField"> {{ 'schemas.field.lock' | sqxTranslate }} @@ -80,7 +81,8 @@ + confirmText="i18n:schemas.field.deleteConfirmText" + confirmRememberKey="deleteField"> {{ 'common.delete' | sqxTranslate }} diff --git a/frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.html b/frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.html index f5924e9e0..8b832f51f 100644 --- a/frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.html +++ b/frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.html @@ -29,7 +29,8 @@
diff --git a/frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts b/frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts index 102b2b80d..0576086dd 100644 --- a/frontend/app/features/schemas/pages/schema/preview/schema-preview-urls-form.component.ts +++ b/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); diff --git a/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html b/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html index 463d50bc6..14434ba82 100644 --- a/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html +++ b/frontend/app/features/schemas/pages/schema/rules/schema-field-rules-form.component.html @@ -39,7 +39,8 @@
diff --git a/frontend/app/features/schemas/pages/schema/schema-page.component.html b/frontend/app/features/schemas/pages/schema/schema-page.component.html index 295baacf5..af9467156 100644 --- a/frontend/app/features/schemas/pages/schema/schema-page.component.html +++ b/frontend/app/features/schemas/pages/schema/schema-page.component.html @@ -40,7 +40,8 @@ + confirmText="i18n:schemas.deleteConfirmText" + confirmRememberKey="deleteSchema"> {{ 'common.delete' | sqxTranslate }} diff --git a/frontend/app/features/settings/pages/backups/backup.component.html b/frontend/app/features/settings/pages/backups/backup.component.html index c4b8cdfe8..5068bc9a7 100644 --- a/frontend/app/features/settings/pages/backups/backup.component.html +++ b/frontend/app/features/settings/pages/backups/backup.component.html @@ -40,7 +40,8 @@
diff --git a/frontend/app/features/settings/pages/clients/client.component.html b/frontend/app/features/settings/pages/clients/client.component.html index 4b3eecec9..1c7a8c04c 100644 --- a/frontend/app/features/settings/pages/clients/client.component.html +++ b/frontend/app/features/settings/pages/clients/client.component.html @@ -12,7 +12,8 @@
diff --git a/frontend/app/features/settings/pages/contributors/contributor.component.html b/frontend/app/features/settings/pages/contributors/contributor.component.html index f047c4879..c26bfde61 100644 --- a/frontend/app/features/settings/pages/contributors/contributor.component.html +++ b/frontend/app/features/settings/pages/contributors/contributor.component.html @@ -14,7 +14,8 @@ diff --git a/frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts b/frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts index 0cb034bd6..4b8d4f678 100644 --- a/frontend/app/features/settings/pages/contributors/import-contributors-dialog.component.ts +++ b/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(); diff --git a/frontend/app/features/settings/pages/languages/language.component.html b/frontend/app/features/settings/pages/languages/language.component.html index 219b13ce4..d23b6feeb 100644 --- a/frontend/app/features/settings/pages/languages/language.component.html +++ b/frontend/app/features/settings/pages/languages/language.component.html @@ -16,7 +16,8 @@
diff --git a/frontend/app/features/settings/pages/patterns/pattern.component.html b/frontend/app/features/settings/pages/patterns/pattern.component.html index 6944ec286..85186665d 100644 --- a/frontend/app/features/settings/pages/patterns/pattern.component.html +++ b/frontend/app/features/settings/pages/patterns/pattern.component.html @@ -27,7 +27,8 @@
diff --git a/frontend/app/features/settings/pages/plans/plan.component.html b/frontend/app/features/settings/pages/plans/plan.component.html index 388c0598c..44aa3c0dc 100644 --- a/frontend/app/features/settings/pages/plans/plan.component.html +++ b/frontend/app/features/settings/pages/plans/plan.component.html @@ -27,6 +27,7 @@
diff --git a/frontend/app/features/settings/pages/workflows/workflow.component.html b/frontend/app/features/settings/pages/workflows/workflow.component.html index de30cf102..0058d3885 100644 --- a/frontend/app/features/settings/pages/workflows/workflow.component.html +++ b/frontend/app/features/settings/pages/workflows/workflow.component.html @@ -5,7 +5,11 @@ {{workflow.displayName}}
- +
@@ -17,7 +21,8 @@
@@ -62,7 +67,10 @@
- + @@ -71,7 +79,15 @@
- +
diff --git a/frontend/app/shared/components/assets/asset-folder.component.html b/frontend/app/shared/components/assets/asset-folder.component.html index 079c6bf76..a6d0864e7 100644 --- a/frontend/app/shared/components/assets/asset-folder.component.html +++ b/frontend/app/shared/components/assets/asset-folder.component.html @@ -26,7 +26,8 @@ + confirmText="i18n:assets.deleteFolderConfirmText" + confirmRememberKey="deleteAssetFolder"> {{ 'common.delete' | sqxTranslate }}
diff --git a/frontend/app/shared/components/assets/asset.component.html b/frontend/app/shared/components/assets/asset.component.html index 529a4f2c6..12a59f209 100644 --- a/frontend/app/shared/components/assets/asset.component.html +++ b/frontend/app/shared/components/assets/asset.component.html @@ -33,14 +33,16 @@ + confirmText="i18n:assets.deleteConfirmText" + confirmRememberKey="deleteAsset"> + confirmText="i18n:assets.removeConfirmText" + confirmRememberKey="removeAsset"> @@ -134,14 +136,16 @@ diff --git a/frontend/app/shared/components/comments/comment.component.html b/frontend/app/shared/components/comments/comment.component.html index 403732ad9..16d95918d 100644 --- a/frontend/app/shared/components/comments/comment.component.html +++ b/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"> diff --git a/frontend/app/shared/components/forms/references-checkboxes.component.ts b/frontend/app/shared/components/forms/references-checkboxes.component.ts index 40c0e63ec..ade5e5137 100644 --- a/frontend/app/shared/components/forms/references-checkboxes.component.ts +++ b/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> implements OnChanges { - private itemCount: number; + private readonly itemCount: number; private contentItems: ReadonlyArray | null = null; @Input() diff --git a/frontend/app/shared/components/forms/references-dropdown.component.ts b/frontend/app/shared/components/forms/references-dropdown.component.ts index e7e8aa2c7..4d457c460 100644 --- a/frontend/app/shared/components/forms/references-dropdown.component.ts +++ b/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 | string> implements OnChanges { + private readonly itemCount: number; private languageField: LanguageDto; private selectedId: string | undefined; - private itemCount: number; @Input() public schemaId: string; diff --git a/frontend/app/shared/components/forms/references-tags.component.ts b/frontend/app/shared/components/forms/references-tags.component.ts index a79297305..dca5d2909 100644 --- a/frontend/app/shared/components/forms/references-tags.component.ts +++ b/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> implements OnChanges { - private itemCount: number; + private readonly itemCount: number; private contentItems: ReadonlyArray | null = null; @Input() diff --git a/frontend/app/shared/guards/load-apps.guard.ts b/frontend/app/shared/guards/load-apps.guard.ts index ead7f12bd..7712803c3 100644 --- a/frontend/app/shared/guards/load-apps.guard.ts +++ b/frontend/app/shared/guards/load-apps.guard.ts @@ -19,6 +19,6 @@ export class LoadAppsGuard implements CanActivate { } public canActivate(): Observable { - return this.appsState.load().pipe(map(a => true)); + return this.appsState.load().pipe(map(_ => true)); } } \ No newline at end of file diff --git a/frontend/app/shared/guards/load-languages.guard.ts b/frontend/app/shared/guards/load-languages.guard.ts index 45ce8f593..c6c18e2be 100644 --- a/frontend/app/shared/guards/load-languages.guard.ts +++ b/frontend/app/shared/guards/load-languages.guard.ts @@ -19,6 +19,6 @@ export class LoadLanguagesGuard implements CanActivate { } public canActivate(): Observable { - return this.languagesState.load().pipe(map(a => true)); + return this.languagesState.load().pipe(map(_ => true)); } } \ No newline at end of file diff --git a/frontend/app/shared/interceptors/auth.interceptor.ts b/frontend/app/shared/interceptors/auth.interceptor.ts index 000d6cdce..50009ec90 100644 --- a/frontend/app/shared/interceptors/auth.interceptor.ts +++ b/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')); } diff --git a/frontend/app/shared/services/assets.service.spec.ts b/frontend/app/shared/services/assets.service.spec.ts index edd344773..041b9beb6 100644 --- a/frontend/app/shared/services/assets.service.spec.ts +++ b/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); diff --git a/frontend/app/shared/services/assets.service.ts b/frontend/app/shared/services/assets.service.ts index 882f48aa1..186282208 100644 --- a/frontend/app/shared/services/assets.service.ts +++ b/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> { + public deleteAssetItem(appName: string, asset: Resource, checkReferrers: boolean, version: Version): Observable> { 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(() => { diff --git a/frontend/app/shared/services/contents.service.spec.ts b/frontend/app/shared/services/contents.service.spec.ts index 9beafccec..199271193 100644 --- a/frontend/app/shared/services/contents.service.spec.ts +++ b/frontend/app/shared/services/contents.service.spec.ts @@ -389,9 +389,9 @@ describe('ContentsService', () => { } }; - contentsService.deleteContent('my-app', resource, version).subscribe(); + contentsService.deleteContent('my-app', resource, true, version).subscribe(); - const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1'); + const req = httpMock.expectOne('http://service/p/api/content/my-app/my-schema/content1?checkReferrers=true'); expect(req.request.method).toEqual('DELETE'); expect(req.request.headers.get('If-Match')).toEqual(version.value); diff --git a/frontend/app/shared/services/contents.service.ts b/frontend/app/shared/services/contents.service.ts index 55e49386e..c5db772f2 100644 --- a/frontend/app/shared/services/contents.service.ts +++ b/frontend/app/shared/services/contents.service.ts @@ -316,10 +316,10 @@ export class ContentsService { pretifyError(`Failed to ${status} content. Please reload.`)); } - public deleteContent(appName: string, resource: Resource, version: Version): Observable> { + public deleteContent(appName: string, resource: Resource, checkReferrers: boolean, version: Version): Observable> { const link = resource._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(() => { diff --git a/frontend/app/shared/services/users-provider.service.ts b/frontend/app/shared/services/users-provider.service.ts index 5901bc07d..87f29e36d 100644 --- a/frontend/app/shared/services/users-provider.service.ts +++ b/frontend/app/shared/services/users-provider.service.ts @@ -27,7 +27,7 @@ export class UsersProviderService { if (!result) { const request = this.usersService.getUser(id).pipe( - catchError(error => { + catchError(() => { return of(new UserDto('Unknown', 'Unknown')); }), publishLast()); diff --git a/frontend/app/shared/state/assets.state.spec.ts b/frontend/app/shared/state/assets.state.spec.ts index 4571ade2a..d2e75f056 100644 --- a/frontend/app/shared/state/assets.state.spec.ts +++ b/frontend/app/shared/state/assets.state.spec.ts @@ -1,3 +1,4 @@ +import { ErrorDto } from '@app/framework'; /* * Squidex Headless CMS * @@ -331,7 +332,7 @@ describe('AssetsState', () => { }); it('should remove asset from snapshot when deleted', () => { - assetsService.setup(x => x.deleteAssetItem(app, asset1, asset1.version)) + assetsService.setup(x => x.deleteAssetItem(app, asset1, true, asset1.version)) .returns(() => of(versioned(newVersion))); assetsState.deleteAsset(asset1).subscribe(); @@ -341,8 +342,37 @@ describe('AssetsState', () => { expect(assetsState.snapshot.tagsAvailable).toEqual({ shared: 1, tag2: 1 }); }); + it('should remove asset from snapshot when when referenced and not confirmed', () => { + assetsService.setup(x => x.deleteAssetItem(app, asset1, false, asset1.version)) + .returns(() => throwError(new ErrorDto(404, 'Referenced'))); + + assetsService.setup(x => x.deleteAssetItem(app, asset1, true, asset1.version)) + .returns(() => of(versioned(newVersion))); + + dialogs.setup(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAnyString())) + .returns(() => of(true)); + + assetsState.deleteAsset(asset1).subscribe(); + + expect(assetsState.snapshot.assets.length).toBe(1); + expect(assetsState.snapshot.assetsPager.numberOfItems).toBe(199); + expect(assetsState.snapshot.tagsAvailable).toEqual({ shared: 1, tag2: 1 }); + }); + + it('should not remove asset when referenced and not confirmed', () => { + assetsService.setup(x => x.deleteAssetItem(app, asset1, true, asset1.version)) + .returns(() => throwError(new ErrorDto(404, 'Referenced'))); + + dialogs.setup(x => x.confirm(It.isAnyString(), It.isAnyString(), It.isAnyString())) + .returns(() => of(false)); + + assetsState.deleteAsset(asset1).pipe(onErrorResumeNext()).subscribe(); + + expect(assetsState.snapshot.assets.length).toBe(2); + }); + it('should remove asset folder from snapshot when deleted', () => { - assetsService.setup(x => x.deleteAssetItem(app, assetFolder1, assetFolder1.version)) + assetsService.setup(x => x.deleteAssetItem(app, assetFolder1, false, assetFolder1.version)) .returns(() => of(versioned(newVersion))); assetsState.deleteAssetFolder(assetFolder1).subscribe(); diff --git a/frontend/app/shared/state/assets.state.ts b/frontend/app/shared/state/assets.state.ts index f52f93156..1db02e762 100644 --- a/frontend/app/shared/state/assets.state.ts +++ b/frontend/app/shared/state/assets.state.ts @@ -6,9 +6,9 @@ */ import { Injectable } from '@angular/core'; -import { compareStrings, DialogService, MathHelper, Pager, shareSubscribed, State, StateSynchronizer } from '@app/framework'; -import { empty, forkJoin, Observable, of, throwError } from 'rxjs'; -import { catchError, finalize, tap } from 'rxjs/operators'; +import { compareStrings, DialogService, ErrorDto, MathHelper, Pager, shareSubscribed, State, StateSynchronizer } from '@app/framework'; +import { EMPTY, forkJoin, Observable, of, throwError } from 'rxjs'; +import { catchError, finalize, switchMap, tap } from 'rxjs/operators'; import { AnnotateAssetDto, AssetDto, AssetFolderDto, AssetsService, RenameAssetFolderDto } from './../services/assets.service'; import { AppsState } from './apps.state'; import { Query, QueryFullTextSynchronizer } from './query'; @@ -274,7 +274,7 @@ export class AssetsState extends State { public moveAsset(asset: AssetDto, parentId?: string) { if (asset.parentId === parentId) { - return empty(); + return EMPTY; } this.next(s => { @@ -298,7 +298,7 @@ export class AssetsState extends State { public moveAssetFolder(assetFolder: AssetFolderDto, parentId?: string) { if (assetFolder.id === parentId || assetFolder.parentId === parentId) { - return empty(); + return EMPTY; } this.next(s => { @@ -320,8 +320,27 @@ export class AssetsState extends State { shareSubscribed(this.dialogs)); } - public deleteAsset(asset: AssetDto): Observable { - return this.assetsService.deleteAssetItem(this.appName, asset, asset.version).pipe( + public deleteAsset(asset: AssetDto) { + return this.assetsService.deleteAssetItem(this.appName, asset, true, asset.version).pipe( + catchError((error: ErrorDto) => { + if (error.statusCode === 400) { + return this.dialogs.confirm( + 'i18n:assets.deleteReferrerConfirmTitle', + 'i18n:assets.deleteReferrerConfirmText', + 'deleteReferencedAsset' + ).pipe( + switchMap(confirmed => { + if (confirmed) { + return this.assetsService.deleteAssetItem(this.appName, asset, false, asset.version); + } else { + return EMPTY; + } + }) + ); + } else { + return throwError(error); + } + }), tap(() => { this.next(s => { const assets = s.assets.filter(x => x.id !== asset.id); @@ -336,7 +355,7 @@ export class AssetsState extends State { } public deleteAssetFolder(assetFolder: AssetFolderDto): Observable { - return this.assetsService.deleteAssetItem(this.appName, assetFolder, assetFolder.version).pipe( + return this.assetsService.deleteAssetItem(this.appName, assetFolder, false, assetFolder.version).pipe( tap(() => { this.next(s => { const assetFolders = s.assetFolders.filter(x => x.id !== assetFolder.id); diff --git a/frontend/app/shared/state/contents.forms-helpers.ts b/frontend/app/shared/state/contents.forms-helpers.ts index c8c2dfa26..cc09196d9 100644 --- a/frontend/app/shared/state/contents.forms-helpers.ts +++ b/frontend/app/shared/state/contents.forms-helpers.ts @@ -71,7 +71,7 @@ export class PartitionConfig { } export class CompiledRule { - private function: Function; + private readonly function: Function; public get field() { return this.rule.field; diff --git a/frontend/app/shared/state/contents.forms.visitors.ts b/frontend/app/shared/state/contents.forms.visitors.ts index f39f8e452..e4f9e8243 100644 --- a/frontend/app/shared/state/contents.forms.visitors.ts +++ b/frontend/app/shared/state/contents.forms.visitors.ts @@ -39,7 +39,7 @@ export function getContentValue(content: ContentDto, language: LanguageDto, fiel fieldValue = reference[fieldInvariant]; } - let value: string | undefined = undefined; + let value: string | undefined; if (Types.isObject(fieldValue)) { value = fieldValue[language.iso2Code]; diff --git a/frontend/app/shared/state/contents.state.ts b/frontend/app/shared/state/contents.state.ts index be7117ee1..2f34bdde3 100644 --- a/frontend/app/shared/state/contents.state.ts +++ b/frontend/app/shared/state/contents.state.ts @@ -7,8 +7,8 @@ import { Injectable } from '@angular/core'; import { DialogService, ErrorDto, Pager, shareSubscribed, State, StateSynchronizer, Types, Version, Versioned } from '@app/framework'; -import { empty, forkJoin, Observable, of } from 'rxjs'; -import { catchError, finalize, switchMap, tap } from 'rxjs/operators'; +import { EMPTY, forkJoin, Observable, of } from 'rxjs'; +import { catchError, finalize, map, switchMap, tap } from 'rxjs/operators'; import { ContentDto, ContentsService, StatusInfo } from './../services/contents.service'; import { SchemaDto } from './../services/schemas.service'; import { AppsState } from './apps.state'; @@ -16,6 +16,8 @@ import { SavedQuery } from './queries'; import { Query, QuerySynchronizer } from './query'; import { SchemasState } from './schemas.state'; +type Updated = { content: ContentDto, error?: ErrorDto }; + interface Snapshot { // The current comments. contents: ReadonlyArray; @@ -141,7 +143,7 @@ export abstract class ContentsStateBase extends State { public loadIfNotLoaded(): Observable { if (this.snapshot.isLoaded) { - return empty(); + return EMPTY; } return this.loadInternal(false); @@ -153,7 +155,7 @@ export abstract class ContentsStateBase extends State { private loadInternalCore(isReload: boolean) { if (!this.appName || !this.schemaName) { - return empty(); + return EMPTY; } this.next({ isLoading: true }); @@ -221,39 +223,82 @@ export abstract class ContentsStateBase extends State { shareSubscribed(this.dialogs, {silent: true})); } - public changeManyStatus(contents: ReadonlyArray, status: string, dueTime: string | null): Observable { - return forkJoin( - contents.map(c => - this.contentsService.putStatus(this.appName, c, status, dueTime, c.version).pipe( - catchError(error => of(error))))).pipe( + public changeManyStatus(contentsToChange: ReadonlyArray, status: string, dueTime: string | null): Observable { + return this.updateManyStatus(contentsToChange, status, dueTime).pipe( tap(results => { - const error = results.find(x => x instanceof ErrorDto); + const errors = results.filter(x => !!x.error); + + if (errors.length > 0) { + const errror = errors[0].error!; - if (error) { - this.dialogs.notifyError(error); + if (errors.length === contentsToChange.length) { + throw errror; + } else { + this.dialogs.notifyError(errror); + } } - return of(error); + this.next(s => { + let contents = s.contents; + + for (const updated of results.filter(x => !x.error).map(x => x.content)) { + contents = contents.replaceBy('id', updated); + } + + return { ...s, contents }; + }); }), - switchMap(() => this.loadInternalCore(false)), - shareSubscribed(this.dialogs, { silent: true })); + shareSubscribed(this.dialogs)); } - public deleteMany(contents: ReadonlyArray): Observable { - return forkJoin( - contents.map(c => - this.contentsService.deleteContent(this.appName, c, c.version).pipe( - catchError(error => of(error))))).pipe( + public deleteMany(contentsToDelete: ReadonlyArray) { + return this.deleteManyCore(contentsToDelete, true).pipe( + switchMap(results => { + const referenced = results.filter(x => x.error?.statusCode === 400).map(x => x.content); + + if (referenced.length > 0) { + return this.dialogs.confirm( + 'i18n:contents.deleteReferrerConfirmTitle', + 'i18n:contents.deleteReferrerConfirmText', + 'deleteReferencedAsset' + ).pipe( + switchMap(confirmed => { + if (confirmed) { + return this.deleteManyCore(referenced, false); + } else { + return of([]); + } + }) + ); + } else { + return of(results); + } + }), tap(results => { - const error = results.find(x => x instanceof ErrorDto); + const errors = results.filter(x => !!x.error); + + if (errors.length > 0) { + const errror = errors[0].error!; - if (error) { - this.dialogs.notifyError(error); + if (errors.length === contentsToDelete.length) { + throw errror; + } else { + this.dialogs.notifyError(errror); + } } - return of(error); + this.next(s => { + let contents = s.contents; + let contentsPager = s.contentsPager; + + for (const content of results.filter(x => !x.error).map(x => x.content)) { + contents = contents.filter(x => x.id !== content.id); + contentsPager = contentsPager.decrementCount(); + } + + return { ...s, contents, contentsPager }; + }); }), - switchMap(() => this.loadInternal(false)), shareSubscribed(this.dialogs, { silent: true })); } @@ -329,6 +374,26 @@ export abstract class ContentsStateBase extends State { } } + private deleteManyCore(contents: ReadonlyArray, checkReferrers: boolean): Observable> { + return forkJoin( + contents.map(c => this.deleteCore(c, checkReferrers))); + } + + private updateManyStatus(contents: ReadonlyArray, status: string, dueTime: string | null): Observable> { + return forkJoin( + contents.map(c => this.updateStatus(c, status, dueTime))); + } + + private deleteCore(content: ContentDto, checkReferrers: boolean): Observable { + return this.contentsService.deleteContent(this.appName, content, checkReferrers, content.version).pipe( + map(() => ({ content })), catchError(error => of({ content, error }))); + } + + private updateStatus(content: ContentDto, status: string, dueTime: string | null): Observable { + return this.contentsService.putStatus(this.appName, content, status, dueTime, content.version).pipe( + map(x => ({ content: x })), catchError(error => of({ content, error }))); + } + public abstract get schemaId(): string; public abstract get schemaName(): string; diff --git a/frontend/app/shared/state/contributors.state.spec.ts b/frontend/app/shared/state/contributors.state.spec.ts index 5a6847c40..5953a99c5 100644 --- a/frontend/app/shared/state/contributors.state.spec.ts +++ b/frontend/app/shared/state/contributors.state.spec.ts @@ -7,7 +7,7 @@ import { ErrorDto } from '@app/framework'; import { ContributorDto, ContributorsPayload, ContributorsService, ContributorsState, DialogService, Pager, versioned } from '@app/shared/internal'; -import { empty, of, throwError } from 'rxjs'; +import { EMPTY, of, throwError } from 'rxjs'; import { catchError, onErrorResumeNext } from 'rxjs/operators'; import { IMock, It, Mock, Times } from 'typemoq'; import { createContributors } from './../services/contributors.service.spec'; @@ -164,7 +164,7 @@ describe('ContributorsState', () => { catchError(err => { error = err; - return empty(); + return EMPTY; }) ).subscribe(); @@ -183,7 +183,7 @@ describe('ContributorsState', () => { catchError(err => { error = err; - return empty(); + return EMPTY; }) ).subscribe(); diff --git a/frontend/app/shared/state/rule-events.state.ts b/frontend/app/shared/state/rule-events.state.ts index d849711d2..9c714087b 100644 --- a/frontend/app/shared/state/rule-events.state.ts +++ b/frontend/app/shared/state/rule-events.state.ts @@ -7,7 +7,7 @@ import { Injectable } from '@angular/core'; import { DialogService, Pager, Router2State, shareSubscribed, State } from '@app/framework'; -import { empty, Observable } from 'rxjs'; +import { EMPTY, Observable } from 'rxjs'; import { finalize, tap } from 'rxjs/operators'; import { RuleEventDto, RulesService } from './../services/rules.service'; import { AppsState } from './apps.state'; @@ -122,7 +122,7 @@ export class RuleEventsState extends State { public filterByRule(ruleId?: string) { if (ruleId === this.snapshot.ruleId) { - return empty(); + return EMPTY; } this.next(s => ({ ...s, ruleEventsPager: s.ruleEventsPager.reset(), ruleId })); diff --git a/frontend/app/shared/state/schemas.state.ts b/frontend/app/shared/state/schemas.state.ts index 4ba4a8327..a5da0ba4b 100644 --- a/frontend/app/shared/state/schemas.state.ts +++ b/frontend/app/shared/state/schemas.state.ts @@ -7,7 +7,7 @@ import { Injectable } from '@angular/core'; import { compareStrings, defined, DialogService, shareMapSubscribed, shareSubscribed, State, Types, Version } from '@app/framework'; -import { empty, Observable, of } from 'rxjs'; +import { EMPTY, Observable, of } from 'rxjs'; import { catchError, finalize, tap } from 'rxjs/operators'; import { AddFieldDto, CreateSchemaDto, FieldDto, FieldRule, NestedFieldDto, RootFieldDto, SchemaDetailsDto, SchemaDto, SchemasService, UpdateFieldDto, UpdateSchemaDto, UpdateUIFields } from './../services/schemas.service'; import { AppsState } from './apps.state'; @@ -125,7 +125,7 @@ export class SchemasState extends State { public loadIfNotLoaded(): Observable { if (this.snapshot.isLoaded) { - return empty(); + return EMPTY; } return this.loadInternal(false); diff --git a/frontend/app/shell/pages/home/home-page.component.ts b/frontend/app/shell/pages/home/home-page.component.ts index a277baac0..42da3b5f5 100644 --- a/frontend/app/shell/pages/home/home-page.component.ts +++ b/frontend/app/shell/pages/home/home-page.component.ts @@ -30,7 +30,7 @@ export class HomePageComponent { this.authService.loginPopup() .subscribe(() => { this.router.navigate(['/app']); - }, error => { + }, _ => { this.showLoginError = true; }); } From 5ee8c619ec7fb5112557d1f8f4a6002eada7987d Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 30 Sep 2020 14:37:18 +0200 Subject: [PATCH 07/11] Rule fixes. --- .../Rules/IRuleEnqueuer.cs | 2 +- .../Rules/RuleDomainObject.cs | 2 +- .../Rules/RuleEnqueuer.cs | 48 +++++++++---------- .../Squidex/Config/Orleans/OrleansServices.cs | 2 +- .../Rules/RuleDomainObjectTests.cs | 2 +- .../Rules/RuleEnqueuerTests.cs | 2 +- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs index 0a06a363e..76615f166 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleEnqueuer.cs @@ -14,6 +14,6 @@ namespace Squidex.Domain.Apps.Entities.Rules { public interface IRuleEnqueuer { - Task Enqueue(Rule rule, Guid ruleId, Envelope @event); + Task EnqueueAsync(Rule rule, Guid ruleId, Envelope @event); } } \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs index b5310a537..2be7ad683 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs @@ -100,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var @event = SimpleMapper.Map(command, new RuleManuallyTriggered { RuleId = Snapshot.Id, AppId = Snapshot.AppId }); - await ruleEnqueuer.Enqueue(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event)); + await ruleEnqueuer.EnqueueAsync(Snapshot.RuleDef, Snapshot.Id, Envelope.Create(@event)); return null; } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs index aa883e68c..fc01ca334 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs @@ -50,46 +50,46 @@ namespace Squidex.Domain.Apps.Entities.Rules this.localCache = localCache; } - public async Task Enqueue(Rule rule, Guid ruleId, Envelope @event) + public async Task EnqueueAsync(Rule rule, Guid ruleId, Envelope @event) { Guard.NotNull(rule, nameof(rule)); Guard.NotNull(@event, nameof(@event)); - using (localCache.StartContext()) - { - var jobs = await ruleService.CreateJobsAsync(rule, ruleId, @event); + var jobs = await ruleService.CreateJobsAsync(rule, ruleId, @event); - foreach (var (job, ex) in jobs) + foreach (var (job, ex) in jobs) + { + if (ex != null) { - if (ex != null) - { - await ruleEventRepository.EnqueueAsync(job, null); + await ruleEventRepository.EnqueueAsync(job, null); - await ruleEventRepository.UpdateAsync(job, new RuleJobUpdate - { - JobResult = RuleJobResult.Failed, - ExecutionResult = RuleResult.Failed, - ExecutionDump = ex.ToString(), - Finished = job.Created - }); - } - else + await ruleEventRepository.UpdateAsync(job, new RuleJobUpdate { - await ruleEventRepository.EnqueueAsync(job, job.Created); - } + JobResult = RuleJobResult.Failed, + ExecutionResult = RuleResult.Failed, + ExecutionDump = ex.ToString(), + Finished = job.Created + }); + } + else + { + await ruleEventRepository.EnqueueAsync(job, job.Created); } } } public async Task On(Envelope @event) { - if (@event.Payload is AppEvent appEvent) + using (localCache.StartContext()) { - var rules = await GetRulesAsync(appEvent.AppId.Id); - - foreach (var ruleEntity in rules) + if (@event.Payload is AppEvent appEvent) { - await Enqueue(ruleEntity.RuleDef, ruleEntity.Id, @event); + var rules = await GetRulesAsync(appEvent.AppId.Id); + + foreach (var ruleEntity in rules) + { + await EnqueueAsync(ruleEntity.RuleDef, ruleEntity.Id, @event); + } } } } diff --git a/backend/src/Squidex/Config/Orleans/OrleansServices.cs b/backend/src/Squidex/Config/Orleans/OrleansServices.cs index 080301dd3..41ec6a8ce 100644 --- a/backend/src/Squidex/Config/Orleans/OrleansServices.cs +++ b/backend/src/Squidex/Config/Orleans/OrleansServices.cs @@ -59,10 +59,10 @@ namespace Squidex.Config.Orleans options.HostSelf = false; }); + builder.AddIncomingGrainCallFilter(); builder.AddIncomingGrainCallFilter(); builder.AddIncomingGrainCallFilter(); builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); builder.AddIncomingGrainCallFilter(); var orleansPortSilo = config.GetOptionalValue("orleans:siloPort", 11111); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs index dd049faae..9ebc25b89 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDomainObjectTests.cs @@ -165,7 +165,7 @@ namespace Squidex.Domain.Apps.Entities.Rules Assert.Null(result); - A.CallTo(() => ruleEnqueuer.Enqueue(sut.Snapshot.RuleDef, sut.Id, + A.CallTo(() => ruleEnqueuer.EnqueueAsync(sut.Snapshot.RuleDef, sut.Id, A>.That.Matches(x => x.Payload is RuleManuallyTriggered))) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index 72e6fef39..2b111bc95 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities.Rules A.CallTo(() => ruleService.CreateJobsAsync(rule.RuleDef, rule.Id, @event, true)) .Returns(new List<(RuleJob, Exception?)> { (job, null) }); - await sut.Enqueue(rule.RuleDef, rule.Id, @event); + await sut.EnqueueAsync(rule.RuleDef, rule.Id, @event); A.CallTo(() => ruleEventRepository.EnqueueAsync(job, now, default)) .MustHaveHappened(); From a795a717d47b32430b3f702e7458829bfa7d4ef1 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 30 Sep 2020 16:07:27 +0200 Subject: [PATCH 08/11] Don't cache grain. --- .../Diagnostics/OrleansAppsHealthCheck.cs | 11 ++-- .../Apps/Plans/UsageGate.cs | 12 +++-- .../Comments/CommentsCommandMiddleware.cs | 9 ++-- .../UsageTrackerCommandMiddleware.cs | 15 ++++-- .../Orleans/AsyncLocalTests.cs | 54 +++++++++++++++++++ 5 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs index 729ef1441..2ba4138f9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs @@ -17,20 +17,25 @@ namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics { public sealed class OrleansAppsHealthCheck : IHealthCheck { - private readonly IAppsByNameIndexGrain index; + private readonly IGrainFactory grainFactory; public OrleansAppsHealthCheck(IGrainFactory grainFactory) { Guard.NotNull(grainFactory, nameof(grainFactory)); - index = grainFactory.GetGrain(SingleGrain.Id); + this.grainFactory = grainFactory; } public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { - await index.CountAsync(); + await GetGrain().CountAsync(); return HealthCheckResult.Healthy("Orleans must establish communication."); } + + private IAppsByNameIndexGrain GetGrain() + { + return grainFactory.GetGrain(SingleGrain.Id); + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs index 25c9a56c0..a377c34e7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IAppPlansProvider appPlansProvider; private readonly IApiUsageTracker apiUsageTracker; - private readonly IUsageNotifierGrain usageLimitNotifier; + private readonly IGrainFactory grainFactory; public UsageGate(IAppPlansProvider appPlansProvider, IApiUsageTracker apiUsageTracker, IGrainFactory grainFactory) { @@ -34,8 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans this.appPlansProvider = appPlansProvider; this.apiUsageTracker = apiUsageTracker; - - usageLimitNotifier = grainFactory.GetGrain(SingleGrain.Id); + this.grainFactory = grainFactory; } public virtual async Task IsBlockedAsync(IAppEntity app, string? clientId, DateTime today) @@ -72,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans Users = users }; - usageLimitNotifier.NotifyAsync(notification).Forget(); + GetGrain().NotifyAsync(notification).Forget(); TrackNotified(appId); } @@ -83,6 +82,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans return isBlocked; } + private IUsageNotifierGrain GetGrain() + { + return grainFactory.GetGrain(SingleGrain.Id); + } + private bool HasNotifiedBefore(Guid appId) { return memoryCache.Get(appId); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs index 860159c27..b1dd45d06 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsCommandMiddleware.cs @@ -52,13 +52,16 @@ namespace Squidex.Domain.Apps.Entities.Comments private async Task ExecuteCommandAsync(CommandContext context, CommentsCommand commentsCommand) { - var grain = grainFactory.GetGrain(commentsCommand.CommentsId); - - var result = await grain.ExecuteAsync(commentsCommand.AsJ()); + var result = await GetGrain(commentsCommand).ExecuteAsync(commentsCommand.AsJ()); context.Complete(result.Value); } + private ICommentsGrain GetGrain(CommentsCommand commentsCommand) + { + return grainFactory.GetGrain(commentsCommand.CommentsId); + } + private static bool IsMention(CreateComment createComment) { return createComment.IsMention; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs index 8431cc114..24d75a853 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs @@ -17,13 +17,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware { - private readonly IUsageTrackerGrain usageTrackerGrain; + private readonly IGrainFactory grainFactory; public UsageTrackerCommandMiddleware(IGrainFactory grainFactory) { Guard.NotNull(grainFactory, nameof(grainFactory)); - usageTrackerGrain = grainFactory.GetGrain(SingleGrain.Id); + this.grainFactory = grainFactory; } public async Task HandleAsync(CommandContext context, NextDelegate next) @@ -31,13 +31,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking switch (context.Command) { case DeleteRule deleteRule: - await usageTrackerGrain.RemoveTargetAsync(deleteRule.RuleId); + await GetGrain().RemoveTargetAsync(deleteRule.RuleId); break; case CreateRule createRule: { if (createRule.Trigger is UsageTrigger usage) { - await usageTrackerGrain.AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays); + await GetGrain().AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays); } break; @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { if (ruleUpdated.Trigger is UsageTrigger usage) { - await usageTrackerGrain.UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays); + await GetGrain().UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays); } break; @@ -56,5 +56,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking await next(context); } + + private IUsageTrackerGrain GetGrain() + { + return grainFactory.GetGrain(SingleGrain.Id); + } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs new file mode 100644 index 000000000..d320d32ae --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs @@ -0,0 +1,54 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Orleans; +using Orleans.TestingHost; +using Xunit; + +namespace Squidex.Infrastructure.Orleans +{ + [Trait("Category", "Dependencies")] + public class AsyncLocalTests + { + public interface IAsyncLocalGrain : IGrainWithStringKey + { + public Task GetValueAsync(); + } + + public class AsyncLocalGrain : Grain, IAsyncLocalGrain + { + private readonly AsyncLocal temp = new AsyncLocal(); + + public Task GetValueAsync() + { + temp.Value++; + + return Task.FromResult(temp.Value); + } + } + + [Fact] + public async Task Should_use_async_local() + { + var cluster = + new TestClusterBuilder(1) + .Build(); + + await cluster.DeployAsync(); + + var grain = cluster.GrainFactory.GetGrain(SingleGrain.Id); + + var result1 = await grain.GetValueAsync(); + var result2 = await grain.GetValueAsync(); + + Assert.Equal(1, result1); + Assert.Equal(1, result2); + } + } +} From f1e16a0253e25c3e494c51efabbf245dd90a6384 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 30 Sep 2020 17:01:42 +0200 Subject: [PATCH 09/11] Tests fixed --- .../Rules/RuleEnqueuerTests.cs | 3 --- .../Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs index 2b111bc95..cbff2b81a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs @@ -90,9 +90,6 @@ namespace Squidex.Domain.Apps.Entities.Rules A.CallTo(() => ruleEventRepository.EnqueueAsync(job, now, default)) .MustHaveHappened(); - - A.CallTo(() => localCache.StartContext()) - .MustHaveHappened(); } [Fact] diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs index d320d32ae..90a632d84 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs @@ -47,8 +47,14 @@ namespace Squidex.Infrastructure.Orleans var result1 = await grain.GetValueAsync(); var result2 = await grain.GetValueAsync(); + await cluster.KillSiloAsync(cluster.Silos[0]); + await cluster.StartAdditionalSiloAsync(); + + var result3 = await grain.GetValueAsync(); + Assert.Equal(1, result1); Assert.Equal(1, result2); + Assert.Equal(1, result3); } } } From 292718333da070538a4de586b22f6b2e8525eea0 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 30 Sep 2020 17:26:44 +0200 Subject: [PATCH 10/11] Small fixes. --- .../EventSourcing/MongoEventStore.cs | 9 ++++++--- .../Squidex.Infrastructure/Orleans/IBackgroundGrain.cs | 2 -- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs index 8398c0771..9a1e0c534 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Driver; +using MongoDB.Driver.Core.Clusters; using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.EventSourcing @@ -31,7 +32,7 @@ namespace Squidex.Infrastructure.EventSourcing get { return Collection; } } - public bool IsReplicaSet { get; } + public bool IsReplicaSet { get; private set; } public MongoEventStore(IMongoDatabase database, IEventNotifier notifier) : base(database) @@ -51,9 +52,9 @@ namespace Squidex.Infrastructure.EventSourcing return new MongoCollectionSettings { WriteConcern = WriteConcern.WMajority }; } - protected override Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) + protected override async Task SetupCollectionAsync(IMongoCollection collection, CancellationToken ct = default) { - return collection.Indexes.CreateManyAsync(new[] + await collection.Indexes.CreateManyAsync(new[] { new CreateIndexModel( Index @@ -75,6 +76,8 @@ namespace Squidex.Infrastructure.EventSourcing Unique = true }) }, ct); + + IsReplicaSet = Database.Client.Cluster.Description.Type == ClusterType.ReplicaSet; } } } \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs index b23313685..e30295b8a 100644 --- a/backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs +++ b/backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs @@ -7,13 +7,11 @@ using System.Threading.Tasks; using Orleans; -using Orleans.Concurrency; namespace Squidex.Infrastructure.Orleans { public interface IBackgroundGrain : IGrainWithStringKey { - [OneWay] Task ActivateAsync(); } } From 1d93567fc666626c1744993555ef14abfdf1801f Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 30 Sep 2020 17:32:10 +0200 Subject: [PATCH 11/11] Increase feature version. --- .../Areas/Api/Controllers/News/Service/FeaturesService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs b/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs index 58e16646e..a0bbae023 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/News/Service/FeaturesService.cs @@ -16,7 +16,7 @@ namespace Squidex.Areas.Api.Controllers.News.Service { public sealed class FeaturesService { - private const int FeatureVersion = 9; + private const int FeatureVersion = 11; private readonly QueryContext flatten = QueryContext.Default.Flatten(); private readonly IContentsClient client;