From 64607ddbaa3d5f07cec26d484625f45f7c406095 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 19 Feb 2025 12:17:04 +0100 Subject: [PATCH 01/58] Disable webhook firing if disable in configuration. (#18383) --- .../BackgroundJobs/Jobs/WebhookFiring.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs index e6d25aff0e..07d8a09d9a 100644 --- a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/WebhookFiring.cs @@ -51,6 +51,12 @@ public class WebhookFiring : IRecurringBackgroundJob public async Task RunJobAsync() { + if (_webhookSettings.Enabled is false) + { + _logger.LogInformation("WebhookFiring task will not run as it has been globally disabled via configuration"); + return; + } + IEnumerable requests; using (ICoreScope scope = _coreScopeProvider.CreateCoreScope()) { From c5c64f2b54bfeb3f49d07fbca827b4719db6f95c Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 21 Feb 2025 09:48:34 +0100 Subject: [PATCH 02/58] Ported changes from Umbraco 13 around not throwing exceptions with identity IDs that can't be parsed to an integer or GUID (#18389) * Ported changes from Umbraco 13 around not throwing exceptions with identity IDs that can't be parsed to an integer or GUID. * Revert async changes (better done systemically in a separate update). --- .../Security/BackOfficeUserStore.cs | 21 +++++++----- .../Security/MemberUserStore.cs | 32 ++++++++++++++----- .../Security/UmbracoUserStore.cs | 17 ++-------- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index 03d1c35c56..588aed7f63 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -1,5 +1,6 @@ using System.Data; using System.Data.Common; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; @@ -436,8 +437,7 @@ public class BackOfficeUserStore : throw new ArgumentNullException(nameof(user)); } - IUser? found = FindUserFromString(user.Id); - if (found is not null) + if (TryFindUserFromString(user.Id, out IUser? found)) { DisableAsync(found).GetAwaiter().GetResult(); } @@ -469,8 +469,7 @@ public class BackOfficeUserStore : cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); - IUser? user = FindUserFromString(userId); - if (user == null) + if (TryFindUserFromString(userId, out IUser? user) is false) { return Task.FromResult((BackOfficeIdentityUser?)null)!; } @@ -478,22 +477,28 @@ public class BackOfficeUserStore : return Task.FromResult(AssignLoginsCallback(_mapper.Map(user)))!; } - private IUser? FindUserFromString(string userId) + private bool TryFindUserFromString(string userId, [NotNullWhen(true)] out IUser? user) { // We could use ResolveEntityIdFromIdentityId here, but that would require multiple DB calls, so let's not. if (TryConvertIdentityIdToInt(userId, out var id)) { - return GetAsync(id).GetAwaiter().GetResult(); + user = GetAsync(id).GetAwaiter().GetResult(); + return user is not null; } // We couldn't directly convert the ID to an int, this is because the user logged in with external login. // So we need to look up the user by key. if (Guid.TryParse(userId, out Guid key)) { - return GetAsync(key).GetAwaiter().GetResult(); + user = GetAsync(key).GetAwaiter().GetResult(); + return user is not null; } - throw new InvalidOperationException($"Unable to resolve user with ID {userId}"); + // Maybe we have some other format of user Id from an external login flow, so don't throw but return null. + // We won't be able to find the user via this ID in a local database lookup so we'll handle the same as if they don't exist. + + user = null; + return false; } protected override async Task ResolveEntityIdFromIdentityId(string? identityId) diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index 929ab55a3d..21c4207224 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -2,7 +2,6 @@ using System.Data; using System.Globalization; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; @@ -322,9 +321,15 @@ public class MemberUserStore : UmbracoUserStore(user)))!; } - protected override Task ResolveEntityIdFromIdentityId(string? identityId) + private bool TryResolveEntityIdFromIdentityId(string? identityId, out int entityId) { - if (TryConvertIdentityIdToInt(identityId, out var id)) + if (TryConvertIdentityIdToInt(identityId, out entityId)) { - return Task.FromResult(id); + return true; } if (Guid.TryParse(identityId, out Guid key)) @@ -346,10 +351,21 @@ public class MemberUserStore : UmbracoUserStore ResolveEntityIdFromIdentityId(string? identityId) + { + if (TryResolveEntityIdFromIdentityId(identityId, out var entityId)) + { + return Task.FromResult(entityId); + } + throw new InvalidOperationException($"Unable to resolve user with ID {identityId}"); } diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs index 51bda8a863..6b015bda58 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserStore.cs @@ -35,29 +35,18 @@ public abstract class UmbracoUserStore [Obsolete("Use TryConvertIdentityIdToInt instead. Scheduled for removal in V15.")] protected static int UserIdToInt(string? userId) { - if (TryUserIdToInt(userId, out int result)) + if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return result; } - throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture"); - } - - protected static bool TryUserIdToInt(string? userId, out int result) - { - if (int.TryParse(userId, NumberStyles.Integer, CultureInfo.InvariantCulture, out result)) - { - return true; - } - if (Guid.TryParse(userId, out Guid key)) { // Reverse the IntExtensions.ToGuid - result = BitConverter.ToInt32(key.ToByteArray(), 0); - return true; + return BitConverter.ToInt32(key.ToByteArray(), 0); } - return false; + throw new InvalidOperationException($"Unable to convert user ID ({userId})to int using InvariantCulture"); } protected abstract Task ResolveEntityIdFromIdentityId(string? identityId); From a0ddaefaa18a9ffe22a99d271ecdfc130de9b216 Mon Sep 17 00:00:00 2001 From: DitteKKoustrup Date: Fri, 21 Feb 2025 11:50:08 +0100 Subject: [PATCH 03/58] Add localization for Approved color/Color Picker Data type (#18411) * Add localization for Approved color/Color Picker Data type * Apply suggestions from code review Co-authored-by: Andy Butland --------- Co-authored-by: Andy Butland --- src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts | 10 ++++++++-- src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts | 6 ++++++ src/Umbraco.Web.UI.Client/src/assets/lang/en.ts | 6 ++++++ .../color-picker/Umbraco.ColorPicker.ts | 8 ++++---- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index d15a8a3d40..c17a8bde3d 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -1217,6 +1217,12 @@ export default { colorpicker: { noColors: 'Du har ikke konfigureret nogen godkendte farver', }, + colorPickerConfigurations: { + colorsTitle: 'Farver', + colorsDescription: 'Tilføj, fjern eller sorter farver', + showLabelTitle: 'Inkluder label?', + showLabelDescription: 'Gemmer farver som et Json-objekt, der både indeholder farvens hex streng og label, i stedet for kun at gemme hex strengen.', + }, contentPicker: { allowedItemTypes: 'Du kan kun vælge følgende type(r) dokumenter: %0%', defineDynamicRoot: 'Definer Dynamisk Udgangspunkt', @@ -2444,8 +2450,8 @@ export default { searchResults: 'resultater', }, propertyEditorPicker: { - title: 'Select Property Editor', - openPropertyEditorPicker: 'Select Property Editor', + title: 'Vælg Property Editor', + openPropertyEditorPicker: 'Vælg Property Editor', }, healthcheck: { checkSuccessMessage: "Value is set to the recommended value: '%0%'.", diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index 3bfd73c949..b43cdd7861 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -1241,6 +1241,12 @@ export default { colorpicker: { noColors: 'You have not configured any approved colors', }, + colorPickerConfigurations: { + colorsTitle: 'Colors', + colorsDescription: 'Add, remove or sort colors', + showLabelTitle: 'Include labels?', + showLabelDescription: 'Stores colors as a JSON object containing both the color hex string and label, rather than just the hex string.', + }, contentPicker: { allowedItemTypes: 'You can only select items of type(s): %0%', defineDynamicRoot: 'Specify root node', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 6759094593..320236b6d4 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -1257,6 +1257,12 @@ export default { colorpicker: { noColors: 'You have not configured any approved colours', }, + colorPickerConfigurations: { + colorsTitle: 'Colours', + colorsDescription: 'Add, remove or sort colours', + showLabelTitle: 'Include labels?', + showLabelDescription: 'Stores colours as a JSON object containing both the colour hex string and label, rather than just the hex string.', + }, contentPicker: { allowedItemTypes: 'You can only select items of type(s): %0%', pickedTrashedItem: 'You have picked a content item currently deleted or in the recycle bin', diff --git a/src/Umbraco.Web.UI.Client/src/packages/property-editors/color-picker/Umbraco.ColorPicker.ts b/src/Umbraco.Web.UI.Client/src/packages/property-editors/color-picker/Umbraco.ColorPicker.ts index fc1be64c3e..f4d632bbe1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/property-editors/color-picker/Umbraco.ColorPicker.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/property-editors/color-picker/Umbraco.ColorPicker.ts @@ -10,15 +10,15 @@ export const manifest: ManifestPropertyEditorSchema = { properties: [ { alias: 'useLabel', - label: 'Include labels?', + label: '#colorPickerConfigurations_showLabelTitle', description: - 'Stores colors as a Json object containing both the color hex string and label, rather than just the hex string.', + '{umbLocalize: colorPickerConfigurations_showLabelDescription}', propertyEditorUiAlias: 'Umb.PropertyEditorUi.Toggle', }, { alias: 'items', - label: 'Colors', - description: 'Add, remove or sort colors', + label: '#colorPickerConfigurations_colorsTitle', + description: '{umbLocalize: colorPickerConfigurations_colorsDescription}', propertyEditorUiAlias: 'Umb.PropertyEditorUi.ColorSwatchesEditor', }, ], From d5e2efbbaa53225e9bb041d93d72f52a2b095180 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 21 Feb 2025 16:58:23 +0100 Subject: [PATCH 04/58] V15: A user cannot switch back to the default language (#18414) * chore: prettier * fix: loads all extensions everytime but register only new localizations this ensures that the browser is updated even if you switch back to a previously loaded language * test: adds a test to check if we can switch between already loaded languages --- .../src/assets/lang/en.ts | 3 +- .../registry/localization.registry.test.ts | 46 ++++++++++++++++++- .../registry/localization.registry.ts | 44 ++++++++++-------- 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 320236b6d4..eba1078fc0 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -1261,7 +1261,8 @@ export default { colorsTitle: 'Colours', colorsDescription: 'Add, remove or sort colours', showLabelTitle: 'Include labels?', - showLabelDescription: 'Stores colours as a JSON object containing both the colour hex string and label, rather than just the hex string.', + showLabelDescription: + 'Stores colours as a JSON object containing both the colour hex string and label, rather than just the hex string.', }, contentPicker: { allowedItemTypes: 'You can only select items of type(s): %0%', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.test.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.test.ts index 03b59277d5..62e7959a9a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.test.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.test.ts @@ -4,15 +4,30 @@ import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registr import type { ManifestLocalization } from '../extensions/localization.extension.js'; //#region Localizations -const english: ManifestLocalization = { +const englishUk: ManifestLocalization = { type: 'localization', alias: 'test.en', - name: 'Test English', + name: 'Test English (UK)', + meta: { + culture: 'en', + localizations: { + general: { + color: 'Colour', + }, + }, + }, +}; + +const english: ManifestLocalization = { + type: 'localization', + alias: 'test.en-us', + name: 'Test English (US)', meta: { culture: 'en-us', direction: 'ltr', localizations: { general: { + color: 'Color', close: 'Close', logout: 'Log out', withInlineToken: '{0} {1}', @@ -72,6 +87,7 @@ const danishRegional: ManifestLocalization = { //#endregion describe('UmbLocalizeController', () => { + umbExtensionsRegistry.register(englishUk); umbExtensionsRegistry.register(english); umbExtensionsRegistry.register(danish); umbExtensionsRegistry.register(danishRegional); @@ -111,6 +127,32 @@ describe('UmbLocalizeController', () => { expect(current).to.have.property('general_logout', 'Log out'); }); + it('should be able to switch to the fallback language', async () => { + // Verify that the document language and direction is set correctly to the default language + expect(document.documentElement.lang).to.equal(english.meta.culture); + expect(document.documentElement.dir).to.equal(english.meta.direction); + + // Switch to the fallback language, which is the UK version of English + registry.loadLanguage('en'); + await aTimeout(0); + + expect(document.documentElement.lang).to.equal('en'); + expect(document.documentElement.dir).to.equal('ltr'); + + const current = registry.localizations.get(englishUk.meta.culture); + expect(current).to.have.property('general_color', 'Colour'); + + // And switch back again + registry.loadLanguage('en-us'); + await aTimeout(0); + + expect(document.documentElement.lang).to.equal('en-us'); + expect(document.documentElement.dir).to.equal('ltr'); + + const newCurrent = registry.localizations.get(english.meta.culture); + expect(newCurrent).to.have.property('general_color', 'Color'); + }); + it('should load a new language', async () => { registry.loadLanguage(danish.meta.culture); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.ts index 24ebb1ad55..5fd55b451c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.ts @@ -48,33 +48,39 @@ export class UmbLocalizationRegistry { combineLatest([this.currentLanguage, extensionRegistry.byType('localization')]).subscribe( async ([currentLanguage, extensions]) => { const locale = new Intl.Locale(currentLanguage); - const filteredExt = extensions.filter( + const currentLanguageExtensions = extensions.filter( (ext) => ext.meta.culture.toLowerCase() === locale.baseName.toLowerCase() || ext.meta.culture.toLowerCase() === locale.language.toLowerCase(), ); - // Only get the extensions that are not already loading/loaded: - const diff = filteredExt.filter((ext) => !this.#loadedExtAliases.includes(ext.alias)); - if (diff.length !== 0) { - // got new localizations to load: - const translations = await Promise.all(diff.map(this.#loadExtension)); + // If there are no extensions for the current language, return early + if (!currentLanguageExtensions.length) return; - if (translations.length) { - umbLocalizationManager.registerManyLocalizations(translations); + // Register the new translations only if they have not been registered before + const diff = currentLanguageExtensions.filter((ext) => !this.#loadedExtAliases.includes(ext.alias)); - // Set the document language - const newLang = locale.baseName.toLowerCase(); - if (document.documentElement.lang.toLowerCase() !== newLang) { - document.documentElement.lang = newLang; - } + // Load all localizations + const translations = await Promise.all(currentLanguageExtensions.map(this.#loadExtension)); - // Set the document direction to the direction of the primary language - const newDir = translations[0].$dir ?? 'ltr'; - if (document.documentElement.dir !== newDir) { - document.documentElement.dir = newDir; - } - } + // If there are no translations, return early + if (!translations.length) return; + + if (diff.length) { + const filteredTranslations = translations.filter((t) => diff.some((ext) => ext.meta.culture === t.$code)); + umbLocalizationManager.registerManyLocalizations(filteredTranslations); + } + + // Set the document language + const newLang = locale.baseName.toLowerCase(); + if (document.documentElement.lang.toLowerCase() !== newLang) { + document.documentElement.lang = newLang; + } + + // Set the document direction to the direction of the primary language + const newDir = translations[0].$dir ?? 'ltr'; + if (document.documentElement.dir !== newDir) { + document.documentElement.dir = newDir; } }, ); From c3b51301ccd9c02fa518eb94b2989cec955405cd Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 21 Feb 2025 23:17:23 +0100 Subject: [PATCH 05/58] Adds warning to publish descendants dialog when force re-publish is selected (15) (#18410) --- .../src/assets/lang/da-dk.ts | 3 +++ .../src/assets/lang/en-us.ts | 2 ++ .../src/assets/lang/en.ts | 2 ++ ...-publish-with-descendants-modal.element.ts | 26 ++++++++++++++++--- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index c17a8bde3d..baf690ad33 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -298,6 +298,9 @@ export default { removeTextBox: 'Fjern denne tekstboks', contentRoot: 'Indholdsrod', includeUnpublished: 'Inkluder ikke-udgivet indhold.', + forceRepublish: 'Udgiv uændrede elementer.', + forceRepublishWarning: 'ADVARSEL: Udgivelse af alle sider under denne i indholdstræet, uanset om de er ændret eller ej, kan være en ressourcekrævende og langvarig proces.', + forceRepublishAdvisory: 'Dette bør ikke være nødvendigt under normale omstændigheder, så fortsæt kun med denne handling, hvis du er sikker på, at det er nødvendigt.', isSensitiveValue: 'Denne værdi er skjult.Hvis du har brug for adgang til at se denne værdi, bedes du\n kontakte din web-administrator.\n ', isSensitiveValue_short: 'Denne værdi er skjult.', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index b43cdd7861..b1bedf57ad 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -320,6 +320,8 @@ export default { contentRoot: 'Content root', includeUnpublished: 'Include unpublished content items.', forceRepublish: 'Publish unchanged items.', + forceRepublishWarning: 'WARNING: Publishing all pages below this one in the content tree, whether or not they have changed, can be an expensive and long-running operation.', + forceRepublishAdvisory: 'This should not be necessary in normal circumstances so please only proceed with this option selected if you are certain it is required.', isSensitiveValue: 'This value is hidden. If you need access to view this value please contact your\n website administrator.\n ', isSensitiveValue_short: 'This value is hidden.', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index eba1078fc0..9add9072f8 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -317,6 +317,8 @@ export default { contentRoot: 'Content root', includeUnpublished: 'Include unpublished content items.', forceRepublish: 'Publish unchanged items.', + forceRepublishWarning: 'WARNING: Publishing all pages below this one in the content tree, whether or not they have changed, can be an expensive and long-running operation.', + forceRepublishAdvisory: 'This should not be necessary in normal circumstances so please only proceed with this option selected if you are certain it is required.', isSensitiveValue: 'This value is hidden. If you need access to view this value please contact your\n website administrator.\n ', isSensitiveValue_short: 'This value is hidden.', diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts index 8f2a0315c1..0705b49b74 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/publish-with-descendants/modal/document-publish-with-descendants-modal.element.ts @@ -5,7 +5,7 @@ import type { UmbDocumentPublishWithDescendantsModalValue, } from './document-publish-with-descendants-modal.token.js'; import { css, customElement, html, state } from '@umbraco-cms/backoffice/external/lit'; -import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { umbConfirmModal, UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; @@ -80,7 +80,25 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE ); } - #submit() { + #onIncludeUnpublishedDescendantsChange() { + this.#includeUnpublishedDescendants = !this.#includeUnpublishedDescendants; + } + + async #onForceRepublishChange() { + this.#forceRepublish = !this.#forceRepublish; + } + + async #submit() { + + if (this.#forceRepublish) { + await umbConfirmModal(this, { + headline: this.localize.term('content_forceRepublishWarning'), + content: this.localize.term('content_forceRepublishAdvisory'), + color: 'warning', + confirmLabel: this.localize.term('actions_publish'), + }); + } + this.value = { selection: this.#selectionManager.getSelection(), includeUnpublishedDescendants: this.#includeUnpublishedDescendants, @@ -122,7 +140,7 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE id="includeUnpublishedDescendants" label=${this.localize.term('content_includeUnpublished')} ?checked=${this.value?.includeUnpublishedDescendants} - @change=${() => (this.#includeUnpublishedDescendants = !this.#includeUnpublishedDescendants)}> + @change=${this.#onIncludeUnpublishedDescendantsChange}> @@ -130,7 +148,7 @@ export class UmbDocumentPublishWithDescendantsModalElement extends UmbModalBaseE id="forceRepublish" label=${this.localize.term('content_forceRepublish')} ?checked=${this.value?.forceRepublish} - @change=${() => (this.#forceRepublish = !this.#forceRepublish)}> + @change=${this.#onForceRepublishChange}>
From de2114b8c5baecbb63294a31ba936d9ab478ba24 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sat, 22 Feb 2025 09:39:58 +0100 Subject: [PATCH 06/58] Adds warning to publish descendants dialog when force re-publish is selected. (#18409) --- src/Umbraco.Core/EmbeddedResources/Lang/da.xml | 3 +++ src/Umbraco.Core/EmbeddedResources/Lang/en.xml | 2 ++ src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml | 2 ++ .../src/views/content/overlays/publishdescendants.html | 9 ++++++++- 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index 73ef388cb1..ceaf24aea8 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -298,6 +298,9 @@ Fjern denne tekstboks Indholdsrod Inkluder ikke-udgivet indhold. + Udgiv uændrede elementer. + ADVARSEL: Udgivelse af alle sider under denne i indholdstræet, uanset om de er ændret eller ej, kan være en ressourcekrævende og langvarig proces. + Dette bør ikke være nødvendigt under normale omstændigheder, så fortsæt kun med denne handling, hvis du er sikker på, at det er nødvendigt. Denne værdi er skjult. Hvis du har brug for adgang til at se denne værdi, bedes du kontakte din administrator. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index cf79f426b7..a0494a0204 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -310,6 +310,8 @@ Content root Include unpublished content items. Publish unchanged items. + WARNING: Publishing all pages below this one in the content tree, whether or not they have changed, can be an expensive and long-running operation. + This should not be necessary in normal circumstances so please only proceed with this option selected if you are certain it is required. This value is hidden. If you need access to view this value please contact your website administrator. diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml index bd387b4a07..74d87e8979 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml @@ -309,6 +309,8 @@ Content root Include unpublished content items. Publish unchanged items. + WARNING: Publishing all pages below this one in the content tree, whether or not they have changed, can be an expensive and long-running operation. + This should not be necessary in normal circumstances so please only proceed with this option selected if you are certain it is required. This value is hidden. If you need access to view this value please contact your website administrator. diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html index a4d337ac71..106f363c71 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/publishdescendants.html @@ -28,7 +28,10 @@ show-labels="true">
- +
+

+

+
@@ -59,6 +62,10 @@ show-labels="true">
+
+

+

+
From a81cec963dc364facc54617771b91613f9411ad3 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Sun, 23 Feb 2025 19:48:28 +0100 Subject: [PATCH 07/58] Fixes count of message displayed when more than the maximum amount of blocks are added to a block list. (#18418) --- .../block-list-editor/property-editor-ui-block-list.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts index 7360ae4d45..18f0285203 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-list/property-editors/block-list-editor/property-editor-ui-block-list.element.ts @@ -272,7 +272,7 @@ export class UmbPropertyEditorUIBlockListElement this.addValidator( 'rangeOverflow', - () => this.localize.term('validation_entriesExceed', this._limitMax, this.#entriesContext.getLength()), + () => this.localize.term('validation_entriesExceed', this._limitMax, this.#entriesContext.getLength() - (this._limitMax || 0)), () => !!this._limitMax && this.#entriesContext.getLength() > this._limitMax, ); From f64eb1261d9373c638627d3aca2a7c5e3a109f71 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Feb 2025 08:31:31 +0100 Subject: [PATCH 08/58] set value so items can be removed (#18404) --- .../src/packages/core/repository/repository-items.manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts index 9404b5d54a..cb5dc78cd3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/repository-items.manager.ts @@ -213,7 +213,7 @@ export class UmbRepositoryItemsManager exte this.observe( asObservable(), (data) => { - this.#items.append(this.#sortByUniques(data)); + this.#items.setValue(this.#sortByUniques(data)); }, ObserveRepositoryAlias, ); From 6b50797693204aefca4fd0f5cf50733f870e61e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Sun, 23 Feb 2025 20:30:22 +0100 Subject: [PATCH 09/58] also correct blocks despite markup haven't been touched in this session --- .../src/packages/rte/components/rte-base.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts index 1d45e10605..9e4c70c5c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts @@ -153,7 +153,7 @@ export abstract class UmbPropertyEditorUiRteElementBase extends UmbLitElement im // If we don't have a value set from the outside or an internal value, we don't want to set the value. // This is added to prevent the block list from setting an empty value on startup. - if (!this._latestMarkup && !this._value?.markup) { + if (this._value?.markup === undefined) { return; } From 70be31b8f8a3c57c3a9556cfa93fe5d94a82b1d6 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 24 Feb 2025 13:27:04 +0100 Subject: [PATCH 10/58] Added obsoletion methods for extension methods to be removed in Umbraco 16 (#18394) * Added obsoletion methods for extension methods to be removed in Umbraco 16. * Updated obsolete messages to reference Umbraco 17 * Fix typo --------- Co-authored-by: mole --- .../Extensions/AssemblyExtensions.cs | 2 ++ .../Extensions/CollectionExtensions.cs | 2 +- .../Extensions/DataTableExtensions.cs | 3 +++ .../Extensions/DelegateExtensions.cs | 2 ++ src/Umbraco.Core/Extensions/EnumExtensions.cs | 3 ++- .../Extensions/ExpressionExtensions.cs | 25 ------------------- .../Extensions/KeyValuePairExtensions.cs | 1 + .../NameValueCollectionExtensions.cs | 3 +++ .../Extensions/ObjectExtensions.cs | 4 +++ .../Extensions/PublishedContentExtensions.cs | 2 ++ .../Extensions/ThreadExtensions.cs | 1 + .../FriendlyPublishedContentExtensions.cs | 1 + 12 files changed, 22 insertions(+), 27 deletions(-) delete mode 100644 src/Umbraco.Core/Extensions/ExpressionExtensions.cs diff --git a/src/Umbraco.Core/Extensions/AssemblyExtensions.cs b/src/Umbraco.Core/Extensions/AssemblyExtensions.cs index b7cf65c414..488dc38c3c 100644 --- a/src/Umbraco.Core/Extensions/AssemblyExtensions.cs +++ b/src/Umbraco.Core/Extensions/AssemblyExtensions.cs @@ -47,6 +47,7 @@ public static class AssemblyExtensions /// /// /// + [Obsolete("This extension method is no longer used and will be removed in Umbraco 17.")] public static FileInfo GetAssemblyFile(this Assembly assembly) { var codeBase = assembly.Location; @@ -94,6 +95,7 @@ public static class AssemblyExtensions /// /// /// + [Obsolete("This extension method is no longer used and will be removed in Umbraco 17.")] public static FileInfo? GetAssemblyFile(this AssemblyName assemblyName) { var codeBase = assemblyName.CodeBase; diff --git a/src/Umbraco.Core/Extensions/CollectionExtensions.cs b/src/Umbraco.Core/Extensions/CollectionExtensions.cs index fd2f976d50..6e8903bf0f 100644 --- a/src/Umbraco.Core/Extensions/CollectionExtensions.cs +++ b/src/Umbraco.Core/Extensions/CollectionExtensions.cs @@ -2,7 +2,7 @@ namespace Umbraco.Cms.Core.Extensions; public static class CollectionExtensions { - // Easiest way to return a collection with 1 item, probably not the most performant + [Obsolete("Please replace uses of this extension method with collection expression. This method will be removed in Umbraco 17.")] public static ICollection ToSingleItemCollection(this T item) => new T[] { item }; } diff --git a/src/Umbraco.Core/Extensions/DataTableExtensions.cs b/src/Umbraco.Core/Extensions/DataTableExtensions.cs index 10fa51deaf..557a856472 100644 --- a/src/Umbraco.Core/Extensions/DataTableExtensions.cs +++ b/src/Umbraco.Core/Extensions/DataTableExtensions.cs @@ -21,6 +21,7 @@ public static class DataTableExtensions /// This has been migrated from the Node class and uses proper locking now. It is now used by the Node class and the /// DynamicPublishedContent extensions for legacy reasons. /// + [Obsolete("This no longer has a use in Umbraco and so will be removed in Umbraco 17.")] public static DataTable GenerateDataTable( string tableAlias, Func>> getHeaders, @@ -60,6 +61,7 @@ public static class DataTableExtensions /// /// This is for legacy code, I didn't want to go creating custom classes for these /// + [Obsolete("This no longer has a use in Umbraco and so will be removed in Umbraco 17.")] public static List>, IEnumerable>>> CreateTableData() => new List>, IEnumerable>>>(); @@ -73,6 +75,7 @@ public static class DataTableExtensions /// /// This is for legacy code, I didn't want to go creating custom classes for these /// + [Obsolete("This no longer has a use in Umbraco and so will be removed in Umbraco 17.")] public static void AddRowData( List>, IEnumerable>>> rowData, IEnumerable> standardVals, diff --git a/src/Umbraco.Core/Extensions/DelegateExtensions.cs b/src/Umbraco.Core/Extensions/DelegateExtensions.cs index 621ef46438..4528d9f0b8 100644 --- a/src/Umbraco.Core/Extensions/DelegateExtensions.cs +++ b/src/Umbraco.Core/Extensions/DelegateExtensions.cs @@ -8,6 +8,7 @@ namespace Umbraco.Extensions; public static class DelegateExtensions { + [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static Attempt RetryUntilSuccessOrTimeout(this Func> task, TimeSpan timeout, TimeSpan pause) { if (pause.TotalMilliseconds < 0) @@ -31,6 +32,7 @@ public static class DelegateExtensions return Attempt.Fail(); } + [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static Attempt RetryUntilSuccessOrMaxAttempts(this Func> task, int totalAttempts, TimeSpan pause) { if (pause.TotalMilliseconds < 0) diff --git a/src/Umbraco.Core/Extensions/EnumExtensions.cs b/src/Umbraco.Core/Extensions/EnumExtensions.cs index 4d07c1d382..be31c9e76c 100644 --- a/src/Umbraco.Core/Extensions/EnumExtensions.cs +++ b/src/Umbraco.Core/Extensions/EnumExtensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. namespace Umbraco.Extensions @@ -17,6 +17,7 @@ namespace Umbraco.Extensions /// /// true if any of the flags/bits are set within the enum value; otherwise, false. /// + [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static bool HasFlagAny(this T value, T flags) where T : Enum { diff --git a/src/Umbraco.Core/Extensions/ExpressionExtensions.cs b/src/Umbraco.Core/Extensions/ExpressionExtensions.cs deleted file mode 100644 index 12476c9506..0000000000 --- a/src/Umbraco.Core/Extensions/ExpressionExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System.Linq.Expressions; - -namespace Umbraco.Cms.Core; - -internal static class ExpressionExtensions -{ - public static Expression> True() => f => true; - - public static Expression> False() => f => false; - - public static Expression> Or(this Expression> left, Expression> right) - { - InvocationExpression invokedExpr = Expression.Invoke(right, left.Parameters); - return Expression.Lambda>(Expression.OrElse(left.Body, invokedExpr), left.Parameters); - } - - public static Expression> And(this Expression> left, Expression> right) - { - InvocationExpression invokedExpr = Expression.Invoke(right, left.Parameters); - return Expression.Lambda>(Expression.AndAlso(left.Body, invokedExpr), left.Parameters); - } -} diff --git a/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs b/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs index 7189c4cc15..760700a624 100644 --- a/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs +++ b/src/Umbraco.Core/Extensions/KeyValuePairExtensions.cs @@ -12,6 +12,7 @@ public static class KeyValuePairExtensions /// Implements key/value pair deconstruction. /// /// Allows for foreach ((var k, var v) in ...). + [Obsolete("Please replace uses of this extension method with native language features. This method will be removed in Umbraco 17.")] public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) { key = kvp.Key; diff --git a/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs b/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs index f8fdcdc83f..e5e780ccbc 100644 --- a/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs +++ b/src/Umbraco.Core/Extensions/NameValueCollectionExtensions.cs @@ -8,6 +8,7 @@ namespace Umbraco.Extensions; public static class NameValueCollectionExtensions { + [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static IEnumerable> AsEnumerable(this NameValueCollection nvc) { foreach (var key in nvc.AllKeys) @@ -16,9 +17,11 @@ public static class NameValueCollectionExtensions } } + [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static bool ContainsKey(this NameValueCollection collection, string key) => collection.Keys.Cast().Any(k => (string)k == key); + [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static T? GetValue(this NameValueCollection collection, string key, T defaultIfNotFound) { if (collection.ContainsKey(key) == false) diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index 50fe788da0..c720c85ecc 100644 --- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs +++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs @@ -44,6 +44,7 @@ public static class ObjectExtensions /// Disposes the object if it implements . /// /// The object. + [Obsolete("Please replace uses of this extension method with (input as IDisposable)?.Dispose(). This extension method will be removed in Umbraco 17.")] public static void DisposeIfDisposable(this object input) { if (input is IDisposable disposable) @@ -66,6 +67,7 @@ public static class ObjectExtensions /// /// The input. /// + [Obsolete("This extension method is not longer used and will be removed in Umbraco 17.")] public static T? SafeCast(this object input) { if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) @@ -353,6 +355,7 @@ public static class ObjectExtensions /// /// /// + [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static IDictionary? ToDictionary( this T o, params Expression>[] ignoreProperties) => o?.ToDictionary(ignoreProperties @@ -532,6 +535,7 @@ public static class ObjectExtensions /// /// Properties to ignore /// + [Obsolete("Use of this can be replaced with RouteValueDictionary or HtmlHelper.AnonymousObjectToHtmlAttributes(). The method will be removed in Umbraco 17.")] public static IDictionary ToDictionary(this object o, params string[] ignoreProperties) { if (o != null) diff --git a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs index d77634f909..a2204c9fb5 100644 --- a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs @@ -3298,6 +3298,7 @@ public static class PublishedContentExtensions /// /// /// The children of the content. + [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static DataTable ChildrenAsTable( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -3640,6 +3641,7 @@ public static class PublishedContentExtensions where T : class, IPublishedContent => Children(content, variationContextAccessor, StaticServiceProvider.Instance.GetRequiredService(), culture); + [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static DataTable ChildrenAsTable( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, diff --git a/src/Umbraco.Core/Extensions/ThreadExtensions.cs b/src/Umbraco.Core/Extensions/ThreadExtensions.cs index b1e5515b88..c365038071 100644 --- a/src/Umbraco.Core/Extensions/ThreadExtensions.cs +++ b/src/Umbraco.Core/Extensions/ThreadExtensions.cs @@ -7,6 +7,7 @@ namespace Umbraco.Extensions; public static class ThreadExtensions { + [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static void SanitizeThreadCulture(this Thread thread) { // get the current culture diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs index 38ba31245b..31171cc880 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs @@ -669,6 +669,7 @@ public static class FriendlyPublishedContentExtensions /// null) /// /// The children of the content. + [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static DataTable ChildrenAsTable(this IPublishedContent content, string contentTypeAliasFilter = "", string? culture = null) => content.ChildrenAsTable( From 5011241a54153125cbe8a97f4377db8231aa1fae Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Feb 2025 13:47:49 +0100 Subject: [PATCH 11/58] remove unused href (#18413) --- .../src/packages/members/member/collection/action/manifests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts index 7d5a1ee1a8..b031c8083d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/collection/action/manifests.ts @@ -9,7 +9,6 @@ export const manifests: Array = [ weight: 200, meta: { label: '#general_create', - href: 'section/member-management/workspace/member/create/member-type-1-id', // TODO: remove hardcoded member type id }, element: () => import('./create-member-collection-action.element.js'), conditions: [ From a482ab239a82a1a10a0c61adfacc6c8629868f59 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Feb 2025 14:03:26 +0100 Subject: [PATCH 12/58] Feature: Bulk Delete/Trash referenced by (#18393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add trash confirm modal * make referenceRepo optional + adjust styling * add referenceRepository to media trash action * wip entity-item-ref extension point * clean up * add ref list element * fix styling * Update document-item-ref.element.ts * move item repo * implement for input member * enable action slot * add null check * fix sorting again * fix sorting again * use member element * add draft styling back * move item repository * implement for user input * pass readonly and standalone props * make editPath a state * Update member-item-ref.element.ts * Fix user item ref * remove open button * remove unused * remove unused * check for section permission * add null check * change to use entity-item-ref element * register media item ref * add fallback element * show 3 references * wip data mapper concept * add unique to modal route registration * add unique to modal router * remove unused id * Update member-item-ref.element.ts * append unique * compare with old value * only recreate the controller if the entity type changes * fix console warning * implement for document item ref * Added $type to ReferenceResponseModels * move logic to item data resolver * render draft as a tag * Update document-item-ref.element.ts * generate server models * add more helpers to data resolver * export resolver * add observables * use observables in document item ref * add data resolver to tree item * add observable state * use const * align models * get icon from document type object * observe name and state * update observed value when a new item is set * update method name * update method names * pass model type * pass context type * use api prop instead of context * use api prop instead of context * fix types * use addUniquePaths for modal registration * add fallback * use ref list * use reference items for media * make mapper name more generic * make default ref item always readonly * export types * temp fake variants array * add variants array to model * Update media-references-workspace-info-app.element.ts * add variants to model * hardcode fake array * register media ref item * update mock data * dot not allow conditions for data mappers * add data mapper * prefix info routes * prefix all ref routes * return undefined if there is not edit path * add types for reference data source + repository * split trash with relation into its own action * render descendants with references * fix length check * set standalone attribute * implement for media * move trash * wip delete with relation * move to element * fix name collision * require data source identifier * use management api mapper * add management api mapper * fix type errors * Update index.ts * align naming * show references when deleting a document * Update delete-with-relation-modal.element.ts * use deleteWithRelation kind for media * clean up * localize trash * fix type * Update trash-with-relation.action.ts * override confirm methods in trash and delete actions * Update index.ts * export constants * Limit referenced-by document and media endpoints to references only. * Update document-reference-table.element.ts * add methods to get bulk references for documents * wip bulk trash * add todo comment * implement path pattern for media item * clean up * more clean up * sort imports * member edit path pattern * clean up * remove unused variant id * export extension types * wip bulk trash with relation * debounce incoming events * look up items + notify * add todo * temp solution to make it non breaking * add bulk delete * better description * implement methods for are referenced * change to use bulk trash with relation * implement delete with relation kind * deprecation warnings * move files * move files * export const * use correct kind * align naming * upper case Trash * correct uui-text impl * add comment about the v2 name * fix circular depdendencies * rename const * split to module * import global components * more explicit naming --------- Co-authored-by: Sven Geusens Co-authored-by: Andy Butland Co-authored-by: Niels Lyngsø --- src/Umbraco.Web.UI.Client/package.json | 1 + .../src/assets/lang/en.ts | 4 + .../entity-action/entity-deleted.event.ts | 10 + .../src/packages/core/entity-action/index.ts | 1 + .../bulk-delete/bulk-delete.action.kind.ts | 24 ++ .../common/bulk-delete/bulk-delete.action.ts | 121 ++++++++ .../common/bulk-delete/constants.ts | 4 + .../common/bulk-delete/index.ts | 2 + .../common/bulk-delete/manifests.ts | 4 + .../common/bulk-delete/types.ts | 20 ++ .../common/trash/constants.ts | 1 + .../trash/trash-repository.interface.ts | 7 +- .../common/trash/trash.action.kind.ts | 5 +- .../common/trash/trash.action.ts | 20 +- .../common/{index.ts => types.ts} | 1 + .../packages/core/entity-bulk-action/index.ts | 10 +- .../core/entity-bulk-action/manifests.ts | 3 + .../packages/core/entity-bulk-action/types.ts | 4 + .../default-item-ref.element.ts | 0 .../entity-item-ref.element.ts | 4 +- .../entity-item-ref.extension.ts | 0 .../entity-item-ref/global-components.ts | 1 + .../entity-item-ref/index.ts | 2 - .../entity-item-ref/types.ts | 0 .../core/entity-item/global-components.ts | 1 + .../src/packages/core/entity-item/index.ts | 1 + .../src/packages/core/entity-item/types.ts | 5 + .../src/packages/core/entity/index.ts | 1 - .../src/packages/core/entity/types.ts | 4 - .../src/packages/core/entry-point.ts | 1 + .../entity-bulk-action.extension.ts | 4 +- .../packages/core/recycle-bin/constants.ts | 1 + .../bulk-trash/bulk-trash.action.kind.ts | 29 ++ .../bulk-trash/bulk-trash.action.ts | 122 ++++++++ .../bulk-trash/constants.ts | 4 + .../entity-bulk-action/bulk-trash/index.ts | 2 + .../bulk-trash/manifests.ts | 4 + .../entity-bulk-action/bulk-trash/types.ts | 20 ++ .../entity-bulk-action/constants.ts | 1 + .../recycle-bin/entity-bulk-action/index.ts | 1 + .../recycle-bin/entity-bulk-action/types.ts | 1 + .../src/packages/core/recycle-bin/index.ts | 3 +- .../packages/core/recycle-bin/manifests.ts | 2 + .../recycle-bin-repository-base.ts | 19 +- .../recycle-bin-tree-item.context.ts | 5 +- .../src/packages/core/recycle-bin/types.ts | 1 + .../repository/data-mapper/data-mapper.ts | 10 +- .../data-mapper/management-api/constants.ts | 2 +- .../management-api-data-mapper.ts | 10 +- .../mapping/data-mapping-resolver.ts | 12 +- .../mapping/data-mapping.extension.ts | 12 +- .../repository/data-mapper/mapping/types.ts | 2 +- .../detail/detail-repository-base.ts | 15 +- .../src/packages/core/vite.config.ts | 1 + .../entity-bulk-actions/constants.ts | 1 - .../entity-bulk-actions/manifests.ts | 3 +- .../entity-bulk-actions/trash/manifests.ts | 31 -- .../documents/recycle-bin/constants.ts | 1 + .../entity-action/bulk-trash}/constants.ts | 0 .../entity-action/bulk-trash}/index.ts | 0 .../entity-action/bulk-trash/manifests.ts | 36 +++ .../bulk-trash}/repository/constants.ts | 0 .../bulk-trash}/repository/index.ts | 0 .../bulk-trash}/repository/manifests.ts | 0 .../repository/trash.repository.ts | 12 +- .../recycle-bin/entity-action/constants.ts | 1 + .../recycle-bin/entity-action/manifests.ts | 2 + ...ference-response.management-api.mapping.ts | 4 +- .../document-reference.repository.ts | 5 + .../document-reference.server.data.ts | 34 ++- .../reference/repository/manifests.ts | 8 +- .../media/entity-bulk-actions/constants.ts | 1 - .../media/entity-bulk-actions/manifests.ts | 3 +- .../entity-bulk-actions/trash/manifests.ts | 25 -- .../media/media/recycle-bin/constants.ts | 1 + .../entity-action/bulk-trash}/constants.ts | 0 .../entity-action/bulk-trash}/index.ts | 0 .../entity-action/bulk-trash/manifests.ts | 30 ++ .../bulk-trash}/repository/constants.ts | 0 .../bulk-trash}/repository/index.ts | 0 .../bulk-trash}/repository/manifests.ts | 0 .../repository/trash.repository.ts | 12 +- .../recycle-bin/entity-action/constants.ts | 1 + .../recycle-bin/entity-action/manifests.ts | 2 + .../media/reference/repository/manifests.ts | 8 +- ...ference-response.management-api.mapping.ts | 4 +- .../repository/media-reference.repository.ts | 11 + .../repository/media-reference.server.data.ts | 34 ++- .../packages/relations/relations/constants.ts | 2 + .../bulk-delete-with-relation.action.kind.ts | 17 ++ .../bulk-delete-with-relation.action.ts | 22 ++ .../entity-actions/bulk-delete/constants.ts | 2 + .../entity-actions/bulk-delete/index.ts | 2 + .../entity-actions/bulk-delete/manifests.ts | 9 + ...bulk-delete-with-relation-modal.element.ts | 86 ++++++ .../bulk-delete-with-relation-modal.token.ts | 18 ++ .../bulk-delete/modal/constants.ts | 1 + .../bulk-delete/modal/manifests.ts | 8 + .../entity-actions/bulk-delete/types.ts | 18 ++ .../bulk-trash-with-relation.action.kind.ts | 17 ++ .../bulk-trash-with-relation.action.ts | 22 ++ .../entity-actions/bulk-trash/constants.ts | 2 + .../entity-actions/bulk-trash/index.ts | 2 + .../entity-actions/bulk-trash/manifests.ts | 6 + .../bulk-trash-with-relation-modal.element.ts | 86 ++++++ .../bulk-trash-with-relation-modal.token.ts | 18 ++ .../bulk-trash/modal/constants.ts | 1 + .../bulk-trash/modal/manifests.ts | 8 + .../entity-actions/bulk-trash/types.ts | 18 ++ ...m-bulk-action-entity-references.element.ts | 134 +++++++++ .../packages/relations/relations/manifests.ts | 4 + .../relations/relations/reference/types.ts | 14 + .../entity-bulk-actions/manifests.ts | 7 +- src/Umbraco.Web.UI.Client/tsconfig.json | 1 + .../utils/all-umb-consts/imports.ts | 265 +++++++++--------- 115 files changed, 1341 insertions(+), 264 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-deleted.event.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/bulk-delete.action.kind.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/bulk-delete.action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/constants.ts rename src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/{index.ts => types.ts} (73%) rename src/Umbraco.Web.UI.Client/src/packages/core/{entity => entity-item}/entity-item-ref/default-item-ref.element.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/core/{entity => entity-item}/entity-item-ref/entity-item-ref.element.ts (98%) rename src/Umbraco.Web.UI.Client/src/packages/core/{entity => entity-item}/entity-item-ref/entity-item-ref.extension.ts (100%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/global-components.ts rename src/Umbraco.Web.UI.Client/src/packages/core/{entity => entity-item}/entity-item-ref/index.ts (53%) rename src/Umbraco.Web.UI.Client/src/packages/core/{entity => entity-item}/entity-item-ref/types.ts (100%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-item/global-components.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/bulk-trash.action.kind.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/bulk-trash.action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/types.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/manifests.ts rename src/Umbraco.Web.UI.Client/src/packages/documents/documents/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/constants.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/documents/documents/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/index.ts (100%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/manifests.ts rename src/Umbraco.Web.UI.Client/src/packages/documents/documents/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/repository/constants.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/documents/documents/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/repository/index.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/documents/documents/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/repository/manifests.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/documents/documents/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/repository/trash.repository.ts (78%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/constants.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/manifests.ts rename src/Umbraco.Web.UI.Client/src/packages/media/media/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/constants.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/media/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/index.ts (100%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/manifests.ts rename src/Umbraco.Web.UI.Client/src/packages/media/media/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/repository/constants.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/media/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/repository/index.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/media/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/repository/manifests.ts (100%) rename src/Umbraco.Web.UI.Client/src/packages/media/media/{entity-bulk-actions/trash => recycle-bin/entity-action/bulk-trash}/repository/trash.repository.ts (75%) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/bulk-delete-with-relation.action.kind.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/bulk-delete-with-relation.action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/bulk-delete-with-relation-modal.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/bulk-delete-with-relation-modal.token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/bulk-trash-with-relation.action.kind.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/bulk-trash-with-relation.action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/index.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/bulk-trash-with-relation-modal.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/bulk-trash-with-relation-modal.token.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/constants.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/types.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/local-components/confirm-bulk-action-entity-references.element.ts diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index f012dd2ac1..369297a9bb 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -45,6 +45,7 @@ "./entity-bulk-action": "./dist-cms/packages/core/entity-bulk-action/index.js", "./entity-create-option-action": "./dist-cms/packages/core/entity-create-option-action/index.js", "./entity": "./dist-cms/packages/core/entity/index.js", + "./entity-item": "./dist-cms/packages/core/entity-item/index.js", "./event": "./dist-cms/packages/core/event/index.js", "./extension-registry": "./dist-cms/packages/core/extension-registry/index.js", "./health-check": "./dist-cms/packages/health-check/index.js", diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 9add9072f8..031f436480 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -513,6 +513,10 @@ export default { confirmlogout: 'Are you sure?', confirmSure: 'Are you sure?', confirmTrash: (name: string) => `Are you sure you want to move ${name} to the Recycle Bin?`, + confirmBulkTrash: (total: number) => + `Are you sure you want to move ${total} ${total === 1 ? 'item' : 'items'} to the Recycle Bin?`, + confirmBulkDelete: (total: number) => + `Are you sure you want to delete ${total} ${total === 1 ? 'item' : 'items'}?`, cut: 'Cut', editDictionary: 'Edit dictionary item', editLanguage: 'Edit language', diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-deleted.event.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-deleted.event.ts new file mode 100644 index 0000000000..9e2bfa0548 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/entity-deleted.event.ts @@ -0,0 +1,10 @@ +import type { UmbEntityActionEventArgs } from './entity-action.event.js'; +import { UmbEntityActionEvent } from './entity-action.event.js'; + +export class UmbEntityDeletedEvent extends UmbEntityActionEvent { + static readonly TYPE = 'entity-deleted'; + + constructor(args: UmbEntityActionEventArgs) { + super(UmbEntityDeletedEvent.TYPE, args); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts index 37d12fa9f0..886f167a42 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/index.ts @@ -6,6 +6,7 @@ export * from './entity-action-base.js'; export * from './entity-action-list.element.js'; export * from './entity-action.event.js'; export * from './entity-updated.event.js'; +export * from './entity-deleted.event.js'; export type * from './types.js'; export { UmbRequestReloadStructureForEntityEvent } from './request-reload-structure-for-entity.event.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/bulk-delete.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/bulk-delete.action.kind.ts new file mode 100644 index 0000000000..c0a727b3e0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/bulk-delete.action.kind.ts @@ -0,0 +1,24 @@ +import { UMB_ENTITY_BULK_ACTION_DEFAULT_KIND_MANIFEST } from '../../default/default.action.kind.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_ENTITY_BULK_ACTION_DELETE_KIND = 'delete'; + +export const UMB_ENTITY_BULK_ACTION_DELETE_KIND_MANIFEST: UmbExtensionManifestKind = { + type: 'kind', + alias: 'Umb.Kind.EntityBulkAction.Delete', + matchKind: UMB_ENTITY_BULK_ACTION_DELETE_KIND, + matchType: 'entityBulkAction', + manifest: { + ...UMB_ENTITY_BULK_ACTION_DEFAULT_KIND_MANIFEST.manifest, + type: 'entityBulkAction', + kind: UMB_ENTITY_BULK_ACTION_DELETE_KIND, + api: () => import('./bulk-delete.action.js'), + weight: 1100, + meta: { + icon: 'icon-trash', + label: '#actions_delete', + }, + }, +}; + +export const manifest = UMB_ENTITY_BULK_ACTION_DELETE_KIND_MANIFEST; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/bulk-delete.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/bulk-delete.action.ts new file mode 100644 index 0000000000..8da90bffc5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/bulk-delete.action.ts @@ -0,0 +1,121 @@ +import { UmbEntityBulkActionBase } from '../../entity-bulk-action-base.js'; +import type { MetaEntityBulkActionDeleteKind } from './types.js'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; +import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { + UmbEntityDeletedEvent, + UmbRequestReloadChildrenOfEntityEvent, + UmbRequestReloadStructureForEntityEvent, +} from '@umbraco-cms/backoffice/entity-action'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import { UMB_ENTITY_CONTEXT, type UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbDetailRepository, UmbItemRepository } from '@umbraco-cms/backoffice/repository'; + +export class UmbDeleteEntityBulkAction< + MetaKindType extends MetaEntityBulkActionDeleteKind = MetaEntityBulkActionDeleteKind, +> extends UmbEntityBulkActionBase { + #localize = new UmbLocalizationController(this); + _items: Array = []; + + override async execute() { + if (this.selection?.length === 0) { + throw new Error('No items selected.'); + } + + // TODO: Move item look up to a future bulk action context + await this.#requestItems(); + await this._confirmDelete(this._items); + await this.#requestBulkDelete(this.selection); + } + + protected async _confirmDelete(items: Array) { + const headline = '#actions_delete'; + const message = '#defaultdialogs_confirmBulkDelete'; + + // TODO: consider showing more details about the items being deleted + await umbConfirmModal(this._host, { + headline, + content: this.#localize.string(message, items.length), + color: 'danger', + confirmLabel: '#actions_delete', + }); + } + + async #requestItems() { + const itemRepository = await createExtensionApiByAlias>( + this, + this.args.meta.itemRepositoryAlias, + ); + + const { data } = await itemRepository.requestItems(this.selection); + + this._items = data ?? []; + } + + async #requestBulkDelete(uniques: Array) { + const detailRepository = await createExtensionApiByAlias>( + this, + this.args.meta.detailRepositoryAlias, + ); + + const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT); + + const succeeded: Array = []; + + for (const unique of uniques) { + const { error } = await detailRepository.delete(unique); + + if (error) { + const notification = { data: { message: error.message } }; + notificationContext?.peek('danger', notification); + } else { + succeeded.push(unique); + } + } + + if (succeeded.length > 0) { + const notification = { + data: { message: `Deleted ${succeeded.length} ${succeeded.length === 1 ? 'item' : 'items'}` }, + }; + notificationContext?.peek('positive', notification); + } + + await this.#notify(succeeded); + } + + async #notify(succeeded: Array) { + const entityContext = await this.getContext(UMB_ENTITY_CONTEXT); + if (!entityContext) throw new Error('Entity Context is not available'); + + const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + if (!eventContext) throw new Error('Event Context is not available'); + + const entityType = entityContext.getEntityType(); + const unique = entityContext.getUnique(); + + if (entityType && unique !== undefined) { + const args = { entityType, unique }; + + const reloadChildren = new UmbRequestReloadChildrenOfEntityEvent(args); + eventContext.dispatchEvent(reloadChildren); + + const reloadStructure = new UmbRequestReloadStructureForEntityEvent(args); + eventContext.dispatchEvent(reloadStructure); + } + + const succeededItems = this._items.filter((item) => succeeded.includes(item.unique)); + + succeededItems.forEach((item) => { + const deletedEvent = new UmbEntityDeletedEvent({ + unique: item.unique, + entityType: item.entityType, + }); + + eventContext.dispatchEvent(deletedEvent); + }); + } +} + +export { UmbDeleteEntityBulkAction as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/constants.ts new file mode 100644 index 0000000000..b88413e675 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/constants.ts @@ -0,0 +1,4 @@ +export { + UMB_ENTITY_BULK_ACTION_DELETE_KIND_MANIFEST, + UMB_ENTITY_BULK_ACTION_DELETE_KIND, +} from './bulk-delete.action.kind.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/index.ts new file mode 100644 index 0000000000..07e2f108e4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/index.ts @@ -0,0 +1,2 @@ +export * from './bulk-delete.action.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/manifests.ts new file mode 100644 index 0000000000..558f12b16c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/manifests.ts @@ -0,0 +1,4 @@ +import { manifest as deleteKindManifest } from './bulk-delete.action.kind.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [deleteKindManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/types.ts new file mode 100644 index 0000000000..17ff0c6f48 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/bulk-delete/types.ts @@ -0,0 +1,20 @@ +import type { + ManifestEntityBulkAction, + MetaEntityBulkActionDefaultKind, +} from '@umbraco-cms/backoffice/extension-registry'; + +export interface ManifestEntityBulkActionDeleteKind extends ManifestEntityBulkAction { + type: 'entityBulkAction'; + kind: 'delete'; +} + +export interface MetaEntityBulkActionDeleteKind extends MetaEntityBulkActionDefaultKind { + detailRepositoryAlias: string; + itemRepositoryAlias: string; +} + +declare global { + interface UmbExtensionManifestMap { + umbManifestEntityBulkActionDeleteKind: ManifestEntityBulkActionDeleteKind; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/constants.ts new file mode 100644 index 0000000000..f919f4aea6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/constants.ts @@ -0,0 +1 @@ +export const UMB_ENTITY_BULK_ACTION_TRASH_KIND = 'trash'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash-repository.interface.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash-repository.interface.ts index 195b2f7724..8570bfc8ad 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash-repository.interface.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash-repository.interface.ts @@ -1,7 +1,12 @@ import type { UmbRepositoryErrorResponse } from '../../../repository/types.js'; import type { UmbBulkTrashRequestArgs } from './types.js'; import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; - +/** + * @deprecated since 15.3.0. Will be removed in 17.0.0. Call trash method on UmbRecycleBin repositories instead. + * @exports + * @interface UmbBulkTrashRepository + * @augments UmbApi + */ export interface UmbBulkTrashRepository extends UmbApi { requestBulkTrash(args: UmbBulkTrashRequestArgs): Promise; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash.action.kind.ts index 2c36d6dc35..dd6d351192 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash.action.kind.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash.action.kind.ts @@ -1,15 +1,16 @@ import { UMB_ENTITY_BULK_ACTION_DEFAULT_KIND_MANIFEST } from '../../default/default.action.kind.js'; +import { UMB_ENTITY_BULK_ACTION_TRASH_KIND } from './constants.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifest: UmbExtensionManifestKind = { type: 'kind', alias: 'Umb.Kind.EntityBulkAction.Trash', - matchKind: 'trash', + matchKind: UMB_ENTITY_BULK_ACTION_TRASH_KIND, matchType: 'entityBulkAction', manifest: { ...UMB_ENTITY_BULK_ACTION_DEFAULT_KIND_MANIFEST.manifest, type: 'entityBulkAction', - kind: 'trash', + kind: UMB_ENTITY_BULK_ACTION_TRASH_KIND, api: () => import('./trash.action.js'), weight: 700, forEntityTypes: [], diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash.action.ts index 70cbc7ffd5..84d27501cc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/trash/trash.action.ts @@ -1,6 +1,6 @@ import type { UmbBulkTrashRepository } from './trash-repository.interface.js'; import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; -import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-action'; +import { UmbEntityBulkActionBase, type UmbEntityBulkActionArgs } from '@umbraco-cms/backoffice/entity-bulk-action'; import { UmbRequestReloadChildrenOfEntityEvent, UmbRequestReloadStructureForEntityEvent, @@ -9,8 +9,26 @@ import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; import type { MetaEntityBulkActionTrashKind } from '@umbraco-cms/backoffice/extension-registry'; +import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbDeprecation } from '@umbraco-cms/backoffice/utils'; +/** + * @deprecated since v15.3.0. Will be removed in v17.0.0. import `UmbMediaTrashEntityBulkAction` from @umbraco-cms/backoffice/recycle-bin instead. + * @exports + * @class UmbMediaTrashEntityBulkAction + * @augments {UmbEntityBulkActionBase} + */ export class UmbMediaTrashEntityBulkAction extends UmbEntityBulkActionBase { + constructor(host: UmbControllerHost, args: UmbEntityBulkActionArgs) { + super(host, args); + + new UmbDeprecation({ + removeInVersion: '17.0.0', + deprecated: 'UmbMediaTrashEntityBulkAction', + solution: 'import UmbMediaTrashEntityBulkAction from @umbraco-cms/backoffice/recycle-bin instead.', + }).warn(); + } + async execute() { if (this.selection?.length === 0) return; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/types.ts similarity index 73% rename from src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/types.ts index e3330f3081..7dd2feb9d1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/common/types.ts @@ -1,3 +1,4 @@ export type * from './duplicate-to/index.js'; export type * from './move-to/index.js'; export type * from './trash/index.js'; +export type * from './bulk-delete/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/index.ts index bcc30c388e..91126a4727 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/index.ts @@ -1,8 +1,12 @@ -export type * from './common/index.js'; export * from './entity-bulk-action-base.js'; export * from './entity-bulk-action.element.js'; -export type * from './entity-bulk-action.interface.js'; -export type * from './entity-bulk-action-element.interface.js'; +export * from './common/bulk-delete/index.js'; export type * from './types.js'; export { UMB_ENTITY_BULK_ACTION_DEFAULT_KIND_MANIFEST } from './default/default.action.kind.js'; +export { + UMB_ENTITY_BULK_ACTION_DELETE_KIND_MANIFEST, + UMB_ENTITY_BULK_ACTION_DELETE_KIND, +} from './common/bulk-delete/bulk-delete.action.kind.js'; + +export { UMB_ENTITY_BULK_ACTION_TRASH_KIND } from './common/trash/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/manifests.ts index 57fab22a3d..2d3fde2e92 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/manifests.ts @@ -2,6 +2,8 @@ import { manifests as defaultEntityBulkActionManifests } from './default/manifes import { manifests as duplicateEntityBulkActionManifests } from './common/duplicate-to/manifests.js'; import { manifests as moveToEntityBulkActionManifests } from './common/move-to/manifests.js'; import { manifests as trashEntityBulkActionManifests } from './common/trash/manifests.js'; +import { manifests as deleteEntityBulkActionManifests } from './common/bulk-delete/manifests.js'; + import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ @@ -9,4 +11,5 @@ export const manifests: Array = ...duplicateEntityBulkActionManifests, ...moveToEntityBulkActionManifests, ...trashEntityBulkActionManifests, + ...deleteEntityBulkActionManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/types.ts index 49faf0bca5..6e72415531 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-bulk-action/types.ts @@ -1,5 +1,9 @@ import type { MetaEntityBulkAction } from '../extension-registry/extensions/entity-bulk-action.extension.js'; +export type * from './common/types.js'; +export type * from './entity-bulk-action.interface.js'; +export type * from './entity-bulk-action-element.interface.js'; + export interface UmbEntityBulkActionArgs { entityType: string; meta: MetaArgsType; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/default-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/default-item-ref.element.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/default-item-ref.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/default-item-ref.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/entity-item-ref.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts similarity index 98% rename from src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/entity-item-ref.element.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts index de07c6a3d5..f239f3ebe4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/entity-item-ref.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.element.ts @@ -1,13 +1,13 @@ -import type { UmbEntityModel } from '../types.js'; import type { ManifestEntityItemRef } from './entity-item-ref.extension.js'; import { customElement, property, type PropertyValueMap, state, css, html } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbExtensionsElementInitializer } from '@umbraco-cms/backoffice/extension-api'; import { UMB_MARK_ATTRIBUTE_NAME } from '@umbraco-cms/backoffice/const'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; import './default-item-ref.element.js'; -import { UmbRoutePathAddendumContext } from '@umbraco-cms/backoffice/router'; @customElement('umb-entity-item-ref') export class UmbEntityItemRefElement extends UmbLitElement { diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/entity-item-ref.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.extension.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/entity-item-ref.extension.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/entity-item-ref.extension.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/global-components.ts new file mode 100644 index 0000000000..b04a6b66a0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/global-components.ts @@ -0,0 +1 @@ +import './entity-item-ref.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/index.ts similarity index 53% rename from src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/index.ts index 338849e069..0dfe5acf10 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/index.ts @@ -1,3 +1 @@ -import './entity-item-ref.element.js'; - export * from './entity-item-ref.element.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/types.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/core/entity/entity-item-ref/types.ts rename to src/Umbraco.Web.UI.Client/src/packages/core/entity-item/entity-item-ref/types.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/global-components.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/global-components.ts new file mode 100644 index 0000000000..a518da7233 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/global-components.ts @@ -0,0 +1 @@ +import './entity-item-ref/global-components.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts new file mode 100644 index 0000000000..5ada9eda75 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/index.ts @@ -0,0 +1 @@ +export * from './entity-item-ref/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts new file mode 100644 index 0000000000..31240f7f67 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-item/types.ts @@ -0,0 +1,5 @@ +import type { UmbNamedEntityModel } from '@umbraco-cms/backoffice/entity'; + +export interface UmbDefaultItemModel extends UmbNamedEntityModel { + icon?: string; +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts index 7fbfc9d7f6..737d7bf482 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity/index.ts @@ -1,4 +1,3 @@ export { UMB_ENTITY_CONTEXT } from './entity.context-token.js'; export { UmbEntityContext } from './entity.context.js'; -export * from './entity-item-ref/index.js'; export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts index 09ec5529e2..f2fcb7dde9 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity/types.ts @@ -8,7 +8,3 @@ export interface UmbEntityModel { export interface UmbNamedEntityModel extends UmbEntityModel { name: string; } - -export interface UmbDefaultItemModel extends UmbNamedEntityModel { - icon?: string; -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts index df77e8a18b..cf71700dce 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entry-point.ts @@ -9,6 +9,7 @@ import { UmbExtensionsApiInitializer, type UmbEntryPointOnInit } from '@umbraco- import './property-action/components/index.js'; import './menu/components/index.js'; import './extension-registry/components/index.js'; +import './entity-item/global-components.js'; export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => { new UmbExtensionsApiInitializer(host, extensionRegistry, 'globalContext', [host]); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/entity-bulk-action.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/entity-bulk-action.extension.ts index 969a1e28a6..4d5b2adcb5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/entity-bulk-action.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/extension-registry/extensions/entity-bulk-action.extension.ts @@ -17,7 +17,9 @@ export interface ManifestEntityBulkAction { +export interface ManifestEntityBulkActionDefaultKind< + MetaKindType extends MetaEntityBulkActionDefaultKind = MetaEntityBulkActionDefaultKind, +> extends ManifestEntityBulkAction { type: 'entityBulkAction'; kind: 'default'; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/constants.ts index 27f0cb6150..19138819b5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/constants.ts @@ -2,3 +2,4 @@ export * from './conditions/is-not-trashed/constants.js'; export * from './conditions/is-trashed/constants.js'; export * from './contexts/is-trashed/constants.js'; export * from './entity-action/constants.js'; +export * from './entity-bulk-action/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/bulk-trash.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/bulk-trash.action.kind.ts new file mode 100644 index 0000000000..1e659cc7e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/bulk-trash.action.kind.ts @@ -0,0 +1,29 @@ +import { UMB_ENTITY_BULK_ACTION_DEFAULT_KIND_MANIFEST } from '@umbraco-cms/backoffice/entity-bulk-action'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +/* TODO: v17: rename kind to trash + this is named v2 to avoid a name clash. The original trash kind is deprecated. +We have added a constant to try and prevent too big a breaking change when renaming. */ +export const UMB_ENTITY_BULK_ACTION_TRASH_KIND = 'trashV2'; + +export const UMB_ENTITY_BULK_ACTION_TRASH_KIND_MANIFEST: UmbExtensionManifestKind = { + type: 'kind', + alias: 'Umb.Kind.EntityBulkAction.Trash', + matchKind: UMB_ENTITY_BULK_ACTION_TRASH_KIND, + matchType: 'entityBulkAction', + manifest: { + ...UMB_ENTITY_BULK_ACTION_DEFAULT_KIND_MANIFEST.manifest, + type: 'entityBulkAction', + kind: UMB_ENTITY_BULK_ACTION_TRASH_KIND, + api: () => import('./bulk-trash.action.js'), + weight: 1150, + meta: { + icon: 'icon-trash', + label: '#actions_trash', + itemRepositoryAlias: '', + recycleBinRepositoryAlias: '', + }, + }, +}; + +export const manifest = UMB_ENTITY_BULK_ACTION_TRASH_KIND_MANIFEST; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/bulk-trash.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/bulk-trash.action.ts new file mode 100644 index 0000000000..384e111ed5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/bulk-trash.action.ts @@ -0,0 +1,122 @@ +import type { UmbRecycleBinRepository } from '../../recycle-bin-repository.interface.js'; +import { UmbEntityTrashedEvent } from '../../entity-action/trash/index.js'; +import type { MetaEntityBulkActionTrashKind } from './types.js'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; +import { umbConfirmModal } from '@umbraco-cms/backoffice/modal'; +import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; +import { + UmbRequestReloadChildrenOfEntityEvent, + UmbRequestReloadStructureForEntityEvent, +} from '@umbraco-cms/backoffice/entity-action'; +import { UmbEntityBulkActionBase } from '@umbraco-cms/backoffice/entity-bulk-action'; +import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; +import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; +import { UMB_ENTITY_CONTEXT } from '@umbraco-cms/backoffice/entity'; +import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; + +export class UmbTrashEntityBulkAction< + MetaKindType extends MetaEntityBulkActionTrashKind = MetaEntityBulkActionTrashKind, +> extends UmbEntityBulkActionBase { + #localize = new UmbLocalizationController(this); + _items: Array = []; + + override async execute() { + if (this.selection?.length === 0) { + throw new Error('No items selected.'); + } + + // TODO: Move item look up to a future bulk action context + await this.#requestItems(); + await this._confirmTrash(this._items); + await this.#requestBulkTrash(this.selection); + } + + protected async _confirmTrash(items: Array) { + const headline = '#actions_trash'; + const message = '#defaultdialogs_confirmBulkTrash'; + + // TODO: consider showing more details about the items being trashed + await umbConfirmModal(this._host, { + headline, + content: this.#localize.string(message, items.length), + color: 'danger', + confirmLabel: '#actions_trash', + }); + } + + async #requestItems() { + const itemRepository = await createExtensionApiByAlias>( + this, + this.args.meta.itemRepositoryAlias, + ); + + const { data } = await itemRepository.requestItems(this.selection); + + this._items = data ?? []; + } + + async #requestBulkTrash(uniques: Array) { + const recycleBinRepository = await createExtensionApiByAlias( + this, + this.args.meta.recycleBinRepositoryAlias, + ); + + const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT); + + const succeeded: Array = []; + + for (const unique of uniques) { + const { error } = await recycleBinRepository.requestTrash({ unique }); + + if (error) { + const notification = { data: { message: error.message } }; + notificationContext?.peek('danger', notification); + } else { + succeeded.push(unique); + } + } + + if (succeeded.length > 0) { + const notification = { + data: { message: `Trashed ${succeeded.length} ${succeeded.length === 1 ? 'item' : 'items'}` }, + }; + notificationContext?.peek('positive', notification); + } + + await this.#notify(succeeded); + } + + async #notify(succeeded: Array) { + const entityContext = await this.getContext(UMB_ENTITY_CONTEXT); + if (!entityContext) throw new Error('Entity Context is not available'); + + const eventContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT); + if (!eventContext) throw new Error('Event Context is not available'); + + const entityType = entityContext.getEntityType(); + const unique = entityContext.getUnique(); + + if (entityType && unique !== undefined) { + const args = { entityType, unique }; + + const reloadChildren = new UmbRequestReloadChildrenOfEntityEvent(args); + eventContext.dispatchEvent(reloadChildren); + + const reloadStructure = new UmbRequestReloadStructureForEntityEvent(args); + eventContext.dispatchEvent(reloadStructure); + } + + const succeededItems = this._items.filter((item) => succeeded.includes(item.unique)); + + succeededItems.forEach((item) => { + const trashedEvent = new UmbEntityTrashedEvent({ + unique: item.unique, + entityType: item.entityType, + }); + + eventContext.dispatchEvent(trashedEvent); + }); + } +} + +export { UmbTrashEntityBulkAction as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/constants.ts new file mode 100644 index 0000000000..65c101c9fb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/constants.ts @@ -0,0 +1,4 @@ +export { + UMB_ENTITY_BULK_ACTION_TRASH_KIND_MANIFEST, + UMB_ENTITY_BULK_ACTION_TRASH_KIND, +} from './bulk-trash.action.kind.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/index.ts new file mode 100644 index 0000000000..a281a02351 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/index.ts @@ -0,0 +1,2 @@ +export * from './bulk-trash.action.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/manifests.ts new file mode 100644 index 0000000000..066b290beb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/manifests.ts @@ -0,0 +1,4 @@ +import { manifest as trashKindManifest } from './bulk-trash.action.kind.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [trashKindManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/types.ts new file mode 100644 index 0000000000..8a8492c24c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/bulk-trash/types.ts @@ -0,0 +1,20 @@ +import type { + ManifestEntityBulkAction, + MetaEntityBulkActionDefaultKind, +} from '@umbraco-cms/backoffice/extension-registry'; + +export interface ManifestEntityBulkActionTrashKind extends ManifestEntityBulkAction { + type: 'entityBulkAction'; + kind: 'trash'; +} + +export interface MetaEntityBulkActionTrashKind extends MetaEntityBulkActionDefaultKind { + recycleBinRepositoryAlias: string; + itemRepositoryAlias: string; +} + +declare global { + interface UmbExtensionManifestMap { + umbManifestEntityBulkActionTrashKind: ManifestEntityBulkActionTrashKind; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/constants.ts new file mode 100644 index 0000000000..e96d7ef38d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/constants.ts @@ -0,0 +1 @@ +export * from './bulk-trash/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/index.ts new file mode 100644 index 0000000000..b88b7cd9ad --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/index.ts @@ -0,0 +1 @@ +export * from './bulk-trash/index.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/types.ts new file mode 100644 index 0000000000..83d23b4b0c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/entity-bulk-action/types.ts @@ -0,0 +1 @@ +export type * from './bulk-trash/types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/index.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/index.ts index e27c2c13ac..83cdb218b8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/index.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/index.ts @@ -1,5 +1,6 @@ -export * from './entity-action/index.js'; export * from './constants.js'; +export * from './entity-action/index.js'; +export * from './entity-bulk-action/index.js'; export type * from './types.js'; export { UmbRecycleBinRepositoryBase } from './recycle-bin-repository-base.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/manifests.ts index f5fc0ea7da..b9eabac4a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/manifests.ts @@ -2,6 +2,7 @@ import { manifests as conditionManifests } from './conditions/manifests.js'; import { manifests as emptyRecycleBinEntityActionManifests } from './entity-action/empty-recycle-bin/manifests.js'; import { manifests as restoreFromRecycleBinEntityActionManifests } from './entity-action/restore-from-recycle-bin/manifests.js'; import { manifests as trashEntityActionManifests } from './entity-action/trash/manifests.js'; +import { manifests as trashEntityBulkActionManifests } from './entity-bulk-action/bulk-trash/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; @@ -11,5 +12,6 @@ export const manifests: Array = ...emptyRecycleBinEntityActionManifests, ...restoreFromRecycleBinEntityActionManifests, ...trashEntityActionManifests, + ...trashEntityBulkActionManifests, ...treeManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/recycle-bin-repository-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/recycle-bin-repository-base.ts index b7c101717f..e246dac49d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/recycle-bin-repository-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/recycle-bin-repository-base.ts @@ -8,6 +8,7 @@ import type { UmbRecycleBinRestoreRequestArgs, UmbRecycleBinTrashRequestArgs, } from './types.js'; +import type { UmbNotificationHandler } from '@umbraco-cms/backoffice/notification'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; @@ -21,6 +22,9 @@ import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; */ export abstract class UmbRecycleBinRepositoryBase extends UmbRepositoryBase implements UmbRecycleBinRepository { #recycleBinSource: UmbRecycleBinDataSource; + #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; + #requestTrashSuccessNotification?: UmbNotificationHandler; + #requestRestoreSuccessNotification?: UmbNotificationHandler; /** * Creates an instance of UmbRecycleBinRepositoryBase. @@ -31,6 +35,10 @@ export abstract class UmbRecycleBinRepositoryBase extends UmbRepositoryBase impl constructor(host: UmbControllerHost, recycleBinSource: UmbRecycleBinDataSourceConstructor) { super(host); this.#recycleBinSource = new recycleBinSource(this); + + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (context) => { + this.#notificationContext = context; + }); } /** @@ -43,9 +51,9 @@ export abstract class UmbRecycleBinRepositoryBase extends UmbRepositoryBase impl const { error } = await this.#recycleBinSource.trash(args); if (!error) { - const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT); + this.#requestTrashSuccessNotification?.close(); const notification = { data: { message: `Trashed` } }; - notificationContext.peek('positive', notification); + this.#requestTrashSuccessNotification = this.#notificationContext?.peek('positive', notification); } return { error }; @@ -61,9 +69,9 @@ export abstract class UmbRecycleBinRepositoryBase extends UmbRepositoryBase impl const { error } = await this.#recycleBinSource.restore(args); if (!error) { - const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT); + this.#requestRestoreSuccessNotification?.close(); const notification = { data: { message: `Restored` } }; - notificationContext.peek('positive', notification); + this.#requestRestoreSuccessNotification = this.#notificationContext?.peek('positive', notification); } return { error }; @@ -78,9 +86,8 @@ export abstract class UmbRecycleBinRepositoryBase extends UmbRepositoryBase impl const { error } = await this.#recycleBinSource.empty(); if (!error) { - const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT); const notification = { data: { message: `Recycle Bin Emptied` } }; - notificationContext.peek('positive', notification); + this.#notificationContext?.peek('positive', notification); } return this.#recycleBinSource.empty(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/tree/tree-item/recycle-bin-tree-item.context.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/tree/tree-item/recycle-bin-tree-item.context.ts index 50c6a06dfb..ff1156ed0b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/tree/tree-item/recycle-bin-tree-item.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/tree/tree-item/recycle-bin-tree-item.context.ts @@ -4,6 +4,7 @@ import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import type { UmbActionEventContext } from '@umbraco-cms/backoffice/action'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbEntityTrashedEvent } from '@umbraco-cms/backoffice/recycle-bin'; +import { debounce } from '@umbraco-cms/backoffice/utils'; export class UmbRecycleBinTreeItemContext< RecycleBinTreeItemModelType extends UmbTreeItemModel, @@ -25,6 +26,8 @@ export class UmbRecycleBinTreeItemContext< }); } + #debounceLoadChildren = debounce(() => this.loadChildren(), 100); + #onEntityTrashed = (event: UmbEntityTrashedEvent) => { const entityType = event.getEntityType(); if (!entityType) throw new Error('Entity type is required'); @@ -36,7 +39,7 @@ export class UmbRecycleBinTreeItemContext< } if (supportedEntityTypes.includes(entityType)) { - this.loadChildren(); + this.#debounceLoadChildren(); } }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/types.ts index 587bcc35d1..52db4d7f18 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/recycle-bin/types.ts @@ -1,5 +1,6 @@ export type * from './conditions/types.js'; export type * from './entity-action/types.js'; +export type * from './entity-bulk-action/types.js'; export type { UmbRecycleBinDataSource } from './recycle-bin-data-source.interface.js'; export type { UmbRecycleBinRepository } from './recycle-bin-repository.interface.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/data-mapper.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/data-mapper.ts index 24de99cdd1..88f0fbd274 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/data-mapper.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/data-mapper.ts @@ -1,17 +1,17 @@ -import { UmbDataMappingResolver } from './mapping/data-mapping-resolver.js'; +import { UmbDataSourceDataMappingResolver } from './mapping/data-mapping-resolver.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -export interface UmbDataMapperMapArgs { +export interface UmbDataSourceDataMapperMapArgs { forDataModel: string; forDataSource: string; data: fromModelType; fallback?: (data: fromModelType) => Promise; } -export class UmbDataMapper extends UmbControllerBase { - #dataMappingResolver = new UmbDataMappingResolver(this); +export class UmbDataSourceDataMapper extends UmbControllerBase { + #dataMappingResolver = new UmbDataSourceDataMappingResolver(this); - async map(args: UmbDataMapperMapArgs) { + async map(args: UmbDataSourceDataMapperMapArgs) { if (!args.forDataSource) { throw new Error('data source identifier is required'); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/management-api/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/management-api/constants.ts index 25d3b5812f..52e1d472b8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/management-api/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/management-api/constants.ts @@ -1 +1 @@ -export const UMB_MANAGEMENT_API_DATA_SOURCE_IDENTIFIER = 'Umb.ManagementApi'; +export const UMB_MANAGEMENT_API_DATA_SOURCE_ALIAS = 'Umb.ManagementApi'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/management-api/management-api-data-mapper.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/management-api/management-api-data-mapper.ts index d220ee390d..2bd4c3481a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/management-api/management-api-data-mapper.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/management-api/management-api-data-mapper.ts @@ -1,19 +1,19 @@ -import { UmbDataMapper, type UmbDataMapperMapArgs } from '../data-mapper.js'; -import { UMB_MANAGEMENT_API_DATA_SOURCE_IDENTIFIER } from './constants.js'; +import { UmbDataSourceDataMapper, type UmbDataSourceDataMapperMapArgs } from '../data-mapper.js'; +import { UMB_MANAGEMENT_API_DATA_SOURCE_ALIAS } from './constants.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; export class UmbManagementApiDataMapper extends UmbControllerBase { - #dataMapper = new UmbDataMapper(this); + #dataMapper = new UmbDataSourceDataMapper(this); constructor(host: UmbControllerHost) { super(host); } - map(args: Omit) { + map(args: Omit) { return this.#dataMapper.map({ ...args, - forDataSource: UMB_MANAGEMENT_API_DATA_SOURCE_IDENTIFIER, + forDataSource: UMB_MANAGEMENT_API_DATA_SOURCE_ALIAS, }); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/data-mapping-resolver.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/data-mapping-resolver.ts index 0f7b1c46e1..c0816ee9b8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/data-mapping-resolver.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/data-mapping-resolver.ts @@ -1,12 +1,12 @@ -import type { UmbDataMapping } from './types.js'; +import type { UmbDataSourceDataMapping } from './types.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import { createExtensionApi, type ManifestBase } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -export class UmbDataMappingResolver extends UmbControllerBase { - #apiCache = new Map(); +export class UmbDataSourceDataMappingResolver extends UmbControllerBase { + #apiCache = new Map(); - async resolve(forDataSource: string, forDataModel: string): Promise { + async resolve(forDataSource: string, forDataModel: string): Promise { if (!forDataSource) { throw new Error('data source identifier is required'); } @@ -26,7 +26,7 @@ export class UmbDataMappingResolver extends UmbControllerBase { return this.#apiCache.get(manifest.alias)!; } - const dataMapping = await createExtensionApi(this, manifest); + const dataMapping = await createExtensionApi(this, manifest); if (!dataMapping) { return undefined; @@ -55,7 +55,7 @@ export class UmbDataMappingResolver extends UmbControllerBase { } #getSupportedManifests(forDataSource: string, forDataModel: string) { - const supportedManifests = umbExtensionsRegistry.getByTypeAndFilter('dataMapping', (manifest) => { + const supportedManifests = umbExtensionsRegistry.getByTypeAndFilter('dataSourceDataMapping', (manifest) => { return manifest.forDataSource === forDataSource && manifest.forDataModel === forDataModel; }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/data-mapping.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/data-mapping.extension.ts index 64e85b2722..a64ea7fd02 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/data-mapping.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/data-mapping.extension.ts @@ -1,19 +1,19 @@ -import type { UmbDataMapping } from './types.js'; +import type { UmbDataSourceDataMapping } from './types.js'; import type { ManifestApi } from '@umbraco-cms/backoffice/extension-api'; -export interface ManifestDataMapping - extends ManifestApi { - type: 'dataMapping'; +export interface ManifestDataSourceDataMapping + extends ManifestApi { + type: 'dataSourceDataMapping'; forDataSource: string; forDataModel: string; meta: MetaType; } // eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface MetaDataMapping {} +export interface MetaDataSourceDataMapping {} declare global { interface UmbExtensionManifestMap { - umbManifestDataMapping: ManifestDataMapping; + umbManifestDataSourceDataMapping: ManifestDataSourceDataMapping; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/types.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/types.ts index 4dc1a4883e..3c398c0658 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/data-mapper/mapping/types.ts @@ -1,6 +1,6 @@ import type { UmbApi } from '@umbraco-cms/backoffice/extension-api'; export type * from './data-mapping.extension.js'; -export interface UmbDataMapping extends UmbApi { +export interface UmbDataSourceDataMapping extends UmbApi { map: (data: fromModelType) => Promise; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository-base.ts index 12a77e5437..831cdab778 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/repository/detail/detail-repository-base.ts @@ -3,7 +3,7 @@ import type { UmbRepositoryResponse, UmbRepositoryResponseWithAsObservable } fro import type { UmbDetailDataSource, UmbDetailDataSourceConstructor } from './detail-data-source.interface.js'; import type { UmbDetailRepository } from './detail-repository.interface.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbNotificationContext } from '@umbraco-cms/backoffice/notification'; +import type { UmbNotificationContext, UmbNotificationHandler } from '@umbraco-cms/backoffice/notification'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; import type { UmbContextToken } from '@umbraco-cms/backoffice/context-api'; import type { UmbDetailStore } from '@umbraco-cms/backoffice/store'; @@ -22,6 +22,9 @@ export abstract class UmbDetailRepositoryBase< #detailStore?: UmbDetailStore; protected detailDataSource: UmbDetailDataSourceType; #notificationContext?: UmbNotificationContext; + #createSuccessNotification?: UmbNotificationHandler; + #updateSuccessNotification?: UmbNotificationHandler; + #deleteSuccessNotification?: UmbNotificationHandler; constructor( host: UmbControllerHost, @@ -94,10 +97,10 @@ export abstract class UmbDetailRepositoryBase< if (createdData) { this.#detailStore?.append(createdData); - + this.#createSuccessNotification?.close(); // TODO: how do we handle generic notifications? Is this the correct place to do it? const notification = { data: { message: `Created` } }; - this.#notificationContext!.peek('positive', notification); + this.#createSuccessNotification = this.#notificationContext!.peek('positive', notification); } return { data: createdData, error }; @@ -120,8 +123,9 @@ export abstract class UmbDetailRepositoryBase< this.#detailStore!.updateItem(model.unique, updatedData); // TODO: how do we handle generic notifications? Is this the correct place to do it? + this.#updateSuccessNotification?.close(); const notification = { data: { message: `Saved` } }; - this.#notificationContext!.peek('positive', notification); + this.#updateSuccessNotification = this.#notificationContext!.peek('positive', notification); } return { data: updatedData, error }; @@ -142,9 +146,10 @@ export abstract class UmbDetailRepositoryBase< if (!error) { this.#detailStore!.removeItem(unique); + this.#deleteSuccessNotification?.close(); // TODO: how do we handle generic notifications? Is this the correct place to do it? const notification = { data: { message: `Deleted` } }; - this.#notificationContext!.peek('positive', notification); + this.#deleteSuccessNotification = this.#notificationContext!.peek('positive', notification); } return { error }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts b/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts index 893def16fc..691e31b0b3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts @@ -26,6 +26,7 @@ export default defineConfig({ 'entity-bulk-action/index': './entity-bulk-action/index.ts', 'entity-create-option-action/index': './entity-create-option-action/index.ts', 'entity/index': './entity/index.ts', + 'entity-item/index': './entity-item/index.ts', 'entry-point': 'entry-point.ts', 'event/index': './event/index.ts', 'extension-registry/index': './extension-registry/index.ts', diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/constants.ts index 8d305a6458..8d2371e6f3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/constants.ts @@ -1,3 +1,2 @@ export * from './duplicate-to/constants.js'; export * from './move-to/constants.js'; -export * from './trash/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts index 65ee8658fc..b67ea4c1a0 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/manifests.ts @@ -1,5 +1,4 @@ import { manifests as duplicateToManifests } from './duplicate-to/manifests.js'; import { manifests as moveToManifests } from './move-to/manifests.js'; -import { manifests as trashManifests } from './trash/manifests.js'; -export const manifests: Array = [...duplicateToManifests, ...moveToManifests, ...trashManifests]; +export const manifests: Array = [...duplicateToManifests, ...moveToManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/manifests.ts deleted file mode 100644 index d1ff3f6058..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/manifests.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { UMB_DOCUMENT_COLLECTION_ALIAS } from '../../collection/constants.js'; -import { UMB_DOCUMENT_ENTITY_TYPE } from '../../entity.js'; -import { UMB_USER_PERMISSION_DOCUMENT_DELETE } from '../../user-permissions/constants.js'; -import { UMB_BULK_TRASH_DOCUMENT_REPOSITORY_ALIAS } from './repository/constants.js'; -import { manifests as repositoryManifests } from './repository/manifests.js'; -import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; - -export const manifests: Array = [ - { - type: 'entityBulkAction', - kind: 'trash', - alias: 'Umb.EntityBulkAction.Document.Trash', - name: 'Trash Document Entity Bulk Action', - weight: 10, - forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], - meta: { - bulkTrashRepositoryAlias: UMB_BULK_TRASH_DOCUMENT_REPOSITORY_ALIAS, - }, - conditions: [ - { - alias: UMB_COLLECTION_ALIAS_CONDITION, - match: UMB_DOCUMENT_COLLECTION_ALIAS, - }, - { - alias: 'Umb.Condition.UserPermission.Document', - allOf: [UMB_USER_PERMISSION_DOCUMENT_DELETE], - }, - ], - }, - ...repositoryManifests, -]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts index 0002782c2b..9af32d7bb7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/constants.ts @@ -1,3 +1,4 @@ +export * from './entity-action/constants.js'; export * from './repository/constants.js'; export * from './tree/constants.js'; export const UMB_DOCUMENT_RECYCLE_BIN_ROOT_ENTITY_TYPE = 'document-recycle-bin-root'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/manifests.ts new file mode 100644 index 0000000000..2ac56f94f5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/manifests.ts @@ -0,0 +1,36 @@ +import { UMB_USER_PERMISSION_DOCUMENT_DELETE } from '../../../user-permissions/constants.js'; +import { UMB_DOCUMENT_ENTITY_TYPE } from '../../../entity.js'; +import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../../../item/constants.js'; +import { UMB_DOCUMENT_RECYCLE_BIN_REPOSITORY_ALIAS } from '../../repository/constants.js'; +import { UMB_DOCUMENT_REFERENCE_REPOSITORY_ALIAS } from '../../../reference/constants.js'; +import { UMB_DOCUMENT_COLLECTION_ALIAS } from '../../../collection/constants.js'; +import { manifests as repositoryManifests } from './repository/manifests.js'; +import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; +import { UMB_ENTITY_BULK_ACTION_TRASH_WITH_RELATION_KIND } from '@umbraco-cms/backoffice/relations'; + +export const manifests: Array = [ + { + type: 'entityBulkAction', + kind: UMB_ENTITY_BULK_ACTION_TRASH_WITH_RELATION_KIND, + alias: 'Umb.EntityBulkAction.Document.Trash', + name: 'Trash Document Entity Bulk Action', + weight: 10, + forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE], + meta: { + itemRepositoryAlias: UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS, + recycleBinRepositoryAlias: UMB_DOCUMENT_RECYCLE_BIN_REPOSITORY_ALIAS, + referenceRepositoryAlias: UMB_DOCUMENT_REFERENCE_REPOSITORY_ALIAS, + }, + conditions: [ + { + alias: UMB_COLLECTION_ALIAS_CONDITION, + match: UMB_DOCUMENT_COLLECTION_ALIAS, + }, + { + alias: 'Umb.Condition.UserPermission.Document', + allOf: [UMB_USER_PERMISSION_DOCUMENT_DELETE], + }, + ], + }, + ...repositoryManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/repository/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/repository/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/repository/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/repository/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/repository/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/repository/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/repository/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/repository/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/repository/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/repository/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/repository/trash.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/repository/trash.repository.ts similarity index 78% rename from src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/repository/trash.repository.ts rename to src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/repository/trash.repository.ts index edd03d8400..43c2ed8c1d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-bulk-actions/trash/repository/trash.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/bulk-trash/repository/trash.repository.ts @@ -1,10 +1,14 @@ -import { UmbDocumentRecycleBinServerDataSource } from '../../../recycle-bin/repository/document-recycle-bin.server.data-source.js'; +import { UmbDocumentRecycleBinServerDataSource } from '../../../repository/document-recycle-bin.server.data-source.js'; import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; import type { UmbBulkTrashRepository, UmbBulkTrashRequestArgs } from '@umbraco-cms/backoffice/entity-bulk-action'; import type { UmbRepositoryErrorResponse } from '@umbraco-cms/backoffice/repository'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbDeprecation } from '@umbraco-cms/backoffice/utils'; +/** + * @deprecated since 15.3.0. Will be removed in 17.0.0. Call trash method on UmbDocumentRecycleBinRepository instead. + */ export class UmbBulkTrashDocumentRepository extends UmbRepositoryBase implements UmbBulkTrashRepository { #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; #recycleBinSource = new UmbDocumentRecycleBinServerDataSource(this); @@ -12,6 +16,12 @@ export class UmbBulkTrashDocumentRepository extends UmbRepositoryBase implements constructor(host: UmbControllerHost) { super(host); + new UmbDeprecation({ + removeInVersion: '17.0.0', + deprecated: 'UmbBulkTrashDocumentRepository', + solution: 'Call trash method on UmbDocumentRecycleBinRepository instead.', + }).warn(); + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (notificationContext) => { this.#notificationContext = notificationContext; }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/constants.ts new file mode 100644 index 0000000000..e96d7ef38d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/constants.ts @@ -0,0 +1 @@ +export * from './bulk-trash/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts index db98e6e0bb..6c4cf7b4b2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/recycle-bin/entity-action/manifests.ts @@ -7,6 +7,7 @@ import { } from '../../constants.js'; import { UMB_DOCUMENT_REFERENCE_REPOSITORY_ALIAS } from '../../reference/constants.js'; import { UMB_DOCUMENT_ITEM_REPOSITORY_ALIAS } from '../../item/constants.js'; +import { manifests as bulkTrashManifests } from './bulk-trash/manifests.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS, @@ -71,4 +72,5 @@ export const manifests: Array = [ }, ], }, + ...bulkTrashManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference-response.management-api.mapping.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference-response.management-api.mapping.ts index 97baecf652..5dc5b08754 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference-response.management-api.mapping.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference-response.management-api.mapping.ts @@ -5,11 +5,11 @@ import { type DocumentReferenceResponseModel, } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import type { UmbDataMapping } from '@umbraco-cms/backoffice/repository'; +import type { UmbDataSourceDataMapping } from '@umbraco-cms/backoffice/repository'; export class UmbDocumentReferenceResponseManagementApiDataMapping extends UmbControllerBase - implements UmbDataMapping + implements UmbDataSourceDataMapping { async map(data: DocumentReferenceResponseModel): Promise { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.repository.ts index fa3b1f6dbe..0f1ae193f2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.repository.ts @@ -16,6 +16,11 @@ export class UmbDocumentReferenceRepository extends UmbControllerBase implements return this.#referenceSource.getReferencedBy(unique, skip, take); } + async requestAreReferenced(uniques: Array, skip = 0, take = 20) { + if (!uniques || uniques.length === 0) throw new Error(`uniques is required`); + return this.#referenceSource.getAreReferenced(uniques, skip, take); + } + async requestDescendantsWithReferences(unique: string, skip = 0, take = 20) { if (!unique) throw new Error(`unique is required`); return this.#referenceSource.getReferencedDescendants(unique, skip, take); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.server.data.ts index dd19c40ddf..3b2f7d3de7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/document-reference.server.data.ts @@ -9,7 +9,7 @@ import { UmbManagementApiDataMapper } from '@umbraco-cms/backoffice/repository'; /** * @class UmbDocumentReferenceServerDataSource - * @implements {RepositoryDetailDataSource} + * @implements {UmbEntityReferenceDataSource} */ export class UmbDocumentReferenceServerDataSource extends UmbControllerBase implements UmbEntityReferenceDataSource { #dataMapper = new UmbManagementApiDataMapper(this); @@ -55,6 +55,38 @@ export class UmbDocumentReferenceServerDataSource extends UmbControllerBase impl return { data, error }; } + /** + * Checks if the items are referenced by other items + * @param {Array} uniques - The unique identifiers of the items to fetch + * @param {number} skip - The number of items to skip + * @param {number} take - The number of items to take + * @returns {Promise>>} - Items that are referenced by other items + * @memberof UmbDocumentReferenceServerDataSource + */ + async getAreReferenced( + uniques: Array, + skip: number = 0, + take: number = 20, + ): Promise>> { + const { data, error } = await tryExecuteAndNotify( + this, + DocumentService.getDocumentAreReferenced({ id: uniques, skip, take }), + ); + + if (data) { + const items: Array = data.items.map((item) => { + return { + unique: item.id, + entityType: UMB_DOCUMENT_ENTITY_TYPE, + }; + }); + + return { data: { items, total: data.total } }; + } + + return { data, error }; + } + /** * Returns any descendants of the given unique that is referenced by other items * @param {string} unique - The unique identifier of the item to fetch descendants for diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/manifests.ts index 8e2dc6740b..d7d09175b2 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/reference/repository/manifests.ts @@ -1,5 +1,5 @@ import { UMB_DOCUMENT_REFERENCE_REPOSITORY_ALIAS } from './constants.js'; -import { UMB_MANAGEMENT_API_DATA_SOURCE_IDENTIFIER } from '@umbraco-cms/backoffice/repository'; +import { UMB_MANAGEMENT_API_DATA_SOURCE_ALIAS } from '@umbraco-cms/backoffice/repository'; export const manifests: Array = [ { @@ -9,11 +9,11 @@ export const manifests: Array = [ api: () => import('./document-reference.repository.js'), }, { - type: 'dataMapping', - alias: 'Umb.DataMapping.ManagementApi.DocumentReferenceResponse', + type: 'dataSourceDataMapping', + alias: 'Umb.DataSourceDataMapping.ManagementApi.DocumentReferenceResponse', name: 'Document Reference Response Management Api Data Mapping', api: () => import('./document-reference-response.management-api.mapping.js'), - forDataSource: UMB_MANAGEMENT_API_DATA_SOURCE_IDENTIFIER, + forDataSource: UMB_MANAGEMENT_API_DATA_SOURCE_ALIAS, forDataModel: 'DocumentReferenceResponseModel', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/constants.ts index 35432ce6cd..5e9a915209 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/constants.ts @@ -1,2 +1 @@ export * from './move-to/constants.js'; -export * from './trash/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/manifests.ts index 4930ca0530..397a313678 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/manifests.ts @@ -1,4 +1,3 @@ import { manifests as moveToManifests } from './move-to/manifests.js'; -import { manifests as trashManifests } from './trash/manifests.js'; -export const manifests: Array = [...moveToManifests, ...trashManifests]; +export const manifests: Array = [...moveToManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/manifests.ts deleted file mode 100644 index 48bc80af00..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/manifests.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { UMB_MEDIA_COLLECTION_ALIAS } from '../../constants.js'; -import { UMB_MEDIA_ENTITY_TYPE } from '../../entity.js'; -import { UMB_BULK_TRASH_MEDIA_REPOSITORY_ALIAS } from './constants.js'; -import { manifests as repositoryManifests } from './repository/manifests.js'; -import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; - -const bulkTrashAction: UmbExtensionManifest = { - type: 'entityBulkAction', - kind: 'trash', - alias: 'Umb.EntityBulkAction.Media.Trash', - name: 'Trash Media Entity Bulk Action', - weight: 10, - forEntityTypes: [UMB_MEDIA_ENTITY_TYPE], - meta: { - bulkTrashRepositoryAlias: UMB_BULK_TRASH_MEDIA_REPOSITORY_ALIAS, - }, - conditions: [ - { - alias: UMB_COLLECTION_ALIAS_CONDITION, - match: UMB_MEDIA_COLLECTION_ALIAS, - }, - ], -}; - -export const manifests: Array = [bulkTrashAction, ...repositoryManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts index e05617eb5a..a77985d164 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/constants.ts @@ -1,3 +1,4 @@ export * from './tree/constants.js'; export * from './repository/constants.js'; +export * from './entity-action/constants.js'; export const UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE = 'media-recycle-bin-root'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/manifests.ts new file mode 100644 index 0000000000..f1fccda364 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/manifests.ts @@ -0,0 +1,30 @@ +import { UMB_MEDIA_ENTITY_TYPE } from '../../../entity.js'; +import { UMB_MEDIA_ITEM_REPOSITORY_ALIAS } from '../../../repository/constants.js'; +import { UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS } from '../../repository/constants.js'; +import { UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS } from '../../../reference/constants.js'; +import { UMB_MEDIA_COLLECTION_ALIAS } from '../../../collection/constants.js'; +import { manifests as repositoryManifests } from './repository/manifests.js'; +import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; +import { UMB_ENTITY_BULK_ACTION_TRASH_WITH_RELATION_KIND } from '@umbraco-cms/backoffice/relations'; + +const bulkTrashAction: UmbExtensionManifest = { + type: 'entityBulkAction', + kind: UMB_ENTITY_BULK_ACTION_TRASH_WITH_RELATION_KIND, + alias: 'Umb.EntityBulkAction.Media.Trash', + name: 'Trash Media Entity Bulk Action', + weight: 10, + forEntityTypes: [UMB_MEDIA_ENTITY_TYPE], + meta: { + itemRepositoryAlias: UMB_MEDIA_ITEM_REPOSITORY_ALIAS, + recycleBinRepositoryAlias: UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS, + referenceRepositoryAlias: UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS, + }, + conditions: [ + { + alias: UMB_COLLECTION_ALIAS_CONDITION, + match: UMB_MEDIA_COLLECTION_ALIAS, + }, + ], +}; + +export const manifests: Array = [bulkTrashAction, ...repositoryManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/repository/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/repository/constants.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/repository/constants.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/repository/constants.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/repository/index.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/repository/index.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/repository/index.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/repository/index.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/repository/manifests.ts similarity index 100% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/repository/manifests.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/repository/manifests.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/repository/trash.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/repository/trash.repository.ts similarity index 75% rename from src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/repository/trash.repository.ts rename to src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/repository/trash.repository.ts index d84cd3e956..f75ccf7989 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/entity-bulk-actions/trash/repository/trash.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/bulk-trash/repository/trash.repository.ts @@ -1,10 +1,14 @@ -import { UmbMediaRecycleBinServerDataSource } from '../../../recycle-bin/repository/media-recycle-bin.server.data-source.js'; +import { UmbMediaRecycleBinServerDataSource } from '../../../repository/media-recycle-bin.server.data-source.js'; import { UmbRepositoryBase } from '@umbraco-cms/backoffice/repository'; import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification'; import type { UmbBulkTrashRepository, UmbBulkTrashRequestArgs } from '@umbraco-cms/backoffice/entity-bulk-action'; import type { UmbRepositoryErrorResponse } from '@umbraco-cms/backoffice/repository'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; +import { UmbDeprecation } from '@umbraco-cms/backoffice/utils'; +/** + * @deprecated since 15.3.0. Will be removed in 17.0.0. Call trash method on UmbMediaRecycleBinRepository instead. + */ export class UmbBulkTrashMediaRepository extends UmbRepositoryBase implements UmbBulkTrashRepository { #notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE; #recycleBinSource = new UmbMediaRecycleBinServerDataSource(this); @@ -12,6 +16,12 @@ export class UmbBulkTrashMediaRepository extends UmbRepositoryBase implements Um constructor(host: UmbControllerHost) { super(host); + new UmbDeprecation({ + removeInVersion: '17.0.0', + deprecated: 'UmbBulkTrashDocumentRepository', + solution: 'Call trash method on UmbMediaRecycleBinRepository instead.', + }).warn(); + this.consumeContext(UMB_NOTIFICATION_CONTEXT, (notificationContext) => { this.#notificationContext = notificationContext; }); diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/constants.ts new file mode 100644 index 0000000000..e96d7ef38d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/constants.ts @@ -0,0 +1 @@ +export * from './bulk-trash/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts index cf87461c89..65b3693975 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/recycle-bin/entity-action/manifests.ts @@ -5,6 +5,7 @@ import { } from '../../constants.js'; import { UMB_MEDIA_RECYCLE_BIN_ROOT_ENTITY_TYPE, UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS } from '../constants.js'; import { UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS } from '../../reference/constants.js'; +import { manifests as bulkTrashManifests } from './bulk-trash/manifests.js'; import { UMB_ENTITY_IS_NOT_TRASHED_CONDITION_ALIAS, UMB_ENTITY_IS_TRASHED_CONDITION_ALIAS, @@ -55,4 +56,5 @@ export const manifests: Array = [ recycleBinRepositoryAlias: UMB_MEDIA_RECYCLE_BIN_REPOSITORY_ALIAS, }, }, + ...bulkTrashManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/manifests.ts index c885ccb85c..9b64e888df 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/manifests.ts @@ -1,5 +1,5 @@ import { UMB_MEDIA_REFERENCE_REPOSITORY_ALIAS } from './constants.js'; -import { UMB_MANAGEMENT_API_DATA_SOURCE_IDENTIFIER } from '@umbraco-cms/backoffice/repository'; +import { UMB_MANAGEMENT_API_DATA_SOURCE_ALIAS } from '@umbraco-cms/backoffice/repository'; export const manifests: Array = [ { @@ -9,11 +9,11 @@ export const manifests: Array = [ api: () => import('./media-reference.repository.js'), }, { - type: 'dataMapping', - alias: 'Umb.DataMapping.ManagementApi.MediaReferenceResponse', + type: 'dataSourceDataMapping', + alias: 'Umb.DataSourceDataMapping.ManagementApi.MediaReferenceResponse', name: 'Media Reference Response Management Api Data Mapping', api: () => import('./media-reference-response.management-api.mapping.js'), - forDataSource: UMB_MANAGEMENT_API_DATA_SOURCE_IDENTIFIER, + forDataSource: UMB_MANAGEMENT_API_DATA_SOURCE_ALIAS, forDataModel: 'MediaReferenceResponseModel', }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference-response.management-api.mapping.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference-response.management-api.mapping.ts index f00cf503c2..e9c29762e5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference-response.management-api.mapping.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference-response.management-api.mapping.ts @@ -2,11 +2,11 @@ import { UMB_MEDIA_ENTITY_TYPE } from '../../entity.js'; import type { UmbMediaReferenceModel } from './types.js'; import type { MediaReferenceResponseModel } from '@umbraco-cms/backoffice/external/backend-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; -import type { UmbDataMapping } from '@umbraco-cms/backoffice/repository'; +import type { UmbDataSourceDataMapping } from '@umbraco-cms/backoffice/repository'; export class UmbMediaReferenceResponseManagementApiDataMapping extends UmbControllerBase - implements UmbDataMapping + implements UmbDataSourceDataMapping { async map(data: MediaReferenceResponseModel): Promise { return { diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.repository.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.repository.ts index ab5ac83a5f..53ed8b45e1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.repository.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.repository.ts @@ -2,6 +2,8 @@ import { UmbMediaReferenceServerDataSource } from './media-reference.server.data import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; import type { UmbEntityReferenceRepository } from '@umbraco-cms/backoffice/relations'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; +import type { UmbRepositoryResponse, UmbPagedModel } from '@umbraco-cms/backoffice/repository'; export class UmbMediaReferenceRepository extends UmbControllerBase implements UmbEntityReferenceRepository { #referenceSource: UmbMediaReferenceServerDataSource; @@ -20,6 +22,15 @@ export class UmbMediaReferenceRepository extends UmbControllerBase implements Um if (!unique) throw new Error(`unique is required`); return this.#referenceSource.getReferencedDescendants(unique, skip, take); } + + async requestAreReferenced( + uniques: Array, + skip?: number, + take?: number, + ): Promise>> { + if (!uniques || uniques.length === 0) throw new Error(`uniques is required`); + return this.#referenceSource.getAreReferenced(uniques, skip, take); + } } export default UmbMediaReferenceRepository; diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.server.data.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.server.data.ts index c3ea920753..6f169f00c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.server.data.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/reference/repository/media-reference.server.data.ts @@ -9,7 +9,7 @@ import type { UmbPagedModel, UmbDataSourceResponse } from '@umbraco-cms/backoffi /** * @class UmbMediaReferenceServerDataSource - * @implements {RepositoryDetailDataSource} + * @implements {UmbEntityReferenceDataSource} */ export class UmbMediaReferenceServerDataSource extends UmbControllerBase implements UmbEntityReferenceDataSource { #dataMapper = new UmbManagementApiDataMapper(this); @@ -55,6 +55,38 @@ export class UmbMediaReferenceServerDataSource extends UmbControllerBase impleme return { data, error }; } + /** + * Checks if the items are referenced by other items + * @param {Array} uniques - The unique identifiers of the items to fetch + * @param {number} skip - The number of items to skip + * @param {number} take - The number of items to take + * @returns {Promise>>} - Items that are referenced by other items + * @memberof UmbMediaReferenceServerDataSource + */ + async getAreReferenced( + uniques: Array, + skip: number = 0, + take: number = 20, + ): Promise>> { + const { data, error } = await tryExecuteAndNotify( + this, + MediaService.getMediaAreReferenced({ id: uniques, skip, take }), + ); + + if (data) { + const items: Array = data.items.map((item) => { + return { + unique: item.id, + entityType: UMB_MEDIA_ENTITY_TYPE, + }; + }); + + return { data: { items, total: data.total } }; + } + + return { data, error }; + } + /** * Returns any descendants of the given unique that is referenced by other items * @param {string} unique - The unique identifier of the item to fetch descendants for diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/constants.ts index 1ad52da7ec..6d7855c151 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/constants.ts @@ -1,4 +1,6 @@ export { UMB_RELATION_ENTITY_TYPE } from './entity.js'; export * from './collection/constants.js'; +export * from './entity-actions/bulk-delete/constants.js'; +export * from './entity-actions/bulk-trash/constants.js'; export * from './entity-actions/delete/constants.js'; export * from './entity-actions/trash/constants.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/bulk-delete-with-relation.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/bulk-delete-with-relation.action.kind.ts new file mode 100644 index 0000000000..177f452f17 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/bulk-delete-with-relation.action.kind.ts @@ -0,0 +1,17 @@ +import { UMB_ENTITY_BULK_ACTION_DELETE_KIND_MANIFEST } from '@umbraco-cms/backoffice/entity-bulk-action'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const UMB_ENTITY_BULK_ACTION_DELETE_WITH_RELATION_KIND = 'deleteWithRelation'; + +export const manifest: UmbExtensionManifestKind = { + type: 'kind', + alias: 'Umb.Kind.EntityBulkAction.DeleteWithRelation', + matchKind: UMB_ENTITY_BULK_ACTION_DELETE_WITH_RELATION_KIND, + matchType: 'entityBulkAction', + manifest: { + ...UMB_ENTITY_BULK_ACTION_DELETE_KIND_MANIFEST.manifest, + type: 'entityBulkAction', + kind: UMB_ENTITY_BULK_ACTION_DELETE_WITH_RELATION_KIND, + api: () => import('./bulk-delete-with-relation.action.js'), + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/bulk-delete-with-relation.action.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/bulk-delete-with-relation.action.ts new file mode 100644 index 0000000000..71b26bf653 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/bulk-delete-with-relation.action.ts @@ -0,0 +1,22 @@ +import type { MetaEntityBulkActionDeleteWithRelationKind } from './types.js'; +import { UMB_BULK_DELETE_WITH_RELATION_CONFIRM_MODAL } from './modal/bulk-delete-with-relation-modal.token.js'; +import { UmbDeleteEntityBulkAction } from '@umbraco-cms/backoffice/entity-bulk-action'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; + +export class UmbBulkDeleteWithRelationEntityAction extends UmbDeleteEntityBulkAction { + override async _confirmDelete() { + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + + const modal = modalManager.open(this, UMB_BULK_DELETE_WITH_RELATION_CONFIRM_MODAL, { + data: { + uniques: this.selection, + itemRepositoryAlias: this.args.meta.itemRepositoryAlias, + referenceRepositoryAlias: this.args.meta.referenceRepositoryAlias, + }, + }); + + await modal.onSubmit(); + } +} + +export { UmbBulkDeleteWithRelationEntityAction as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/constants.ts new file mode 100644 index 0000000000..255321445a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/constants.ts @@ -0,0 +1,2 @@ +export * from './modal/constants.js'; +export { UMB_ENTITY_BULK_ACTION_DELETE_WITH_RELATION_KIND } from './bulk-delete-with-relation.action.kind.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/index.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/index.ts new file mode 100644 index 0000000000..64a7503c0f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/index.ts @@ -0,0 +1,2 @@ +export * from './bulk-delete-with-relation.action.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/manifests.ts new file mode 100644 index 0000000000..9d922686b0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/manifests.ts @@ -0,0 +1,9 @@ +import { manifest as deleteKindManifest } from './bulk-delete-with-relation.action.kind.js'; +import { manifests as modalManifests } from './modal/manifests.js'; + +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + deleteKindManifest, + ...modalManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/bulk-delete-with-relation-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/bulk-delete-with-relation-modal.element.ts new file mode 100644 index 0000000000..c7d2a22c79 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/bulk-delete-with-relation-modal.element.ts @@ -0,0 +1,86 @@ +import type { + UmbBulkDeleteWithRelationConfirmModalData, + UmbBulkDeleteWithRelationConfirmModalValue, +} from './bulk-delete-with-relation-modal.token.js'; +import { + html, + customElement, + css, + state, + type PropertyValues, + nothing, + unsafeHTML, +} from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; + +// import of local component +import '../../local-components/confirm-bulk-action-entity-references.element.js'; + +@customElement('umb-bulk-delete-with-relation-confirm-modal') +export class UmbBulkDeleteWithRelationConfirmModalElement extends UmbModalBaseElement< + UmbBulkDeleteWithRelationConfirmModalData, + UmbBulkDeleteWithRelationConfirmModalValue +> { + @state() + _referencesConfig?: any; + + protected override firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + this.#initData(); + } + + async #initData() { + if (!this.data) return; + + this._referencesConfig = { + uniques: this.data.uniques, + itemRepositoryAlias: this.data.itemRepositoryAlias, + referenceRepositoryAlias: this.data.referenceRepositoryAlias, + }; + } + + override render() { + const headline = this.localize.string('#actions_delete'); + const message = '#defaultdialogs_confirmBulkDelete'; + + return html` + +

${unsafeHTML(this.localize.string(message, this.data?.uniques.length))}

+ ${this._referencesConfig + ? html`` + : nothing} + + + + +
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + uui-dialog-layout { + max-inline-size: 60ch; + } + `, + ]; +} + +export { UmbBulkDeleteWithRelationConfirmModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-bulk-delete-with-relation-confirm-modal': UmbBulkDeleteWithRelationConfirmModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/bulk-delete-with-relation-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/bulk-delete-with-relation-modal.token.ts new file mode 100644 index 0000000000..b66ee86cb1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/bulk-delete-with-relation-modal.token.ts @@ -0,0 +1,18 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbBulkDeleteWithRelationConfirmModalData { + uniques: Array; + itemRepositoryAlias: string; + referenceRepositoryAlias: string; +} + +export type UmbBulkDeleteWithRelationConfirmModalValue = undefined; + +export const UMB_BULK_DELETE_WITH_RELATION_CONFIRM_MODAL = new UmbModalToken< + UmbBulkDeleteWithRelationConfirmModalData, + UmbBulkDeleteWithRelationConfirmModalValue +>('Umb.Modal.BulkDeleteWithRelation', { + modal: { + type: 'dialog', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/constants.ts new file mode 100644 index 0000000000..8fcf44dcda --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/constants.ts @@ -0,0 +1 @@ +export * from './bulk-delete-with-relation-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/manifests.ts new file mode 100644 index 0000000000..13a881cd09 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/modal/manifests.ts @@ -0,0 +1,8 @@ +export const manifests: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.BulkDeleteWithRelation', + name: 'Bulk Delete With Relation Modal', + element: () => import('./bulk-delete-with-relation-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/types.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/types.ts new file mode 100644 index 0000000000..b838827362 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-delete/types.ts @@ -0,0 +1,18 @@ +import type { MetaEntityBulkActionDeleteKind } from '@umbraco-cms/backoffice/entity-bulk-action'; +import type { ManifestEntityBulkAction } from '@umbraco-cms/backoffice/extension-registry'; + +export interface ManifestEntityBulkActionDeleteWithRelationKind + extends ManifestEntityBulkAction { + type: 'entityBulkAction'; + kind: 'deleteWithRelation'; +} + +export interface MetaEntityBulkActionDeleteWithRelationKind extends MetaEntityBulkActionDeleteKind { + referenceRepositoryAlias: string; +} + +declare global { + interface UmbExtensionManifestMap { + umbManifestEntityBulkActionDeleteWithRelationKind: ManifestEntityBulkActionDeleteWithRelationKind; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/bulk-trash-with-relation.action.kind.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/bulk-trash-with-relation.action.kind.ts new file mode 100644 index 0000000000..88b74d93c8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/bulk-trash-with-relation.action.kind.ts @@ -0,0 +1,17 @@ +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; +import { UMB_ENTITY_BULK_ACTION_TRASH_KIND_MANIFEST } from '@umbraco-cms/backoffice/recycle-bin'; + +export const UMB_ENTITY_BULK_ACTION_TRASH_WITH_RELATION_KIND = 'trashWithRelation'; + +export const manifest: UmbExtensionManifestKind = { + type: 'kind', + alias: 'Umb.Kind.EntityBulkAction.TrashWithRelation', + matchKind: 'trashWithRelation', + matchType: 'entityBulkAction', + manifest: { + ...UMB_ENTITY_BULK_ACTION_TRASH_KIND_MANIFEST.manifest, + type: 'entityBulkAction', + kind: 'trashWithRelation', + api: () => import('./bulk-trash-with-relation.action.js'), + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/bulk-trash-with-relation.action.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/bulk-trash-with-relation.action.ts new file mode 100644 index 0000000000..40383894fd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/bulk-trash-with-relation.action.ts @@ -0,0 +1,22 @@ +import type { MetaEntityBulkActionTrashWithRelationKind } from './types.js'; +import { UMB_BULK_TRASH_WITH_RELATION_CONFIRM_MODAL } from './modal/constants.js'; +import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; +import { UmbTrashEntityBulkAction } from '@umbraco-cms/backoffice/recycle-bin'; + +export class UmbBulkTrashWithRelationEntityAction extends UmbTrashEntityBulkAction { + override async _confirmTrash() { + const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT); + + const modal = modalManager.open(this, UMB_BULK_TRASH_WITH_RELATION_CONFIRM_MODAL, { + data: { + uniques: this.selection, + itemRepositoryAlias: this.args.meta.itemRepositoryAlias, + referenceRepositoryAlias: this.args.meta.referenceRepositoryAlias, + }, + }); + + await modal.onSubmit(); + } +} + +export { UmbBulkTrashWithRelationEntityAction as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/constants.ts new file mode 100644 index 0000000000..98307596b7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/constants.ts @@ -0,0 +1,2 @@ +export * from './modal/constants.js'; +export { UMB_ENTITY_BULK_ACTION_TRASH_WITH_RELATION_KIND } from './bulk-trash-with-relation.action.kind.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/index.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/index.ts new file mode 100644 index 0000000000..5f67a28b79 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/index.ts @@ -0,0 +1,2 @@ +export * from './bulk-trash-with-relation.action.js'; +export type * from './types.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/manifests.ts new file mode 100644 index 0000000000..d51d0e1981 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/manifests.ts @@ -0,0 +1,6 @@ +import { manifest as trashKindManifest } from './bulk-trash-with-relation.action.kind.js'; +import { manifests as modalManifests } from './modal/manifests.js'; + +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [trashKindManifest, ...modalManifests]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/bulk-trash-with-relation-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/bulk-trash-with-relation-modal.element.ts new file mode 100644 index 0000000000..8785fe42e1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/bulk-trash-with-relation-modal.element.ts @@ -0,0 +1,86 @@ +import type { + UmbBulkTrashWithRelationConfirmModalData, + UmbBulkTrashWithRelationConfirmModalValue, +} from './bulk-trash-with-relation-modal.token.js'; +import { + html, + customElement, + css, + state, + type PropertyValues, + nothing, + unsafeHTML, +} from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; +import { umbFocus } from '@umbraco-cms/backoffice/lit-element'; + +// import of local component +import '../../local-components/confirm-bulk-action-entity-references.element.js'; + +@customElement('umb-bulk-trash-with-relation-confirm-modal') +export class UmbBulkTrashWithRelationConfirmModalElement extends UmbModalBaseElement< + UmbBulkTrashWithRelationConfirmModalData, + UmbBulkTrashWithRelationConfirmModalValue +> { + @state() + _referencesConfig?: any; + + protected override firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + this.#initData(); + } + + async #initData() { + if (!this.data) return; + + this._referencesConfig = { + uniques: this.data.uniques, + itemRepositoryAlias: this.data.itemRepositoryAlias, + referenceRepositoryAlias: this.data.referenceRepositoryAlias, + }; + } + + override render() { + const headline = this.localize.string('#actions_trash'); + const message = '#defaultdialogs_confirmBulkTrash'; + + return html` + +

${unsafeHTML(this.localize.string(message, this.data?.uniques.length))}

+ ${this._referencesConfig + ? html`` + : nothing} + + + + +
+ `; + } + + static override styles = [ + UmbTextStyles, + css` + uui-dialog-layout { + max-inline-size: 60ch; + } + `, + ]; +} + +export { UmbBulkTrashWithRelationConfirmModalElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-bulk-trash-with-relation-confirm-modal': UmbBulkTrashWithRelationConfirmModalElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/bulk-trash-with-relation-modal.token.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/bulk-trash-with-relation-modal.token.ts new file mode 100644 index 0000000000..2726358d21 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/bulk-trash-with-relation-modal.token.ts @@ -0,0 +1,18 @@ +import { UmbModalToken } from '@umbraco-cms/backoffice/modal'; + +export interface UmbBulkTrashWithRelationConfirmModalData { + uniques: Array; + itemRepositoryAlias: string; + referenceRepositoryAlias: string; +} + +export type UmbBulkTrashWithRelationConfirmModalValue = undefined; + +export const UMB_BULK_TRASH_WITH_RELATION_CONFIRM_MODAL = new UmbModalToken< + UmbBulkTrashWithRelationConfirmModalData, + UmbBulkTrashWithRelationConfirmModalValue +>('Umb.Modal.BulkTrashWithRelation', { + modal: { + type: 'dialog', + }, +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/constants.ts new file mode 100644 index 0000000000..6d6d2883f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/constants.ts @@ -0,0 +1 @@ +export * from './bulk-trash-with-relation-modal.token.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/manifests.ts new file mode 100644 index 0000000000..8252cb163d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/modal/manifests.ts @@ -0,0 +1,8 @@ +export const manifests: Array = [ + { + type: 'modal', + alias: 'Umb.Modal.BulkTrashWithRelation', + name: 'Bulk Trash With Relation Modal', + element: () => import('./bulk-trash-with-relation-modal.element.js'), + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/types.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/types.ts new file mode 100644 index 0000000000..e173e1bf97 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/bulk-trash/types.ts @@ -0,0 +1,18 @@ +import type { ManifestEntityBulkAction } from '@umbraco-cms/backoffice/extension-registry'; +import type { MetaEntityBulkActionTrashKind } from '@umbraco-cms/backoffice/recycle-bin'; + +export interface ManifestEntityBulkActionTrashWithRelationKind + extends ManifestEntityBulkAction { + type: 'entityBulkAction'; + kind: 'trashWithRelation'; +} + +export interface MetaEntityBulkActionTrashWithRelationKind extends MetaEntityBulkActionTrashKind { + referenceRepositoryAlias: string; +} + +declare global { + interface UmbExtensionManifestMap { + umbManifestEntityBulkActionTrashWithRelationKind: ManifestEntityBulkActionTrashWithRelationKind; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/local-components/confirm-bulk-action-entity-references.element.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/local-components/confirm-bulk-action-entity-references.element.ts new file mode 100644 index 0000000000..4d07370a05 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/entity-actions/local-components/confirm-bulk-action-entity-references.element.ts @@ -0,0 +1,134 @@ +import type { UmbEntityReferenceRepository } from '../../reference/types.js'; +import { + html, + customElement, + css, + state, + nothing, + type PropertyValues, + property, +} from '@umbraco-cms/backoffice/external/lit'; +import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; +import type { UmbItemRepository } from '@umbraco-cms/backoffice/repository'; +import { createExtensionApiByAlias } from '@umbraco-cms/backoffice/extension-registry'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; + +@customElement('umb-confirm-bulk-action-modal-entity-references') +export class UmbConfirmBulkActionModalEntityReferencesElement extends UmbLitElement { + @property({ type: Object, attribute: false }) + config?: { + uniques: Array; + itemRepositoryAlias: string; + referenceRepositoryAlias: string; + }; + + @state() + _items: Array = []; + + @state() + _totalItems: number = 0; + + #itemRepository?: UmbItemRepository; + #referenceRepository?: UmbEntityReferenceRepository; + + #limitItems = 5; + + protected override firstUpdated(_changedProperties: PropertyValues): void { + super.firstUpdated(_changedProperties); + this.#initData(); + } + + async #initData() { + if (!this.config) { + this.#itemRepository?.destroy(); + this.#referenceRepository?.destroy(); + return; + } + + if (!this.config?.referenceRepositoryAlias) { + throw new Error('Missing referenceRepositoryAlias in config.'); + } + + this.#referenceRepository = await createExtensionApiByAlias( + this, + this.config?.referenceRepositoryAlias, + ); + + if (!this.config?.itemRepositoryAlias) { + throw new Error('Missing itemRepositoryAlias in config.'); + } + + this.#itemRepository = await createExtensionApiByAlias>( + this, + this.config.itemRepositoryAlias, + ); + + this.#loadAreReferenced(); + } + + async #loadAreReferenced() { + if (!this.#referenceRepository) { + throw new Error('Failed to create reference repository.'); + } + + if (!this.#itemRepository) { + throw new Error('Failed to create item repository.'); + } + + if (!this.config?.uniques) { + throw new Error('Missing uniques in config.'); + } + + const { data } = await this.#referenceRepository.requestAreReferenced(this.config.uniques, 0, this.#limitItems); + + if (data) { + this._totalItems = data.total; + const uniques = data.items.map((item) => item.unique).filter((unique) => unique) as Array; + const { data: items } = await this.#itemRepository.requestItems(uniques); + this._items = items ?? []; + } + } + + override render() { + if (this._totalItems === 0) return nothing; + + return html` +
The following items are used by other content.
+ + ${this._items.map( + (item) => + html` `, + )} + + ${this._totalItems > this.#limitItems + ? html`${this.localize.term('references_labelMoreReferences', this._totalItems - this.#limitItems)}` + : nothing} + `; + } + + static override styles = [ + UmbTextStyles, + css` + #reference-headline { + margin-bottom: var(--uui-size-3); + } + + uui-ref-list { + margin-bottom: var(--uui-size-2); + } + `, + ]; +} + +export { UmbConfirmBulkActionModalEntityReferencesElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-confirm-bulk-action-modal-entity-references': UmbConfirmBulkActionModalEntityReferencesElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/manifests.ts index 574ed691cf..fceac92145 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/manifests.ts @@ -1,9 +1,13 @@ +import { manifests as bulkDeleteManifests } from './entity-actions/bulk-delete/manifests.js'; +import { manifests as bulkTrashManifests } from './entity-actions/bulk-trash/manifests.js'; import { manifests as collectionManifests } from './collection/manifests.js'; import { manifests as deleteManifests } from './entity-actions/delete/manifests.js'; import { manifests as trashManifests } from './entity-actions/trash/manifests.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; export const manifests: Array = [ + ...bulkDeleteManifests, + ...bulkTrashManifests, ...collectionManifests, ...deleteManifests, ...trashManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/reference/types.ts b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/reference/types.ts index a84034a642..599ee7559b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/relations/relations/reference/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/relations/relations/reference/types.ts @@ -21,6 +21,13 @@ export interface UmbEntityReferenceRepository extends UmbApi { skip?: number, take?: number, ): Promise>>; + + requestAreReferenced( + uniques: Array, + skip?: number, + take?: number, + ): Promise>>; + requestDescendantsWithReferences?( unique: string, skip?: number, @@ -34,6 +41,13 @@ export interface UmbEntityReferenceDataSource { skip?: number, take?: number, ): Promise>>; + + getAreReferenced( + uniques: Array, + skip?: number, + take?: number, + ): Promise>>; + getReferencedDescendants?( unique: string, skip?: number, diff --git a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-bulk-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-bulk-actions/manifests.ts index f39f855037..3bae91ba41 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-bulk-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/user/user-group/entity-bulk-actions/manifests.ts @@ -1,17 +1,20 @@ import { UMB_USER_GROUP_COLLECTION_ALIAS } from '../collection/index.js'; +import { UMB_USER_GROUP_DETAIL_REPOSITORY_ALIAS, UMB_USER_GROUP_ITEM_REPOSITORY_ALIAS } from '../constants.js'; import { UMB_USER_GROUP_ENTITY_TYPE } from '../entity.js'; +import { UMB_ENTITY_BULK_ACTION_DELETE_KIND } from '@umbraco-cms/backoffice/entity-bulk-action'; import { UMB_COLLECTION_ALIAS_CONDITION } from '@umbraco-cms/backoffice/collection'; export const manifests: Array = [ { type: 'entityBulkAction', + kind: UMB_ENTITY_BULK_ACTION_DELETE_KIND, alias: 'Umb.EntityBulkAction.UserGroup.Delete', name: 'Delete User Group Entity Bulk Action', weight: 400, - api: () => import('./delete/delete.action.js'), forEntityTypes: [UMB_USER_GROUP_ENTITY_TYPE], meta: { - label: 'Delete', + itemRepositoryAlias: UMB_USER_GROUP_ITEM_REPOSITORY_ALIAS, + detailRepositoryAlias: UMB_USER_GROUP_DETAIL_REPOSITORY_ALIAS, }, conditions: [ { diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index da094f92b1..512ce70e9a 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -74,6 +74,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js "./src/packages/core/entity-create-option-action/index.ts" ], "@umbraco-cms/backoffice/entity": ["./src/packages/core/entity/index.ts"], + "@umbraco-cms/backoffice/entity-item": ["./src/packages/core/entity-item/index.ts"], "@umbraco-cms/backoffice/event": ["./src/packages/core/event/index.ts"], "@umbraco-cms/backoffice/extension-registry": ["./src/packages/core/extension-registry/index.ts"], "@umbraco-cms/backoffice/health-check": ["./src/packages/health-check/index.ts"], diff --git a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts index 4d0720208b..0cd1f38783 100644 --- a/src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts +++ b/src/Umbraco.Web.UI.Client/utils/all-umb-consts/imports.ts @@ -38,71 +38,72 @@ import * as import35 from '@umbraco-cms/backoffice/entity-action'; import * as import36 from '@umbraco-cms/backoffice/entity-bulk-action'; import * as import37 from '@umbraco-cms/backoffice/entity-create-option-action'; import * as import38 from '@umbraco-cms/backoffice/entity'; -import * as import39 from '@umbraco-cms/backoffice/event'; -import * as import40 from '@umbraco-cms/backoffice/extension-registry'; -import * as import41 from '@umbraco-cms/backoffice/health-check'; -import * as import42 from '@umbraco-cms/backoffice/help'; -import * as import43 from '@umbraco-cms/backoffice/icon'; -import * as import44 from '@umbraco-cms/backoffice/id'; -import * as import45 from '@umbraco-cms/backoffice/imaging'; -import * as import46 from '@umbraco-cms/backoffice/language'; -import * as import47 from '@umbraco-cms/backoffice/lit-element'; -import * as import48 from '@umbraco-cms/backoffice/localization'; -import * as import49 from '@umbraco-cms/backoffice/log-viewer'; -import * as import50 from '@umbraco-cms/backoffice/media-type'; -import * as import51 from '@umbraco-cms/backoffice/media'; -import * as import52 from '@umbraco-cms/backoffice/member-group'; -import * as import53 from '@umbraco-cms/backoffice/member-type'; -import * as import54 from '@umbraco-cms/backoffice/member'; -import * as import55 from '@umbraco-cms/backoffice/menu'; -import * as import56 from '@umbraco-cms/backoffice/modal'; -import * as import57 from '@umbraco-cms/backoffice/multi-url-picker'; -import * as import58 from '@umbraco-cms/backoffice/notification'; -import * as import59 from '@umbraco-cms/backoffice/object-type'; -import * as import60 from '@umbraco-cms/backoffice/package'; -import * as import61 from '@umbraco-cms/backoffice/partial-view'; -import * as import62 from '@umbraco-cms/backoffice/picker-input'; -import * as import63 from '@umbraco-cms/backoffice/picker'; -import * as import64 from '@umbraco-cms/backoffice/property-action'; -import * as import65 from '@umbraco-cms/backoffice/property-editor'; -import * as import66 from '@umbraco-cms/backoffice/property-type'; -import * as import67 from '@umbraco-cms/backoffice/property'; -import * as import68 from '@umbraco-cms/backoffice/recycle-bin'; -import * as import69 from '@umbraco-cms/backoffice/relation-type'; -import * as import70 from '@umbraco-cms/backoffice/relations'; -import * as import71 from '@umbraco-cms/backoffice/repository'; -import * as import72 from '@umbraco-cms/backoffice/resources'; -import * as import73 from '@umbraco-cms/backoffice/router'; -import * as import74 from '@umbraco-cms/backoffice/rte'; -import * as import75 from '@umbraco-cms/backoffice/script'; -import * as import76 from '@umbraco-cms/backoffice/search'; -import * as import77 from '@umbraco-cms/backoffice/section'; -import * as import78 from '@umbraco-cms/backoffice/server-file-system'; -import * as import79 from '@umbraco-cms/backoffice/settings'; -import * as import80 from '@umbraco-cms/backoffice/sorter'; -import * as import81 from '@umbraco-cms/backoffice/static-file'; -import * as import82 from '@umbraco-cms/backoffice/store'; -import * as import83 from '@umbraco-cms/backoffice/style'; -import * as import84 from '@umbraco-cms/backoffice/stylesheet'; -import * as import85 from '@umbraco-cms/backoffice/sysinfo'; -import * as import86 from '@umbraco-cms/backoffice/tags'; -import * as import87 from '@umbraco-cms/backoffice/template'; -import * as import88 from '@umbraco-cms/backoffice/temporary-file'; -import * as import89 from '@umbraco-cms/backoffice/themes'; -import * as import90 from '@umbraco-cms/backoffice/tiny-mce'; -import * as import91 from '@umbraco-cms/backoffice/tiptap'; -import * as import92 from '@umbraco-cms/backoffice/translation'; -import * as import93 from '@umbraco-cms/backoffice/tree'; -import * as import94 from '@umbraco-cms/backoffice/ufm'; -import * as import95 from '@umbraco-cms/backoffice/user-change-password'; -import * as import96 from '@umbraco-cms/backoffice/user-group'; -import * as import97 from '@umbraco-cms/backoffice/user-permission'; -import * as import98 from '@umbraco-cms/backoffice/user'; -import * as import99 from '@umbraco-cms/backoffice/utils'; -import * as import100 from '@umbraco-cms/backoffice/validation'; -import * as import101 from '@umbraco-cms/backoffice/variant'; -import * as import102 from '@umbraco-cms/backoffice/webhook'; -import * as import103 from '@umbraco-cms/backoffice/workspace'; +import * as import39 from '@umbraco-cms/backoffice/entity-item'; +import * as import40 from '@umbraco-cms/backoffice/event'; +import * as import41 from '@umbraco-cms/backoffice/extension-registry'; +import * as import42 from '@umbraco-cms/backoffice/health-check'; +import * as import43 from '@umbraco-cms/backoffice/help'; +import * as import44 from '@umbraco-cms/backoffice/icon'; +import * as import45 from '@umbraco-cms/backoffice/id'; +import * as import46 from '@umbraco-cms/backoffice/imaging'; +import * as import47 from '@umbraco-cms/backoffice/language'; +import * as import48 from '@umbraco-cms/backoffice/lit-element'; +import * as import49 from '@umbraco-cms/backoffice/localization'; +import * as import50 from '@umbraco-cms/backoffice/log-viewer'; +import * as import51 from '@umbraco-cms/backoffice/media-type'; +import * as import52 from '@umbraco-cms/backoffice/media'; +import * as import53 from '@umbraco-cms/backoffice/member-group'; +import * as import54 from '@umbraco-cms/backoffice/member-type'; +import * as import55 from '@umbraco-cms/backoffice/member'; +import * as import56 from '@umbraco-cms/backoffice/menu'; +import * as import57 from '@umbraco-cms/backoffice/modal'; +import * as import58 from '@umbraco-cms/backoffice/multi-url-picker'; +import * as import59 from '@umbraco-cms/backoffice/notification'; +import * as import60 from '@umbraco-cms/backoffice/object-type'; +import * as import61 from '@umbraco-cms/backoffice/package'; +import * as import62 from '@umbraco-cms/backoffice/partial-view'; +import * as import63 from '@umbraco-cms/backoffice/picker-input'; +import * as import64 from '@umbraco-cms/backoffice/picker'; +import * as import65 from '@umbraco-cms/backoffice/property-action'; +import * as import66 from '@umbraco-cms/backoffice/property-editor'; +import * as import67 from '@umbraco-cms/backoffice/property-type'; +import * as import68 from '@umbraco-cms/backoffice/property'; +import * as import69 from '@umbraco-cms/backoffice/recycle-bin'; +import * as import70 from '@umbraco-cms/backoffice/relation-type'; +import * as import71 from '@umbraco-cms/backoffice/relations'; +import * as import72 from '@umbraco-cms/backoffice/repository'; +import * as import73 from '@umbraco-cms/backoffice/resources'; +import * as import74 from '@umbraco-cms/backoffice/router'; +import * as import75 from '@umbraco-cms/backoffice/rte'; +import * as import76 from '@umbraco-cms/backoffice/script'; +import * as import77 from '@umbraco-cms/backoffice/search'; +import * as import78 from '@umbraco-cms/backoffice/section'; +import * as import79 from '@umbraco-cms/backoffice/server-file-system'; +import * as import80 from '@umbraco-cms/backoffice/settings'; +import * as import81 from '@umbraco-cms/backoffice/sorter'; +import * as import82 from '@umbraco-cms/backoffice/static-file'; +import * as import83 from '@umbraco-cms/backoffice/store'; +import * as import84 from '@umbraco-cms/backoffice/style'; +import * as import85 from '@umbraco-cms/backoffice/stylesheet'; +import * as import86 from '@umbraco-cms/backoffice/sysinfo'; +import * as import87 from '@umbraco-cms/backoffice/tags'; +import * as import88 from '@umbraco-cms/backoffice/template'; +import * as import89 from '@umbraco-cms/backoffice/temporary-file'; +import * as import90 from '@umbraco-cms/backoffice/themes'; +import * as import91 from '@umbraco-cms/backoffice/tiny-mce'; +import * as import92 from '@umbraco-cms/backoffice/tiptap'; +import * as import93 from '@umbraco-cms/backoffice/translation'; +import * as import94 from '@umbraco-cms/backoffice/tree'; +import * as import95 from '@umbraco-cms/backoffice/ufm'; +import * as import96 from '@umbraco-cms/backoffice/user-change-password'; +import * as import97 from '@umbraco-cms/backoffice/user-group'; +import * as import98 from '@umbraco-cms/backoffice/user-permission'; +import * as import99 from '@umbraco-cms/backoffice/user'; +import * as import100 from '@umbraco-cms/backoffice/utils'; +import * as import101 from '@umbraco-cms/backoffice/validation'; +import * as import102 from '@umbraco-cms/backoffice/variant'; +import * as import103 from '@umbraco-cms/backoffice/webhook'; +import * as import104 from '@umbraco-cms/backoffice/workspace'; export const imports = [ { @@ -262,264 +263,268 @@ import * as import103 from '@umbraco-cms/backoffice/workspace'; package: import38 }, { - path: '@umbraco-cms/backoffice/event', + path: '@umbraco-cms/backoffice/entity-item', package: import39 }, { - path: '@umbraco-cms/backoffice/extension-registry', + path: '@umbraco-cms/backoffice/event', package: import40 }, { - path: '@umbraco-cms/backoffice/health-check', + path: '@umbraco-cms/backoffice/extension-registry', package: import41 }, { - path: '@umbraco-cms/backoffice/help', + path: '@umbraco-cms/backoffice/health-check', package: import42 }, { - path: '@umbraco-cms/backoffice/icon', + path: '@umbraco-cms/backoffice/help', package: import43 }, { - path: '@umbraco-cms/backoffice/id', + path: '@umbraco-cms/backoffice/icon', package: import44 }, { - path: '@umbraco-cms/backoffice/imaging', + path: '@umbraco-cms/backoffice/id', package: import45 }, { - path: '@umbraco-cms/backoffice/language', + path: '@umbraco-cms/backoffice/imaging', package: import46 }, { - path: '@umbraco-cms/backoffice/lit-element', + path: '@umbraco-cms/backoffice/language', package: import47 }, { - path: '@umbraco-cms/backoffice/localization', + path: '@umbraco-cms/backoffice/lit-element', package: import48 }, { - path: '@umbraco-cms/backoffice/log-viewer', + path: '@umbraco-cms/backoffice/localization', package: import49 }, { - path: '@umbraco-cms/backoffice/media-type', + path: '@umbraco-cms/backoffice/log-viewer', package: import50 }, { - path: '@umbraco-cms/backoffice/media', + path: '@umbraco-cms/backoffice/media-type', package: import51 }, { - path: '@umbraco-cms/backoffice/member-group', + path: '@umbraco-cms/backoffice/media', package: import52 }, { - path: '@umbraco-cms/backoffice/member-type', + path: '@umbraco-cms/backoffice/member-group', package: import53 }, { - path: '@umbraco-cms/backoffice/member', + path: '@umbraco-cms/backoffice/member-type', package: import54 }, { - path: '@umbraco-cms/backoffice/menu', + path: '@umbraco-cms/backoffice/member', package: import55 }, { - path: '@umbraco-cms/backoffice/modal', + path: '@umbraco-cms/backoffice/menu', package: import56 }, { - path: '@umbraco-cms/backoffice/multi-url-picker', + path: '@umbraco-cms/backoffice/modal', package: import57 }, { - path: '@umbraco-cms/backoffice/notification', + path: '@umbraco-cms/backoffice/multi-url-picker', package: import58 }, { - path: '@umbraco-cms/backoffice/object-type', + path: '@umbraco-cms/backoffice/notification', package: import59 }, { - path: '@umbraco-cms/backoffice/package', + path: '@umbraco-cms/backoffice/object-type', package: import60 }, { - path: '@umbraco-cms/backoffice/partial-view', + path: '@umbraco-cms/backoffice/package', package: import61 }, { - path: '@umbraco-cms/backoffice/picker-input', + path: '@umbraco-cms/backoffice/partial-view', package: import62 }, { - path: '@umbraco-cms/backoffice/picker', + path: '@umbraco-cms/backoffice/picker-input', package: import63 }, { - path: '@umbraco-cms/backoffice/property-action', + path: '@umbraco-cms/backoffice/picker', package: import64 }, { - path: '@umbraco-cms/backoffice/property-editor', + path: '@umbraco-cms/backoffice/property-action', package: import65 }, { - path: '@umbraco-cms/backoffice/property-type', + path: '@umbraco-cms/backoffice/property-editor', package: import66 }, { - path: '@umbraco-cms/backoffice/property', + path: '@umbraco-cms/backoffice/property-type', package: import67 }, { - path: '@umbraco-cms/backoffice/recycle-bin', + path: '@umbraco-cms/backoffice/property', package: import68 }, { - path: '@umbraco-cms/backoffice/relation-type', + path: '@umbraco-cms/backoffice/recycle-bin', package: import69 }, { - path: '@umbraco-cms/backoffice/relations', + path: '@umbraco-cms/backoffice/relation-type', package: import70 }, { - path: '@umbraco-cms/backoffice/repository', + path: '@umbraco-cms/backoffice/relations', package: import71 }, { - path: '@umbraco-cms/backoffice/resources', + path: '@umbraco-cms/backoffice/repository', package: import72 }, { - path: '@umbraco-cms/backoffice/router', + path: '@umbraco-cms/backoffice/resources', package: import73 }, { - path: '@umbraco-cms/backoffice/rte', + path: '@umbraco-cms/backoffice/router', package: import74 }, { - path: '@umbraco-cms/backoffice/script', + path: '@umbraco-cms/backoffice/rte', package: import75 }, { - path: '@umbraco-cms/backoffice/search', + path: '@umbraco-cms/backoffice/script', package: import76 }, { - path: '@umbraco-cms/backoffice/section', + path: '@umbraco-cms/backoffice/search', package: import77 }, { - path: '@umbraco-cms/backoffice/server-file-system', + path: '@umbraco-cms/backoffice/section', package: import78 }, { - path: '@umbraco-cms/backoffice/settings', + path: '@umbraco-cms/backoffice/server-file-system', package: import79 }, { - path: '@umbraco-cms/backoffice/sorter', + path: '@umbraco-cms/backoffice/settings', package: import80 }, { - path: '@umbraco-cms/backoffice/static-file', + path: '@umbraco-cms/backoffice/sorter', package: import81 }, { - path: '@umbraco-cms/backoffice/store', + path: '@umbraco-cms/backoffice/static-file', package: import82 }, { - path: '@umbraco-cms/backoffice/style', + path: '@umbraco-cms/backoffice/store', package: import83 }, { - path: '@umbraco-cms/backoffice/stylesheet', + path: '@umbraco-cms/backoffice/style', package: import84 }, { - path: '@umbraco-cms/backoffice/sysinfo', + path: '@umbraco-cms/backoffice/stylesheet', package: import85 }, { - path: '@umbraco-cms/backoffice/tags', + path: '@umbraco-cms/backoffice/sysinfo', package: import86 }, { - path: '@umbraco-cms/backoffice/template', + path: '@umbraco-cms/backoffice/tags', package: import87 }, { - path: '@umbraco-cms/backoffice/temporary-file', + path: '@umbraco-cms/backoffice/template', package: import88 }, { - path: '@umbraco-cms/backoffice/themes', + path: '@umbraco-cms/backoffice/temporary-file', package: import89 }, { - path: '@umbraco-cms/backoffice/tiny-mce', + path: '@umbraco-cms/backoffice/themes', package: import90 }, { - path: '@umbraco-cms/backoffice/tiptap', + path: '@umbraco-cms/backoffice/tiny-mce', package: import91 }, { - path: '@umbraco-cms/backoffice/translation', + path: '@umbraco-cms/backoffice/tiptap', package: import92 }, { - path: '@umbraco-cms/backoffice/tree', + path: '@umbraco-cms/backoffice/translation', package: import93 }, { - path: '@umbraco-cms/backoffice/ufm', + path: '@umbraco-cms/backoffice/tree', package: import94 }, { - path: '@umbraco-cms/backoffice/user-change-password', + path: '@umbraco-cms/backoffice/ufm', package: import95 }, { - path: '@umbraco-cms/backoffice/user-group', + path: '@umbraco-cms/backoffice/user-change-password', package: import96 }, { - path: '@umbraco-cms/backoffice/user-permission', + path: '@umbraco-cms/backoffice/user-group', package: import97 }, { - path: '@umbraco-cms/backoffice/user', + path: '@umbraco-cms/backoffice/user-permission', package: import98 }, { - path: '@umbraco-cms/backoffice/utils', + path: '@umbraco-cms/backoffice/user', package: import99 }, { - path: '@umbraco-cms/backoffice/validation', + path: '@umbraco-cms/backoffice/utils', package: import100 }, { - path: '@umbraco-cms/backoffice/variant', + path: '@umbraco-cms/backoffice/validation', package: import101 }, { - path: '@umbraco-cms/backoffice/webhook', + path: '@umbraco-cms/backoffice/variant', package: import102 }, { - path: '@umbraco-cms/backoffice/workspace', + path: '@umbraco-cms/backoffice/webhook', package: import103 + }, +{ + path: '@umbraco-cms/backoffice/workspace', + package: import104 } ]; \ No newline at end of file From c431a5abfe3bf364448e482c037c5e56713bd9a9 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 24 Feb 2025 14:49:36 +0100 Subject: [PATCH 13/58] Implement create options for Member Types + Fix issue when single option is a link (#18310) * implement create options for member types * export path consts * add support for first action being a link * add comment --- .../entity-actions-bundle.element.ts | 16 +++- .../common/create/create.action.ts | 82 +++++++++++++------ .../packages/members/member-type/constants.ts | 1 + .../entity-actions/create.action.ts | 18 ---- ...efault-member-type-create-option-action.ts | 20 +++++ .../create/default/manifests.ts | 17 ++++ .../entity-actions/create/manifests.ts | 14 ++++ .../member-type/entity-actions/manifests.ts | 22 ++--- .../packages/members/member-type/manifests.ts | 3 +- .../src/packages/members/member-type/paths.ts | 25 ++++++ 10 files changed, 155 insertions(+), 63 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create.action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/default/default-member-type-create-option-action.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/default/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/members/member-type/paths.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts index 4663f0f237..b873cd7771 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/entity-actions-bundle/entity-actions-bundle.element.ts @@ -28,6 +28,9 @@ export class UmbEntityActionsBundleElement extends UmbLitElement { @state() private _firstActionApi?: UmbEntityAction; + @state() + private _firstActionHref?: string; + @state() _dropdownIsOpen = false; @@ -73,6 +76,8 @@ export class UmbEntityActionsBundleElement extends UmbLitElement { this._firstActionApi = await createExtensionApi(this, this._firstActionManifest, [ { unique: this.unique, entityType: this.entityType, meta: this._firstActionManifest.meta }, ]); + + this._firstActionHref = await this._firstActionApi?.getHref(); } #openContextMenu() { @@ -91,8 +96,14 @@ export class UmbEntityActionsBundleElement extends UmbLitElement { } async #onFirstActionClick(event: PointerEvent) { - event.stopPropagation(); this.#sectionSidebarContext?.closeContextMenu(); + + // skip if href is defined + if (this._firstActionHref) { + return; + } + + event.stopPropagation(); await this._firstActionApi?.execute(); } @@ -133,7 +144,8 @@ export class UmbEntityActionsBundleElement extends UmbLitElement { if (!this._firstActionApi) return nothing; return html` + @click=${this.#onFirstActionClick} + href="${ifDefined(this._firstActionHref)}"> `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.ts b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.ts index f64f03e6f6..9d869b0b97 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/entity-action/common/create/create.action.ts @@ -5,42 +5,59 @@ import { UMB_ENTITY_CREATE_OPTION_ACTION_LIST_MODAL } from './modal/constants.js import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { createExtensionApi, UmbExtensionsManifestInitializer } from '@umbraco-cms/backoffice/extension-api'; -import type { ManifestEntityCreateOptionAction } from '@umbraco-cms/backoffice/entity-create-option-action'; +import { + type UmbExtensionManifestInitializer, + createExtensionApi, + UmbExtensionsManifestInitializer, + type PermittedControllerType, +} from '@umbraco-cms/backoffice/extension-api'; +import type { + ManifestEntityCreateOptionAction, + UmbEntityCreateOptionAction, +} from '@umbraco-cms/backoffice/entity-create-option-action'; export class UmbCreateEntityAction extends UmbEntityActionBase { #hasSingleOption = true; - #singleActionOptionManifest?: ManifestEntityCreateOptionAction; + #optionsInit?: Promise; + #singleOptionApi?: UmbEntityCreateOptionAction; constructor(host: UmbControllerHost, args: UmbEntityActionArgs) { super(host, args); - new UmbExtensionsManifestInitializer( - this, - umbExtensionsRegistry, - 'entityCreateOptionAction', - (ext) => ext.forEntityTypes.includes(this.args.entityType), - async (actionOptions) => { - this.#hasSingleOption = actionOptions.length === 1; - this.#singleActionOptionManifest = this.#hasSingleOption - ? (actionOptions[0].manifest as unknown as ManifestEntityCreateOptionAction) - : undefined; - }, - 'umbEntityActionsObserver', - ); + /* This is wrapped in a promise to confirm whether only one option exists and to ensure + that the API for this option has been created. We both need to wait for any options to + be returned from the registry and for the API to be created. This is a custom promise implementation, + because using .asPromise() on the initializer does not wait for the async API creation in the callback.*/ + this.#optionsInit = new Promise((resolve) => { + new UmbExtensionsManifestInitializer( + this, + umbExtensionsRegistry, + 'entityCreateOptionAction', + (ext) => ext.forEntityTypes.includes(this.args.entityType), + async (actionOptions) => { + this.#hasSingleOption = actionOptions.length === 1; + if (this.#hasSingleOption) { + await this.#createSingleOptionApi(actionOptions); + resolve(); + } else { + resolve(); + } + }, + 'umbEntityActionsObserver', + ); + }); + } + + override async getHref() { + await this.#optionsInit; + const href = await this.#singleOptionApi?.getHref(); + return this.#hasSingleOption && href ? href : undefined; } override async execute() { + await this.#optionsInit; if (this.#hasSingleOption) { - if (!this.#singleActionOptionManifest) throw new Error('No first action manifest found'); - - const api = await createExtensionApi(this, this.#singleActionOptionManifest, [ - { unique: this.args.unique, entityType: this.args.entityType, meta: this.#singleActionOptionManifest.meta }, - ]); - - if (!api) throw new Error(`Could not create api for ${this.#singleActionOptionManifest.alias}`); - - await api.execute(); + await this.#singleOptionApi?.execute(); return; } @@ -54,6 +71,21 @@ export class UmbCreateEntityAction extends UmbEntityActionBase>>, + ) { + const manifest = createOptions[0].manifest; + if (!manifest) throw new Error('No first action manifest found'); + + const api = await createExtensionApi(this, manifest, [ + { unique: this.args.unique, entityType: this.args.entityType, meta: manifest.meta }, + ]); + + if (!api) throw new Error(`Could not create api for ${manifest.alias}`); + + this.#singleOptionApi = api; + } } export { UmbCreateEntityAction as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/constants.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/constants.ts index f7503330fb..75fe65204b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/constants.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/constants.ts @@ -2,5 +2,6 @@ export * from './entity-actions/constants.js'; export * from './repository/constants.js'; export * from './tree/constants.js'; export * from './workspace/constants.js'; +export * from './paths.js'; export { UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE, UMB_MEMBER_TYPE_ENTITY_TYPE } from './entity.js'; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create.action.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create.action.ts deleted file mode 100644 index 0fcd7b6647..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create.action.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; -import type { UmbEntityActionArgs } from '@umbraco-cms/backoffice/entity-action'; -import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action'; - -export class UmbCreateMemberTypeEntityAction extends UmbEntityActionBase { - constructor(host: UmbControllerHostElement, args: UmbEntityActionArgs) { - super(host, args); - } - - override async execute() { - // TODO: Generate the href or retrieve it from something? - history.pushState( - null, - '', - `section/settings/workspace/member-type/create/parent/${this.args.entityType}/${this.args.unique}`, - ); - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/default/default-member-type-create-option-action.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/default/default-member-type-create-option-action.ts new file mode 100644 index 0000000000..99620efd54 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/default/default-member-type-create-option-action.ts @@ -0,0 +1,20 @@ +import type { UmbMemberTypeRootEntityType } from '../../../entity.js'; +import { UMB_CREATE_MEMBER_TYPE_WORKSPACE_PATH_PATTERN } from '../../../paths.js'; +import { UmbEntityCreateOptionActionBase } from '@umbraco-cms/backoffice/entity-create-option-action'; +import type { MetaEntityCreateOptionAction } from '@umbraco-cms/backoffice/entity-create-option-action'; + +export class UmbDefaultMemberTypeCreateOptionAction extends UmbEntityCreateOptionActionBase { + override async getHref() { + const parentEntityType = this.args.entityType as UmbMemberTypeRootEntityType; + if (!parentEntityType) throw new Error('Entity type is required to create a member type'); + + const parentUnique = this.args.unique ?? null; + + return UMB_CREATE_MEMBER_TYPE_WORKSPACE_PATH_PATTERN.generateAbsolute({ + parentEntityType, + parentUnique, + }); + } +} + +export { UmbDefaultMemberTypeCreateOptionAction as api }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/default/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/default/manifests.ts new file mode 100644 index 0000000000..c10dcb5f4e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/default/manifests.ts @@ -0,0 +1,17 @@ +import { UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE } from '../../../entity.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'entityCreateOptionAction', + alias: 'Umb.EntityCreateOptionAction.MemberType.Default', + name: 'Default Member Type Entity Create Option Action', + weight: 1000, + api: () => import('./default-member-type-create-option-action.js'), + forEntityTypes: [UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE], + meta: { + icon: 'icon-user', + label: '#content_membertype', + }, + }, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/manifests.ts new file mode 100644 index 0000000000..a8772bc1b6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/create/manifests.ts @@ -0,0 +1,14 @@ +import { UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE } from '../../entity.js'; +import { manifests as defaultManifests } from './default/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; + +export const manifests: Array = [ + { + type: 'entityAction', + kind: 'create', + alias: 'Umb.EntityAction.MemberType.Create', + name: 'Create Member Type Entity Action', + forEntityTypes: [UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE], + }, + ...defaultManifests, +]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/manifests.ts index a5f28857ca..276b0d5545 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/entity-actions/manifests.ts @@ -1,23 +1,10 @@ import { UMB_MEMBER_TYPE_DETAIL_REPOSITORY_ALIAS, UMB_MEMBER_TYPE_ITEM_REPOSITORY_ALIAS } from '../constants.js'; -import { UMB_MEMBER_TYPE_ENTITY_TYPE, UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE } from '../entity.js'; -import { UmbCreateMemberTypeEntityAction } from './create.action.js'; +import { UMB_MEMBER_TYPE_ENTITY_TYPE } from '../entity.js'; import { manifests as duplicateManifests } from './duplicate/manifests.js'; +import { manifests as createManifests } from './create/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; -export const manifests: Array = [ - { - type: 'entityAction', - kind: 'default', - alias: 'Umb.EntityAction.MemberType.Create', - name: 'Create Member Type Entity Action', - weight: 1200, - api: UmbCreateMemberTypeEntityAction, - forEntityTypes: [UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE], - meta: { - icon: 'icon-add', - label: '#actions_create', - additionalOptions: true, - }, - }, +export const manifests: Array = [ { type: 'entityAction', kind: 'delete', @@ -29,5 +16,6 @@ export const manifests: Array = [ itemRepositoryAlias: UMB_MEMBER_TYPE_ITEM_REPOSITORY_ALIAS, }, }, + ...createManifests, ...duplicateManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/manifests.ts index 1980d33395..96b4dceda1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/manifests.ts @@ -4,10 +4,11 @@ import { manifests as repositoryManifests } from './repository/manifests.js'; import { manifests as searchManifests } from './search/manifests.js'; import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; +import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; import './components/index.js'; -export const manifests: Array = [ +export const manifests: Array = [ ...entityActionsManifests, ...menuManifests, ...repositoryManifests, diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/paths.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/paths.ts new file mode 100644 index 0000000000..702a73dba9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/paths.ts @@ -0,0 +1,25 @@ +import { UMB_MEMBER_TYPE_ENTITY_TYPE, UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE } from './entity.js'; +import { UMB_WORKSPACE_PATH_PATTERN } from '@umbraco-cms/backoffice/workspace'; +import { UMB_SETTINGS_SECTION_PATHNAME } from '@umbraco-cms/backoffice/settings'; +import { UmbPathPattern } from '@umbraco-cms/backoffice/router'; +import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; + +export const UMB_MEMBER_TYPE_WORKSPACE_PATH = UMB_WORKSPACE_PATH_PATTERN.generateAbsolute({ + sectionName: UMB_SETTINGS_SECTION_PATHNAME, + entityType: UMB_MEMBER_TYPE_ENTITY_TYPE, +}); + +export const UMB_MEMBER_TYPE_ROOT_WORKSPACE_PATH = UMB_WORKSPACE_PATH_PATTERN.generateAbsolute({ + sectionName: UMB_SETTINGS_SECTION_PATHNAME, + entityType: UMB_MEMBER_TYPE_ROOT_ENTITY_TYPE, +}); + +export const UMB_CREATE_MEMBER_TYPE_WORKSPACE_PATH_PATTERN = new UmbPathPattern<{ + parentEntityType: UmbEntityModel['entityType']; + parentUnique: UmbEntityModel['unique']; +}>('create/parent/:parentEntityType/:parentUnique', UMB_MEMBER_TYPE_WORKSPACE_PATH); + +export const UMB_EDIT_MEMBER_TYPE_WORKSPACE_PATH_PATTERN = new UmbPathPattern<{ unique: string }>( + 'edit/:unique', + UMB_MEMBER_TYPE_WORKSPACE_PATH, +); From 3e291894e538fb9ffff8d5217f637b88192b3ace Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 24 Feb 2025 18:47:37 +0100 Subject: [PATCH 14/58] Fix validation for blocks in variant block editors (#18438) --- .../BlockEditorValidatorBase.cs | 9 ++ ...stElementLevelVariationTests.Validation.cs | 122 ++++++++++++++++++ .../BlockListElementLevelVariationTests.cs | 4 +- 3 files changed, 133 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs index 243f130875..d0338f1852 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs @@ -27,6 +27,7 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV if (validationContextCulture is null) { + // make sure we extend validation to variant block value (element level variation) IEnumerable validationContextCulturesBeingValidated = isWildcardCulture ? blockEditorData.BlockValue.Expose.Select(e => e.Culture).WhereNotNull().Distinct() : validationContext.CulturesBeingValidated; @@ -38,6 +39,14 @@ public abstract class BlockEditorValidatorBase : ComplexEditorV } } } + else + { + // make sure we extend validation to invariant block values (no element level variation) + foreach (var segment in validationContext.SegmentsBeingValidated.DefaultIfEmpty(null)) + { + elementTypeValidation.AddRange(GetBlockEditorDataValidation(blockEditorData, null, segment)); + } + } return elementTypeValidation; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs index 26a32fe67b..8e8878e9bc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Validation.cs @@ -512,4 +512,126 @@ internal partial class BlockListElementLevelVariationTests Assert.IsEmpty(result.ValidationErrors); } + + [Test] + public async Task Can_Validate_Properties_Variant_Blocks() + { + var elementType = CreateElementTypeWithValidation(ContentVariation.Nothing); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture); + var blockListValue = BlockListPropertyValue( + elementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + // blocks property values use null culture for culture variant block editor properties + new() { Alias = "invariantText", Value = "Valid invariantText content value", Culture = null }, + new() { Alias = "variantText", Value = "Invalid variantText content value", Culture = null }, + }, + new List + { + // blocks property values use null culture for culture variant block editor properties + new() { Alias = "invariantText", Value = "Invalid invariantText settings value", Culture = null }, + new() { Alias = "variantText", Value = "Valid variantText settings value", Culture = null }, + }, + "en-US", + null)); + + // make sure all blocks are exposed as they would be for culture variant properties + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = null } + ]; + + var result = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = + [ + new VariantModel + { + Name = "Name en-US", + Culture = "en-US", + Segment = null, + Properties = [ + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + ] + } + ], + InvariantProperties = [] + }, + contentType); + + var errors = result.ValidationErrors.ToArray(); + Assert.Multiple(() => + { + Assert.AreEqual(2, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == "en-US" && error.Segment == null)); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[1].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[0].value")); + }); + } + + [Test] + public async Task Can_Validate_Missing_Properties_Variant_Blocks() + { + var elementType = CreateElementTypeWithValidation(ContentVariation.Nothing); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture); + var blockListValue = BlockListPropertyValue( + elementType, + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List + { + // missing the mandatory "invariantText" + new() { Alias = "variantText", Value = "Valid variantText content value", Culture = null }, + }, + new List + { + // missing the mandatory "variantText" (which, to add to the confusion, is invariant at block level in this test case) + new() { Alias = "invariantText", Value = "Valid invariantText settings value", Culture = null }, + }, + "en-US", + null)); + + // make sure all blocks are exposed as they would be for culture variant properties + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = null } + ]; + + var result = await ContentValidationService.ValidatePropertiesAsync( + new ContentCreateModel + { + ContentTypeKey = contentType.Key, + Variants = + [ + new VariantModel + { + Name = "Name en-US", + Culture = "en-US", + Segment = null, + Properties = [ + new PropertyValueModel { Alias = "blocks", Value = JsonSerializer.Serialize(blockListValue) } + ] + } + ], + InvariantProperties = [] + }, + contentType); + + var errors = result.ValidationErrors.ToArray(); + Assert.Multiple(() => + { + Assert.AreEqual(2, errors.Length); + Assert.IsTrue(errors.All(error => error.Alias == "blocks" && error.Culture == "en-US" && error.Segment == null)); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".contentData[0].values[?(@.alias == 'invariantText' && @.culture == null && @.segment == null)].value")); + Assert.IsNotNull(errors.FirstOrDefault(error => error.JsonPath == ".settingsData[0].values[?(@.alias == 'variantText' && @.culture == null && @.segment == null)].value")); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs index 5370443a5d..b436d7f17c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs @@ -148,9 +148,9 @@ internal partial class BlockListElementLevelVariationTests : BlockEditorElementV return GetPublishedContent(content.Key); } - private IContentType CreateElementTypeWithValidation() + private IContentType CreateElementTypeWithValidation(ContentVariation contentVariation = ContentVariation.Culture) { - var elementType = CreateElementType(ContentVariation.Culture); + var elementType = CreateElementType(contentVariation); foreach (var propertyType in elementType.PropertyTypes) { propertyType.Mandatory = true; From 347e898190f84cd79ec67fb6e6cb851d2c6f8875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 24 Feb 2025 19:50:29 +0100 Subject: [PATCH 15/58] simplifying the use of props (#18430) --- .../src/packages/rte/components/rte-base.element.ts | 11 +++-------- .../input-tiny-mce/input-tiny-mce.element.ts | 1 + .../tiny-mce/property-editor-ui-tiny-mce.element.ts | 6 +++--- .../components/input-tiptap/input-tiptap.element.ts | 1 + .../tiptap/property-editor-ui-tiptap.element.ts | 6 +++--- 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts index 9e4c70c5c3..07b52ae1b5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/rte/components/rte-base.element.ts @@ -34,7 +34,7 @@ export abstract class UmbPropertyEditorUiRteElementBase extends UmbLitElement im public set value(value: UmbPropertyEditorUiValueType | undefined) { if (!value) { this._value = undefined; - this._markup = this._latestMarkup = ''; + this._markup = ''; this.#managerContext.setLayouts([]); this.#managerContext.setContents([]); this.#managerContext.setSettings([]); @@ -52,7 +52,7 @@ export abstract class UmbPropertyEditorUiRteElementBase extends UmbLitElement im this._value = buildUpValue as UmbPropertyEditorUiValueType; // Only update the actual editor markup if it is not the same as the value. - if (this._latestMarkup !== this._value.markup) { + if (this._markup !== this._value.markup) { this._markup = this._value.markup; } @@ -84,11 +84,6 @@ export abstract class UmbPropertyEditorUiRteElementBase extends UmbLitElement im @state() protected _markup = ''; - /** - * The latest value gotten from the RTE editor. - */ - protected _latestMarkup = ''; - readonly #managerContext = new UmbBlockRteManagerContext(this); readonly #entriesContext = new UmbBlockRteEntriesContext(this); @@ -141,7 +136,7 @@ export abstract class UmbPropertyEditorUiRteElementBase extends UmbLitElement im this._value = undefined; } else { this._value = { - markup: this._latestMarkup, + markup: this._markup, blocks: { layout: { [UMB_BLOCK_RTE_PROPERTY_EDITOR_SCHEMA_ALIAS]: layouts }, contentData: contents, diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts index c61116da94..bcd7a805bc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/components/input-tiny-mce/input-tiny-mce.element.ts @@ -61,6 +61,7 @@ export class UmbInputTinyMceElement extends UUIFormControlMixin(UmbLitElement, ' } override set value(newValue: FormDataEntryValue | FormData) { + if (newValue === this.value) return; super.value = newValue; const newContent = typeof newValue === 'string' ? newValue : ''; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts index 9d375b8f38..bbbf2d8071 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/property-editor-ui-tiny-mce.element.ts @@ -43,16 +43,16 @@ export class UmbPropertyEditorUITinyMceElement extends UmbPropertyEditorUiRteEle // Then get the content of the editor and update the value. // maybe in this way doc.body.innerHTML; - this._latestMarkup = markup; + this._markup = markup; if (this.value) { this.value = { ...this.value, - markup: this._latestMarkup, + markup: this._markup, }; } else { this.value = { - markup: this._latestMarkup, + markup: this._markup, blocks: { layout: {}, contentData: [], diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts index 9213cd8be1..748a3c6f50 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts @@ -19,6 +19,7 @@ const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials'; export class UmbInputTiptapElement extends UmbFormControlMixin(UmbLitElement) { @property({ type: String }) override set value(value: string) { + if (value === this.#value) return; this.#value = value; // Try to set the value to the editor if it is ready. diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts index cff1839022..ae0ba95d23 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/property-editor-ui-tiptap.element.ts @@ -36,16 +36,16 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem this._filterUnusedBlocks(usedContentKeys); - this._latestMarkup = value; + this._markup = value; if (this.value) { this.value = { ...this.value, - markup: this._latestMarkup, + markup: this._markup, }; } else { this.value = { - markup: this._latestMarkup, + markup: this._markup, blocks: { layout: {}, contentData: [], From a4f385ba01f8e0a69e445cfae6d1745e85a0ca80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 24 Feb 2025 22:09:43 +0100 Subject: [PATCH 16/58] Fix: remember validation state when creating (#18432) * only reset state and load if its a new unique * related clean up * return response promise --- .../src/packages/core/utils/state-manager/state.manager.ts | 5 +++++ .../controllers/workspace-is-new-redirect.controller.ts | 2 +- .../entity-detail/entity-detail-workspace-base.ts | 6 ++++++ .../submittable/submittable-workspace-context-base.ts | 1 - .../documents/workspace/document-workspace.context.ts | 7 ++++++- .../media/media/workspace/media-workspace.context.ts | 3 ++- .../member/workspace/member/member-workspace.context.ts | 2 +- 7 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.ts index 890f31b6e2..4c163593df 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/utils/state-manager/state.manager.ts @@ -92,4 +92,9 @@ export class UmbStateManager extends UmbC clear() { this._states.setValue([]); } + + override destroy() { + super.destroy(); + this._states.destroy(); + } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-is-new-redirect.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-is-new-redirect.controller.ts index 37c8bf0bec..3f88fbb826 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-is-new-redirect.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/controllers/workspace-is-new-redirect.controller.ts @@ -34,7 +34,7 @@ export class UmbWorkspaceIsNewRedirectController extends UmbControllerBase { id: unique, }); this.destroy(); - window.history.replaceState({}, '', newPath); + window.history.replaceState(null, '', newPath); } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts index 88321ebd6f..08b2126e01 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/entity-detail/entity-detail-workspace-base.ts @@ -167,6 +167,9 @@ export abstract class UmbEntityDetailWorkspaceContextBase< } async load(unique: string) { + if (unique === this.getUnique() && this._getDataPromise) { + return (await this._getDataPromise) as GetDataType; + } this.resetState(); this.#entityContext.setUnique(unique); this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Loading ${this.getEntityType()} Details` }); @@ -389,8 +392,10 @@ export abstract class UmbEntityDetailWorkspaceContextBase< override resetState() { super.resetState(); + this.loading.clear(); this._data.clear(); this.#allowNavigateAway = false; + this._getDataPromise = undefined; } #checkIfInitialized() { @@ -447,6 +452,7 @@ export abstract class UmbEntityDetailWorkspaceContextBase< ); this._detailRepository?.destroy(); this.#entityContext.destroy(); + this._getDataPromise = undefined; super.destroy(); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts index 367ba75712..80bfa5740b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/workspace/submittable/submittable-workspace-context-base.ts @@ -57,7 +57,6 @@ export abstract class UmbSubmittableWorkspaceContextBase } protected resetState() { - //this.validation.reset(); this.#validationContexts.forEach((context) => context.reset()); this.#isNew.setValue(undefined); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 4f989c98ae..b0c6e04ff6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -177,10 +177,15 @@ export class UmbDocumentWorkspaceContext ]); } + override resetState(): void { + super.resetState(); + this.#isTrashedContext.setIsTrashed(false); + } + override async load(unique: string) { const response = await super.load(unique); - if (response.data) { + if (response?.data) { this.#isTrashedContext.setIsTrashed(response.data.isTrashed); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts index 2a2e35395b..7e072f50b1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media/workspace/media-workspace.context.ts @@ -89,13 +89,14 @@ export class UmbMediaWorkspaceContext public override resetState() { super.resetState(); + this.#isTrashedContext.setIsTrashed(false); this.removeUmbControllerByAlias(UmbWorkspaceIsNewRedirectControllerAlias); } public override async load(unique: string) { const response = await super.load(unique); - if (response.data) { + if (response?.data) { this.#isTrashedContext.setIsTrashed(response.data.isTrashed); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts index 6cab3770f1..121de10724 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/workspace/member/member-workspace.context.ts @@ -78,7 +78,7 @@ export class UmbMemberWorkspaceContext preset: { memberType: { unique: memberTypeUnique, - icon: "icon-user" + icon: 'icon-user', }, }, }); From 9f00e968a6b931086e29c58cf0b5b1311bb9978b Mon Sep 17 00:00:00 2001 From: leekelleher Date: Sun, 23 Feb 2025 17:03:36 +0000 Subject: [PATCH 17/58] Tiptap: adds Font Family toolbar button --- .../mocks/data/data-type/data-type.data.ts | 1 + .../packages/tiptap/extensions/manifests.ts | 11 +++ .../font-family-tiptap-toolbar.element.ts | 89 +++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index e318d6afd0..9103803b11 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -1048,6 +1048,7 @@ export const data: Array = [ 'Umb.Tiptap.Toolbar.Undo', 'Umb.Tiptap.Toolbar.Redo', 'Umb.Tiptap.Toolbar.StyleSelect', + 'Umb.Tiptap.Toolbar.FontFamily', 'Umb.Tiptap.Toolbar.Bold', 'Umb.Tiptap.Toolbar.Italic', 'Umb.Tiptap.Toolbar.TextAlignLeft', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 19c52388a5..3481077c1f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -462,6 +462,17 @@ const toolbarExtensions: Array = [ label: '#general_embed', }, }, + { + type: 'tiptapToolbarExtension', + alias: 'Umb.Tiptap.Toolbar.FontFamily', + name: 'Font Family Tiptap Extension', + element: () => import('./toolbar/font-family-tiptap-toolbar.element.js'), + meta: { + alias: 'umbFontFamily', + icon: 'icon-palette', + label: 'Font family', + }, + }, ]; const extensions = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts new file mode 100644 index 0000000000..7335b4f637 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts @@ -0,0 +1,89 @@ +import { UmbTiptapToolbarButtonElement } from '../../components/toolbar/tiptap-toolbar-button.element.js'; +import type { UmbCascadingMenuItem } from '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; +import { css, customElement, html, ifDefined } from '@umbraco-cms/backoffice/external/lit'; + +import '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; + +@customElement('umb-tiptap-font-family-toolbar-element') +export class UmbTiptapToolbarFontFamilyToolbarElement extends UmbTiptapToolbarButtonElement { + #menu: Array = [ + { + unique: 'font-family-sans-serif', + label: 'Sans serif', + element: this.#getElement('sans-serif', 'Sans serif'), + }, + { + unique: 'font-family-serif', + label: 'Serif', + element: this.#getElement('serif', 'Serif'), + }, + { + unique: 'font-family-monospace', + label: 'Monospace', + element: this.#getElement('monospace', 'Monospace'), + }, + { + unique: 'font-family-cursive', + label: 'Cursive', + element: this.#getElement('cursive', 'Cursive'), + }, + { + unique: 'font-family-fantasy', + label: 'Fantasy', + element: this.#getElement('fantasy', 'Fantasy'), + }, + ]; + + #getElement(fontFamily: string, label: string) { + const menuItem = document.createElement('uui-menu-item'); + menuItem.addEventListener('click', () => { + this.editor + ?.chain() + .focus() + .setMark('textStyle', { style: `font-family: ${fontFamily};` }) + .run(); + }); + + const element = document.createElement('span'); + element.slot = 'label'; + element.textContent = label; + element.style.cssText = `font-family: ${fontFamily};`; + + menuItem.appendChild(element); + + return menuItem; + } + + override render() { + const label = this.localize.string(this.manifest?.meta.label); + return html` + + ${label} + + + + + `; + } + + static override readonly styles = [ + css` + :host { + --uui-button-font-weight: normal; + --uui-menu-item-flat-structure: 1; + } + + uui-button > uui-symbol-expand { + margin-left: var(--uui-size-space-4); + } + `, + ]; +} + +export { UmbTiptapToolbarFontFamilyToolbarElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-tiptap-font-family-toolbar-element': UmbTiptapToolbarFontFamilyToolbarElement; + } +} From d30af299ef6f45097ab41b6588ae348cc2eeaa5b Mon Sep 17 00:00:00 2001 From: leekelleher Date: Sun, 23 Feb 2025 17:03:51 +0000 Subject: [PATCH 18/58] Tiptap: adds Font Size toolbar button --- .../mocks/data/data-type/data-type.data.ts | 1 + .../packages/tiptap/extensions/manifests.ts | 11 ++++ .../font-size-tiptap-toolbar.element.ts | 54 +++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 9103803b11..4308cf874a 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -1049,6 +1049,7 @@ export const data: Array = [ 'Umb.Tiptap.Toolbar.Redo', 'Umb.Tiptap.Toolbar.StyleSelect', 'Umb.Tiptap.Toolbar.FontFamily', + 'Umb.Tiptap.Toolbar.FontSize', 'Umb.Tiptap.Toolbar.Bold', 'Umb.Tiptap.Toolbar.Italic', 'Umb.Tiptap.Toolbar.TextAlignLeft', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 3481077c1f..5f0c360227 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -473,6 +473,17 @@ const toolbarExtensions: Array = [ label: 'Font family', }, }, + { + type: 'tiptapToolbarExtension', + alias: 'Umb.Tiptap.Toolbar.FontSize', + name: 'Font Size Tiptap Extension', + element: () => import('./toolbar/font-size-tiptap-toolbar.element.js'), + meta: { + alias: 'umbFontSize', + icon: 'icon-palette', + label: 'Font size', + }, + }, ]; const extensions = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts new file mode 100644 index 0000000000..8fc96a23c0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts @@ -0,0 +1,54 @@ +import { UmbTiptapToolbarButtonElement } from '../../components/toolbar/tiptap-toolbar-button.element.js'; +import type { UmbCascadingMenuItem } from '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; +import { css, customElement, html, ifDefined } from '@umbraco-cms/backoffice/external/lit'; + +import '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; + +@customElement('umb-tiptap-font-size-toolbar-element') +export class UmbTiptapToolbarFontSizeToolbarElement extends UmbTiptapToolbarButtonElement { + #fontSizes = [8, 10, 12, 14, 16, 18, 24, 36, 48]; + + #menu: Array = this.#fontSizes.map((fontSize) => ({ + unique: `font-size-${fontSize}pt`, + label: `${fontSize}pt`, + execute: () => + this.editor + ?.chain() + .focus() + .setMark('textStyle', { style: `font-size: ${fontSize}pt;` }) + .run(), + })); + + override render() { + const label = this.localize.string(this.manifest?.meta.label); + return html` + + ${label} + + + + + `; + } + + static override readonly styles = [ + css` + :host { + --uui-button-font-weight: normal; + --uui-menu-item-flat-structure: 1; + } + + uui-button > uui-symbol-expand { + margin-left: var(--uui-size-space-4); + } + `, + ]; +} + +export { UmbTiptapToolbarFontSizeToolbarElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-tiptap-font-size-toolbar-element': UmbTiptapToolbarFontSizeToolbarElement; + } +} From da26fd1a3c520a0cb796ac7516bae1b422e68c2e Mon Sep 17 00:00:00 2001 From: leekelleher Date: Sun, 23 Feb 2025 17:04:32 +0000 Subject: [PATCH 19/58] Updates TinyMCE mock data to show the font buttons in the toolbar --- .../src/mocks/data/data-type/data-type.data.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 4308cf874a..b64d712a92 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -1106,6 +1106,8 @@ export const data: Array = [ 'undo', 'redo', 'styles', + 'fontfamily', + 'fontsize', 'bold', 'italic', 'alignleft', @@ -1118,6 +1120,7 @@ export const data: Array = [ 'link', 'unlink', 'anchor', + 'charmap', 'table', 'umbmediapicker', 'umbembeddialog', From 5037cc321f5e72221da1cdf1f62e2d6a4070fe64 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Sun, 23 Feb 2025 19:19:28 +0000 Subject: [PATCH 20/58] Tiptap TextStyle tweaks --- .../tiptap-html-global-attributes.extension.ts | 10 +++++++++- .../toolbar/font-family-tiptap-toolbar.element.ts | 3 ++- .../toolbar/font-size-tiptap-toolbar.element.ts | 9 +++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts index eb6b255c69..1a5bdb1387 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts @@ -1,4 +1,5 @@ import { Extension } from '@tiptap/core'; +import type { Attributes } from '@tiptap/core'; /** * Converts camelCase to kebab-case. @@ -45,7 +46,14 @@ export const HtmlGlobalAttributes = Extension.create (element.style?.length ? element.style : null), + // renderHTML: (attributes) => { + // if (!attributes.style?.length) return null; + // return { style: attributes.style.cssText }; + // }, + // }, + } as Attributes, }, ]; }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts index 7335b4f637..07829f253a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts @@ -37,6 +37,7 @@ export class UmbTiptapToolbarFontFamilyToolbarElement extends UmbTiptapToolbarBu #getElement(fontFamily: string, label: string) { const menuItem = document.createElement('uui-menu-item'); menuItem.addEventListener('click', () => { + //this.editor?.chain().focus().setMark('textStyle', { fontFamily }).run(); this.editor ?.chain() .focus() @@ -47,7 +48,7 @@ export class UmbTiptapToolbarFontFamilyToolbarElement extends UmbTiptapToolbarBu const element = document.createElement('span'); element.slot = 'label'; element.textContent = label; - element.style.cssText = `font-family: ${fontFamily};`; + element.style.fontFamily = fontFamily; menuItem.appendChild(element); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts index 8fc96a23c0..238e91892d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts @@ -6,16 +6,17 @@ import '../../components/cascading-menu-popover/cascading-menu-popover.element.j @customElement('umb-tiptap-font-size-toolbar-element') export class UmbTiptapToolbarFontSizeToolbarElement extends UmbTiptapToolbarButtonElement { - #fontSizes = [8, 10, 12, 14, 16, 18, 24, 36, 48]; + #fontSizes = [8, 10, 12, 14, 16, 18, 24, 36, 48].map((fontSize) => `${fontSize}pt`); #menu: Array = this.#fontSizes.map((fontSize) => ({ - unique: `font-size-${fontSize}pt`, - label: `${fontSize}pt`, + unique: `font-size-${fontSize}`, + label: fontSize, + // execute: () => this.editor?.chain().focus().setMark('textStyle', { fontSize }).run(), execute: () => this.editor ?.chain() .focus() - .setMark('textStyle', { style: `font-size: ${fontSize}pt;` }) + .setMark('textStyle', { style: `font-size: ${fontSize};` }) .run(), })); From 85179dcfd3312a40ad166e006e74f4f8bf544ca9 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Sun, 23 Feb 2025 19:19:40 +0000 Subject: [PATCH 21/58] Updated mock data --- .../mocks/data/data-type/data-type.data.ts | 3 +++ .../src/mocks/data/document/document.data.ts | 22 +++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index b64d712a92..1bd0773b12 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -1050,6 +1050,7 @@ export const data: Array = [ 'Umb.Tiptap.Toolbar.StyleSelect', 'Umb.Tiptap.Toolbar.FontFamily', 'Umb.Tiptap.Toolbar.FontSize', + 'Umb.Tiptap.Toolbar.ClearFormatting', 'Umb.Tiptap.Toolbar.Bold', 'Umb.Tiptap.Toolbar.Italic', 'Umb.Tiptap.Toolbar.TextAlignLeft', @@ -1121,6 +1122,8 @@ export const data: Array = [ 'unlink', 'anchor', 'charmap', + 'rtl', + 'ltr', 'table', 'umbmediapicker', 'umbembeddialog', diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts index 2d378c26fe..c1b72da5fc 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts @@ -912,23 +912,31 @@ export const data: Array = [ blocks: undefined, markup: `

- Some value for the RTE with an external link and an internal link foo foo + Some value for the RTE with an external link and an internal link.

+
+ This is a plain old span tag. + Hello world. +
- - + + - - + + - - + + + + + +
NameAliasVersionDate
Leelke15.32025-03-20
Jacobjov16.02025-06-12
17.02025-11-27
From 86942c34d854002e14084be7f6ac371d61ce599c Mon Sep 17 00:00:00 2001 From: leekelleher Date: Mon, 24 Feb 2025 18:27:03 +0000 Subject: [PATCH 22/58] Enhanced Span extension Changed to be a `Mark`, as `Node`s aren't allowed within paragraphs. Unregistered the `TextStyle` extension, as it conflicts with the global `style` attribute feature. --- ...tiptap-html-global-attributes.extension.ts | 11 +--- .../extensions/tiptap-span.extension.ts | 58 +++++++++++++++---- .../core/rich-text-essentials.tiptap-api.ts | 15 +---- .../packages/tiptap/extensions/manifests.ts | 4 +- .../font-family-tiptap-toolbar.element.ts | 6 +- .../font-size-tiptap-toolbar.element.ts | 7 +-- 6 files changed, 57 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts index 1a5bdb1387..b86b6ecc31 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-html-global-attributes.extension.ts @@ -45,14 +45,9 @@ export const HtmlGlobalAttributes = Extension.create (element.style?.length ? element.style : null), - // renderHTML: (attributes) => { - // if (!attributes.style?.length) return null; - // return { style: attributes.style.cssText }; - // }, - // }, + style: { + parseHTML: (element) => (element.style.length ? element.style.cssText : null), + }, } as Attributes, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts index 084ae6e3d0..a4a375f100 100644 --- a/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/external/tiptap/extensions/tiptap-span.extension.ts @@ -1,32 +1,68 @@ -import { Node, mergeAttributes } from '@tiptap/core'; +import { Mark, mergeAttributes } from '@tiptap/core'; export interface SpanOptions { /** - * HTML attributes to add to the element. + * HTML attributes to add to the span element. * @default {} * @example { class: 'foo' } */ HTMLAttributes: Record; } -export const Span = Node.create({ +export const Span = Mark.create({ name: 'span', - group: 'inline', - - inline: true, - - content: 'inline*', - addOptions() { return { HTMLAttributes: {} }; }, parseHTML() { - return [{ tag: 'span' }]; + return [{ tag: this.name }]; }, renderHTML({ HTMLAttributes }) { - return ['span', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + return [this.name, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]; + }, + + addCommands() { + return { + setSpanStyle: + (styles) => + ({ commands, editor, chain }) => { + if (!styles) return false; + + const existing = editor.getAttributes(this.name)?.style as string; + + if (!existing && !editor.isActive(this.name)) { + return commands.setMark(this.name, { style: styles }); + } + + const rules = ((existing ?? '') + ';' + styles).split(';'); + const items: Record = {}; + + rules + .filter((x) => x) + .forEach((rule) => { + if (rule.trim() !== '') { + const [key, value] = rule.split(':'); + items[key.trim()] = value.trim(); + } + }); + + const style = Object.entries(items) + .map(([key, value]) => `${key}: ${value}`) + .join(';'); + + return commands.updateAttributes(this.name, { style }); + }, + }; }, }); + +declare module '@tiptap/core' { + interface Commands { + span: { + setSpanStyle: (styles?: string) => ReturnType; + }; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts index 481edecdce..b2922ea618 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts @@ -1,13 +1,6 @@ import { UmbTiptapExtensionApiBase } from '../base.js'; import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; -import { - Div, - HtmlGlobalAttributes, - Placeholder, - Span, - StarterKit, - TextStyle, -} from '@umbraco-cms/backoffice/external/tiptap'; +import { Div, HtmlGlobalAttributes, Placeholder, Span, StarterKit } from '@umbraco-cms/backoffice/external/tiptap'; export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionApiBase { #localize = new UmbLocalizationController(this); @@ -21,7 +14,8 @@ export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionA ); }, }), - TextStyle, + Div, + Span, HtmlGlobalAttributes.configure({ types: [ 'bold', @@ -46,13 +40,10 @@ export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionA 'tableHeader', 'tableRow', 'tableCell', - 'textStyle', 'underline', 'umbLink', ], }), - Div, - Span, ]; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 5f0c360227..d447128d42 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -469,7 +469,7 @@ const toolbarExtensions: Array = [ element: () => import('./toolbar/font-family-tiptap-toolbar.element.js'), meta: { alias: 'umbFontFamily', - icon: 'icon-palette', + icon: 'icon-ruler-alt', label: 'Font family', }, }, @@ -480,7 +480,7 @@ const toolbarExtensions: Array = [ element: () => import('./toolbar/font-size-tiptap-toolbar.element.js'), meta: { alias: 'umbFontSize', - icon: 'icon-palette', + icon: 'icon-ruler', label: 'Font size', }, }, diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts index 07829f253a..f5886a2690 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts @@ -38,11 +38,7 @@ export class UmbTiptapToolbarFontFamilyToolbarElement extends UmbTiptapToolbarBu const menuItem = document.createElement('uui-menu-item'); menuItem.addEventListener('click', () => { //this.editor?.chain().focus().setMark('textStyle', { fontFamily }).run(); - this.editor - ?.chain() - .focus() - .setMark('textStyle', { style: `font-family: ${fontFamily};` }) - .run(); + this.editor?.chain().focus().setSpanStyle(`font-family: ${fontFamily};`).run(); }); const element = document.createElement('span'); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts index 238e91892d..84c6241c6a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts @@ -12,12 +12,7 @@ export class UmbTiptapToolbarFontSizeToolbarElement extends UmbTiptapToolbarButt unique: `font-size-${fontSize}`, label: fontSize, // execute: () => this.editor?.chain().focus().setMark('textStyle', { fontSize }).run(), - execute: () => - this.editor - ?.chain() - .focus() - .setMark('textStyle', { style: `font-size: ${fontSize};` }) - .run(), + execute: () => this.editor?.chain().focus().setSpanStyle(`font-size: ${fontSize};`).run(), })); override render() { From 3dfec52d55b8de519055709bc0965cc790f2f626 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Mon, 24 Feb 2025 19:01:33 +0000 Subject: [PATCH 23/58] Toolbar style tweaks --- .../tiptap/components/input-tiptap/tiptap-toolbar.element.ts | 1 + .../style-select/style-select-tiptap-toolbar.element.ts | 2 ++ .../extensions/toolbar/font-family-tiptap-toolbar.element.ts | 2 ++ .../extensions/toolbar/font-size-tiptap-toolbar.element.ts | 2 ++ 4 files changed, 7 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts index dab7f27fd2..9fcf566706 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/tiptap-toolbar.element.ts @@ -116,6 +116,7 @@ export class UmbTiptapToolbarElement extends UmbLitElement { .group { display: inline-flex; + flex-wrap: wrap; align-items: stretch; &:not(:last-child)::after { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select-tiptap-toolbar.element.ts index 32d5479d67..07febbc2c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select-tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select-tiptap-toolbar.element.ts @@ -75,6 +75,8 @@ export class UmbTiptapToolbarStyleSelectToolbarElement extends UmbLitElement { css` :host { --uui-button-font-weight: normal; + + margin-inline-start: var(--uui-size-space-1); } uui-button > uui-symbol-expand { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts index f5886a2690..c210dc997c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts @@ -68,6 +68,8 @@ export class UmbTiptapToolbarFontFamilyToolbarElement extends UmbTiptapToolbarBu :host { --uui-button-font-weight: normal; --uui-menu-item-flat-structure: 1; + + margin-inline-start: var(--uui-size-space-1); } uui-button > uui-symbol-expand { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts index 84c6241c6a..eade259455 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts @@ -32,6 +32,8 @@ export class UmbTiptapToolbarFontSizeToolbarElement extends UmbTiptapToolbarButt :host { --uui-button-font-weight: normal; --uui-menu-item-flat-structure: 1; + + margin-inline-start: var(--uui-size-space-1); } uui-button > uui-symbol-expand { From e448f4b174b1fcd2b18ca9c38f9981a5c43ff387 Mon Sep 17 00:00:00 2001 From: leekelleher Date: Mon, 24 Feb 2025 23:08:18 +0000 Subject: [PATCH 24/58] Added TinyMCE toolbar to Tiptap toolbar mappings for the Font and Style items. --- .../contexts/tiptap-toolbar-configuration.context.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts index 3ce7dc0e5a..7214504198 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/contexts/tiptap-toolbar-configuration.context.ts @@ -35,9 +35,10 @@ export class UmbTiptapToolbarConfigurationContext extends UmbContextBase Date: Tue, 25 Feb 2025 09:44:31 +0100 Subject: [PATCH 25/58] Fix #18431 (#18445) * Keep order from persisted data * refactor to also cover updateCurrent --- .../content/manager/content-data-manager.ts | 29 +++++++++++++++---- .../content/manager/element-data-manager.ts | 29 +++++++++++++++++-- .../src/packages/core/variant/index.ts | 1 + .../src/packages/core/variant/types.ts | 5 ++++ .../packages/core/variant/variant-id.class.ts | 6 +--- .../variant-object-compare.function.ts | 5 ++++ .../entity/entity-workspace-data-manager.ts | 20 +++++++++++++ 7 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/core/variant/variant-object-compare.function.ts diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/manager/content-data-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/manager/content-data-manager.ts index d8d375eb4c..85b6ae51e8 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/manager/content-data-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/manager/content-data-manager.ts @@ -2,7 +2,7 @@ import type { UmbContentDetailModel } from '../types.js'; import { UmbElementWorkspaceDataManager } from './element-data-manager.js'; import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api'; import { appendToFrozenArray, jsonStringComparison } from '@umbraco-cms/backoffice/observable-api'; -import { UmbVariantId, type UmbEntityVariantModel } from '@umbraco-cms/backoffice/variant'; +import { UmbVariantId, umbVariantObjectCompare, type UmbEntityVariantModel } from '@umbraco-cms/backoffice/variant'; export class UmbContentWorkspaceDataManager< ModelType extends UmbContentDetailModel, @@ -19,6 +19,27 @@ export class UmbContentWorkspaceDataManager< this.#variantScaffold = variantScaffold; } + protected override _sortCurrentData = Partial>( + persistedData: Partial, + currentData: GivenType, + ): GivenType { + currentData = super._sortCurrentData(persistedData, currentData); + // Sort the variants in the same order as the persisted data: + const persistedVariants = persistedData.variants; + if (persistedVariants && currentData.variants) { + return { + ...currentData, + variants: [...currentData.variants].sort(function (a, b) { + return ( + persistedVariants.findIndex((x) => umbVariantObjectCompare(x, a)) - + persistedVariants.findIndex((x) => umbVariantObjectCompare(x, b)) + ); + }), + }; + } + return currentData; + } + /** * Sets the variant scaffold data * @param {ModelVariantType} variantScaffold The variant scaffold data @@ -50,8 +71,7 @@ export class UmbContentWorkspaceDataManager< } as ModelVariantType, (x) => variantId.compare(x), ) as Array; - // TODO: I have some trouble with TypeScript here, I does not look like me, but i had to give up. [NL] - this._current.update({ variants: newVariants } as any); + this.updateCurrent({ variants: newVariants } as unknown as ModelType); } else if (this._varies === false) { // TODO: Beware about segments, in this case we need to also consider segments, if its allowed to vary by segments. const invariantVariantId = UmbVariantId.CreateInvariant(); @@ -65,8 +85,7 @@ export class UmbContentWorkspaceDataManager< ...update, } as ModelVariantType, ]; - // TODO: I have some trouble with TypeScript here, I does not look like me, but i had to give up. [NL] - this._current.update({ variants: newVariants } as any); + this.updateCurrent({ variants: newVariants } as unknown as ModelType); } else { throw new Error('Varies by culture is missing'); } diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content/manager/element-data-manager.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content/manager/element-data-manager.ts index dfac318c5d..623f99a2c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content/manager/element-data-manager.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content/manager/element-data-manager.ts @@ -1,8 +1,12 @@ import { UmbMergeContentVariantDataController } from '../controller/merge-content-variant-data.controller.js'; import type { UmbElementDetailModel } from '../types.js'; -import { UmbVariantId } from '@umbraco-cms/backoffice/variant'; +import { UmbVariantId, umbVariantObjectCompare } from '@umbraco-cms/backoffice/variant'; import { UmbEntityWorkspaceDataManager, type UmbWorkspaceDataManager } from '@umbraco-cms/backoffice/workspace'; +function valueObjectCompare(a: any, b: any) { + return a.alias === b.alias && umbVariantObjectCompare(a, b); +} + export class UmbElementWorkspaceDataManager extends UmbEntityWorkspaceDataManager implements UmbWorkspaceDataManager @@ -11,6 +15,27 @@ export class UmbElementWorkspaceDataManager = Partial>( + persistedData: Partial, + currentData: GivenType, + ): GivenType { + currentData = super._sortCurrentData(persistedData, currentData); + // Sort the values in the same order as the persisted data: + const persistedValues = persistedData.values; + if (persistedValues && currentData.values) { + return { + ...currentData, + values: [...currentData.values].sort(function (a, b) { + return ( + persistedValues.findIndex((x) => valueObjectCompare(x, a)) - + persistedValues.findIndex((x) => valueObjectCompare(x, b)) + ); + }), + }; + } + return currentData; + } + #updateLock = 0; initiatePropertyValueChange() { this.#updateLock++; @@ -54,7 +79,7 @@ export class UmbElementWorkspaceDataManager */ public readonly current = this._current.asObservable(); + protected _sortCurrentData = Partial>( + persistedData: Partial, + currentData: GivenType, + ): GivenType { + // do nothing. + return currentData; + } + /** * Gets persisted data * @returns {(ModelType | undefined)} @@ -81,6 +89,12 @@ export class UmbEntityWorkspaceDataManager * @memberof UmbSubmittableWorkspaceDataManager */ setCurrent(data: ModelType | undefined) { + if (data) { + const persistedData = this._persisted.getValue(); + if (persistedData) { + data = this._sortCurrentData(persistedData, data); + } + } this._current.setValue(data); } @@ -90,6 +104,12 @@ export class UmbEntityWorkspaceDataManager * @memberof UmbSubmittableWorkspaceDataManager */ updateCurrent(partialData: Partial) { + if (partialData) { + const persistedData = this._persisted.getValue(); + if (persistedData) { + partialData = this._sortCurrentData(persistedData, partialData); + } + } this._current.update(partialData); } From 8c931d59bdf7d20ef51e29c32c256a16d1596238 Mon Sep 17 00:00:00 2001 From: DitteKKoustrup Date: Tue, 25 Feb 2025 10:03:02 +0100 Subject: [PATCH 26/58] Add Localization to Documentation Types - Structure - Collection (#18396) * Localization changes to Documentation Types - Structure - Collection * added the collections to en-us too * Delete src/Umbraco.Web.UI/Umbraco.Web.UI.sln * Adds collection:noItemsTitle * Adds collection:noItemsTitle --------- Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> --- src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts | 4 ++++ src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts | 1 + src/Umbraco.Web.UI.Client/src/assets/lang/en.ts | 4 ++++ .../input-collection-configuration.element.ts | 2 +- 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts index baf690ad33..f8452c5332 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts @@ -2631,4 +2631,8 @@ export default { toolbar_removeItem: 'Fjern handling', toolbar_emptyGroup: 'Tom', }, + collection: { + noItemsTitle: 'Intet indhold', + addCollectionConfiguration: 'Tilføj samling', + }, } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index b1bedf57ad..a04089c694 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -222,6 +222,7 @@ export default { }, collection: { noItemsTitle: 'No items', + addCollectionConfiguration: 'Add collection', }, content: { isPublished: 'Is Published', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index 031f436480..3a7241cf33 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -2748,4 +2748,8 @@ export default { resetUrlMessage: 'Are you sure you want to reset this URL?', resetUrlLabel: 'Reset', }, + collection: { + noItemsTitle: 'No items', + addCollectionConfiguration: 'Add collection', + }, } as UmbLocalizationDictionary; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-collection-configuration/input-collection-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-collection-configuration/input-collection-configuration.element.ts index 7834dfe9a8..d18d7e3c62 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-collection-configuration/input-collection-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-collection-configuration/input-collection-configuration.element.ts @@ -92,7 +92,7 @@ export class UmbInputCollectionConfigurationElement extends UmbFormControlMixin< id="create-button" color="default" look="placeholder" - label="Configure as a collection" + label=${this.localize.term('collection_addCollectionConfiguration')} href=${this._dataTypePickerModalPath}> `; } From 913205e6142931c19db9825dab1f3684e678fdae Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 25 Feb 2025 11:29:54 +0100 Subject: [PATCH 27/58] Restored minimal default permissions for the writer user group. (#18449) --- .../Migrations/Install/DatabaseDataCreator.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index b805f7763a..379a3a91da 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -195,9 +195,10 @@ internal class DatabaseDataCreator { var userGroupKeyToPermissions = new Dictionary>() { - [Constants.Security.AdminGroupKey] = new[] { ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionAssignDomain.ActionLetter, ActionPublish.ActionLetter, ActionRights.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "7", "T" }, - [Constants.Security.EditorGroupKey] = new[] { ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionPublish.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "T" }, - [Constants.Security.TranslatorGroupKey] = new[] { ActionUpdate.ActionLetter, ActionBrowse.ActionLetter }, + [Constants.Security.AdminGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionAssignDomain.ActionLetter, ActionPublish.ActionLetter, ActionRights.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "7", "T"], + [Constants.Security.EditorGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionDelete.ActionLetter, ActionMove.ActionLetter, ActionCopy.ActionLetter, ActionSort.ActionLetter, ActionRollback.ActionLetter, ActionProtect.ActionLetter, ActionPublish.ActionLetter, ActionUnpublish.ActionLetter, ActionBrowse.ActionLetter, ActionCreateBlueprintFromContent.ActionLetter, ActionNotify.ActionLetter, ":", "5", "T"], + [Constants.Security.WriterGroupKey] = [ActionNew.ActionLetter, ActionUpdate.ActionLetter, ActionBrowse.ActionLetter, ActionNotify.ActionLetter, ":" ], + [Constants.Security.TranslatorGroupKey] = [ActionUpdate.ActionLetter, ActionBrowse.ActionLetter], }; var i = 1; From c19b2286b3431899c3bec510efb2bd7d631f7684 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 25 Feb 2025 12:55:26 +0100 Subject: [PATCH 28/58] feat: adds validation on date from/to inputs in the schedule modal (#18437) --- .../modal/document-schedule-modal.element.ts | 194 ++++++++++++------ 1 file changed, 135 insertions(+), 59 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/schedule-publish/modal/document-schedule-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/schedule-publish/modal/document-schedule-modal.element.ts index 2cf75e6f64..13d8b57ce7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/schedule-publish/modal/document-schedule-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/publishing/schedule-publish/modal/document-schedule-modal.element.ts @@ -6,12 +6,13 @@ import type { UmbDocumentScheduleModalValue, UmbDocumentScheduleSelectionModel, } from './document-schedule-modal.token.js'; -import { css, customElement, html, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, ref, repeat, state, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbModalBaseElement } from '@umbraco-cms/backoffice/modal'; import { UmbTextStyles } from '@umbraco-cms/backoffice/style'; import { UmbSelectionManager } from '@umbraco-cms/backoffice/utils'; +import { umbBindToValidation, UmbValidationContext } from '@umbraco-cms/backoffice/validation'; import type { UmbInputDateElement } from '@umbraco-cms/backoffice/components'; -import type { UUIBooleanInputElement } from '@umbraco-cms/backoffice/external/uui'; +import type { UUIBooleanInputElement, UUIButtonState } from '@umbraco-cms/backoffice/external/uui'; @customElement('umb-document-schedule-modal') export class UmbDocumentScheduleModalElement extends UmbModalBaseElement< @@ -35,6 +36,11 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement< @state() _internalValues: Array = []; + @state() + private _submitButtonState?: UUIButtonState; + + #validation = new UmbValidationContext(this); + #pickableFilter = (option: UmbDocumentVariantOptionModel) => { if (isNotPublishedMandatory(option)) { return true; @@ -91,11 +97,20 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement< this.#selectionManager.setSelection(selected); } - #submit() { - this.value = { - selection: this._internalValues, - }; - this.modalContext?.submit(); + async #submit() { + this._submitButtonState = 'waiting'; + try { + await this.#validation.validate(); + this._submitButtonState = 'success'; + this.value = { + selection: this._internalValues, + }; + this.modalContext?.submit(); + } catch { + this._submitButtonState = 'failed'; + } finally { + this._submitButtonState = undefined; + } } #close() { @@ -146,6 +161,7 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement<
this.localize.term('speechBubbles_scheduleErrReleaseDate1'), + () => { + const value = element.value.toString(); + if (!value) return false; + const date = new Date(value); + return date < new Date(); + }, + ); + } + + #attachValidatorsToUnpublish(element: UmbInputDateElement | null, unique: string) { + if (!element) return; + + element.addValidator( + 'badInput', + () => this.localize.term('speechBubbles_scheduleErrExpireDate1'), + () => { + const value = element.value.toString(); + if (!value) return false; + const date = new Date(value); + return date < new Date(); + }, + ); + + element.addValidator( + 'customError', + () => this.localize.term('speechBubbles_scheduleErrExpireDate2'), + () => { + const value = element.value.toString(); + if (!value) return false; + + // Check if the unpublish date is before the publish date + const variant = this._internalValues.find((s) => s.unique === unique); + if (!variant) return false; + const publishTime = variant.schedule?.publishTime; + if (!publishTime) return false; + + const date = new Date(value); + const publishDate = new Date(publishTime); + return date < publishDate; + }, + ); + } + #renderPublishDateInput(option: UmbDocumentVariantOptionModel, fromDate: string | null, toDate: string | null) { - return html`
- - Publish at -
- this.#onFromDateChange(e, option.unique)} - label=${this.localize.term('general_publishDate')}> -
- ${when( - fromDate, - () => html` - this.#removeFromDate(option.unique)}> - - - `, - )} -
-
-
-
- - Unpublish at -
- this.#onToDateChange(e, option.unique)} - label=${this.localize.term('general_publishDate')}> -
- ${when( - toDate, - () => html` - this.#removeToDate(option.unique)}> - - - `, - )} -
-
-
-
-
`; + return html` +
+ + Publish at +
+ this.#attachValidatorsToPublish(e as UmbInputDateElement))} + ${umbBindToValidation(this)} + type="datetime-local" + .value=${this.#formatDate(fromDate)} + @change=${(e: Event) => this.#onFromDateChange(e, option.unique)} + label=${this.localize.term('general_publishDate')}> +
+ ${when( + fromDate, + () => html` + this.#removeFromDate(option.unique)}> + + + `, + )} +
+
+
+
+ + + Unpublish at +
+ this.#attachValidatorsToUnpublish(e as UmbInputDateElement, option.unique))} + ${umbBindToValidation(this)} + type="datetime-local" + .value=${this.#formatDate(toDate)} + @change=${(e: Event) => this.#onToDateChange(e, option.unique)} + label=${this.localize.term('general_publishDate')}> +
+ ${when( + toDate, + () => html` + this.#removeToDate(option.unique)}> + + + `, + )} +
+
+
+
+
+ `; } #fromDate(unique: string): string | null { @@ -275,6 +347,7 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement< ...variant.schedule, publishTime: null, }; + this.#validation.validate(); this.requestUpdate('_internalValues'); } @@ -285,6 +358,7 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement< ...variant.schedule, unpublishTime: null, }; + this.#validation.validate(); this.requestUpdate('_internalValues'); } @@ -325,6 +399,7 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement< ...variant.schedule, publishTime: this.#getDateValue(e), }; + this.#validation.validate(); this.requestUpdate('_internalValues'); } @@ -335,6 +410,7 @@ export class UmbDocumentScheduleModalElement extends UmbModalBaseElement< ...variant.schedule, unpublishTime: this.#getDateValue(e), }; + this.#validation.validate(); this.requestUpdate('_internalValues'); } From 790c451df18b1eaefb1dac04b458661f141783af Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Tue, 25 Feb 2025 13:25:33 +0100 Subject: [PATCH 29/58] Published status filtering (#18281) * Initial refactor (pending more tests) * Fix structural querying across changing publish states + add tests accordingly * Add tests to validate ancestor and descendant order * Remove axis querying from published status filtering --------- Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> --- .../Services/ApiMediaQueryService.cs | 12 +- .../Query/ExecuteTemplateQueryController.cs | 55 +- .../DeliveryApi/ApiContentRouteBuilder.cs | 29 +- .../DependencyInjection/UmbracoBuilder.cs | 3 + .../ListDescendantsFromCurrentPage.cshtml | 9 +- .../EmbeddedResources/Snippets/SiteMap.cshtml | 7 +- ...rningsWhenPublishingNotificationHandler.cs | 101 +- .../Extensions/PublishedContentExtensions.cs | 3519 +++++++---------- .../PublishedContent/PublishedContentBase.cs | 8 +- .../PublishedValueFallback.cs | 2 +- .../Internal/InternalPublishedContent.cs | 9 +- src/Umbraco.Core/Routing/AliasUrlProvider.cs | 57 +- .../Routing/ContentFinderByUrlAlias.cs | 72 +- .../Routing/DefaultUrlProvider.cs | 61 +- src/Umbraco.Core/Routing/DomainUtilities.cs | 52 +- .../Routing/NewDefaultUrlProvider.cs | 38 +- src/Umbraco.Core/Routing/UrlProvider.cs | 75 +- .../Routing/UrlProviderExtensions.cs | 51 +- ...IPublishedContentStatusFilteringService.cs | 5 + .../IPublishedMediaStatusFilteringService.cs | 5 + .../IPublishedStatusFilteringService.cs | 8 + .../PublishedContentStatusFilteringService.cs | 50 + .../PublishedMediaStatusFilteringService.cs | 19 + .../Routing/RedirectTracker.cs | 64 +- .../PublishedContent.cs | 8 +- .../FriendlyPublishedContentExtensions.cs | 152 +- .../Extensions/PublishedContentExtensions.cs | 31 +- .../Controllers/UmbLoginController.cs | 73 +- .../DocumentNavigationServiceTests.cs | 43 + .../UrlAndDomains/DomainAndUrlsTests.cs | 4 +- .../DeliveryApi/ContentBuilderTests.cs | 8 +- .../DeliveryApi/ContentRouteBuilderTests.cs | 93 +- .../DeliveryApi/DeliveryApiTests.cs | 3 +- .../MultiNodeTreePickerValueConverterTests.cs | 2 +- .../PropertyValueConverterTests.cs | 33 + .../Routing/ContentFinderByUrlAliasTests.cs | 20 +- ...ishedContentStatusFilteringServiceTests.cs | 357 ++ .../Templates/HtmlImageSourceParserTests.cs | 3 +- .../Templates/HtmlLocalLinkParserTests.cs | 11 +- .../Umbraco.Tests.UnitTests.csproj | 4 +- 40 files changed, 2730 insertions(+), 2426 deletions(-) create mode 100644 src/Umbraco.Core/Services/PublishStatus/IPublishedContentStatusFilteringService.cs create mode 100644 src/Umbraco.Core/Services/PublishStatus/IPublishedMediaStatusFilteringService.cs create mode 100644 src/Umbraco.Core/Services/PublishStatus/IPublishedStatusFilteringService.cs create mode 100644 src/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringService.cs create mode 100644 src/Umbraco.Core/Services/PublishStatus/PublishedMediaStatusFilteringService.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringServiceTests.cs diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs index 9dcf85b6ef..0eeeb4d6da 100644 --- a/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs +++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiMediaQueryService.cs @@ -16,12 +16,18 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService private readonly IPublishedMediaCache _publishedMediaCache; private readonly ILogger _logger; private readonly IMediaNavigationQueryService _mediaNavigationQueryService; + private readonly IPublishedMediaStatusFilteringService _publishedMediaStatusFilteringService; - public ApiMediaQueryService(IPublishedMediaCache publishedMediaCache, ILogger logger, IMediaNavigationQueryService mediaNavigationQueryService) + public ApiMediaQueryService( + IPublishedMediaCache publishedMediaCache, + ILogger logger, + IMediaNavigationQueryService mediaNavigationQueryService, + IPublishedMediaStatusFilteringService publishedMediaStatusFilteringService) { _publishedMediaCache = publishedMediaCache; _logger = logger; _mediaNavigationQueryService = mediaNavigationQueryService; + _publishedMediaStatusFilteringService = publishedMediaStatusFilteringService; } /// @@ -71,7 +77,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService break; } - currentChildren = resolvedMedia.Children(null, _publishedMediaCache, _mediaNavigationQueryService); + currentChildren = resolvedMedia.Children(_mediaNavigationQueryService, _publishedMediaStatusFilteringService); } return resolvedMedia; @@ -104,7 +110,7 @@ internal sealed class ApiMediaQueryService : IApiMediaQueryService ? mediaCache.GetById(parentKey) : TryGetByPath(childrenOf, mediaCache); - return parent?.Children(null, _publishedMediaCache, _mediaNavigationQueryService) ?? Array.Empty(); + return parent?.Children(_mediaNavigationQueryService, _publishedMediaStatusFilteringService) ?? Array.Empty(); } private IEnumerable? ApplyFilters(IEnumerable source, IEnumerable filters) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs index 3f22c3931c..b491b45f59 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Query/ExecuteTemplateQueryController.cs @@ -1,12 +1,13 @@ using System.Diagnostics; using System.Linq.Expressions; -using System.Runtime.Versioning; using System.Text; using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.ViewModels.Template.Query; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Models.TemplateQuery; using Umbraco.Cms.Core.PublishedCache; @@ -20,14 +21,46 @@ namespace Umbraco.Cms.Api.Management.Controllers.Template.Query; public class ExecuteTemplateQueryController : TemplateQueryControllerBase { private readonly IPublishedContentQuery _publishedContentQuery; - private readonly IVariationContextAccessor _variationContextAccessor; private readonly IPublishedValueFallback _publishedValueFallback; private readonly IContentTypeService _contentTypeService; - private readonly IPublishedContentCache _contentCache; private readonly IDocumentNavigationQueryService _documentNavigationQueryService; + private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService; private static readonly string _indent = $"{Environment.NewLine} "; + public ExecuteTemplateQueryController( + IPublishedContentQuery publishedContentQuery, + IPublishedValueFallback publishedValueFallback, + IContentTypeService contentTypeService, + IDocumentNavigationQueryService documentNavigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + { + _publishedContentQuery = publishedContentQuery; + _publishedValueFallback = publishedValueFallback; + _contentTypeService = contentTypeService; + _documentNavigationQueryService = documentNavigationQueryService; + _publishedContentStatusFilteringService = publishedContentStatusFilteringService; + } + + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] + public ExecuteTemplateQueryController( + IPublishedContentQuery publishedContentQuery, + IVariationContextAccessor variationContextAccessor, + IPublishedValueFallback publishedValueFallback, + IContentTypeService contentTypeService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService documentNavigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + : this( + publishedContentQuery, + publishedValueFallback, + contentTypeService, + documentNavigationQueryService, + publishedContentStatusFilteringService) + { + } + + [Obsolete("Please use the non-obsolete constructor. Will be removed in V17.")] public ExecuteTemplateQueryController( IPublishedContentQuery publishedContentQuery, IVariationContextAccessor variationContextAccessor, @@ -35,13 +68,13 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase IContentTypeService contentTypeService, IPublishedContentCache contentCache, IDocumentNavigationQueryService documentNavigationQueryService) + : this( + publishedContentQuery, + publishedValueFallback, + contentTypeService, + documentNavigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()) { - _publishedContentQuery = publishedContentQuery; - _variationContextAccessor = variationContextAccessor; - _publishedValueFallback = publishedValueFallback; - _contentTypeService = contentTypeService; - _contentCache = contentCache; - _documentNavigationQueryService = documentNavigationQueryService; } [HttpPost("execute")] @@ -118,13 +151,13 @@ public class ExecuteTemplateQueryController : TemplateQueryControllerBase queryExpression.Append(".ChildrenOfType(\"").Append(model.DocumentTypeAlias).Append("\")"); return rootContent == null ? Enumerable.Empty() - : rootContent.ChildrenOfType(_variationContextAccessor, _contentCache, _documentNavigationQueryService, model.DocumentTypeAlias); + : rootContent.ChildrenOfType(_documentNavigationQueryService, _publishedContentStatusFilteringService, model.DocumentTypeAlias); } queryExpression.Append(".Children()"); return rootContent == null ? Enumerable.Empty() - : rootContent.Children(_variationContextAccessor, _contentCache, _documentNavigationQueryService); + : rootContent.Children(_documentNavigationQueryService, _publishedContentStatusFilteringService); } private IEnumerable ApplyFiltering(IEnumerable? filters, IEnumerable contentQuery, StringBuilder queryExpression) diff --git a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs index c5d93d979c..1868ee0aea 100644 --- a/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs +++ b/src/Umbraco.Core/DeliveryApi/ApiContentRouteBuilder.cs @@ -42,7 +42,7 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder requestSettings.OnChange(settings => _requestSettings = settings); } - [Obsolete("Use constructor that takes an IPublishStatusQueryService instead, scheduled for removal in v17")] + [Obsolete("Use the non-obsolete constructor, scheduled for removal in v17")] public ApiContentRouteBuilder( IApiContentPathProvider apiContentPathProvider, IOptions globalSettings, @@ -80,7 +80,12 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder contentPath = contentPath.EnsureStartsWith("/"); - IPublishedContent root = GetRoot(content, isPreview); + IPublishedContent? root = GetRoot(content, isPreview); + if (root is null) + { + return null; + } + var rootPath = root.UrlSegment(_variationContextAccessor, culture) ?? string.Empty; if (_globalSettings.HideTopLevelNodeFromPath == false) @@ -127,19 +132,21 @@ public sealed class ApiContentRouteBuilder : IApiContentRouteBuilder private static bool IsInvalidContentPath(string? path) => path.IsNullOrWhiteSpace() || "#".Equals(path); - private IPublishedContent GetRoot(IPublishedContent content, bool isPreview) + private IPublishedContent? GetRoot(IPublishedContent content, bool isPreview) { - if (isPreview is false) + if (content.Level == 1) { - return content.Root(_variationContextAccessor, _contentCache, _navigationQueryService, _publishStatusQueryService); + return content; } - _navigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); - IEnumerable rootContent = rootKeys.Select(x => _contentCache.GetById(true, x)).WhereNotNull(); + if (_navigationQueryService.TryGetAncestorsKeys(content.Key, out IEnumerable ancestorKeys) is false) + { + return null; + } - // in very edge case scenarios during preview, content.Root() does not map to the root. - // we'll code our way around it for the time being. - return rootContent.FirstOrDefault(root => root.IsAncestorOrSelf(content)) - ?? content.Root(_variationContextAccessor, _contentCache, _navigationQueryService, _publishStatusQueryService); + Guid[] ancestorKeysAsArray = ancestorKeys as Guid[] ?? ancestorKeys.ToArray(); + return ancestorKeysAsArray.Length > 0 + ? _contentCache.GetById(isPreview, ancestorKeysAsArray.Last()) + : content; } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 95c6da574a..3d36c67d3a 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -387,6 +387,9 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddUnique(x => x.GetRequiredService()); Services.AddUnique(x => x.GetRequiredService()); + Services.AddUnique(); + Services.AddUnique(); + // Register a noop IHtmlSanitizer & IMarkdownSanitizer to be replaced Services.AddUnique(); Services.AddUnique(); diff --git a/src/Umbraco.Core/EmbeddedResources/Snippets/ListDescendantsFromCurrentPage.cshtml b/src/Umbraco.Core/EmbeddedResources/Snippets/ListDescendantsFromCurrentPage.cshtml index f174b38495..8fcde8691b 100644 --- a/src/Umbraco.Core/EmbeddedResources/Snippets/ListDescendantsFromCurrentPage.cshtml +++ b/src/Umbraco.Core/EmbeddedResources/Snippets/ListDescendantsFromCurrentPage.cshtml @@ -7,15 +7,14 @@ @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @inject IPublishedValueFallback PublishedValueFallback @inject IPublishedUrlProvider PublishedUrlProvider -@inject IVariationContextAccessor VariationContextAccessor -@inject IPublishedContentCache PublishedContentCache @inject IDocumentNavigationQueryService DocumentNavigationQueryService +@inject IPublishedContentStatusFilteringService PublishedContentStatusFilteringService @* This snippet creates links for every single page (no matter how deep) below the page currently being viewed by the website visitor, displayed as nested unordered HTML lists. *@ -@{ var selection = Model?.Content.Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService).Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); } +@{ var selection = Model?.Content.Children(DocumentNavigationQueryService, PublishedContentStatusFilteringService).Where(x => x.IsVisible(PublishedValueFallback)).ToArray(); } @* Ensure that the Current Page has children *@ @if (selection?.Length > 0) @@ -34,7 +33,7 @@ @* if this child page has any children, where the property umbracoNaviHide is not True *@ @{ var children = item - .Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService) + .Children(DocumentNavigationQueryService, PublishedContentStatusFilteringService) .Where(x => x.IsVisible(PublishedValueFallback)) .ToArray(); @@ -68,7 +67,7 @@ @* if the page has any children, where the property umbracoNaviHide is not True *@ @{ var children = item - .Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService) + .Children(DocumentNavigationQueryService, PublishedContentStatusFilteringService) .Where(x => x.IsVisible(PublishedValueFallback)) .ToArray(); diff --git a/src/Umbraco.Core/EmbeddedResources/Snippets/SiteMap.cshtml b/src/Umbraco.Core/EmbeddedResources/Snippets/SiteMap.cshtml index 9f2c1c4193..b657b70d21 100644 --- a/src/Umbraco.Core/EmbeddedResources/Snippets/SiteMap.cshtml +++ b/src/Umbraco.Core/EmbeddedResources/Snippets/SiteMap.cshtml @@ -7,9 +7,8 @@ @inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage @inject IPublishedValueFallback PublishedValueFallback @inject IPublishedUrlProvider PublishedUrlProvider -@inject IVariationContextAccessor VariationContextAccessor -@inject IPublishedContentCache PublishedContentCache @inject IDocumentNavigationQueryService DocumentNavigationQueryService +@inject IPublishedContentStatusFilteringService PublishedContentStatusFilteringService @* This snippet makes a list of links of all visible pages of the site, as nested unordered HTML lists. @@ -17,7 +16,7 @@ - It uses a local method called Traverse() to select and display the markup and links. *@ -@{ var selection = Model?.Content.Root(PublishedContentCache, DocumentNavigationQueryService); } +@{ var selection = Model?.Content.Root(DocumentNavigationQueryService, PublishedContentStatusFilteringService); }
@* Render the sitemap by passing the root node to the traverse method, below *@ @@ -33,7 +32,7 @@ @* Select visible children *@ var selection = node - .Children(VariationContextAccessor, PublishedContentCache, DocumentNavigationQueryService) + .Children(DocumentNavigationQueryService, PublishedContentStatusFilteringService) .Where(x => x.IsVisible(PublishedValueFallback) && x.Level <= maxLevelForSitemap) .ToArray(); diff --git a/src/Umbraco.Core/Events/AddUnroutableContentWarningsWhenPublishingNotificationHandler.cs b/src/Umbraco.Core/Events/AddUnroutableContentWarningsWhenPublishingNotificationHandler.cs index ad457d287c..bb94a68d79 100644 --- a/src/Umbraco.Core/Events/AddUnroutableContentWarningsWhenPublishingNotificationHandler.cs +++ b/src/Umbraco.Core/Events/AddUnroutableContentWarningsWhenPublishingNotificationHandler.cs @@ -1,6 +1,8 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; @@ -24,11 +26,75 @@ public class AddUnroutableContentWarningsWhenPublishingNotificationHandler : INo private readonly ILoggerFactory _loggerFactory; private readonly UriUtility _uriUtility; private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IPublishedContentCache _publishedContentCache; private readonly IDocumentNavigationQueryService _navigationQueryService; + private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService; private readonly IEventMessagesFactory _eventMessagesFactory; private readonly ContentSettings _contentSettings; + public AddUnroutableContentWarningsWhenPublishingNotificationHandler( + IPublishedRouter publishedRouter, + IUmbracoContextAccessor umbracoContextAccessor, + ILanguageService languageService, + ILocalizedTextService localizedTextService, + IContentService contentService, + IVariationContextAccessor variationContextAccessor, + ILoggerFactory loggerFactory, + UriUtility uriUtility, + IPublishedUrlProvider publishedUrlProvider, + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService, + IEventMessagesFactory eventMessagesFactory, + IOptions contentSettings) + { + _publishedRouter = publishedRouter; + _umbracoContextAccessor = umbracoContextAccessor; + _languageService = languageService; + _localizedTextService = localizedTextService; + _contentService = contentService; + _variationContextAccessor = variationContextAccessor; + _loggerFactory = loggerFactory; + _uriUtility = uriUtility; + _publishedUrlProvider = publishedUrlProvider; + _navigationQueryService = navigationQueryService; + _publishedContentStatusFilteringService = publishedContentStatusFilteringService; + _eventMessagesFactory = eventMessagesFactory; + _contentSettings = contentSettings.Value; + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public AddUnroutableContentWarningsWhenPublishingNotificationHandler( + IPublishedRouter publishedRouter, + IUmbracoContextAccessor umbracoContextAccessor, + ILanguageService languageService, + ILocalizedTextService localizedTextService, + IContentService contentService, + IVariationContextAccessor variationContextAccessor, + ILoggerFactory loggerFactory, + UriUtility uriUtility, + IPublishedUrlProvider publishedUrlProvider, + IPublishedContentCache publishedContentCache, + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService, + IEventMessagesFactory eventMessagesFactory, + IOptions contentSettings) + : this( + publishedRouter, + umbracoContextAccessor, + languageService, + localizedTextService, + contentService, + variationContextAccessor, + loggerFactory, + uriUtility, + publishedUrlProvider, + navigationQueryService, + publishedContentStatusFilteringService, + eventMessagesFactory, + contentSettings) + { + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] public AddUnroutableContentWarningsWhenPublishingNotificationHandler( IPublishedRouter publishedRouter, IUmbracoContextAccessor umbracoContextAccessor, @@ -43,20 +109,21 @@ public class AddUnroutableContentWarningsWhenPublishingNotificationHandler : INo IDocumentNavigationQueryService navigationQueryService, IEventMessagesFactory eventMessagesFactory, IOptions contentSettings) + : this( + publishedRouter, + umbracoContextAccessor, + languageService, + localizedTextService, + contentService, + variationContextAccessor, + loggerFactory, + uriUtility, + publishedUrlProvider, + navigationQueryService, + StaticServiceProvider.Instance.GetRequiredService(), + eventMessagesFactory, + contentSettings) { - _publishedRouter = publishedRouter; - _umbracoContextAccessor = umbracoContextAccessor; - _languageService = languageService; - _localizedTextService = localizedTextService; - _contentService = contentService; - _variationContextAccessor = variationContextAccessor; - _loggerFactory = loggerFactory; - _uriUtility = uriUtility; - _publishedUrlProvider = publishedUrlProvider; - _publishedContentCache = publishedContentCache; - _navigationQueryService = navigationQueryService; - _eventMessagesFactory = eventMessagesFactory; - _contentSettings = contentSettings.Value; } public async Task HandleAsync(ContentPublishedNotification notification, CancellationToken cancellationToken) @@ -99,8 +166,8 @@ public class AddUnroutableContentWarningsWhenPublishingNotificationHandler : INo _loggerFactory.CreateLogger(), _uriUtility, _publishedUrlProvider, - _publishedContentCache, - _navigationQueryService)).ToArray(); + _navigationQueryService, + _publishedContentStatusFilteringService)).ToArray(); EventMessages eventMessages = _eventMessagesFactory.Get(); diff --git a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs index a2204c9fb5..6b9b0359d7 100644 --- a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs @@ -123,36 +123,42 @@ public static class PublishedContentExtensions /// /// The content type. /// The content. - /// The content cache. /// The query service for the in-memory navigation structure. + /// /// The parent of content, of the given content type, else null. + public static T? Parent( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) + where T : class, IPublishedContent + { + ArgumentNullException.ThrowIfNull(content); + + return content.GetParent(navigationQueryService, publishedStatusFilteringService) as T; + } + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? Parent( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService) where T : class, IPublishedContent - { - ArgumentNullException.ThrowIfNull(content); - - return content.GetParent(publishedCache, navigationQueryService) as T; - } + => content.Parent(navigationQueryService, GetPublishedStatusFilteringService(content)); private static IPublishedContent? GetParent( this IPublishedContent content, - IPublishedCache publishedCache, - INavigationQueryService navigationQueryService) + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) { - IPublishedContent? parent; - if (navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey)) - { - parent = parentKey.HasValue ? publishedCache.GetById(parentKey.Value) : null; - } - else + if (navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey) is false) { throw new KeyNotFoundException($"Content with key '{content.Key}' was not found in the in-memory navigation structure."); } - return parent; + // parent key is null if content is at root + return parentKey.HasValue + ? publishedStatusFilteringService.FilterAvailable([parentKey.Value], null).FirstOrDefault() + : null; } #endregion @@ -221,30 +227,6 @@ public static class PublishedContentExtensions public static bool IsInvariantOrHasCulture(this IPublishedContent content, string culture) => !content.ContentType.VariesByCulture() || content.Cultures.ContainsKey(culture ?? string.Empty); - /// - /// Filters a sequence of to return invariant items, and items that are published for - /// the specified culture. - /// - /// The content items. - /// - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null). - /// - internal static IEnumerable WhereIsInvariantOrHasCulture(this IEnumerable contents, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent - { - if (contents == null) - { - throw new ArgumentNullException(nameof(contents)); - } - - culture = culture ?? variationContextAccessor.VariationContext?.Culture ?? string.Empty; - - // either does not vary by culture, or has the specified culture - return contents.Where(x => !x.ContentType.VariesByCulture() || HasCulture(x, culture)); - } - /// /// Gets the culture date of the content item. /// @@ -521,106 +503,95 @@ public static class PublishedContentExtensions /// Gets the ancestors of the content. /// /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The ancestors of the content, in down-top order. /// Does not consider the content itself. + public static IEnumerable Ancestors( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) + => content.AncestorsOrSelf(navigationQueryService, publishedStatusFilteringService, false, null); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Ancestors( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService) => - content.AncestorsOrSelf( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - false, - null); + IPublishStatusQueryService publishStatusQueryService) + => content.Ancestors(navigationQueryService, GetPublishedStatusFilteringService(content)); - /// - /// Gets the ancestors of the content. - /// - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The ancestors of the content, in down-top order. - /// Does not consider the content itself. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Ancestors( this IPublishedContent content, IPublishedCache publishedCache, - INavigationQueryService navigationQueryService) => Ancestors( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService()); + INavigationQueryService navigationQueryService) + => content.Ancestors(navigationQueryService, GetPublishedStatusFilteringService(content)); /// /// Gets the ancestors of the content, at a level lesser or equal to a specified level. /// /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The level. /// The ancestors of the content, at a level lesser or equal to the specified level, in down-top order. /// Does not consider the content itself. Only content that are "high enough" in the tree are returned. + public static IEnumerable Ancestors( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int maxLevel) + => content.AncestorsOrSelf( + navigationQueryService, + publishedStatusFilteringService, + false, + n => n.Level <= maxLevel); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Ancestors( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, - int maxLevel) => - content.AncestorsOrSelf( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - false, - n => n.Level <= maxLevel); + int maxLevel) + => content.Ancestors(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); - /// - /// Gets the ancestors of the content, at a level lesser or equal to a specified level. - /// - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The level. - /// The ancestors of the content, at a level lesser or equal to the specified level, in down-top order. - /// Does not consider the content itself. Only content that are "high enough" in the tree are returned. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Ancestors( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - int maxLevel) => - Ancestors( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - maxLevel); - + int maxLevel) + => content.Ancestors(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); /// /// Gets the ancestors of the content, of a specified content type. /// /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The content type. /// The ancestors of the content, of the specified content type, in down-top order. /// Does not consider the content itself. Returns all ancestors, of the specified content type. + public static IEnumerable Ancestors( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string contentTypeAlias) + { + ArgumentNullException.ThrowIfNull(content); + + return content.EnumerateAncestorsOrSelfInternal( + navigationQueryService, + publishedStatusFilteringService, + false, + contentTypeAlias); + } + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Ancestors( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -628,82 +599,49 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string contentTypeAlias) - { - ArgumentNullException.ThrowIfNull(content); + => content.Ancestors(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias); - return content.EnumerateAncestorsOrSelfInternal( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - false, - contentTypeAlias); - } - - /// - /// Gets the ancestors of the content, of a specified content type. - /// - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The content type. - /// The ancestors of the content, of the specified content type, in down-top order. - /// Does not consider the content itself. Returns all ancestors, of the specified content type. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Ancestors( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias) => - Ancestors( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias); + string contentTypeAlias) + => content.Ancestors(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias); /// /// Gets the ancestors of the content, of a specified content type. /// /// The content type. /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The ancestors of the content, of the specified content type, in down-top order. /// Does not consider the content itself. Returns all ancestors, of the specified content type. + public static IEnumerable Ancestors( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) + where T : class, IPublishedContent + => content.Ancestors(navigationQueryService, publishedStatusFilteringService).OfType(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Ancestors( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService) - where T : class, IPublishedContent => - content.Ancestors(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService).OfType(); + where T : class, IPublishedContent + => content.Ancestors(navigationQueryService, GetPublishedStatusFilteringService(content)); - /// - /// Gets the ancestors of the content, of a specified content type. - /// - /// The content type. - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The ancestors of the content, of the specified content type, in down-top order. - /// Does not consider the content itself. Returns all ancestors, of the specified content type. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Ancestors( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService) - where T : class, IPublishedContent => - Ancestors( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService()); + where T : class, IPublishedContent + => content.Ancestors(navigationQueryService, GetPublishedStatusFilteringService(content)); /// /// Gets the ancestors of the content, at a level lesser or equal to a specified level, and of a specified content @@ -711,10 +649,7 @@ public static class PublishedContentExtensions /// /// The content type. /// The content. - /// - /// The content cache. - /// The query service for the in-memory navigation structure. - /// + /// /// The level. /// /// The ancestors of the content, at a level lesser or equal to the specified level, and of the specified @@ -724,6 +659,15 @@ public static class PublishedContentExtensions /// Does not consider the content itself. Only content that are "high enough" in the trees, and of the /// specified content type, are returned. /// + public static IEnumerable Ancestors( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int maxLevel) + where T : class, IPublishedContent + => content.Ancestors(navigationQueryService, publishedStatusFilteringService, maxLevel).OfType(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Ancestors( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -731,91 +675,53 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, int maxLevel) - where T : class, IPublishedContent => - content.Ancestors(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, maxLevel).OfType(); + where T : class, IPublishedContent + => content.Ancestors(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); - /// - /// Gets the ancestors of the content, at a level lesser or equal to a specified level, and of a specified content - /// type. - /// - /// The content type. - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The level. - /// - /// The ancestors of the content, at a level lesser or equal to the specified level, and of the specified - /// content type, in down-top order. - /// - /// - /// Does not consider the content itself. Only content that are "high enough" in the trees, and of the - /// specified content type, are returned. - /// - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Ancestors( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, int maxLevel) - where T : class, IPublishedContent => - Ancestors( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - maxLevel); + where T : class, IPublishedContent + => content.Ancestors(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); /// /// Gets the content and its ancestors. /// /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The content and its ancestors, in down-top order. + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) + => content.AncestorsOrSelf(navigationQueryService, publishedStatusFilteringService, true, null); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService) => - content.AncestorsOrSelf( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - true, - null); + IPublishStatusQueryService publishStatusQueryService) + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content)); - /// - /// Gets the content and its ancestors. - /// - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The content and its ancestors, in down-top order. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IPublishedCache publishedCache, - INavigationQueryService navigationQueryService) => - AncestorsOrSelf( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService()); + INavigationQueryService navigationQueryService) + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content)); /// /// Gets the content and its ancestors, at a level lesser or equal to a specified level. /// /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The level. /// /// The content and its ancestors, at a level lesser or equal to the specified level, @@ -825,61 +731,60 @@ public static class PublishedContentExtensions /// Only content that are "high enough" in the tree are returned. So it may or may not begin /// with the content itself, depending on its level. /// + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int maxLevel) + => content.AncestorsOrSelf( + navigationQueryService, + publishedStatusFilteringService, + true, + n => n.Level <= maxLevel); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, - int maxLevel) => - content.AncestorsOrSelf( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - true, - n => n.Level <= maxLevel); + int maxLevel) + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); - /// - /// Gets the content and its ancestors, at a level lesser or equal to a specified level. - /// - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The level. - /// - /// The content and its ancestors, at a level lesser or equal to the specified level, - /// in down-top order. - /// - /// - /// Only content that are "high enough" in the tree are returned. So it may or may not begin - /// with the content itself, depending on its level. - /// - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - int maxLevel) => - AncestorsOrSelf( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - maxLevel); + int maxLevel) + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); /// /// Gets the content and its ancestors, of a specified content type. /// /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The content type. /// The content and its ancestors, of the specified content type, in down-top order. /// May or may not begin with the content itself, depending on its content type. + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string contentTypeAlias) + { + ArgumentNullException.ThrowIfNull(content); + + return content.EnumerateAncestorsOrSelfInternal( + navigationQueryService, + publishedStatusFilteringService, + true, + contentTypeAlias); + } + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -887,81 +792,49 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string contentTypeAlias) - { - ArgumentNullException.ThrowIfNull(content); + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias); - return content.EnumerateAncestorsOrSelfInternal( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - true, - contentTypeAlias); - } - - /// - /// Gets the content and its ancestors, of a specified content type. - /// - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The content type. - /// The content and its ancestors, of the specified content type, in down-top order. - /// May or may not begin with the content itself, depending on its content type. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias) => - AncestorsOrSelf( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias); + string contentTypeAlias) + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias); /// /// Gets the content and its ancestors, of a specified content type. /// /// The content type. /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The content and its ancestors, of the specified content type, in down-top order. /// May or may not begin with the content itself, depending on its content type. + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) + where T : class, IPublishedContent + => content.AncestorsOrSelf(navigationQueryService, publishedStatusFilteringService).OfType(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService) - where T : class, IPublishedContent => - content.AncestorsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService).OfType(); + where T : class, IPublishedContent + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content)); - /// - /// Gets the content and its ancestors, of a specified content type. - /// - /// The content type. - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The content and its ancestors, of the specified content type, in down-top order. - /// May or may not begin with the content itself, depending on its content type. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService) - where T : class, IPublishedContent => AncestorsOrSelf( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService()); + where T : class, IPublishedContent + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content)); /// /// Gets the content and its ancestor, at a lever lesser or equal to a specified level, and of a specified content @@ -969,16 +842,23 @@ public static class PublishedContentExtensions /// /// The content type. /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The level. /// /// The content and its ancestors, at a level lesser or equal to the specified level, and of the specified /// content type, in down-top order. /// /// May or may not begin with the content itself, depending on its level and content type. + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int maxLevel) + where T : class, IPublishedContent + => content.AncestorsOrSelf(navigationQueryService, publishedStatusFilteringService, maxLevel).OfType(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -986,210 +866,175 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, int maxLevel) - where T : class, IPublishedContent => - content.AncestorsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, maxLevel).OfType(); + where T : class, IPublishedContent + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); - /// - /// Gets the content and its ancestor, at a lever lesser or equal to a specified level, and of a specified content - /// type. - /// - /// The content type. - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The level. - /// - /// The content and its ancestors, at a level lesser or equal to the specified level, and of the specified - /// content type, in down-top order. - /// - /// May or may not begin with the content itself, depending on its level and content type. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, int maxLevel) - where T : class, IPublishedContent => AncestorsOrSelf( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - maxLevel); + where T : class, IPublishedContent + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); /// /// Gets the ancestor of the content, ie its parent. /// /// The content. - /// The content cache. /// The query service for the in-memory navigation structure. + /// /// The ancestor of the content. /// This method is here for consistency purposes but does not make much sense. + public static IPublishedContent? Ancestor( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) + => content.GetParent(navigationQueryService, publishedStatusFilteringService); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? Ancestor( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService) - => content.GetParent(publishedCache, navigationQueryService); + => content.Ancestor(navigationQueryService, GetPublishedStatusFilteringService(content)); /// /// Gets the nearest ancestor of the content, at a lever lesser or equal to a specified level. /// /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// The query service for the in-memory navigation structure. + /// /// The level. /// The nearest (in down-top order) ancestor of the content, at a level lesser or equal to the specified level. /// Does not consider the content itself. May return null. + public static IPublishedContent? Ancestor( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int maxLevel) + => content + .EnumerateAncestors(navigationQueryService, publishedStatusFilteringService, false) + .FirstOrDefault(x => x.Level <= maxLevel); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? Ancestor( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, - int maxLevel) => - content.EnumerateAncestors( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - false).FirstOrDefault(x => x.Level <= maxLevel); + int maxLevel) + => content.Ancestor(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); - /// - /// Gets the nearest ancestor of the content, at a lever lesser or equal to a specified level. - /// - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The level. - /// The nearest (in down-top order) ancestor of the content, at a level lesser or equal to the specified level. - /// Does not consider the content itself. May return null. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? Ancestor( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - int maxLevel) => - Ancestor( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - maxLevel); + int maxLevel) + => content.Ancestor(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); /// /// Gets the nearest ancestor of the content, of a specified content type. /// /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The content type alias. /// The nearest (in down-top order) ancestor of the content, of the specified content type. /// Does not consider the content itself. May return null. public static IPublishedContent? Ancestor( this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, string contentTypeAlias) { ArgumentNullException.ThrowIfNull(content); return content .EnumerateAncestorsOrSelfInternal( - variationContextAccessor, - publishedCache, navigationQueryService, - publishStatusQueryService, + publishedStatusFilteringService, false, - contentTypeAlias).FirstOrDefault(); + contentTypeAlias) + .FirstOrDefault(); } - /// - /// Gets the nearest ancestor of the content, of a specified content type. - /// - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The content type alias. - /// The nearest (in down-top order) ancestor of the content, of the specified content type. - /// Does not consider the content itself. May return null. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IPublishedContent? Ancestor( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + IPublishStatusQueryService publishStatusQueryService, + string contentTypeAlias) + => content.Ancestor(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? Ancestor( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias) => - Ancestor( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias); + string contentTypeAlias) + => content.Ancestor(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias); /// /// Gets the nearest ancestor of the content, of a specified content type. /// /// The content type. /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The nearest (in down-top order) ancestor of the content, of the specified content type. /// Does not consider the content itself. May return null. + public static T? Ancestor( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) + where T : class, IPublishedContent + => content.Ancestors(navigationQueryService, publishedStatusFilteringService).FirstOrDefault(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? Ancestor( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService) - where T : class, IPublishedContent => - content.Ancestors(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService).FirstOrDefault(); + where T : class, IPublishedContent + => content.Ancestor(navigationQueryService, GetPublishedStatusFilteringService(content)); - /// - /// Gets the nearest ancestor of the content, of a specified content type. - /// - /// The content type. - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The nearest (in down-top order) ancestor of the content, of the specified content type. - /// Does not consider the content itself. May return null. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? Ancestor( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService) - where T : class, IPublishedContent => - Ancestor( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService()); + where T : class, IPublishedContent + => content.Ancestor(navigationQueryService, GetPublishedStatusFilteringService(content)); /// /// Gets the nearest ancestor of the content, at the specified level and of the specified content type. /// /// The content type. /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The level. /// The ancestor of the content, at the specified level and of the specified content type. /// /// Does not consider the content itself. If the ancestor at the specified level is /// not of the specified type, returns null. /// + public static T? Ancestor( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int maxLevel) + where T : class, IPublishedContent + => content.Ancestors(navigationQueryService, publishedStatusFilteringService, maxLevel).FirstOrDefault(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? Ancestor( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -1197,41 +1042,17 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, int maxLevel) - where T : class, IPublishedContent => - content.Ancestors( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - maxLevel).FirstOrDefault(); + where T : class, IPublishedContent + => content.Ancestor(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); - /// - /// Gets the nearest ancestor of the content, at the specified level and of the specified content type. - /// - /// The content type. - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The level. - /// The ancestor of the content, at the specified level and of the specified content type. - /// - /// Does not consider the content itself. If the ancestor at the specified level is - /// not of the specified type, returns null. - /// - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? Ancestor( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, int maxLevel) - where T : class, IPublishedContent => - Ancestor( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - maxLevel); + where T : class, IPublishedContent + => content.Ancestor(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); /// /// Gets the content or its nearest ancestor. @@ -1245,62 +1066,65 @@ public static class PublishedContentExtensions /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level. /// /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// The query service for the in-memory navigation structure. + /// /// The level. /// The content or its nearest (in down-top order) ancestor, at a level lesser or equal to the specified level. /// May or may not return the content itself depending on its level. May return null. + public static IPublishedContent AncestorOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int maxLevel) + => content + .EnumerateAncestors(navigationQueryService, publishedStatusFilteringService, true) + .FirstOrDefault(x => x.Level <= maxLevel) ?? content; + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent AncestorOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, - int maxLevel) => - content.EnumerateAncestors( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - true) - .FirstOrDefault(x => x.Level <= maxLevel) ?? content; + int maxLevel) + => content.AncestorOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); - /// - /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level. - /// - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The level. - /// The content or its nearest (in down-top order) ancestor, at a level lesser or equal to the specified level. - /// May or may not return the content itself depending on its level. May return null. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent AncestorOrSelf( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - int maxLevel) => - AncestorOrSelf( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - maxLevel); + int maxLevel) + => content.AncestorOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); /// /// Gets the content or its nearest ancestor, of a specified content type. /// /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The content type. /// The content or its nearest (in down-top order) ancestor, of the specified content type. /// May or may not return the content itself depending on its content type. May return null. + public static IPublishedContent AncestorOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string contentTypeAlias) + { + ArgumentNullException.ThrowIfNull(content); + + return content + .EnumerateAncestorsOrSelfInternal( + navigationQueryService, + publishedStatusFilteringService, + true, + contentTypeAlias) + .FirstOrDefault() ?? content; + } + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent AncestorOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -1308,83 +1132,49 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string contentTypeAlias) - { - ArgumentNullException.ThrowIfNull(content); + => content.AncestorOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias); - return content - .EnumerateAncestorsOrSelfInternal( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - true, - contentTypeAlias) - .FirstOrDefault() ?? content; - } - - /// - /// Gets the content or its nearest ancestor, of a specified content type. - /// - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The content type. - /// The content or its nearest (in down-top order) ancestor, of the specified content type. - /// May or may not return the content itself depending on its content type. May return null. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent AncestorOrSelf( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string contentTypeAlias) => AncestorOrSelf( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias); + string contentTypeAlias) + => content.AncestorOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias); /// /// Gets the content or its nearest ancestor, of a specified content type. /// /// The content type. /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The content or its nearest (in down-top order) ancestor, of the specified content type. /// May or may not return the content itself depending on its content type. May return null. + public static T? AncestorOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) + where T : class, IPublishedContent => + content.AncestorsOrSelf(navigationQueryService, publishedStatusFilteringService).FirstOrDefault(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? AncestorOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService) - where T : class, IPublishedContent => - content.AncestorsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService).FirstOrDefault(); + where T : class, IPublishedContent + => content.AncestorOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content)); - /// - /// Gets the content or its nearest ancestor, of a specified content type. - /// - /// The content type. - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The content or its nearest (in down-top order) ancestor, of the specified content type. - /// May or may not return the content itself depending on its content type. May return null. - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? AncestorOrSelf( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService) - where T : class, IPublishedContent => - AncestorOrSelf( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService()); + where T : class, IPublishedContent + => content.AncestorOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content)); /// /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level, and of a specified @@ -1392,12 +1182,19 @@ public static class PublishedContentExtensions /// /// The content type. /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// The level. /// + public static T? AncestorOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int maxLevel) + where T : class, IPublishedContent + => content.AncestorsOrSelf(navigationQueryService, publishedStatusFilteringService, maxLevel).FirstOrDefault(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? AncestorOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -1405,40 +1202,33 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, int maxLevel) - where T : class, IPublishedContent => - content.AncestorsOrSelf( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - maxLevel) - .FirstOrDefault(); + where T : class, IPublishedContent + => content.AncestorOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); - /// - /// Gets the content or its nearest ancestor, at a lever lesser or equal to a specified level, and of a specified - /// content type. - /// - /// The content type. - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// The level. - /// - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? AncestorOrSelf( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, int maxLevel) - where T : class, IPublishedContent => - AncestorOrSelf( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - maxLevel); + where T : class, IPublishedContent + => content.AncestorOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), maxLevel); + public static IEnumerable AncestorsOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + bool orSelf, + Func? func) + { + IEnumerable ancestorsOrSelf = content.EnumerateAncestors( + navigationQueryService, + publishedStatusFilteringService, + orSelf); + return func == null ? ancestorsOrSelf : ancestorsOrSelf.Where(func); + } + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -1447,53 +1237,26 @@ public static class PublishedContentExtensions IPublishStatusQueryService publishStatusQueryService, bool orSelf, Func? func) - { - IEnumerable ancestorsOrSelf = content.EnumerateAncestors( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - orSelf); - return func == null ? ancestorsOrSelf : ancestorsOrSelf.Where(func); - } + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), orSelf, func); - [Obsolete("Use the overload with IVariationContextAccessor and IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable AncestorsOrSelf( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, bool orSelf, - Func? func) => - AncestorsOrSelf( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - orSelf, - func); + Func? func) + => content.AncestorsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), orSelf, func); - /// - /// Enumerates ancestors of the content, bottom-up. - /// - /// The content. - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// - /// Indicates whether the content should be included. - /// Enumerates bottom-up ie walking up the tree (parent, grand-parent, etc). - internal static IEnumerable EnumerateAncestors( + private static IEnumerable EnumerateAncestors( this IPublishedContent? content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, bool orSelf) { ArgumentNullException.ThrowIfNull(content); - return content.EnumerateAncestorsOrSelfInternal(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, orSelf); + return content.EnumerateAncestorsOrSelfInternal(navigationQueryService, publishedStatusFilteringService, orSelf); } #endregion @@ -1504,39 +1267,56 @@ public static class PublishedContentExtensions /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified . /// /// The content. - /// The content cache. /// The query service for the in-memory navigation structure. + /// /// Indicates whether the specified content should be included. /// /// The breadcrumbs (ancestors and self, top to bottom) for the specified . /// + public static IEnumerable Breadcrumbs( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + bool andSelf = true) => + content.AncestorsOrSelf(navigationQueryService, publishedStatusFilteringService, andSelf, null).Reverse(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Breadcrumbs( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - bool andSelf = true) => - content.AncestorsOrSelf(publishedCache, navigationQueryService, andSelf, null).Reverse(); + bool andSelf = true) + => content.Breadcrumbs(navigationQueryService, GetPublishedStatusFilteringService(content), andSelf); /// /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level /// higher or equal to . /// /// The content. - /// The content cache. /// The query service for the in-memory navigation structure. + /// /// The minimum level. /// Indicates whether the specified content should be included. /// /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher /// or equal to . /// + public static IEnumerable Breadcrumbs( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int minLevel, + bool andSelf = true) + => content.AncestorsOrSelf(navigationQueryService, publishedStatusFilteringService, andSelf, n => n.Level >= minLevel).Reverse(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Breadcrumbs( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, int minLevel, - bool andSelf = true) => - content.AncestorsOrSelf(publishedCache, navigationQueryService, andSelf, n => n.Level >= minLevel).Reverse(); + bool andSelf = true) + => content.Breadcrumbs(navigationQueryService, GetPublishedStatusFilteringService(content), minLevel, andSelf); /// /// Gets the breadcrumbs (ancestors and self, top to bottom) for the specified at a level @@ -1544,56 +1324,38 @@ public static class PublishedContentExtensions /// /// The root content type. /// The content. - /// The content cache. /// The query service for the in-memory navigation structure. + /// /// Indicates whether the specified content should be included. /// /// The breadcrumbs (ancestors and self, top to bottom) for the specified at a level higher /// or equal to the specified root content type . /// + public static IEnumerable Breadcrumbs( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + bool andSelf = true) + where T : class, IPublishedContent + => content + .AncestorsOrSelf(navigationQueryService, publishedStatusFilteringService, andSelf, null) + .TakeWhile(n => n is T) + .Reverse(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Breadcrumbs( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, bool andSelf = true) where T : class, IPublishedContent - { - static IEnumerable TakeUntil(IEnumerable source, Func predicate) - { - foreach (IPublishedContent item in source) - { - yield return item; - if (predicate(item)) - { - yield break; - } - } - } - - return TakeUntil(content.AncestorsOrSelf(publishedCache, navigationQueryService, andSelf, null), n => n is T).Reverse(); - } + => content.Breadcrumbs(navigationQueryService, GetPublishedStatusFilteringService(content), andSelf); #endregion #region Axes: descendants, descendants-or-self - /// - /// Returns all DescendantsOrSelf of all content referenced - /// - /// - /// Variation context accessor. - /// - /// - /// - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// - /// - /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot - /// + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelfOfType( this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, @@ -1601,58 +1363,34 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string docTypeAlias, - string? culture = null) => parentNodes.SelectMany(x => - x.DescendantsOrSelfOfType(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, docTypeAlias, culture)); - - /// - /// Returns all DescendantsOrSelf of all content referenced - /// - /// - /// Variation context accessor. - /// - /// - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// - /// - /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot - /// - [Obsolete("Use the overload with IPublishStatusQueryService instead, scheduled for removal in v17")] - public static IEnumerable DescendantsOrSelfOfType( - this IEnumerable parentNodes, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, - INavigationQueryService navigationQueryService, - string docTypeAlias, - string? culture = null) => - DescendantsOrSelfOfType( - parentNodes, - variationContextAccessor, - publishedCache, + string? culture = null) + { + IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray(); + return parentNodesAsArray.DescendantsOrSelfOfType( navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), + GetPublishedStatusFilteringService(parentNodesAsArray.First()), docTypeAlias, culture); + } - /// - /// Returns all DescendantsOrSelf of all content referenced - /// - /// - /// Variation context accessor. - /// - /// - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// - /// - /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot - /// + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IEnumerable DescendantsOrSelfOfType( + this IEnumerable parentNodes, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string docTypeAlias, + string? culture = null) + { + IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray(); + return parentNodesAsArray.DescendantsOrSelfOfType( + navigationQueryService, + GetPublishedStatusFilteringService(parentNodesAsArray.First()), + docTypeAlias, + culture); + } + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, @@ -1660,24 +1398,15 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - parentNodes.SelectMany(x => x.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, culture)); + where T : class, IPublishedContent + { + IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray(); + return parentNodesAsArray.DescendantsOrSelf( + navigationQueryService, + GetPublishedStatusFilteringService(parentNodesAsArray.First()), + culture); + } - /// - /// Returns all DescendantsOrSelf of all content referenced - /// - /// - /// Variation context accessor. - /// - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// - /// - /// - /// This can be useful in order to return all nodes in an entire site by a type when combined with TypedContentAtRoot - /// [Obsolete("Use the overload with IPublishStatusQueryService instead, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IEnumerable parentNodes, @@ -1685,14 +1414,15 @@ public static class PublishedContentExtensions IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? culture = null) - where T : class, IPublishedContent => - DescendantsOrSelf( - parentNodes, - variationContextAccessor, - publishedCache, + where T : class, IPublishedContent + { + IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray(); + return parentNodesAsArray.DescendantsOrSelf( navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), + GetPublishedStatusFilteringService(parentNodesAsArray.First()), culture); + } + // as per XPath 1.0 specs �2.2, // - the descendant axis contains the descendants of the context node; a descendant is a child or a child of a child and so on; thus @@ -1714,96 +1444,107 @@ public static class PublishedContentExtensions // - children and descendants occur before following siblings. public static IEnumerable Descendants( this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, - string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, false, null, culture); - - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] - public static IEnumerable Descendants( - this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, - INavigationQueryService navigationQueryService, - string? culture = null) => - Descendants( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - culture); + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + => content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, false, null, culture); + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, + string? culture = null) + => content.Descendants(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] + public static IEnumerable Descendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? culture = null) + => content.Descendants(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + + public static IEnumerable Descendants( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, int level, - string? culture = null) => - content.DescendantsOrSelf( - variationContextAccessor, - publishedCache, + string? culture = null) + => content.DescendantsOrSelf( navigationQueryService, - publishStatusQueryService, + publishedStatusFilteringService, false, p => p.Level >= level, culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] + public static IEnumerable Descendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + IPublishStatusQueryService publishStatusQueryService, + int level, + string? culture = null) + => content.Descendants(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, int level, - string? culture = null) => - Descendants( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); + string? culture = null) + => content.Descendants(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); public static IEnumerable DescendantsOfType( this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, string contentTypeAlias, - string? culture = null) => - content.EnumerateDescendantsOrSelfInternal( - variationContextAccessor, - publishedCache, + string? culture = null) + => content.EnumerateDescendantsOrSelfInternal( navigationQueryService, - publishStatusQueryService, + publishedStatusFilteringService, culture, false, contentTypeAlias); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] + public static IEnumerable DescendantsOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + IPublishStatusQueryService publishStatusQueryService, + string contentTypeAlias, + string? culture = null) + => content.DescendantsOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable DescendantsOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string contentTypeAlias, - string? culture = null) => - DescendantsOfType( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); + string? culture = null) + => content.DescendantsOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + public static IEnumerable Descendants( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + where T : class, IPublishedContent + => content.Descendants(navigationQueryService, publishedStatusFilteringService, culture).OfType(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -1811,19 +1552,29 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.Descendants(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, culture).OfType(); + where T : class, IPublishedContent + => content.Descendants(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? culture = null) - where T : class, IPublishedContent => - content.Descendants(variationContextAccessor, publishedCache, navigationQueryService, culture).OfType(); + where T : class, IPublishedContent + => content.Descendants(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + public static IEnumerable Descendants( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int level, + string? culture = null) + where T : class, IPublishedContent + => content.Descendants(navigationQueryService, publishedStatusFilteringService, level, culture).OfType(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -1832,10 +1583,10 @@ public static class PublishedContentExtensions IPublishStatusQueryService publishStatusQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - content.Descendants(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, level, culture).OfType(); + where T : class, IPublishedContent + => content.Descendants(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -1843,40 +1594,44 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - Descendants( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); + where T : class, IPublishedContent + => content.Descendants(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); + public static IEnumerable DescendantsOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + => content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, true, null, culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, - string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, true, null, culture); + string? culture = null) + => content.DescendantsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string? culture = null) => - DescendantsOrSelf( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - culture); + string? culture = null) + => content.DescendantsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + public static IEnumerable DescendantsOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int level, + string? culture = null) + => content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, true, p => p.Level >= level, culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -1884,60 +1639,62 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, int level, - string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, true, p => p.Level >= level, culture); + string? culture = null) + => content.DescendantsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, int level, - string? culture = null) => - DescendantsOrSelf( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); + string? culture = null) + => content.DescendantsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); public static IEnumerable DescendantsOrSelfOfType( this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, string contentTypeAlias, - string? culture = null) => - content.EnumerateDescendantsOrSelfInternal( - variationContextAccessor, - publishedCache, + string? culture = null) + => content.EnumerateDescendantsOrSelfInternal( navigationQueryService, - publishStatusQueryService, + publishedStatusFilteringService, culture, true, contentTypeAlias); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] + public static IEnumerable DescendantsOrSelfOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + IPublishStatusQueryService publishStatusQueryService, + string contentTypeAlias, + string? culture = null) + => content.DescendantsOrSelfOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string contentTypeAlias, - string? culture = null) => - DescendantsOrSelfOfType( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); + string? culture = null) + => content.DescendantsOrSelfOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + public static IEnumerable DescendantsOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + where T : class, IPublishedContent + => content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, culture).OfType(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -1945,24 +1702,29 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, culture).OfType(); + where T : class, IPublishedContent + => content.DescendantsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? culture = null) - where T : class, IPublishedContent => - content.DescendantsOrSelf( - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - culture); + where T : class, IPublishedContent + => content.DescendantsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + public static IEnumerable DescendantsOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int level, + string? culture = null) + where T : class, IPublishedContent + => content.DescendantsOrSelf(navigationQueryService, publishedStatusFilteringService, level, culture).OfType(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -1971,10 +1733,10 @@ public static class PublishedContentExtensions IPublishStatusQueryService publishStatusQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - content.DescendantsOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, level, culture).OfType(); + where T : class, IPublishedContent + => content.DescendantsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -1982,45 +1744,46 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - DescendantsOrSelf( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); + where T : class, IPublishedContent + => content.DescendantsOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); + public static IPublishedContent? Descendant( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + => content.Children(navigationQueryService, publishedStatusFilteringService, culture)?.FirstOrDefault(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, - string? culture = null) => - content.Children( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - culture)?.FirstOrDefault(); + string? culture = null) + => content.Descendant(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string? culture = null) => - Descendant( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - culture); + string? culture = null) + => content.Descendant(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + public static IPublishedContent? Descendant( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int level, + string? culture = null) + => content + .EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, false, culture) + .FirstOrDefault(x => x.Level == level); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2028,62 +1791,66 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, int level, - string? culture = null) => content - .EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, false, culture).FirstOrDefault(x => x.Level == level); + string? culture = null) + => content.Descendant(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, int level, - string? culture = null) => - content - .EnumerateDescendants( - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - false, - culture) - .FirstOrDefault(x => x.Level == level); + string? culture = null) + => content.Descendant(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); public static IPublishedContent? DescendantOfType( this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, string contentTypeAlias, - string? culture = null) => content + string? culture = null) + => content .EnumerateDescendantsOrSelfInternal( - variationContextAccessor, - publishedCache, navigationQueryService, - publishStatusQueryService, + publishedStatusFilteringService, culture, false, contentTypeAlias) .FirstOrDefault(); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] + public static IPublishedContent? DescendantOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + IPublishStatusQueryService publishStatusQueryService, + string contentTypeAlias, + string? culture = null) + => content.DescendantOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? DescendantOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string contentTypeAlias, - string? culture = null) => - DescendantOfType( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); + string? culture = null) + => content.DescendantOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + public static T? Descendant( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + where T : class, IPublishedContent + => content + .EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, false, culture) + .FirstOrDefault(x => x is T) as T; + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2091,25 +1858,29 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, false, culture).FirstOrDefault(x => x is T) as T; + where T : class, IPublishedContent + => content.Descendant(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? culture = null) - where T : class, IPublishedContent => - Descendant( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - culture); + where T : class, IPublishedContent + => content.Descendant(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + public static T? Descendant( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int level, + string? culture = null) + where T : class, IPublishedContent + => content.Descendant(navigationQueryService, publishedStatusFilteringService, level, culture) as T; + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2118,10 +1889,10 @@ public static class PublishedContentExtensions IPublishStatusQueryService publishStatusQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - content.Descendant(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, level, culture) as T; + where T : class, IPublishedContent + => content.Descendant(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2129,42 +1900,48 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - Descendant( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); + where T : class, IPublishedContent + => content.Descendant(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); public static IPublishedContent DescendantOrSelf( this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishStatusQueryService publishStatusQueryService, - string? culture = null) => - content.EnumerateDescendants( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + => content.EnumerateDescendants( + navigationQueryService, + publishedStatusFilteringService, true, culture) .FirstOrDefault() ?? content; - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, - string? culture = null) => - DescendantOrSelf( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - culture); + IPublishStatusQueryService publishStatusQueryService, + string? culture = null) + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] + public static IPublishedContent DescendantOrSelf( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + string? culture = null) + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + + public static IPublishedContent? DescendantOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int level, + string? culture = null) + => content + .EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, true, culture) + .FirstOrDefault(x => x.Level == level); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2172,33 +1949,36 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, int level, - string? culture = null) => content - .EnumerateDescendants( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - true, - culture) - .FirstOrDefault(x => x.Level == level); + string? culture = null) + => content.DescendantOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, int level, - string? culture = null) => - DescendantOrSelf( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); + string? culture = null) + => content.DescendantOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); + public static IPublishedContent? DescendantOrSelfOfType( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string contentTypeAlias, + string? culture = null) + => content + .EnumerateDescendantsOrSelfInternal( + navigationQueryService, + publishedStatusFilteringService, + culture, + true, + contentTypeAlias) + .FirstOrDefault(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? DescendantOrSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2206,34 +1986,30 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string contentTypeAlias, - string? culture = null) => content - .EnumerateDescendantsOrSelfInternal( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - culture, - true, - contentTypeAlias) - .FirstOrDefault(); + string? culture = null) + => content.DescendantOrSelfOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? DescendantOrSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string contentTypeAlias, - string? culture = null) => - DescendantOrSelfOfType( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); + string? culture = null) + => content.DescendantOrSelfOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + public static T? DescendantOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + where T : class, IPublishedContent + => content + .EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, true, culture) + .FirstOrDefault(x => x is T) as T; + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2241,25 +2017,29 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, true, culture).FirstOrDefault(x => x is T) as T; + where T : class, IPublishedContent + => content.DescendantOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? culture = null) - where T : class, IPublishedContent => - DescendantOrSelf( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - culture); + where T : class, IPublishedContent + => content.DescendantOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + public static T? DescendantOrSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + int level, + string? culture = null) + where T : class, IPublishedContent + => content.DescendantOrSelf(navigationQueryService, publishedStatusFilteringService, level, culture) as T; + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2268,10 +2048,10 @@ public static class PublishedContentExtensions IPublishStatusQueryService publishStatusQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - content.DescendantOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, level, culture) as T; + where T : class, IPublishedContent + => content.DescendantOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQuery instead, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2279,44 +2059,32 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - DescendantOrSelf( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); + where T : class, IPublishedContent + => content.DescendantOrSelf(navigationQueryService, GetPublishedStatusFilteringService(content), level, culture); - internal static IEnumerable DescendantsOrSelf( + private static IEnumerable DescendantsOrSelf( this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, bool orSelf, Func? func, - string? culture = null) => - content.EnumerateDescendants(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, orSelf, culture) - .Where(x => func == null || func(x)); + string? culture = null) + => content + .EnumerateDescendants(navigationQueryService, publishedStatusFilteringService, orSelf, culture) + .Where(x => func == null || func(x)); - internal static IEnumerable EnumerateDescendants( + private static IEnumerable EnumerateDescendants( this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, bool orSelf, string? culture = null) { ArgumentNullException.ThrowIfNull(content); foreach (IPublishedContent desc in content.EnumerateDescendantsOrSelfInternal( - variationContextAccessor, - publishedCache, navigationQueryService, - publishStatusQueryService, + publishedStatusFilteringService, culture, orSelf)) { @@ -2324,28 +2092,6 @@ public static class PublishedContentExtensions } } - internal static IEnumerable EnumerateDescendants( - this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, - INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, - string? culture = null) - { - yield return content; - - foreach (IPublishedContent desc in content.EnumerateDescendantsOrSelfInternal( - variationContextAccessor, - publishedCache, - navigationQueryService, - publishStatusQueryService, - culture, - false)) - { - yield return desc; - } - } - #endregion #region Axes: children @@ -2354,9 +2100,8 @@ public static class PublishedContentExtensions /// Gets the children of the content item. /// /// The content item. - /// /// - /// + /// /// /// The specific culture to get the URL children for. Default is null which will use the current culture in /// @@ -2379,6 +2124,14 @@ public static class PublishedContentExtensions /// However, if an empty string is specified only invariant children are returned. /// /// + public static IEnumerable Children( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + => GetChildren(navigationQueryService, publishedStatusFilteringService, content.Key, null, culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, @@ -2386,59 +2139,23 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - { - IEnumerable children = GetChildren(navigationQueryService, publishedCache, content.Key, publishStatusQueryService, variationContextAccessor, null, culture); + => content.Children(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - return children.FilterByCulture(culture, variationContextAccessor); - } - - /// - /// Gets the children of the content item. - /// - /// The content item. - /// - /// - /// - /// The specific culture to get the URL children for. Default is null which will use the current culture in - /// - /// - /// - /// - /// Gets children that are available for the specified culture. - /// Children are sorted by their sortOrder. - /// - /// For culture, - /// if null is used the current culture is used. - /// If an empty string is used only invariant children are returned. - /// If "*" is used all children are returned. - /// - /// - /// If a variant culture is specified or there is a current culture in the then the - /// Children returned - /// will include both the variant children matching the culture AND the invariant children because the invariant - /// children flow with the current culture. - /// However, if an empty string is specified only invariant children are returned. - /// - /// - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? culture = null) - { - IPublishStatusQueryService publishStatusQueryService = StaticServiceProvider.Instance.GetRequiredService(); - return Children(content, variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, culture); - } + => content.Children(navigationQueryService, GetPublishedStatusFilteringService(content), culture); /// /// Gets the children of the content, filtered by a predicate. /// /// The content. - /// The accessor for VariationContext /// - /// + /// /// The predicate. /// /// The specific culture to filter for. If null is used the current culture is used. (Default is @@ -2449,6 +2166,15 @@ public static class PublishedContentExtensions /// /// Children are sorted by their sortOrder. /// + public static IEnumerable Children( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + Func predicate, + string? culture = null) + => content.Children(navigationQueryService, publishedStatusFilteringService, culture).Where(predicate); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2456,90 +2182,61 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, Func predicate, - string? culture = null) => - content.Children(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, culture).Where(predicate); + string? culture = null) + => content.Children(navigationQueryService, GetPublishedStatusFilteringService(content), predicate, culture); - /// - /// Gets the children of the content, filtered by a predicate. - /// - /// The content. - /// The accessor for VariationContext - /// - /// The predicate. - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// - /// The children of the content, filtered by the predicate. - /// - /// Children are sorted by their sortOrder. - /// - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, Func predicate, - string? culture = null) => - content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture).Where(predicate); + string? culture = null) + => content.Children(navigationQueryService, GetPublishedStatusFilteringService(content), predicate, culture); /// /// Gets the children of the content, of any of the specified types. /// /// The content. - /// /// - /// The accessor for the VariationContext + /// + /// The content type alias. /// /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) /// - /// The content type alias. - /// The children of the content, of any of the specified types. - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] - public static IEnumerable ChildrenOfType( - this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, - INavigationQueryService navigationQueryService, - string? contentTypeAlias, - string? culture = null) - { - IPublishStatusQueryService publishStatusQueryService = StaticServiceProvider.Instance.GetRequiredService(); - return ChildrenOfType(content, variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, contentTypeAlias, culture); - } - - /// - /// Gets the children of the content, of any of the specified types. - /// - /// The content. - /// - /// - /// - /// The accessor for the VariationContext - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// The content type alias. /// The children of the content, of any of the specified types. public static IEnumerable ChildrenOfType( this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, string? contentTypeAlias, string? culture = null) - { - IEnumerable children = contentTypeAlias is not null - ? GetChildren(navigationQueryService, publishedCache, content.Key, publishStatusQueryService, variationContextAccessor, contentTypeAlias, culture) + => contentTypeAlias is not null + ? GetChildren(navigationQueryService, publishedStatusFilteringService, content.Key, contentTypeAlias, culture) : []; - return children.FilterByCulture(culture, variationContextAccessor); - } + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] + public static IEnumerable ChildrenOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + IPublishStatusQueryService publishStatusQueryService, + string? contentTypeAlias, + string? culture = null) + => content.ChildrenOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] + public static IEnumerable ChildrenOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + string? contentTypeAlias, + string? culture = null) + => content.ChildrenOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); /// /// Gets the children of the content, of a given content type. @@ -2558,6 +2255,15 @@ public static class PublishedContentExtensions /// /// Children are sorted by their sortOrder. /// + public static IEnumerable Children( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + where T : class, IPublishedContent + => content.Children(navigationQueryService, publishedStatusFilteringService, culture).OfType(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2565,56 +2271,61 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.Children(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, culture).OfType(); + where T : class, IPublishedContent + => content.Children(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - /// - /// Gets the children of the content, of a given content type. - /// - /// The content type. - /// The content. - /// The accessor for the VariationContext - /// - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// - /// The children of content, of the given content type. - /// - /// Children are sorted by their sortOrder. - /// - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? culture = null) - where T : class, IPublishedContent => - content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture).OfType(); + where T : class, IPublishedContent + => content.Children(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + public static IPublishedContent? FirstChild( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + => content + .Children(navigationQueryService, publishedStatusFilteringService, culture) + .FirstOrDefault(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, - string? culture = null) => - content.Children(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, culture)?.FirstOrDefault(); + string? culture = null) + => content.FirstChild(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - string? culture = null) => - FirstChild(content, variationContextAccessor, publishedCache, navigationQueryService, StaticServiceProvider.Instance.GetRequiredService(), culture); + string? culture = null) + => content.FirstChild(navigationQueryService, GetPublishedStatusFilteringService(content), culture); /// /// Gets the first child of the content, of a given content type. /// + public static IPublishedContent? FirstChildOfType( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string contentTypeAlias, + string? culture = null) + => content + .ChildrenOfType(navigationQueryService, publishedStatusFilteringService, contentTypeAlias, culture) + .FirstOrDefault(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? FirstChildOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2622,30 +2333,30 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string contentTypeAlias, - string? culture = null) => content - .ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, contentTypeAlias, culture) - .FirstOrDefault(); + string? culture = null) + => content.FirstChildOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); - /// - /// Gets the first child of the content, of a given content type. - /// - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? FirstChildOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string contentTypeAlias, - string? culture = null) => - FirstChildOfType( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); + string? culture = null) + => content.FirstChildOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + public static IPublishedContent? FirstChild( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + Func predicate, + string? culture = null) + => content + .Children(navigationQueryService, publishedStatusFilteringService, predicate, culture) + .FirstOrDefault(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2654,25 +2365,29 @@ public static class PublishedContentExtensions IPublishStatusQueryService publishStatusQueryService, Func predicate, string? culture = null) - => content.Children(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, predicate, culture)?.FirstOrDefault(); + => content.FirstChild(navigationQueryService, GetPublishedStatusFilteringService(content), predicate, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, Func predicate, - string? culture = null) => - FirstChild( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - predicate, - culture); + string? culture = null) + => content.FirstChild(navigationQueryService, GetPublishedStatusFilteringService(content), predicate, culture); + public static IPublishedContent? FirstChild( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + Guid uniqueId, + string? culture = null) + => content + .Children(navigationQueryService, publishedStatusFilteringService, x => x.Key == uniqueId, culture) + .FirstOrDefault(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2680,26 +2395,30 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, Guid uniqueId, - string? culture = null) => content - .Children(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, x => x.Key == uniqueId, culture)?.FirstOrDefault(); + string? culture = null) + => content.FirstChild(navigationQueryService, GetPublishedStatusFilteringService(content), uniqueId, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, Guid uniqueId, - string? culture = null) => - FirstChild( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - uniqueId, - culture); + string? culture = null) + => content.FirstChild(navigationQueryService, GetPublishedStatusFilteringService(content), uniqueId, culture); + public static T? FirstChild( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + where T : class, IPublishedContent + => content + .Children(navigationQueryService, publishedStatusFilteringService, culture) + .FirstOrDefault(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2707,19 +2426,31 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.Children(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, culture)?.FirstOrDefault(); + where T : class, IPublishedContent + => content.FirstChild(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? culture = null) - where T : class, IPublishedContent => - FirstChild(content, variationContextAccessor, publishedCache, navigationQueryService, StaticServiceProvider.Instance.GetRequiredService(), culture); + where T : class, IPublishedContent + => content.FirstChild(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + public static T? FirstChild( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + Func predicate, + string? culture = null) + where T : class, IPublishedContent + => content + .Children(navigationQueryService, publishedStatusFilteringService, culture) + .FirstOrDefault(predicate); + + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2728,10 +2459,10 @@ public static class PublishedContentExtensions IPublishStatusQueryService publishStatusQueryService, Func predicate, string? culture = null) - where T : class, IPublishedContent => - content.Children(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, culture)?.FirstOrDefault(predicate); + where T : class, IPublishedContent + => content.FirstChild(navigationQueryService, GetPublishedStatusFilteringService(content), predicate, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService instead, scheduled for removal in v17")] public static T? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2739,15 +2470,8 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, Func predicate, string? culture = null) - where T : class, IPublishedContent => - FirstChild( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - predicate, - culture); + where T : class, IPublishedContent + => content.FirstChild(navigationQueryService, GetPublishedStatusFilteringService(content), predicate, culture); #endregion @@ -2758,127 +2482,96 @@ public static class PublishedContentExtensions /// /// The content. /// The navigation service - /// Variation context accessor. - /// + /// /// /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) /// - /// The content cache instance. /// The siblings of the content. /// /// Note that in V7 this method also return the content node self. /// public static IEnumerable Siblings( this IPublishedContent content, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IVariationContextAccessor variationContextAccessor, - IPublishStatusQueryService publishStatusQueryService, - string? culture = null) => - SiblingsAndSelf(content, publishedCache, navigationQueryService, variationContextAccessor, publishStatusQueryService, culture) - ?.Where(x => x.Id != content.Id) ?? Enumerable.Empty(); - - /// - /// Gets the siblings of the content. - /// - /// The content. - /// The navigation service - /// Variation context accessor. - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// The content cache instance. - /// The siblings of the content. - /// - /// Note that in V7 this method also return the content node self. - /// - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] - public static IEnumerable Siblings( - this IPublishedContent content, - IPublishedCache publishedCache, - INavigationQueryService navigationQueryService, - IVariationContextAccessor variationContextAccessor, - string? culture = null) => - Siblings( - content, - publishedCache, - navigationQueryService, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - culture); - - /// - /// Gets the siblings of the content, of a given content type. - /// - /// The content. - /// Variation context accessor. - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// - /// - /// - /// The content type alias. - /// The siblings of the content, of the given content type. - /// - /// Note that in V7 this method also return the content node self. - /// - public static IEnumerable SiblingsOfType( - this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, - INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, - string contentTypeAlias, - string? culture = null) => - SiblingsAndSelfOfType(content, variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, contentTypeAlias, culture) + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + => content + .SiblingsAndSelf(navigationQueryService, publishedStatusFilteringService, culture) .Where(x => x.Id != content.Id); + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IEnumerable Siblings( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + IVariationContextAccessor variationContextAccessor, + IPublishStatusQueryService publishStatusQueryService, + string? culture = null) + => content.Siblings(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IEnumerable Siblings( + this IPublishedContent content, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + IVariationContextAccessor variationContextAccessor, + string? culture = null) + => content.Siblings(navigationQueryService, GetPublishedStatusFilteringService(content), culture); + /// /// Gets the siblings of the content, of a given content type. /// /// The content. - /// Variation context accessor. + /// + /// /// /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) /// - /// - /// /// The content type alias. /// The siblings of the content, of the given content type. /// /// Note that in V7 this method also return the content node self. /// - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + public static IEnumerable SiblingsOfType( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string contentTypeAlias, + string? culture = null) + => content + .SiblingsAndSelfOfType(navigationQueryService, publishedStatusFilteringService, contentTypeAlias, culture) + .Where(x => x.Id != content.Id); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IEnumerable SiblingsOfType( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + IPublishStatusQueryService publishStatusQueryService, + string contentTypeAlias, + string? culture = null) + => content.SiblingsOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable SiblingsOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string contentTypeAlias, - string? culture = null) => - SiblingsOfType( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); + string? culture = null) + => content.SiblingsOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); /// /// Gets the siblings of the content, of a given content type. /// /// The content type. /// The content. - /// Variation context accessor. - /// /// - /// + /// /// /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) @@ -2887,6 +2580,17 @@ public static class PublishedContentExtensions /// /// Note that in V7 this method also return the content node self. /// + public static IEnumerable Siblings( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + where T : class, IPublishedContent + => content + .SiblingsAndSelf(navigationQueryService, publishedStatusFilteringService, culture) + .Where(x => x.Id != content.Id); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Siblings( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -2894,126 +2598,77 @@ public static class PublishedContentExtensions INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - SiblingsAndSelf(content, variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, culture) - ?.Where(x => x.Id != content.Id) ?? Enumerable.Empty(); + where T : class, IPublishedContent + => content.Siblings(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - /// - /// Gets the siblings of the content, of a given content type. - /// - /// The content type. - /// The content. - /// Variation context accessor. - /// - /// - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// The siblings of the content, of the given content type. - /// - /// Note that in V7 this method also return the content node self. - /// - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Siblings( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? culture = null) - where T : class, IPublishedContent => - Siblings( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - culture); + where T : class, IPublishedContent + => content.Siblings(navigationQueryService, GetPublishedStatusFilteringService(content), culture); /// /// Gets the siblings of the content including the node itself to indicate the position. /// /// The content. - /// Cache instance. /// The navigation service. - /// Variation context accessor. - /// + /// /// /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) /// /// The siblings of the content including the node itself. - public static IEnumerable? SiblingsAndSelf( + public static IEnumerable SiblingsAndSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + => content.SiblingsAndSelfInternal(navigationQueryService, publishedStatusFilteringService, null, culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IEnumerable SiblingsAndSelf( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - { - var success = navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey); + => content.SiblingsAndSelf(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - if (success is false || parentKey is null) - { - if (navigationQueryService.TryGetRootKeys(out IEnumerable childrenKeys) is false) - { - return null; - } - - culture ??= variationContextAccessor.VariationContext?.Culture ?? string.Empty; - return childrenKeys - .Where(x => publishStatusQueryService.IsDocumentPublished(x , culture)) - .Select(publishedCache.GetById) - .WhereNotNull() - .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); - } - - return navigationQueryService.TryGetChildrenKeys(parentKey.Value, out IEnumerable siblingKeys) is false - ? null - : siblingKeys.Select(publishedCache.GetById).WhereNotNull(); - } - - /// - /// Gets the siblings of the content including the node itself to indicate the position. - /// - /// The content. - /// Cache instance. - /// The navigation service. - /// Variation context accessor. - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// The siblings of the content including the node itself. - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] - public static IEnumerable? SiblingsAndSelf( + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IEnumerable SiblingsAndSelf( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IVariationContextAccessor variationContextAccessor, - string? culture = null) => - SiblingsAndSelf( - content, - publishedCache, - navigationQueryService, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - culture); + string? culture = null) + => content.SiblingsAndSelf(navigationQueryService, GetPublishedStatusFilteringService(content), culture); /// /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. /// /// The content. - /// Variation context accessor. + /// + /// /// /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) /// - /// - /// /// The content type alias. - /// /// The siblings of the content including the node itself, of the given content type. + public static IEnumerable SiblingsAndSelfOfType( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string contentTypeAlias, + string? culture = null) + => content.SiblingsAndSelfInternal(navigationQueryService, publishedStatusFilteringService, contentTypeAlias, culture); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable SiblingsAndSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -3022,75 +2677,41 @@ public static class PublishedContentExtensions IPublishStatusQueryService publishStatusQueryService, string contentTypeAlias, string? culture = null) - { - var parentExists = navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey); + => content.SiblingsAndSelfOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); - IPublishedContent? parent = parentKey is null - ? null - : publishedCache.GetById(parentKey.Value); - - if (parentExists && parent is not null) - { - return parent.ChildrenOfType(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, contentTypeAlias, culture); - } - - if (navigationQueryService.TryGetRootKeysOfType(contentTypeAlias, out IEnumerable rootKeysOfType) is false) - { - return []; - } - - culture ??= variationContextAccessor.VariationContext?.Culture ?? string.Empty; - return rootKeysOfType - .Where(x => publishStatusQueryService.IsDocumentPublished(x, culture)) - .Select(publishedCache.GetById) - .WhereNotNull() - .WhereIsInvariantOrHasCulture(variationContextAccessor, culture); - } - - /// - /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. - /// - /// The content. - /// Variation context accessor. - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// - /// The content type alias. - /// - /// The siblings of the content including the node itself, of the given content type. - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable SiblingsAndSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string contentTypeAlias, - string? culture = null) => - SiblingsAndSelfOfType( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); + string? culture = null) + => content.SiblingsAndSelfOfType(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeAlias, culture); /// /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. /// /// The content type. /// The content. - /// Variation context accessor. /// - /// + /// /// /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) /// - /// /// The siblings of the content including the node itself, of the given content type. + public static IEnumerable SiblingsAndSelf( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + where T : class, IPublishedContent + => content + .SiblingsAndSelfInternal(navigationQueryService, publishedStatusFilteringService, null, culture) + .OfType(); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable SiblingsAndSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -3099,57 +2720,17 @@ public static class PublishedContentExtensions IPublishStatusQueryService publishStatusQueryService, string? culture = null) where T : class, IPublishedContent - { - var parentSuccess = navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey); - IPublishedContent? parent = parentKey is null ? null : publishedCache.GetById(parentKey.Value); + => content.SiblingsAndSelf(navigationQueryService, GetPublishedStatusFilteringService(content), culture); - if (parentSuccess is false || parent is null) - { - var rootSuccess = navigationQueryService.TryGetRootKeys(out IEnumerable rootKeys); - if (rootSuccess is false) - { - return []; - } - - culture ??= variationContextAccessor.VariationContext?.Culture ?? string.Empty; - return rootKeys - .Where(x => publishStatusQueryService.IsDocumentPublished(x, culture)) - .Select(publishedCache.GetById) - .WhereNotNull() - .WhereIsInvariantOrHasCulture(variationContextAccessor, culture) - .OfType(); - } - - return parent.Children(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, culture); - } - - /// - /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. - /// - /// The content type. - /// The content. - /// Variation context accessor. - /// - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// - /// The siblings of the content including the node itself, of the given content type. - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable SiblingsAndSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, string? culture = null) - where T : class, IPublishedContent => SiblingsAndSelf( - content, - variationContextAccessor, - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService(), - culture); + where T : class, IPublishedContent + => content.SiblingsAndSelf(navigationQueryService, GetPublishedStatusFilteringService(content), culture); #endregion @@ -3159,10 +2740,8 @@ public static class PublishedContentExtensions /// Gets the root content (ancestor or self at level 1) for the specified . /// /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// /// The root content (ancestor or self at level 1) for the specified . /// @@ -3171,37 +2750,27 @@ public static class PublishedContentExtensions /// with maxLevel /// set to 1. /// + public static IPublishedContent Root( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) + => content.AncestorOrSelf(navigationQueryService, publishedStatusFilteringService, 1); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent Root( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService) => content.AncestorOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, 1); + IPublishStatusQueryService publishStatusQueryService) + => content.Root(navigationQueryService, GetPublishedStatusFilteringService(content)); - /// - /// Gets the root content (ancestor or self at level 1) for the specified . - /// - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// - /// The root content (ancestor or self at level 1) for the specified . - /// - /// - /// This is the same as calling - /// with maxLevel - /// set to 1. - /// - [Obsolete("Use the overload with IVariationContextAccessor & IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent Root( this IPublishedContent content, IPublishedCache publishedCache, - INavigationQueryService navigationQueryService) => Root( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService()); + INavigationQueryService navigationQueryService) + => content.Root(navigationQueryService, GetPublishedStatusFilteringService(content)); /// /// Gets the root content (ancestor or self at level 1) for the specified if it's of the @@ -3209,10 +2778,8 @@ public static class PublishedContentExtensions /// /// The content type. /// The content. - /// - /// The content cache. /// The query service for the in-memory navigation structure. - /// + /// /// /// The root content (ancestor or self at level 1) for the specified of content type /// . @@ -3222,44 +2789,30 @@ public static class PublishedContentExtensions /// with /// maxLevel set to 1. /// + public static T? Root( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) + where T : class, IPublishedContent + => content.AncestorOrSelf(navigationQueryService, publishedStatusFilteringService, 1); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? Root( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService) - where T : class, IPublishedContent => - content.AncestorOrSelf(variationContextAccessor, publishedCache, navigationQueryService, publishStatusQueryService, 1); + where T : class, IPublishedContent + => content.Root(navigationQueryService, GetPublishedStatusFilteringService(content)); - /// - /// Gets the root content (ancestor or self at level 1) for the specified if it's of the - /// specified content type . - /// - /// The content type. - /// The content. - /// The content cache. - /// The query service for the in-memory navigation structure. - /// - /// The root content (ancestor or self at level 1) for the specified of content type - /// . - /// - /// - /// This is the same as calling - /// with - /// maxLevel set to 1. - /// - [Obsolete("Use the overload with IVariationContextAccessor & PublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? Root( this IPublishedContent content, IPublishedCache publishedCache, INavigationQueryService navigationQueryService) - where T : class, IPublishedContent => - Root( - content, - StaticServiceProvider.Instance.GetRequiredService(), - publishedCache, - navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService()); + where T : class, IPublishedContent + => content.Root(navigationQueryService, GetPublishedStatusFilteringService(content)); #endregion @@ -3285,8 +2838,8 @@ public static class PublishedContentExtensions /// Gets the children of the content in a DataTable. /// /// The content. - /// Variation context accessor. /// + /// /// The content type service. /// The media type service. /// The member type service. @@ -3296,9 +2849,21 @@ public static class PublishedContentExtensions /// The specific culture to filter for. If null is used the current culture is used. (Default is /// null) /// - /// /// The children of the content. [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] + public static DataTable ChildrenAsTable( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + IContentTypeService contentTypeService, + IMediaTypeService mediaTypeService, + IMemberTypeService memberTypeService, + IPublishedUrlProvider publishedUrlProvider, + string contentTypeAliasFilter = "", + string? culture = null) + => content.GenerateDataTable(navigationQueryService, publishedStatusFilteringService, contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); + + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static DataTable ChildrenAsTable( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, @@ -3310,30 +2875,12 @@ public static class PublishedContentExtensions IPublishedUrlProvider publishedUrlProvider, string contentTypeAliasFilter = "", string? culture = null) - => GenerateDataTable(content, variationContextAccessor, publishedCache, navigationQueryService, contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); + => content.GenerateDataTable(navigationQueryService, GetPublishedStatusFilteringService(content), contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); - /// - /// Gets the children of the content in a DataTable. - /// - /// The content. - /// Variation context accessor. - /// - /// The content type service. - /// The media type service. - /// The member type service. - /// The published url provider. - /// An optional content type alias. - /// - /// The specific culture to filter for. If null is used the current culture is used. (Default is - /// null) - /// - /// - /// The children of the content. private static DataTable GenerateDataTable( - IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, + this IPublishedContent content, INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, IContentTypeService contentTypeService, IMediaTypeService mediaTypeService, IMemberTypeService memberTypeService, @@ -3341,12 +2888,12 @@ public static class PublishedContentExtensions string contentTypeAliasFilter = "", string? culture = null) { + IPublishedContent[] children = content.Children(navigationQueryService, publishedStatusFilteringService, culture).ToArray(); IPublishedContent? firstNode = contentTypeAliasFilter.IsNullOrWhiteSpace() - ? content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture)?.Any() ?? false - ? content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture)?.ElementAt(0) + ? children.Length > 0 + ? children.ElementAt(0) : null - : content.Children(variationContextAccessor, publishedCache, navigationQueryService, culture) - ?.FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAliasFilter)); + : children.FirstOrDefault(x => x.ContentType.Alias.InvariantEquals(contentTypeAliasFilter)); if (firstNode == null) { // No children found @@ -3368,7 +2915,7 @@ public static class PublishedContentExtensions List>, IEnumerable>>> tableData = DataTableExtensions.CreateTableData(); IOrderedEnumerable? children = - content.Children(variationContextAccessor, publishedCache, navigationQueryService)?.OrderBy(x => x.SortOrder); + content.Children(navigationQueryService, publishedStatusFilteringService).OrderBy(x => x.SortOrder); if (children is not null) { // loop through each child and create row data for it @@ -3385,7 +2932,7 @@ public static class PublishedContentExtensions var standardVals = new Dictionary { { "Id", n.Id }, - { "NodeName", n.Name(variationContextAccessor) }, + { "NodeName", n.Name(null, culture) }, { "NodeTypeAlias", n.ContentType.Alias }, { "CreateDate", n.CreateDate }, { "UpdateDate", n.UpdateDate }, @@ -3465,181 +3012,135 @@ public static class PublishedContentExtensions #endregion - public static IPublishedContent? Ancestor(this IPublishedContent content, int maxLevel) - { - return content.Ancestor(GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); - } - + => content.Ancestor(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel); public static IPublishedContent? Ancestor(this IPublishedContent content, string contentTypeAlias) - { - return content.Ancestor(GetPublishedCache(content), GetNavigationQueryService(content), contentTypeAlias); - } - + => content.Ancestor(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias); public static T? Ancestor(this IPublishedContent content, int maxLevel) where T : class, IPublishedContent - { - return Ancestor(content, GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); - } - + => content.Ancestor(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel); public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) - { - return content.Ancestors(GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); - } - + => content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel); public static IEnumerable Ancestors(this IPublishedContent content, string contentTypeAlias) - { - return content.Ancestors(GetPublishedCache(content), GetNavigationQueryService(content), contentTypeAlias); - } - + => content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias); public static IEnumerable Ancestors(this IPublishedContent content) where T : class, IPublishedContent - { - return Ancestors(content, GetPublishedCache(content), GetNavigationQueryService(content)); - } - + => content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content)); public static IEnumerable Ancestors(this IPublishedContent content, int maxLevel) where T : class, IPublishedContent - { - return Ancestors(content, GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); - } + => content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel); public static IPublishedContent AncestorOrSelf(this IPublishedContent content, int maxLevel) - { - return AncestorOrSelf(content, GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); - } + => content.AncestorOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel); public static IPublishedContent AncestorOrSelf(this IPublishedContent content, string contentTypeAlias) - { - return AncestorOrSelf(content, GetPublishedCache(content), GetNavigationQueryService(content), contentTypeAlias); - } + => content.AncestorOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias); public static T? AncestorOrSelf(this IPublishedContent content, int maxLevel) where T : class, IPublishedContent - { - return AncestorOrSelf(content, GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); - } + => content.AncestorOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel); public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) - { - return content.AncestorsOrSelf(GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); - } + => content.AncestorsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel); public static IEnumerable AncestorsOrSelf(this IPublishedContent content, string contentTypeAlias) - { - return content.Ancestors(GetPublishedCache(content), GetNavigationQueryService(content), contentTypeAlias); - } + => content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias); public static IEnumerable AncestorsOrSelf(this IPublishedContent content, int maxLevel) where T : class, IPublishedContent - { - return AncestorsOrSelf(content, GetPublishedCache(content), GetNavigationQueryService(content), maxLevel); - } + => content.AncestorsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), maxLevel); - public static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, - Func? func) - { - return AncestorsOrSelf(content, GetPublishedCache(content), GetNavigationQueryService(content), orSelf, func); - } + public static IEnumerable AncestorsOrSelf(this IPublishedContent content, bool orSelf, Func? func) + => content.AncestorsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), orSelf, func); public static IEnumerable Breadcrumbs( this IPublishedContent content, bool andSelf = true) => - content.Breadcrumbs(GetPublishedCache(content), GetNavigationQueryService(content), andSelf); + content.Breadcrumbs(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), andSelf); public static IEnumerable Breadcrumbs( this IPublishedContent content, int minLevel, bool andSelf = true) => - content.Breadcrumbs(GetPublishedCache(content), GetNavigationQueryService(content), minLevel, andSelf); + content.Breadcrumbs(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), minLevel, andSelf); public static IEnumerable Breadcrumbs( this IPublishedContent content, bool andSelf = true) where T : class, IPublishedContent=> - content.Breadcrumbs(GetPublishedCache(content), GetNavigationQueryService(content), andSelf); + content.Breadcrumbs(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), andSelf); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - => Children(content, variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), publishStatusQueryService, culture); + => content.Children(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor? variationContextAccessor, string? culture = null) - => Children(content, variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), StaticServiceProvider.Instance.GetRequiredService(), culture); + => content.Children(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, Func predicate, - string? culture = null) => - content.Children(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), publishStatusQueryService, culture).Where(predicate); + string? culture = null) + => content.Children(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, - string? culture = null) => - Children( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - predicate, - culture); + string? culture = null) + => content.Children(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable ChildrenOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string? contentTypeAlias, string? culture = null) - { - IEnumerable children = contentTypeAlias is not null - ? GetChildren(GetNavigationQueryService(content), GetPublishedCache(content), content.Key, publishStatusQueryService, variationContextAccessor, contentTypeAlias, culture) - : []; + => content.ChildrenOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); - return children.FilterByCulture(culture, variationContextAccessor); - } - - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable ChildrenOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? contentTypeAlias, string? culture = null) - { - IPublishStatusQueryService publishStatusQueryService = StaticServiceProvider.Instance.GetRequiredService(); - return ChildrenOfType(content, variationContextAccessor, publishStatusQueryService, contentTypeAlias, culture); - } + => content.ChildrenOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.Children(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), publishStatusQueryService, culture).OfType(); + where T : class, IPublishedContent + => content.Children(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Children( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent => - Children(content, variationContextAccessor, StaticServiceProvider.Instance.GetRequiredService(), culture); + where T : class, IPublishedContent + => content.Children(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static DataTable ChildrenAsTable( @@ -3651,760 +3152,576 @@ public static class PublishedContentExtensions IPublishedUrlProvider publishedUrlProvider, string contentTypeAliasFilter = "", string? culture = null) - => GenerateDataTable(content, variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); + => content.GenerateDataTable(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeService, mediaTypeService, memberTypeService, publishedUrlProvider, contentTypeAliasFilter, culture); public static IEnumerable DescendantsOrSelfOfType( this IEnumerable parentNodes, - IVariationContextAccessor variationContextAccessor, - IPublishStatusQueryService publishStatusQueryService, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, string docTypeAlias, - string? culture = null) => - parentNodes.SelectMany(x => x.DescendantsOrSelfOfType( - variationContextAccessor, - GetPublishedCache(parentNodes.First()), - GetNavigationQueryService(parentNodes.First()), - publishStatusQueryService, + string? culture = null) + => parentNodes.SelectMany(x => x.DescendantsOrSelfOfType( + navigationQueryService, + publishedStatusFilteringService, docTypeAlias, culture)); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IEnumerable DescendantsOrSelfOfType( + this IEnumerable parentNodes, + IVariationContextAccessor variationContextAccessor, + IPublishStatusQueryService publishStatusQueryService, + string docTypeAlias, + string? culture = null) + { + IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray(); + return parentNodesAsArray.DescendantsOrSelfOfType( + GetNavigationQueryService(parentNodesAsArray.First()), + GetPublishedStatusFilteringService(parentNodesAsArray.First()), + docTypeAlias, + culture); + } + + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelfOfType( this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string docTypeAlias, - string? culture = null) => - DescendantsOrSelfOfType( - parentNodes, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), + string? culture = null) + { + IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray(); + return parentNodesAsArray.DescendantsOrSelfOfType( + GetNavigationQueryService(parentNodesAsArray.First()), + GetPublishedStatusFilteringService(parentNodesAsArray.First()), docTypeAlias, culture); + } + public static IEnumerable DescendantsOrSelf( + this IEnumerable parentNodes, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? culture = null) + where T : class, IPublishedContent + => parentNodes.SelectMany(x => x.DescendantsOrSelf( + navigationQueryService, + publishedStatusFilteringService, + culture)); + + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - parentNodes.SelectMany( - x => x.DescendantsOrSelf( - variationContextAccessor, - GetPublishedCache(parentNodes.First()), - GetNavigationQueryService(parentNodes.First()), - publishStatusQueryService, - culture)); + where T : class, IPublishedContent + { + IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray(); + return parentNodesAsArray.DescendantsOrSelf( + GetNavigationQueryService(parentNodesAsArray.First()), + GetPublishedStatusFilteringService(parentNodesAsArray.First()), + culture); + } - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IEnumerable parentNodes, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent => - DescendantsOrSelf(parentNodes, variationContextAccessor, StaticServiceProvider.Instance.GetRequiredService(), culture); - - public static IEnumerable Descendants( - this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishStatusQueryService publishStatusQueryService, - string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), publishStatusQueryService, false, null, culture); - - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] - public static IEnumerable Descendants( - this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - string? culture = null) => - Descendants(content, variationContextAccessor, StaticServiceProvider.Instance.GetRequiredService(), culture); - - - public static IEnumerable Descendants( - this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishStatusQueryService publishStatusQueryService, - int level, - string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), publishStatusQueryService, false, p => p.Level >= level, culture); - - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] - public static IEnumerable Descendants( - this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - int level, - string? culture = null) => - Descendants( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - level, + where T : class, IPublishedContent + { + IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray(); + return parentNodesAsArray.DescendantsOrSelf( + GetNavigationQueryService(parentNodesAsArray.First()), + GetPublishedStatusFilteringService(parentNodesAsArray.First()), culture); + } + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IEnumerable Descendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishStatusQueryService publishStatusQueryService, + string? culture = null) + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IEnumerable Descendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + string? culture = null) + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IEnumerable Descendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + IPublishStatusQueryService publishStatusQueryService, + int level, + string? culture = null) + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); + + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] + public static IEnumerable Descendants( + this IPublishedContent content, + IVariationContextAccessor variationContextAccessor, + int level, + string? culture = null) + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); + + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, - string contentTypeAlias, string? culture = null) => - content.EnumerateDescendantsOrSelfInternal( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - culture, - false, - contentTypeAlias); + string contentTypeAlias, + string? culture = null) + => content.DescendantsOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, - string? culture = null) => - DescendantsOfType( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); - + string? culture = null) + => content.DescendantsOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.Descendants(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), publishStatusQueryService, culture).OfType(); + where T : class, IPublishedContent + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent => - Descendants(content, variationContextAccessor, StaticServiceProvider.Instance.GetRequiredService(), culture); - + where T : class, IPublishedContent + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - content.Descendants(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), publishStatusQueryService, level, culture).OfType(); + where T : class, IPublishedContent + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Descendants( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent => - Descendants( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); - + where T : class, IPublishedContent + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, - string? culture = null) => - content.DescendantsOrSelf(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), publishStatusQueryService, true, null, culture); + string? culture = null) + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, - string? culture = null) => - DescendantsOrSelf( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - culture); - + string? culture = null) + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, int level, - string? culture = null) => - content.DescendantsOrSelf( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - true, - p => p.Level >= level, - culture); + string? culture = null) + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, - string? culture = null) => - DescendantsOrSelf( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); + string? culture = null) + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string contentTypeAlias, - string? culture = null) => - content.EnumerateDescendantsOrSelfInternal( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - culture, - true, - contentTypeAlias); + string? culture = null) + => content.DescendantsOrSelfOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, - string? culture = null) => - DescendantsOrSelfOfType( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); - + string? culture = null) + => content.DescendantsOrSelfOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.DescendantsOrSelf( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - culture).OfType(); + where T : class, IPublishedContent + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent => - DescendantsOrSelf( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - culture); - + where T : class, IPublishedContent + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - content.DescendantsOrSelf( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - level, - culture).OfType(); + where T : class, IPublishedContent + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable DescendantsOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent => - DescendantsOrSelf( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); - + where T : class, IPublishedContent + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, - string? culture = null) => - content.Children(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), publishStatusQueryService, culture)?.FirstOrDefault(); + string? culture = null) + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, - string? culture = null) => - Descendant( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - culture); - + string? culture = null) + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, int level, - string? culture = null) => content - .EnumerateDescendants( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - false, - culture).FirstOrDefault(x => x.Level == level); + string? culture = null) + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, - string? culture = null) => - Descendant( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); - + string? culture = null) + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? DescendantOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string contentTypeAlias, - string? culture = null) => content - .EnumerateDescendantsOrSelfInternal( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - culture, - false, - contentTypeAlias) - .FirstOrDefault(); + string? culture = null) + => content.DescendantOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? DescendantOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, - string? culture = null) => - DescendantOfType( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); - + string? culture = null) + => content.DescendantOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.EnumerateDescendants( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - false, - culture) - .FirstOrDefault(x => x is T) as T; + where T : class, IPublishedContent + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent => - Descendant( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - culture); - + where T : class, IPublishedContent + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - content.Descendant( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - level, - culture) as T; + where T : class, IPublishedContent + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? Descendant( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent => - Descendant( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); - + where T : class, IPublishedContent + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, int level, - string? culture = null) => content - .EnumerateDescendants( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - true, - culture).FirstOrDefault(x => x.Level == level); + string? culture = null) + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, - string? culture = null) => DescendantOrSelf(content, variationContextAccessor, StaticServiceProvider.Instance.GetRequiredService(), level, culture); - + string? culture = null) + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? DescendantOrSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string contentTypeAlias, - string? culture = null) => content - .EnumerateDescendantsOrSelfInternal( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - culture, - true, - contentTypeAlias) - .FirstOrDefault(); + string? culture = null) + => content.DescendantOrSelfOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? DescendantOrSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, - string? culture = null) => - DescendantOrSelfOfType( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); - + string? culture = null) + => content.DescendantOrSelfOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.EnumerateDescendants( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - true, - culture).FirstOrDefault(x => x is T) as T; + where T : class, IPublishedContent + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent => - DescendantOrSelf( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - culture); + where T : class, IPublishedContent + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, int level, string? culture = null) - where T : class, IPublishedContent => - content.DescendantOrSelf(variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), publishStatusQueryService, level, culture) as T; + where T : class, IPublishedContent + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? DescendantOrSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, int level, string? culture = null) - where T : class, IPublishedContent => - DescendantOrSelf( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - level, - culture); - - + where T : class, IPublishedContent + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, - string? culture = null) => - content.Children( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - culture)?.FirstOrDefault(); + string? culture = null) + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, - string? culture = null) => - FirstChild(content, variationContextAccessor, StaticServiceProvider.Instance.GetRequiredService(), culture); - + string? culture = null) + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? FirstChildOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string contentTypeAlias, - string? culture = null) => - content.ChildrenOfType( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - contentTypeAlias, - culture)?.FirstOrDefault(); + string? culture = null) + => content.FirstChildOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? FirstChildOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, - string? culture = null) => - FirstChildOfType( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - contentTypeAlias, - culture); - + string? culture = null) + => content.FirstChildOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, Func predicate, - string? culture = null) => - content.Children( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - predicate, - culture)?.FirstOrDefault(); + string? culture = null) + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) - => content.Children(variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), predicate, culture)?.FirstOrDefault(); - + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, Guid uniqueId, - string? culture = null) => content - .Children( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - x => x.Key == uniqueId, - culture)?.FirstOrDefault(); + string? culture = null) + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), uniqueId, culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IPublishedContent? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Guid uniqueId, - string? culture = null) => - FirstChild( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - uniqueId, - culture); - + string? culture = null) + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), uniqueId, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - content.Children( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - culture)?.FirstOrDefault(); + where T : class, IPublishedContent + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent => - FirstChild( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - culture); - + where T : class, IPublishedContent + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, Func predicate, string? culture = null) - where T : class, IPublishedContent => - content.Children( - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - culture)?.FirstOrDefault(predicate); + where T : class, IPublishedContent + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static T? FirstChild( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, Func predicate, string? culture = null) - where T : class, IPublishedContent => - FirstChild( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - predicate, - culture); + where T : class, IPublishedContent + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture); - [Obsolete( - "Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Siblings( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, - string? culture = null) => - Siblings(content, GetPublishedCache(content), GetNavigationQueryService(content), variationContextAccessor, culture); + string? culture = null) + => content.Siblings(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete( - "Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable SiblingsOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, - string? culture = null) => - SiblingsOfType(content, variationContextAccessor, - GetPublishedCache(content), GetNavigationQueryService(content), contentTypeAlias, culture); + string? culture = null) + => content.SiblingsOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); - [Obsolete( - "Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable Siblings( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent => - Siblings(content, variationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), culture); + where T : class, IPublishedContent + => content.Siblings(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete( - "Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable? SiblingsAndSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, - string? culture = null) => SiblingsAndSelf(content, GetPublishedCache(content), GetNavigationQueryService(content), variationContextAccessor, culture); + string? culture = null) + => content.SiblingsAndSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete( - "Please use IPublishedCache and IDocumentNavigationQueryService or IMediaNavigationQueryService directly. This will be removed in a future version of Umbraco")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable SiblingsAndSelfOfType( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string contentTypeAlias, - string? culture = null) => SiblingsAndSelfOfType(content, variationContextAccessor, GetPublishedCache(content), - GetNavigationQueryService(content), contentTypeAlias, culture); - + string? culture = null) + => content.SiblingsAndSelfOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable SiblingsAndSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, IPublishStatusQueryService publishStatusQueryService, string? culture = null) - where T : class, IPublishedContent => - SiblingsAndSelf( - content, - variationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - publishStatusQueryService, - culture); + where T : class, IPublishedContent + => content.SiblingsAndSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); - [Obsolete("Use the overload with IPublishStatusQueryService, scheduled for removal in v17")] + [Obsolete("Use the overload with INavigationQueryService and IPublishedStatusFilteringService, scheduled for removal in v17")] public static IEnumerable SiblingsAndSelf( this IPublishedContent content, IVariationContextAccessor variationContextAccessor, string? culture = null) - where T : class, IPublishedContent => SiblingsAndSelf( - content, - variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - culture); - + where T : class, IPublishedContent + => content.SiblingsAndSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); private static INavigationQueryService GetNavigationQueryService(IPublishedContent content) { - switch (content.ContentType.ItemType) + switch (content.ItemType) { case PublishedItemType.Content: return StaticServiceProvider.Instance.GetRequiredService(); @@ -4413,17 +3730,16 @@ public static class PublishedContentExtensions default: throw new NotSupportedException("Unsupported content type."); } - } - private static IPublishedCache GetPublishedCache(IPublishedContent content) + private static IPublishedStatusFilteringService GetPublishedStatusFilteringService(IPublishedContent content) { - switch (content.ContentType.ItemType) + switch (content.ItemType) { case PublishedItemType.Content: - return StaticServiceProvider.Instance.GetRequiredService(); + return StaticServiceProvider.Instance.GetRequiredService(); case PublishedItemType.Media: - return StaticServiceProvider.Instance.GetRequiredService(); + return StaticServiceProvider.Instance.GetRequiredService(); default: throw new NotSupportedException("Unsupported content type."); } @@ -4431,10 +3747,8 @@ public static class PublishedContentExtensions private static IEnumerable GetChildren( INavigationQueryService navigationQueryService, - IPublishedCache publishedCache, + IPublishedStatusFilteringService publishedStatusFilteringService, Guid parentKey, - IPublishStatusQueryService publishStatusQueryService, - IVariationContextAccessor? variationContextAccessor, string? contentTypeAlias = null, string? culture = null) { @@ -4446,38 +3760,20 @@ public static class PublishedContentExtensions { return []; } + // We need to filter what keys are published, as calling the GetById // with a non-existing published node, will get cache misses and call the DB // making it a very slow operation. - culture ??= variationContextAccessor?.VariationContext?.Culture ?? string.Empty; - - return childrenKeys - .Where(x => publishStatusQueryService.IsDocumentPublished(x, culture)) - .Select(publishedCache.GetById) - .WhereNotNull() + return publishedStatusFilteringService + .FilterAvailable(childrenKeys, culture) .OrderBy(x => x.SortOrder); } - private static IEnumerable FilterByCulture( - this IEnumerable contentNodes, - string? culture, - IVariationContextAccessor? variationContextAccessor) - { - // Determine the culture if not provided - culture ??= variationContextAccessor?.VariationContext?.Culture ?? string.Empty; - - return culture == "*" - ? contentNodes - : contentNodes.Where(x => x.IsInvariantOrHasCulture(culture)); - } - private static IEnumerable EnumerateDescendantsOrSelfInternal( this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, string? culture, bool orSelf, string? contentTypeAlias = null) @@ -4499,12 +3795,8 @@ public static class PublishedContentExtensions yield break; } - culture ??= variationContextAccessor?.VariationContext?.Culture ?? string.Empty; - IEnumerable descendants = descendantsKeys - .Where(x => publishStatusQueryService.IsDocumentPublished(x, culture)) - .Select(publishedCache.GetById) - .WhereNotNull() - .FilterByCulture(culture, variationContextAccessor); + IEnumerable descendants = publishedStatusFilteringService + .FilterAvailable(descendantsKeys, culture); foreach (IPublishedContent descendant in descendants) { @@ -4514,10 +3806,8 @@ public static class PublishedContentExtensions private static IEnumerable EnumerateAncestorsOrSelfInternal( this IPublishedContent content, - IVariationContextAccessor variationContextAccessor, - IPublishedCache publishedCache, INavigationQueryService navigationQueryService, - IPublishStatusQueryService publishStatusQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, bool orSelf, string? contentTypeAlias = null, string? culture = null) @@ -4539,19 +3829,50 @@ public static class PublishedContentExtensions yield break; } - culture ??= variationContextAccessor.VariationContext?.Culture ?? string.Empty; - foreach (Guid ancestorKey in ancestorsKeys) + IEnumerable ancestors = publishedStatusFilteringService.FilterAvailable(ancestorsKeys, culture); + foreach (IPublishedContent ancestor in ancestors) { - if (publishStatusQueryService.IsDocumentPublished(ancestorKey, culture) is false) - { - yield break; - } - - IPublishedContent? ancestor = publishedCache.GetById(ancestorKey); - if (ancestor is not null) - { - yield return ancestor; - } + yield return ancestor; } } + + private static IEnumerable SiblingsAndSelfInternal( + this IPublishedContent content, + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, + string? contentTypeAlias, + string? culture) + { + if (navigationQueryService.TryGetParentKey(content.Key, out Guid? parentKey) is false) + { + return []; + } + + if (parentKey.HasValue) + { + IEnumerable childrenKeys; + var foundChildrenKeys = contentTypeAlias is null + ? navigationQueryService.TryGetChildrenKeys(parentKey.Value, out childrenKeys) + : navigationQueryService.TryGetChildrenKeysOfType(parentKey.Value, contentTypeAlias, out childrenKeys); + + return foundChildrenKeys + ? publishedStatusFilteringService.FilterAvailable(childrenKeys, culture) + : []; + } + + IEnumerable rootKeys; + var foundRootKeys = contentTypeAlias is null + ? navigationQueryService.TryGetRootKeys(out rootKeys) + : navigationQueryService.TryGetRootKeysOfType(contentTypeAlias, out rootKeys); + + if (foundRootKeys) + { + IEnumerable rootKeysArray = rootKeys as Guid[] ?? rootKeys.ToArray(); + return rootKeysArray.Contains(content.Key) + ? publishedStatusFilteringService.FilterAvailable(rootKeysArray, culture) + : []; + } + + return []; + } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs index bf41498c43..3687e1b7f3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentBase.cs @@ -90,23 +90,23 @@ namespace Umbraco.Cms.Core.Models.PublishedContent private IEnumerable GetChildren() { INavigationQueryService? navigationQueryService; - IPublishedCache? publishedCache; + IPublishedStatusFilteringService? publishedStatusFilteringService; switch (ContentType.ItemType) { case PublishedItemType.Content: - publishedCache = StaticServiceProvider.Instance.GetRequiredService(); navigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + publishedStatusFilteringService = StaticServiceProvider.Instance.GetRequiredService(); break; case PublishedItemType.Media: - publishedCache = StaticServiceProvider.Instance.GetRequiredService(); navigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + publishedStatusFilteringService = StaticServiceProvider.Instance.GetRequiredService(); break; default: throw new NotImplementedException("Level is not implemented for " + ContentType.ItemType); } - return this.Children(_variationContextAccessor, publishedCache, navigationQueryService); + return this.Children(navigationQueryService, publishedStatusFilteringService); } } } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs index bec5250e01..3360b0e78a 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedValueFallback.cs @@ -188,7 +188,7 @@ public class PublishedValueFallback : IPublishedValueFallback IPublishedProperty? property; // if we are here, content's property has no value do { - content = content?.Parent(StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()); + content = content?.Parent(StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()); IPublishedPropertyType? propertyType = content?.ContentType.GetPropertyType(alias); diff --git a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs index f8bffbba77..90c4070752 100644 --- a/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs +++ b/src/Umbraco.Core/PublishedCache/Internal/InternalPublishedContent.cs @@ -70,7 +70,7 @@ public sealed class InternalPublishedContent : IPublishedContent public PublishedItemType ItemType => PublishedItemType.Content; [Obsolete("Please use TryGetParentKey() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")] - public IPublishedContent? Parent => this.Parent(StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()); + public IPublishedContent? Parent => this.Parent(StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()); public bool IsDraft(string? culture = null) => false; @@ -78,9 +78,8 @@ public sealed class InternalPublishedContent : IPublishedContent [Obsolete("Please use TryGetChildrenKeys() on IDocumentNavigationQueryService or IMediaNavigationQueryService instead. Scheduled for removal in V16.")] public IEnumerable Children => this.Children( - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()); + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()); public IEnumerable ChildrenForAllCultures => Children; @@ -102,7 +101,7 @@ public sealed class InternalPublishedContent : IPublishedContent IPublishedContent? content = this; while (content != null && (property == null || property.HasValue() == false)) { - content = content.Parent(StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()); + content = content.Parent(StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()); property = content?.GetProperty(alias); } diff --git a/src/Umbraco.Core/Routing/AliasUrlProvider.cs b/src/Umbraco.Core/Routing/AliasUrlProvider.cs index 59e9e1d381..0809108564 100644 --- a/src/Umbraco.Core/Routing/AliasUrlProvider.cs +++ b/src/Umbraco.Core/Routing/AliasUrlProvider.cs @@ -18,8 +18,8 @@ public class AliasUrlProvider : IUrlProvider private readonly IPublishedValueFallback _publishedValueFallback; private readonly ISiteDomainMapper _siteDomainMapper; private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IPublishedContentCache _contentCache; private readonly IDocumentNavigationQueryService _navigationQueryService; + private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService; private readonly UriUtility _uriUtility; private RequestHandlerSettings _requestConfig; @@ -29,21 +29,62 @@ public class AliasUrlProvider : IUrlProvider UriUtility uriUtility, IPublishedValueFallback publishedValueFallback, IUmbracoContextAccessor umbracoContextAccessor, - IPublishedContentCache contentCache, - IDocumentNavigationQueryService navigationQueryService) + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) { _requestConfig = requestConfig.CurrentValue; _siteDomainMapper = siteDomainMapper; _uriUtility = uriUtility; _publishedValueFallback = publishedValueFallback; _umbracoContextAccessor = umbracoContextAccessor; - _contentCache = contentCache; _navigationQueryService = navigationQueryService; + _publishedContentStatusFilteringService = publishedContentStatusFilteringService; requestConfig.OnChange(x => _requestConfig = x); } - [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public AliasUrlProvider( + IOptionsMonitor requestConfig, + ISiteDomainMapper siteDomainMapper, + UriUtility uriUtility, + IPublishedValueFallback publishedValueFallback, + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + : this( + requestConfig, + siteDomainMapper, + uriUtility, + publishedValueFallback, + umbracoContextAccessor, + navigationQueryService, + publishedContentStatusFilteringService) + { + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public AliasUrlProvider( + IOptionsMonitor requestConfig, + ISiteDomainMapper siteDomainMapper, + UriUtility uriUtility, + IPublishedValueFallback publishedValueFallback, + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) + : this( + requestConfig, + siteDomainMapper, + uriUtility, + publishedValueFallback, + umbracoContextAccessor, + navigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] public AliasUrlProvider( IOptionsMonitor requestConfig, ISiteDomainMapper siteDomainMapper, @@ -56,8 +97,8 @@ public class AliasUrlProvider : IUrlProvider uriUtility, publishedValueFallback, umbracoContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -108,7 +149,7 @@ public class AliasUrlProvider : IUrlProvider while (domainUris == null && n != null) { // move to parent node - n = n.Parent(_contentCache, _navigationQueryService); + n = n.Parent(_navigationQueryService, _publishedContentStatusFilteringService); domainUris = n == null ? null : DomainUtilities.DomainsForNode(umbracoContext.Domains, _siteDomainMapper, n.Id, current, false); diff --git a/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs b/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs index 39cd4c8854..7f16b1fd48 100644 --- a/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs +++ b/src/Umbraco.Core/Routing/ContentFinderByUrlAlias.cs @@ -24,14 +24,46 @@ public class ContentFinderByUrlAlias : IContentFinder private readonly ILogger _logger; private readonly IPublishedValueFallback _publishedValueFallback; private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IPublishedContentCache _contentCache; private readonly IDocumentNavigationQueryService _documentNavigationQueryService; - private readonly IPublishStatusQueryService _publishStatusQueryService; - private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService; /// /// Initializes a new instance of the class. /// + public ContentFinderByUrlAlias( + ILogger logger, + IPublishedValueFallback publishedValueFallback, + IUmbracoContextAccessor umbracoContextAccessor, + IDocumentNavigationQueryService documentNavigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + { + _publishedValueFallback = publishedValueFallback; + _umbracoContextAccessor = umbracoContextAccessor; + _documentNavigationQueryService = documentNavigationQueryService; + _publishedContentStatusFilteringService = publishedContentStatusFilteringService; + _logger = logger; + } + + [Obsolete("Please use tne non-obsolete constructor instead. Scheduled removal in v17")] + public ContentFinderByUrlAlias( + ILogger logger, + IPublishedValueFallback publishedValueFallback, + IVariationContextAccessor variationContextAccessor, + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService documentNavigationQueryService, + IPublishStatusQueryService publishStatusQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + : this( + logger, + publishedValueFallback, + umbracoContextAccessor, + documentNavigationQueryService, + publishedContentStatusFilteringService) + { + } + + [Obsolete("Please use tne non-obsolete constructor instead. Scheduled removal in v17")] public ContentFinderByUrlAlias( ILogger logger, IPublishedValueFallback publishedValueFallback, @@ -40,17 +72,17 @@ public class ContentFinderByUrlAlias : IContentFinder IPublishedContentCache contentCache, IDocumentNavigationQueryService documentNavigationQueryService, IPublishStatusQueryService publishStatusQueryService) + : this( + logger, + publishedValueFallback, + umbracoContextAccessor, + documentNavigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()) { - _publishedValueFallback = publishedValueFallback; - _variationContextAccessor = variationContextAccessor; - _umbracoContextAccessor = umbracoContextAccessor; - _contentCache = contentCache; - _documentNavigationQueryService = documentNavigationQueryService; - _publishStatusQueryService = publishStatusQueryService; - _logger = logger; } - [Obsolete("Please use constructor that takes an IPublishStatusQueryService instead. Scheduled removal in v17")] + + [Obsolete("Please use tne non-obsolete constructor instead. Scheduled removal in v17")] public ContentFinderByUrlAlias( ILogger logger, IPublishedValueFallback publishedValueFallback, @@ -58,14 +90,12 @@ public class ContentFinderByUrlAlias : IContentFinder IUmbracoContextAccessor umbracoContextAccessor, IPublishedContentCache contentCache, IDocumentNavigationQueryService documentNavigationQueryService) - : this( - logger, - publishedValueFallback, - variationContextAccessor, - umbracoContextAccessor, - contentCache, - documentNavigationQueryService, - StaticServiceProvider.Instance.GetRequiredService()) + : this( + logger, + publishedValueFallback, + umbracoContextAccessor, + documentNavigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -169,14 +199,14 @@ public class ContentFinderByUrlAlias : IContentFinder if (rootNodeId > 0) { IPublishedContent? rootNode = cache?.GetById(rootNodeId); - return rootNode?.Descendants(_variationContextAccessor, _contentCache, _documentNavigationQueryService, _publishStatusQueryService).FirstOrDefault(x => IsMatch(x, test1, test2)); + return rootNode?.Descendants(_documentNavigationQueryService, _publishedContentStatusFilteringService).FirstOrDefault(x => IsMatch(x, test1, test2)); } if (cache is not null) { foreach (IPublishedContent rootContent in cache.GetAtRoot()) { - IPublishedContent? c = rootContent.DescendantsOrSelf(_variationContextAccessor, _contentCache, _documentNavigationQueryService, _publishStatusQueryService) + IPublishedContent? c = rootContent.DescendantsOrSelf(_documentNavigationQueryService, _publishedContentStatusFilteringService) .FirstOrDefault(x => IsMatch(x, test1, test2)); if (c != null) { diff --git a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs index 72f1dac37e..1ec57e20e5 100644 --- a/src/Umbraco.Core/Routing/DefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/DefaultUrlProvider.cs @@ -23,8 +23,8 @@ public class DefaultUrlProvider : IUrlProvider private readonly ILogger _logger; private readonly ISiteDomainMapper _siteDomainMapper; private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IPublishedContentCache _contentCache; private readonly IDocumentNavigationQueryService _navigationQueryService; + private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService; private readonly UriUtility _uriUtility; private RequestHandlerSettings _requestSettings; @@ -35,8 +35,8 @@ public class DefaultUrlProvider : IUrlProvider IUmbracoContextAccessor umbracoContextAccessor, UriUtility uriUtility, ILocalizationService localizationService, - IPublishedContentCache contentCache, - IDocumentNavigationQueryService navigationQueryService) + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) { _requestSettings = requestSettings.CurrentValue; _logger = logger; @@ -44,13 +44,58 @@ public class DefaultUrlProvider : IUrlProvider _umbracoContextAccessor = umbracoContextAccessor; _uriUtility = uriUtility; _localizationService = localizationService; - _contentCache = contentCache; _navigationQueryService = navigationQueryService; + _publishedContentStatusFilteringService = publishedContentStatusFilteringService; requestSettings.OnChange(x => _requestSettings = x); } - [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public DefaultUrlProvider( + IOptionsMonitor requestSettings, + ILogger logger, + ISiteDomainMapper siteDomainMapper, + IUmbracoContextAccessor umbracoContextAccessor, + UriUtility uriUtility, + ILocalizationService localizationService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + : this( + requestSettings, + logger, + siteDomainMapper, + umbracoContextAccessor, + uriUtility, + localizationService, + navigationQueryService, + publishedContentStatusFilteringService) + { + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public DefaultUrlProvider( + IOptionsMonitor requestSettings, + ILogger logger, + ISiteDomainMapper siteDomainMapper, + IUmbracoContextAccessor umbracoContextAccessor, + UriUtility uriUtility, + ILocalizationService localizationService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) + : this( + requestSettings, + logger, + siteDomainMapper, + umbracoContextAccessor, + uriUtility, + localizationService, + navigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] public DefaultUrlProvider( IOptionsMonitor requestSettings, ILogger logger, @@ -65,8 +110,8 @@ public class DefaultUrlProvider : IUrlProvider umbracoContextAccessor, uriUtility, localizationService, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -101,7 +146,7 @@ public class DefaultUrlProvider : IUrlProvider // n is null at root while (domainUris == null && n != null) { - n = n.Parent(_contentCache, _navigationQueryService); // move to parent node + n = n.Parent(_navigationQueryService, _publishedContentStatusFilteringService); // move to parent node domainUris = n == null ? null : DomainUtilities.DomainsForNode(umbracoContext.Domains, _siteDomainMapper, n.Id, current); diff --git a/src/Umbraco.Core/Routing/DomainUtilities.cs b/src/Umbraco.Core/Routing/DomainUtilities.cs index 0f8296c919..560285b5cf 100644 --- a/src/Umbraco.Core/Routing/DomainUtilities.cs +++ b/src/Umbraco.Core/Routing/DomainUtilities.cs @@ -17,21 +17,7 @@ namespace Umbraco.Cms.Core.Routing { #region Document Culture - /// - /// Gets the culture assigned to a document by domains, in the context of a current Uri. - /// - /// The document identifier. - /// The document path. - /// An optional current Uri. - /// An Umbraco context. - /// The site domain helper. - /// The culture assigned to the document by domains. - /// - /// In 1:1 multilingual setup, a document contains several cultures (there is not - /// one document per culture), and domains, withing the context of a current Uri, assign - /// a culture to that document. - /// - [Obsolete("Please use the method taking all parameters. This overload will be removed in V17.")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static string? GetCultureFromDomains( int contentId, string contentPath, @@ -45,8 +31,28 @@ namespace Umbraco.Cms.Core.Routing umbracoContext, siteDomainMapper, StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()); + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] + public static string? GetCultureFromDomains( + int contentId, + string contentPath, + Uri? current, + IUmbracoContext umbracoContext, + ISiteDomainMapper siteDomainMapper, + IDomainCache domainCache, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService) + => GetCultureFromDomains( + contentId, + contentPath, + current, + umbracoContext, + siteDomainMapper, + domainCache, + navigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()); /// /// Gets the culture assigned to a document by domains, in the context of a current Uri. @@ -57,8 +63,8 @@ namespace Umbraco.Cms.Core.Routing /// An Umbraco context. /// The site domain helper. /// The domain cache. - /// The published content cache. /// The navigation query service. + /// /// The culture assigned to the document by domains. /// /// In 1:1 multilingual setup, a document contains several cultures (there is not @@ -72,8 +78,8 @@ namespace Umbraco.Cms.Core.Routing IUmbracoContext umbracoContext, ISiteDomainMapper siteDomainMapper, IDomainCache domainCache, - IPublishedCache publishedCache, - INavigationQueryService navigationQueryService) + INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService) { if (umbracoContext == null) { @@ -85,7 +91,7 @@ namespace Umbraco.Cms.Core.Routing current = umbracoContext.CleanedUmbracoUrl; } - var domainNodeId = GetAncestorNodeWithDomainsAssigned(contentId, umbracoContext, domainCache, publishedCache, navigationQueryService); + var domainNodeId = GetAncestorNodeWithDomainsAssigned(contentId, umbracoContext, domainCache, navigationQueryService, publishedStatusFilteringService); DomainAndUri? domain = domainNodeId.HasValue ? DomainForNode(umbracoContext.Domains, siteDomainMapper, domainNodeId.Value, current) @@ -107,13 +113,13 @@ namespace Umbraco.Cms.Core.Routing return umbracoContext.Domains?.DefaultCulture; } - private static int? GetAncestorNodeWithDomainsAssigned(int contentId, IUmbracoContext umbracoContext, IDomainCache domainCache, IPublishedCache publishedCache, INavigationQueryService navigationQueryService) + private static int? GetAncestorNodeWithDomainsAssigned(int contentId, IUmbracoContext umbracoContext, IDomainCache domainCache, INavigationQueryService navigationQueryService, IPublishedStatusFilteringService publishedStatusFilteringService) { IPublishedContent? content = umbracoContext.Content.GetById(contentId); var hasDomains = ContentHasAssignedDomains(content, domainCache); while (content is not null && !hasDomains) { - content = content.Parent(publishedCache, navigationQueryService); + content = content.Parent(navigationQueryService, publishedStatusFilteringService); hasDomains = content is not null && domainCache.HasAssigned(content.Id, true); } diff --git a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs index 7f66f21933..3b140021a8 100644 --- a/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs +++ b/src/Umbraco.Core/Routing/NewDefaultUrlProvider.cs @@ -1,7 +1,9 @@ using System.Globalization; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -23,6 +25,7 @@ public class NewDefaultUrlProvider : IUrlProvider private readonly IIdKeyMap _idKeyMap; private readonly IDocumentUrlService _documentUrlService; private readonly IDocumentNavigationQueryService _navigationQueryService; + private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService; private readonly ILocalizedTextService? _localizedTextService; private readonly ILogger _logger; private readonly ISiteDomainMapper _siteDomainMapper; @@ -41,7 +44,8 @@ public class NewDefaultUrlProvider : IUrlProvider IDomainCache domainCache, IIdKeyMap idKeyMap, IDocumentUrlService documentUrlService, - IDocumentNavigationQueryService navigationQueryService) + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) { _requestSettings = requestSettings.CurrentValue; _logger = logger; @@ -54,10 +58,40 @@ public class NewDefaultUrlProvider : IUrlProvider _idKeyMap = idKeyMap; _documentUrlService = documentUrlService; _navigationQueryService = navigationQueryService; + _publishedContentStatusFilteringService = publishedContentStatusFilteringService; requestSettings.OnChange(x => _requestSettings = x); } + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public NewDefaultUrlProvider( + IOptionsMonitor requestSettings, + ILogger logger, + ISiteDomainMapper siteDomainMapper, + IUmbracoContextAccessor umbracoContextAccessor, + UriUtility uriUtility, + ILocalizationService localizationService, + IPublishedContentCache publishedContentCache, + IDomainCache domainCache, + IIdKeyMap idKeyMap, + IDocumentUrlService documentUrlService, + IDocumentNavigationQueryService navigationQueryService) + : this( + requestSettings, + logger, + siteDomainMapper, + umbracoContextAccessor, + uriUtility, + localizationService, + publishedContentCache, + domainCache, + idKeyMap, + documentUrlService, + navigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + #region GetOtherUrls /// @@ -97,7 +131,7 @@ public class NewDefaultUrlProvider : IUrlProvider // n is null at root while (domainUris == null && n != null) { - n = n.Parent(_publishedContentCache, _navigationQueryService); // move to parent node + n = n.Parent(_navigationQueryService, _publishedContentStatusFilteringService); // move to parent node domainUris = n == null ? null : DomainUtilities.DomainsForNode(_domainCache, _siteDomainMapper, n.Id, current); diff --git a/src/Umbraco.Core/Routing/UrlProvider.cs b/src/Umbraco.Core/Routing/UrlProvider.cs index f034fbf8b9..16dc21956b 100644 --- a/src/Umbraco.Core/Routing/UrlProvider.cs +++ b/src/Umbraco.Core/Routing/UrlProvider.cs @@ -25,9 +25,49 @@ namespace Umbraco.Cms.Core.Routing /// The list of URL providers. /// The list of media URL providers. /// The current variation accessor. - /// The content cache. /// The query service for the in-memory navigation structure. - /// The publish status query service, to query if a given content is published in a given culture. + /// + public UrlProvider( + IUmbracoContextAccessor umbracoContextAccessor, + IOptions routingSettings, + UrlProviderCollection urlProviders, + MediaUrlProviderCollection mediaUrlProviders, + IVariationContextAccessor variationContextAccessor, + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + { + _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _urlProviders = urlProviders; + _mediaUrlProviders = mediaUrlProviders; + _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); + _navigationQueryService = navigationQueryService; + _publishedContentStatusFilteringService = publishedContentStatusFilteringService; + Mode = routingSettings.Value.UrlProviderMode; + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public UrlProvider( + IUmbracoContextAccessor umbracoContextAccessor, + IOptions routingSettings, + UrlProviderCollection urlProviders, + MediaUrlProviderCollection mediaUrlProviders, + IVariationContextAccessor variationContextAccessor, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService, + IPublishStatusQueryService publishStatusQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + : this( + umbracoContextAccessor, + routingSettings, + urlProviders, + mediaUrlProviders, + variationContextAccessor, + navigationQueryService, + publishedContentStatusFilteringService) + { + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] public UrlProvider( IUmbracoContextAccessor umbracoContextAccessor, IOptions routingSettings, @@ -37,18 +77,18 @@ namespace Umbraco.Cms.Core.Routing IPublishedContentCache contentCache, IDocumentNavigationQueryService navigationQueryService, IPublishStatusQueryService publishStatusQueryService) + : this( + umbracoContextAccessor, + routingSettings, + urlProviders, + mediaUrlProviders, + variationContextAccessor, + navigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()) { - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _urlProviders = urlProviders; - _mediaUrlProviders = mediaUrlProviders; - _variationContextAccessor = variationContextAccessor ?? throw new ArgumentNullException(nameof(variationContextAccessor)); - _contentCache = contentCache; - _navigationQueryService = navigationQueryService; - _publishStatusQueryService = publishStatusQueryService; - Mode = routingSettings.Value.UrlProviderMode; } - [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] public UrlProvider( IUmbracoContextAccessor umbracoContextAccessor, IOptions routingSettings, @@ -63,13 +103,12 @@ namespace Umbraco.Cms.Core.Routing urlProviders, mediaUrlProviders, variationContextAccessor, - contentCache, navigationQueryService, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService()) { } - [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] public UrlProvider( IUmbracoContextAccessor umbracoContextAccessor, IOptions routingSettings, @@ -82,9 +121,8 @@ namespace Umbraco.Cms.Core.Routing urlProviders, mediaUrlProviders, variationContextAccessor, - StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -92,9 +130,8 @@ namespace Umbraco.Cms.Core.Routing private readonly IEnumerable _urlProviders; private readonly IEnumerable _mediaUrlProviders; private readonly IVariationContextAccessor _variationContextAccessor; - private readonly IPublishedContentCache _contentCache; private readonly IDocumentNavigationQueryService _navigationQueryService; - private readonly IPublishStatusQueryService _publishStatusQueryService; + private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService; /// /// Gets or sets the provider URL mode. @@ -173,7 +210,7 @@ namespace Umbraco.Cms.Core.Routing // be nice with tests, assume things can be null, ultimately fall back to invariant // (but only for variant content of course) // We need to check all ancestors because urls are variant even for invariant content, if an ancestor is variant. - if (culture == null && content.AncestorsOrSelf(_variationContextAccessor, _contentCache, _navigationQueryService, _publishStatusQueryService).Any(x => x.ContentType.VariesByCulture())) + if (culture == null && content.AncestorsOrSelf(_navigationQueryService, _publishedContentStatusFilteringService).Any(x => x.ContentType.VariesByCulture())) { culture = _variationContextAccessor?.VariationContext?.Culture ?? string.Empty; } diff --git a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs index 6606ff8f6c..a4e7fbc209 100644 --- a/src/Umbraco.Core/Routing/UrlProviderExtensions.cs +++ b/src/Umbraco.Core/Routing/UrlProviderExtensions.cs @@ -14,7 +14,7 @@ namespace Umbraco.Extensions; public static class UrlProviderExtensions { - [Obsolete("Use GetContentUrlsAsync that takes all parameters. Will be removed in V17.")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static async Task> GetContentUrlsAsync( this IContent content, IPublishedRouter publishedRouter, @@ -36,8 +36,35 @@ public static class UrlProviderExtensions logger, uriUtility, publishedUrlProvider, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()); + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()); + + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] + public static async Task> GetContentUrlsAsync( + this IContent content, + IPublishedRouter publishedRouter, + IUmbracoContext umbracoContext, + ILanguageService languageService, + ILocalizedTextService textService, + IContentService contentService, + IVariationContextAccessor variationContextAccessor, + ILogger logger, + UriUtility uriUtility, + IPublishedUrlProvider publishedUrlProvider, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) + => await content.GetContentUrlsAsync( + publishedRouter, + umbracoContext, + languageService, + textService, + contentService, + variationContextAccessor, + logger, + uriUtility, + publishedUrlProvider, + navigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()); /// /// Gets the URLs of the content item. @@ -57,8 +84,8 @@ public static class UrlProviderExtensions ILogger logger, UriUtility uriUtility, IPublishedUrlProvider publishedUrlProvider, - IPublishedContentCache contentCache, - IDocumentNavigationQueryService navigationQueryService) + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) { ArgumentNullException.ThrowIfNull(content); ArgumentNullException.ThrowIfNull(publishedRouter); @@ -95,7 +122,7 @@ public static class UrlProviderExtensions // get all URLs for all cultures // in a HashSet, so de-duplicates too - foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider, contentCache, navigationQueryService)) + foreach (UrlInfo cultureUrl in await GetContentUrlsByCultureAsync(content, cultures, publishedRouter, umbracoContext, contentService, textService, variationContextAccessor, logger, uriUtility, publishedUrlProvider, navigationQueryService, publishedContentStatusFilteringService)) { urls.Add(cultureUrl); } @@ -146,8 +173,8 @@ public static class UrlProviderExtensions ILogger logger, UriUtility uriUtility, IPublishedUrlProvider publishedUrlProvider, - IPublishedContentCache contentCache, - IDocumentNavigationQueryService navigationQueryService) + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) { var result = new List(); @@ -186,7 +213,7 @@ public static class UrlProviderExtensions // got a URL, deal with collisions, add URL default: // detect collisions, etc - Attempt hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility, contentCache, navigationQueryService); + Attempt hasCollision = await DetectCollisionAsync(logger, content, url, culture, umbracoContext, publishedRouter, textService, variationContextAccessor, uriUtility, navigationQueryService, publishedContentStatusFilteringService); if (hasCollision.Success && hasCollision.Result is not null) { result.Add(hasCollision.Result); @@ -243,8 +270,8 @@ public static class UrlProviderExtensions ILocalizedTextService textService, IVariationContextAccessor variationContextAccessor, UriUtility uriUtility, - IPublishedContentCache contentCache, - IDocumentNavigationQueryService navigationQueryService) + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) { // test for collisions on the 'main' URL var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute); @@ -283,7 +310,7 @@ public static class UrlProviderExtensions while (o != null) { l.Add(o.Name(variationContextAccessor)!); - o = o.Parent(contentCache, navigationQueryService); + o = o.Parent(navigationQueryService, publishedContentStatusFilteringService); } l.Reverse(); diff --git a/src/Umbraco.Core/Services/PublishStatus/IPublishedContentStatusFilteringService.cs b/src/Umbraco.Core/Services/PublishStatus/IPublishedContentStatusFilteringService.cs new file mode 100644 index 0000000000..1c54de959f --- /dev/null +++ b/src/Umbraco.Core/Services/PublishStatus/IPublishedContentStatusFilteringService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IPublishedContentStatusFilteringService : IPublishedStatusFilteringService +{ +} diff --git a/src/Umbraco.Core/Services/PublishStatus/IPublishedMediaStatusFilteringService.cs b/src/Umbraco.Core/Services/PublishStatus/IPublishedMediaStatusFilteringService.cs new file mode 100644 index 0000000000..5841bacd03 --- /dev/null +++ b/src/Umbraco.Core/Services/PublishStatus/IPublishedMediaStatusFilteringService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IPublishedMediaStatusFilteringService : IPublishedStatusFilteringService +{ +} diff --git a/src/Umbraco.Core/Services/PublishStatus/IPublishedStatusFilteringService.cs b/src/Umbraco.Core/Services/PublishStatus/IPublishedStatusFilteringService.cs new file mode 100644 index 0000000000..1cdffea26c --- /dev/null +++ b/src/Umbraco.Core/Services/PublishStatus/IPublishedStatusFilteringService.cs @@ -0,0 +1,8 @@ +using Umbraco.Cms.Core.Models.PublishedContent; + +namespace Umbraco.Cms.Core.Services.Navigation; + +public interface IPublishedStatusFilteringService +{ + IEnumerable FilterAvailable(IEnumerable candidateKeys, string? culture); +} diff --git a/src/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringService.cs b/src/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringService.cs new file mode 100644 index 0000000000..e31ae8e347 --- /dev/null +++ b/src/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringService.cs @@ -0,0 +1,50 @@ +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services.Navigation; + +internal sealed class PublishedContentStatusFilteringService : IPublishedContentStatusFilteringService +{ + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IPublishStatusQueryService _publishStatusQueryService; + private readonly IPreviewService _previewService; + private readonly IPublishedContentCache _publishedContentCache; + + public PublishedContentStatusFilteringService( + IVariationContextAccessor variationContextAccessor, + IPublishStatusQueryService publishStatusQueryService, + IPreviewService previewService, + IPublishedContentCache publishedContentCache) + { + _variationContextAccessor = variationContextAccessor; + _publishStatusQueryService = publishStatusQueryService; + _previewService = previewService; + _publishedContentCache = publishedContentCache; + } + + public IEnumerable FilterAvailable(IEnumerable candidateKeys, string? culture) + { + culture ??= _variationContextAccessor.VariationContext?.Culture ?? string.Empty; + + Guid[] candidateKeysAsArray = candidateKeys as Guid[] ?? candidateKeys.ToArray(); + if (candidateKeysAsArray.Length == 0) + { + return []; + } + + var preview = _previewService.IsInPreview(); + candidateKeys = preview + ? candidateKeysAsArray + : candidateKeysAsArray.Where(key => _publishStatusQueryService.IsDocumentPublished(key, culture)); + + return WhereIsInvariantOrHasCulture(candidateKeys, culture, preview).ToArray(); + } + + private IEnumerable WhereIsInvariantOrHasCulture(IEnumerable keys, string culture, bool preview) + => keys + .Select(key => _publishedContentCache.GetById(preview, key)) + .WhereNotNull() + .Where(content => content.ContentType.VariesByCulture() is false + || content.Cultures.ContainsKey(culture)); +} diff --git a/src/Umbraco.Core/Services/PublishStatus/PublishedMediaStatusFilteringService.cs b/src/Umbraco.Core/Services/PublishStatus/PublishedMediaStatusFilteringService.cs new file mode 100644 index 0000000000..3c91e1d7bb --- /dev/null +++ b/src/Umbraco.Core/Services/PublishStatus/PublishedMediaStatusFilteringService.cs @@ -0,0 +1,19 @@ +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.Services.Navigation; + +// NOTE: this class is basically a no-op implementation of IPublishStatusQueryService, because the published +// content extensions need a media equivalent to the content implementation. +// incidentally, if we'll ever support variant and/or draft media, this comes in really handy :-) +internal sealed class PublishedMediaStatusFilteringService : IPublishedMediaStatusFilteringService +{ + private readonly IPublishedMediaCache _publishedMediaCache; + + public PublishedMediaStatusFilteringService(IPublishedMediaCache publishedMediaCache) + => _publishedMediaCache = publishedMediaCache; + + public IEnumerable FilterAvailable(IEnumerable candidateKeys, string? culture) + => candidateKeys.Select(_publishedMediaCache.GetById).WhereNotNull().ToArray(); +} diff --git a/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs index e26fa7d35c..9a84a6ee56 100644 --- a/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs +++ b/src/Umbraco.Infrastructure/Routing/RedirectTracker.cs @@ -1,27 +1,68 @@ using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; -using Umbraco.Cms.Core.Web; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Routing { internal class RedirectTracker : IRedirectTracker { - private readonly IVariationContextAccessor _variationContextAccessor; private readonly ILocalizationService _localizationService; private readonly IRedirectUrlService _redirectUrlService; private readonly IPublishedContentCache _contentCache; private readonly IDocumentNavigationQueryService _navigationQueryService; private readonly ILogger _logger; private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService; + public RedirectTracker( + ILocalizationService localizationService, + IRedirectUrlService redirectUrlService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService, + ILogger logger, + IPublishedUrlProvider publishedUrlProvider, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + { + _localizationService = localizationService; + _redirectUrlService = redirectUrlService; + _contentCache = contentCache; + _navigationQueryService = navigationQueryService; + _logger = logger; + _publishedUrlProvider = publishedUrlProvider; + _publishedContentStatusFilteringService = publishedContentStatusFilteringService; + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public RedirectTracker( + IVariationContextAccessor variationContextAccessor, + ILocalizationService localizationService, + IRedirectUrlService redirectUrlService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService, + ILogger logger, + IPublishedUrlProvider publishedUrlProvider, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + : this( + localizationService, + redirectUrlService, + contentCache, + navigationQueryService, + logger, + publishedUrlProvider, + publishedContentStatusFilteringService) + { + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] public RedirectTracker( IVariationContextAccessor variationContextAccessor, ILocalizationService localizationService, @@ -30,14 +71,15 @@ namespace Umbraco.Cms.Infrastructure.Routing IDocumentNavigationQueryService navigationQueryService, ILogger logger, IPublishedUrlProvider publishedUrlProvider) + : this( + localizationService, + redirectUrlService, + contentCache, + navigationQueryService, + logger, + publishedUrlProvider, + StaticServiceProvider.Instance.GetRequiredService()) { - _variationContextAccessor = variationContextAccessor; - _localizationService = localizationService; - _redirectUrlService = redirectUrlService; - _contentCache = contentCache; - _navigationQueryService = navigationQueryService; - _logger = logger; - _publishedUrlProvider = publishedUrlProvider; } /// @@ -50,12 +92,12 @@ namespace Umbraco.Cms.Infrastructure.Routing } // Get the default affected cultures by going up the tree until we find the first culture variant entity (default to no cultures) - var defaultCultures = new Lazy(() => entityContent.AncestorsOrSelf(_contentCache, _navigationQueryService).FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? Array.Empty()); + var defaultCultures = new Lazy(() => entityContent.AncestorsOrSelf(_navigationQueryService, _publishedContentStatusFilteringService).FirstOrDefault(a => a.Cultures.Any())?.Cultures.Keys.ToArray() ?? Array.Empty()); // Get all language ISO codes (in case we're dealing with invariant content with variant ancestors) var languageIsoCodes = new Lazy(() => _localizationService.GetAllLanguages().Select(x => x.IsoCode).ToArray()); - foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_variationContextAccessor, _contentCache, _navigationQueryService)) + foreach (IPublishedContent publishedContent in entityContent.DescendantsOrSelf(_navigationQueryService, _publishedContentStatusFilteringService)) { // If this entity defines specific cultures, use those instead of the default ones IEnumerable cultures = publishedContent.Cultures.Any() ? publishedContent.Cultures.Keys : defaultCultures.Value; diff --git a/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs b/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs index 79d23397e9..29a865f099 100644 --- a/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs +++ b/src/Umbraco.PublishedCache.HybridCache/PublishedContent.cs @@ -270,22 +270,22 @@ internal class PublishedContent : PublishedContentBase private IPublishedContent? GetParent() { INavigationQueryService? navigationQueryService; - IPublishedCache? publishedCache; + IPublishedStatusFilteringService? publishedStatusFilteringService; switch (ContentType.ItemType) { case PublishedItemType.Content: - publishedCache = StaticServiceProvider.Instance.GetRequiredService(); navigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + publishedStatusFilteringService = StaticServiceProvider.Instance.GetRequiredService(); break; case PublishedItemType.Media: - publishedCache = StaticServiceProvider.Instance.GetRequiredService(); navigationQueryService = StaticServiceProvider.Instance.GetRequiredService(); + publishedStatusFilteringService = StaticServiceProvider.Instance.GetRequiredService(); break; default: throw new NotImplementedException("Level is not implemented for " + ContentType.ItemType); } - return this.Parent(publishedCache, navigationQueryService); + return this.Parent(navigationQueryService, publishedStatusFilteringService); } } diff --git a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs index 31171cc880..921c815e2f 100644 --- a/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/FriendlyPublishedContentExtensions.cs @@ -25,9 +25,6 @@ public static class FriendlyPublishedContentExtensions private static IPublishedContentCache PublishedContentCache { get; } = StaticServiceProvider.Instance.GetRequiredService(); - private static IPublishedMediaCache PublishedMediaCache { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - private static IDocumentNavigationQueryService DocumentNavigationQueryService { get; } = StaticServiceProvider.Instance.GetRequiredService(); @@ -70,9 +67,6 @@ public static class FriendlyPublishedContentExtensions private static IMemberTypeService MemberTypeService { get; } = StaticServiceProvider.Instance.GetRequiredService(); - private static IPublishStatusQueryService PublishStatusQueryService { get; } = - StaticServiceProvider.Instance.GetRequiredService(); - private static INavigationQueryService GetNavigationQueryService(IPublishedContent content) { switch (content.ContentType.ItemType) @@ -84,17 +78,16 @@ public static class FriendlyPublishedContentExtensions default: throw new NotSupportedException("Unsupported content type."); } - } - private static IPublishedCache GetPublishedCache(IPublishedContent content) + private static IPublishedStatusFilteringService GetPublishedStatusFilteringService(IPublishedContent content) { switch (content.ContentType.ItemType) { case PublishedItemType.Content: - return PublishedContentCache; + return StaticServiceProvider.Instance.GetRequiredService(); case PublishedItemType.Media: - return PublishedMediaCache; + return StaticServiceProvider.Instance.GetRequiredService(); default: throw new NotSupportedException("Unsupported content type."); } @@ -246,7 +239,7 @@ public static class FriendlyPublishedContentExtensions /// set to 1. /// public static IPublishedContent Root(this IPublishedContent content) - => content.Root(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService); + => content.Root(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content)); /// /// Gets the root content (ancestor or self at level 1) for the specified if it's of the @@ -265,7 +258,7 @@ public static class FriendlyPublishedContentExtensions /// public static T? Root(this IPublishedContent content) where T : class, IPublishedContent - => content.Root(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService); + => content.Root(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content)); /// /// Gets the parent of the content item. @@ -275,7 +268,7 @@ public static class FriendlyPublishedContentExtensions /// The parent of content of the specified content type or null. public static T? Parent(this IPublishedContent content) where T : class, IPublishedContent - => content.Parent(GetPublishedCache(content), GetNavigationQueryService(content)); + => content.Parent(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content)); /// /// Gets the parent of the content item. @@ -283,7 +276,7 @@ public static class FriendlyPublishedContentExtensions /// The content. /// The parent of content or null. public static IPublishedContent? Parent(this IPublishedContent content) - => content.Parent(GetPublishedCache(content), GetNavigationQueryService(content)); + => content.Parent(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content)); /// /// Gets the ancestors of the content. @@ -292,7 +285,7 @@ public static class FriendlyPublishedContentExtensions /// The ancestors of the content, in down-top order. /// Does not consider the content itself. public static IEnumerable Ancestors(this IPublishedContent content) - => content.Ancestors(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService); + => content.Ancestors(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content)); /// /// Gets the content and its ancestors. @@ -300,7 +293,7 @@ public static class FriendlyPublishedContentExtensions /// The content. /// The content and its ancestors, in down-top order. public static IEnumerable AncestorsOrSelf(this IPublishedContent content) - => content.AncestorsOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService); + => content.AncestorsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content)); /// /// Gets the content and its ancestors, of a specified content type. @@ -311,7 +304,7 @@ public static class FriendlyPublishedContentExtensions /// May or may not begin with the content itself, depending on its content type. public static IEnumerable AncestorsOrSelf(this IPublishedContent content) where T : class, IPublishedContent - => content.AncestorsOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService); + => content.AncestorsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content)); /// /// Gets the ancestor of the content, i.e. its parent. @@ -319,7 +312,7 @@ public static class FriendlyPublishedContentExtensions /// The content. /// The ancestor of the content. public static IPublishedContent? Ancestor(this IPublishedContent content) - => content.Ancestor(GetPublishedCache(content), GetNavigationQueryService(content)); + => content.Ancestor(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content)); /// /// Gets the nearest ancestor of the content, of a specified content type. @@ -330,7 +323,7 @@ public static class FriendlyPublishedContentExtensions /// Does not consider the content itself. May return null. public static T? Ancestor(this IPublishedContent content) where T : class, IPublishedContent - => content.Ancestor(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService); + => content.Ancestor(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content)); /// /// Gets the content or its nearest ancestor, of a specified content type. @@ -341,7 +334,7 @@ public static class FriendlyPublishedContentExtensions /// May or may not return the content itself depending on its content type. May return null. public static T? AncestorOrSelf(this IPublishedContent content) where T : class, IPublishedContent - => content.AncestorOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService); + => content.AncestorOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content)); /// /// Returns all DescendantsOrSelf of all content referenced @@ -358,7 +351,19 @@ public static class FriendlyPublishedContentExtensions /// public static IEnumerable DescendantsOrSelfOfType( this IEnumerable parentNodes, string docTypeAlias, string? culture = null) - => parentNodes.DescendantsOrSelfOfType(VariationContextAccessor, GetPublishedCache(parentNodes.First()), GetNavigationQueryService(parentNodes.First()), PublishStatusQueryService, docTypeAlias, culture); + { + IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray(); + if (parentNodesAsArray.Length == 0) + { + return []; + } + + return parentNodesAsArray.DescendantsOrSelfOfType( + GetNavigationQueryService(parentNodesAsArray.First()), + GetPublishedStatusFilteringService(parentNodesAsArray.First()), + docTypeAlias, + culture); + } /// /// Returns all DescendantsOrSelf of all content referenced @@ -376,77 +381,88 @@ public static class FriendlyPublishedContentExtensions this IEnumerable parentNodes, string? culture = null) where T : class, IPublishedContent - => parentNodes.DescendantsOrSelf(VariationContextAccessor, GetPublishedCache(parentNodes.First()), GetNavigationQueryService(parentNodes.First()), PublishStatusQueryService, culture); + { + IPublishedContent[] parentNodesAsArray = parentNodes as IPublishedContent[] ?? parentNodes.ToArray(); + if (parentNodesAsArray.Length == 0) + { + return []; + } + + return parentNodesAsArray.DescendantsOrSelf( + GetNavigationQueryService(parentNodesAsArray.First()), + GetPublishedStatusFilteringService(parentNodesAsArray.First()), + culture); + } public static IEnumerable Descendants(this IPublishedContent content, string? culture = null) - => content.Descendants(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); public static IEnumerable Descendants(this IPublishedContent content, int level, string? culture = null) - => content.Descendants(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture); + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); public static IEnumerable DescendantsOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.DescendantsOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture); + => content.DescendantsOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); public static IEnumerable Descendants(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.Descendants(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); public static IEnumerable Descendants(this IPublishedContent content, int level, string? culture = null) where T : class, IPublishedContent - => content.Descendants(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture); + => content.Descendants(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); public static IEnumerable DescendantsOrSelf( this IPublishedContent content, string? culture = null) - => content.DescendantsOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string? culture = null) - => content.DescendantsOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture); + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); public static IEnumerable DescendantsOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.DescendantsOrSelfOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture); + => content.DescendantsOrSelfOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); public static IEnumerable DescendantsOrSelf(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.DescendantsOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); public static IEnumerable DescendantsOrSelf(this IPublishedContent content, int level, string? culture = null) where T : class, IPublishedContent - => content.DescendantsOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture); + => content.DescendantsOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); public static IPublishedContent? Descendant(this IPublishedContent content, string? culture = null) - => content.Descendant(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); public static IPublishedContent? Descendant(this IPublishedContent content, int level, string? culture = null) - => content.Descendant(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture); + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); public static IPublishedContent? DescendantOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.DescendantOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture); + => content.DescendantOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); public static T? Descendant(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.Descendant(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); public static T? Descendant(this IPublishedContent content, int level, string? culture = null) where T : class, IPublishedContent - => content.Descendant(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture); + => content.Descendant(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); public static IPublishedContent DescendantOrSelf(this IPublishedContent content, string? culture = null) - => content.DescendantOrSelf(VariationContextAccessor, PublishStatusQueryService, culture); + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); public static IPublishedContent? DescendantOrSelf(this IPublishedContent content, int level, string? culture = null) - => content.DescendantOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture); + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); public static IPublishedContent? DescendantOrSelfOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.DescendantOrSelfOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture); + => content.DescendantOrSelfOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); public static T? DescendantOrSelf(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.DescendantOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); public static T? DescendantOrSelf(this IPublishedContent content, int level, string? culture = null) where T : class, IPublishedContent - => content.DescendantOrSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, level, culture); + => content.DescendantOrSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), level, culture); /// /// Gets the children of the content item. @@ -474,7 +490,7 @@ public static class FriendlyPublishedContentExtensions /// /// public static IEnumerable Children(this IPublishedContent content, string? culture = null) - => content.Children(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.Children(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); /// /// Gets the children of the content, filtered by a predicate. @@ -493,7 +509,7 @@ public static class FriendlyPublishedContentExtensions this IPublishedContent content, Func predicate, string? culture = null) - => content.Children(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, predicate, culture); + => content.Children(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture); /// /// Gets the children of the content, of any of the specified types. @@ -506,7 +522,7 @@ public static class FriendlyPublishedContentExtensions /// The content type alias. /// The children of the content, of any of the specified types. public static IEnumerable? ChildrenOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.ChildrenOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture); + => content.ChildrenOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); /// /// Gets the children of the content, of a given content type. @@ -523,30 +539,30 @@ public static class FriendlyPublishedContentExtensions /// public static IEnumerable? Children(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.Children(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.Children(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); public static IPublishedContent? FirstChild(this IPublishedContent content, string? culture = null) - => content.FirstChild(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); /// /// Gets the first child of the content, of a given content type. /// public static IPublishedContent? FirstChildOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.FirstChildOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture); + => content.FirstChildOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); public static IPublishedContent? FirstChild(this IPublishedContent content, Func predicate, string? culture = null) - => content.FirstChild(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, predicate, culture); + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture); public static IPublishedContent? FirstChild(this IPublishedContent content, Guid uniqueId, string? culture = null) - => content.FirstChild(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, uniqueId, culture); + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), uniqueId, culture); public static T? FirstChild(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.FirstChild(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); public static T? FirstChild(this IPublishedContent content, Func predicate, string? culture = null) where T : class, IPublishedContent - => content.FirstChild(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, predicate, culture); + => content.FirstChild(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), predicate, culture); /// /// Gets the siblings of the content. @@ -561,7 +577,7 @@ public static class FriendlyPublishedContentExtensions /// Note that in V7 this method also return the content node self. /// public static IEnumerable? Siblings(this IPublishedContent content, string? culture = null) - => content.Siblings(GetPublishedCache(content), GetNavigationQueryService(content), VariationContextAccessor, PublishStatusQueryService, culture); + => content.Siblings(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); /// /// Gets the siblings of the content, of a given content type. @@ -577,7 +593,7 @@ public static class FriendlyPublishedContentExtensions /// Note that in V7 this method also return the content node self. /// public static IEnumerable? SiblingsOfType(this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.SiblingsOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture); + => content.SiblingsOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); /// /// Gets the siblings of the content, of a given content type. @@ -594,7 +610,7 @@ public static class FriendlyPublishedContentExtensions /// public static IEnumerable? Siblings(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.Siblings(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.Siblings(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); /// /// Gets the siblings of the content including the node itself to indicate the position. @@ -608,7 +624,7 @@ public static class FriendlyPublishedContentExtensions public static IEnumerable? SiblingsAndSelf( this IPublishedContent content, string? culture = null) - => content.SiblingsAndSelf(GetPublishedCache(content), GetNavigationQueryService(content), VariationContextAccessor, PublishStatusQueryService, culture); + => content.SiblingsAndSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); /// /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. @@ -624,7 +640,7 @@ public static class FriendlyPublishedContentExtensions this IPublishedContent content, string contentTypeAlias, string? culture = null) - => content.SiblingsAndSelfOfType(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, contentTypeAlias, culture); + => content.SiblingsAndSelfOfType(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), contentTypeAlias, culture); /// /// Gets the siblings of the content including the node itself to indicate the position, of a given content type. @@ -638,7 +654,7 @@ public static class FriendlyPublishedContentExtensions /// The siblings of the content including the node itself, of the given content type. public static IEnumerable? SiblingsAndSelf(this IPublishedContent content, string? culture = null) where T : class, IPublishedContent - => content.SiblingsAndSelf(VariationContextAccessor, GetPublishedCache(content), GetNavigationQueryService(content), PublishStatusQueryService, culture); + => content.SiblingsAndSelf(GetNavigationQueryService(content), GetPublishedStatusFilteringService(content), culture); /// /// Gets the url of the content item. @@ -671,17 +687,15 @@ public static class FriendlyPublishedContentExtensions /// The children of the content. [Obsolete("This method is no longer used in Umbraco. The method will be removed in Umbraco 17.")] public static DataTable ChildrenAsTable(this IPublishedContent content, string contentTypeAliasFilter = "", string? culture = null) - => - content.ChildrenAsTable( - VariationContextAccessor, - GetPublishedCache(content), - GetNavigationQueryService(content), - ContentTypeService, - MediaTypeService, - MemberTypeService, - PublishedUrlProvider, - contentTypeAliasFilter, - culture); + => content.ChildrenAsTable( + GetNavigationQueryService(content), + GetPublishedStatusFilteringService(content), + ContentTypeService, + MediaTypeService, + MemberTypeService, + PublishedUrlProvider, + contentTypeAliasFilter, + culture); /// /// Gets the url for a media. diff --git a/src/Umbraco.Web.Common/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Web.Common/Extensions/PublishedContentExtensions.cs index d9ca747ba3..640d73ad10 100644 --- a/src/Umbraco.Web.Common/Extensions/PublishedContentExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/PublishedContentExtensions.cs @@ -17,22 +17,7 @@ public static class PublishedContentExtensions { #region Variations - /// - /// Gets the culture assigned to a document by domains, in the context of a current Uri. - /// - /// The document. - /// - /// The site domain helper. - /// An optional current Uri. - /// The culture assigned to the document by domains. - /// - /// - /// In 1:1 multilingual setup, a document contains several cultures (there is not - /// one document per culture), and domains, withing the context of a current Uri, assign - /// a culture to that document. - /// - /// - [Obsolete("Please use the method taking all parameters. This overload will be removed in V17.")] + [Obsolete("Use the overload with IPublishedStatusFilteringService, scheduled for removal in v17")] public static string? GetCultureFromDomains( this IPublishedContent content, IUmbracoContextAccessor umbracoContextAccessor, @@ -43,6 +28,19 @@ public static class PublishedContentExtensions return DomainUtilities.GetCultureFromDomains(content.Id, content.Path, current, umbracoContext, siteDomainHelper); } + public static string? GetCultureFromDomains( + this IPublishedContent content, + IUmbracoContextAccessor umbracoContextAccessor, + ISiteDomainMapper siteDomainHelper, + IDomainCache domainCache, + IPublishedCache publishedCache, + INavigationQueryService navigationQueryService, + Uri? current = null) + { + IUmbracoContext umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); + return DomainUtilities.GetCultureFromDomains(content.Id, content.Path, current, umbracoContext, siteDomainHelper, domainCache, publishedCache, navigationQueryService); + } + /// /// Gets the culture assigned to a document by domains, in the context of a current Uri. /// @@ -68,6 +66,7 @@ public static class PublishedContentExtensions IDomainCache domainCache, IPublishedCache publishedCache, INavigationQueryService navigationQueryService, + IPublishedStatusFilteringService publishedStatusFilteringService, Uri? current = null) { IUmbracoContext umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); diff --git a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs index 57666cbc4f..ecad1a3484 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs @@ -23,8 +23,8 @@ public class UmbLoginController : SurfaceController private readonly IMemberManager _memberManager; private readonly IMemberSignInManager _signInManager; private readonly ITwoFactorLoginService _twoFactorLoginService; - private readonly IPublishedContentCache _contentCache; private readonly IDocumentNavigationQueryService _navigationQueryService; + private readonly IPublishedContentStatusFilteringService _publishedContentStatusFilteringService; [ActivatorUtilitiesConstructor] public UmbLoginController( @@ -37,18 +37,75 @@ public class UmbLoginController : SurfaceController IMemberSignInManager signInManager, IMemberManager memberManager, ITwoFactorLoginService twoFactorLoginService, - IPublishedContentCache contentCache, - IDocumentNavigationQueryService navigationQueryService) + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) { _signInManager = signInManager; _memberManager = memberManager; _twoFactorLoginService = twoFactorLoginService; - _contentCache = contentCache; _navigationQueryService = navigationQueryService; + _publishedContentStatusFilteringService = publishedContentStatusFilteringService; } - [Obsolete("Use the constructor that takes all parameters. Scheduled for removal in V17.")] + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public UmbLoginController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager signInManager, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService, + IPublishedContentStatusFilteringService publishedContentStatusFilteringService) + : this( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider, + signInManager, + memberManager, + twoFactorLoginService, + navigationQueryService, + publishedContentStatusFilteringService) + { + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] + public UmbLoginController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager signInManager, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService, + IPublishedContentCache contentCache, + IDocumentNavigationQueryService navigationQueryService) + : this( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider, + signInManager, + memberManager, + twoFactorLoginService, + navigationQueryService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V17.")] public UmbLoginController( IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, @@ -69,8 +126,8 @@ public class UmbLoginController : SurfaceController signInManager, memberManager, twoFactorLoginService, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -102,7 +159,7 @@ public class UmbLoginController : SurfaceController // If it's not a local URL we'll redirect to the root of the current site. return Redirect(Url.IsLocalUrl(model.RedirectUrl) ? model.RedirectUrl - : CurrentPage!.AncestorOrSelf(_contentCache, _navigationQueryService, 1)!.Url(PublishedUrlProvider)); + : CurrentPage!.AncestorOrSelf(_navigationQueryService, _publishedContentStatusFilteringService, 1)!.Url(PublishedUrlProvider)); } // Redirect to current URL by default. diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs index 6d1db8132f..d1ad93da3a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/DocumentNavigationServiceTests.cs @@ -281,4 +281,47 @@ public partial class DocumentNavigationServiceTests : DocumentNavigationServiceT Assert.AreEqual(3, allSiblingsList.Count); }); } + + // a lot of structural querying assumes a specific order of descendants, so let's ensure that. + [Test] + public void Descendants_Are_In_Top_Down_Order_Of_Structure() + { + var result = DocumentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(Root.Key, out IEnumerable descendantsKeys); + Assert.IsTrue(result); + + var descendantsKeysAsArray = descendantsKeys.ToArray(); + Assert.AreEqual(9, descendantsKeysAsArray.Length); + + Assert.Multiple(() => + { + Assert.AreEqual(Root.Key, descendantsKeysAsArray[0]); + Assert.AreEqual(Child1.Key, descendantsKeysAsArray[1]); + Assert.AreEqual(Grandchild1.Key, descendantsKeysAsArray[2]); + Assert.AreEqual(Grandchild2.Key, descendantsKeysAsArray[3]); + Assert.AreEqual(Child2.Key, descendantsKeysAsArray[4]); + Assert.AreEqual(Grandchild3.Key, descendantsKeysAsArray[5]); + Assert.AreEqual(GreatGrandchild1.Key, descendantsKeysAsArray[6]); + Assert.AreEqual(Child3.Key, descendantsKeysAsArray[7]); + Assert.AreEqual(Grandchild4.Key, descendantsKeysAsArray[8]); + }); + } + + // a lot of structural querying assumes a specific order of ancestors, so let's ensure that. + [Test] + public void Ancestors_Are_In_Down_Top_Order() + { + var result = DocumentNavigationQueryService.TryGetAncestorsOrSelfKeys(GreatGrandchild1.Key, out IEnumerable ancestorsKeys); + Assert.IsTrue(result); + + var ancestorKeysAsArray = ancestorsKeys.ToArray(); + Assert.AreEqual(4, ancestorKeysAsArray.Length); + + Assert.Multiple(() => + { + Assert.AreEqual(GreatGrandchild1.Key, ancestorKeysAsArray[0]); + Assert.AreEqual(Grandchild3.Key, ancestorKeysAsArray[1]); + Assert.AreEqual(Child2.Key, ancestorKeysAsArray[2]); + Assert.AreEqual(Root.Key, ancestorKeysAsArray[3]); + }); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs index 144f987da8..ae1e877e15 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.BackOffice/UrlAndDomains/DomainAndUrlsTests.cs @@ -413,6 +413,6 @@ public class DomainAndUrlsTests : UmbracoIntegrationTest GetRequiredService>(), GetRequiredService(), GetRequiredService(), - GetRequiredService(), - GetRequiredService()).GetAwaiter().GetResult(); + GetRequiredService(), + GetRequiredService()).GetAwaiter().GetResult(); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs index aa94d9147b..99ccbd7a61 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentBuilderTests.cs @@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PublishedCache; -using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -36,7 +36,11 @@ public class ContentBuilderTests : DeliveryApiTests .Setup(p => p.GetContentPath(It.IsAny(), It.IsAny())) .Returns((IPublishedContent c, string? culture) => $"url:{c.UrlSegment}"); - var routeBuilder = CreateContentRouteBuilder(apiContentRouteProvider.Object, CreateGlobalSettings()); + var navigationQueryServiceMock = new Mock(); + IEnumerable ancestorsKeys = []; + navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(key, out ancestorsKeys)).Returns(true); + + var routeBuilder = CreateContentRouteBuilder(apiContentRouteProvider.Object, CreateGlobalSettings(), navigationQueryService: navigationQueryServiceMock.Object); var builder = new ApiContentBuilder(new ApiContentNameProvider(), routeBuilder, CreateOutputExpansionStrategyAccessor()); var result = builder.Build(content.Object); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs index c0001bf067..9da66ba10c 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/ContentRouteBuilderTests.cs @@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Navigation; using Umbraco.Extensions; @@ -26,7 +27,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var rootKey = Guid.NewGuid(); var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object); var result = builder.Build(root); Assert.IsNotNull(result); Assert.AreEqual("/", result.Path); @@ -47,13 +48,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); IEnumerable ancestorsKeys = [rootKey]; - navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object, contentCache: contentCache); var result = builder.Build(child); Assert.IsNotNull(result); Assert.AreEqual("/the-child", result.Path); @@ -77,14 +78,14 @@ public class ContentRouteBuilderTests : DeliveryApiTests var grandchild = SetupInvariantPublishedContent("The Grandchild", grandchildKey, navigationQueryServiceMock, child); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); - Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), grandchild.Key)).Returns(grandchild); IEnumerable ancestorsKeys = [childKey, rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(grandchildKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object, contentCache: contentCache); var result = builder.Build(grandchild); Assert.IsNotNull(result); Assert.AreEqual("/the-child/the-grandchild", result.Path); @@ -104,13 +105,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests var child = SetupVariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); IEnumerable ancestorsKeys = [rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(false, navigationQueryServiceMock.Object, contentCache: contentCache); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); Assert.AreEqual("/the-child-en-us", result.Path); @@ -136,13 +137,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); IEnumerable ancestorsKeys = [rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(false, navigationQueryServiceMock.Object, contentCache: contentCache); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); Assert.AreEqual("/the-child", result.Path); @@ -168,13 +169,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests var child = SetupVariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); IEnumerable ancestorsKeys = [rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(false, contentCache: contentCache, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(false, navigationQueryServiceMock.Object, contentCache: contentCache); var result = builder.Build(child, "en-us"); Assert.IsNotNull(result); Assert.AreEqual("/the-child-en-us", result.Path); @@ -197,7 +198,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests var content = new Mock(); content.SetupGet(c => c.ItemType).Returns(itemType); - var builder = CreateApiContentRouteBuilder(true); + var builder = CreateApiContentRouteBuilder(true, Mock.Of()); Assert.Throws(() => builder.Build(content.Object)); } @@ -236,9 +237,9 @@ public class ContentRouteBuilderTests : DeliveryApiTests var grandchild = SetupInvariantPublishedContent("The Grandchild", grandchildKey, navigationQueryServiceMock, child); var contentCache = Mock.Of(); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); - Mock.Get(contentCache).Setup(x => x.GetById(grandchild.Key)).Returns(grandchild); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), grandchild.Key)).Returns(grandchild); IEnumerable grandchildAncestorsKeys = [childKey, rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(grandchildKey, out grandchildAncestorsKeys)).Returns(true); @@ -262,17 +263,17 @@ public class ContentRouteBuilderTests : DeliveryApiTests var rootKey = Guid.NewGuid(); var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); - IEnumerable rootKeys = rootKey.Yield(); - navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true); - var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root, false); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(true, root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(true, child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); - var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, contentCache: contentCache, isPreview: true, navigationQueryService: navigationQueryServiceMock.Object); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + + var builder = CreateApiContentRouteBuilder(hideTopLevelNodeFromPath, navigationQueryServiceMock.Object, contentCache: contentCache, isPreview: true); var result = builder.Build(child); Assert.IsNotNull(result); Assert.AreEqual($"/{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}{childKey:D}", result.Path); @@ -289,17 +290,17 @@ public class ContentRouteBuilderTests : DeliveryApiTests var rootKey = Guid.NewGuid(); var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock); - IEnumerable rootKeys = rootKey.Yield(); - navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true); - var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root, false); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(true, root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(true, child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); - var builder = CreateApiContentRouteBuilder(true, addTrailingSlash, contentCache: contentCache, isPreview: true, navigationQueryService: navigationQueryServiceMock.Object); + IEnumerable ancestorsKeys = [rootKey]; + navigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); + + var builder = CreateApiContentRouteBuilder(true, navigationQueryServiceMock.Object, addTrailingSlash, contentCache: contentCache, isPreview: true); var result = builder.Build(child); Assert.IsNotNull(result); Assert.AreEqual(addTrailingSlash, result.Path.EndsWith("/")); @@ -314,9 +315,6 @@ public class ContentRouteBuilderTests : DeliveryApiTests var rootKey = Guid.NewGuid(); var root = SetupInvariantPublishedContent("The Root", rootKey, navigationQueryServiceMock, published: false); - IEnumerable rootKeys = rootKey.Yield(); - navigationQueryServiceMock.Setup(x => x.TryGetRootKeys(out rootKeys)).Returns(true); - var childKey = Guid.NewGuid(); var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); @@ -324,13 +322,13 @@ public class ContentRouteBuilderTests : DeliveryApiTests requestPreviewServiceMock.Setup(m => m.IsPreview()).Returns(isPreview); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); IEnumerable ancestorsKeys = [rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, isPreview: isPreview, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(true, navigationQueryServiceMock.Object, contentCache: contentCache, isPreview: isPreview); var result = builder.Build(child); if (isPreview) @@ -358,8 +356,8 @@ public class ContentRouteBuilderTests : DeliveryApiTests var child = SetupInvariantPublishedContent("The Child", childKey, navigationQueryServiceMock, root); var contentCache = CreatePublishedContentCache("#"); - Mock.Get(contentCache).Setup(x => x.GetById(root.Key)).Returns(root); - Mock.Get(contentCache).Setup(x => x.GetById(child.Key)).Returns(child); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), root.Key)).Returns(root); + Mock.Get(contentCache).Setup(x => x.GetById(It.IsAny(), child.Key)).Returns(child); var apiContentPathProvider = new Mock(); apiContentPathProvider @@ -369,7 +367,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests IEnumerable ancestorsKeys = [rootKey]; navigationQueryServiceMock.Setup(x=>x.TryGetAncestorsKeys(childKey, out ancestorsKeys)).Returns(true); - var builder = CreateApiContentRouteBuilder(true, contentCache: contentCache, apiContentPathProvider: apiContentPathProvider.Object, navigationQueryService: navigationQueryServiceMock.Object); + var builder = CreateApiContentRouteBuilder(true, navigationQueryServiceMock.Object, contentCache: contentCache, apiContentPathProvider: apiContentPathProvider.Object); var result = builder.Build(root); Assert.NotNull(result); Assert.AreEqual("/my-custom-path-for-the-root", result.Path); @@ -432,7 +430,12 @@ public class ContentRouteBuilderTests : DeliveryApiTests string Url(IPublishedContent content, string? culture) { - var ancestorsOrSelf = content.AncestorsOrSelf(variantContextAccessor, contentCache, navigationQueryService, PublishStatusQueryService).ToArray(); + var publishedContentStatusFilteringService = new PublishedContentStatusFilteringService( + variantContextAccessor, + PublishStatusQueryService, + Mock.Of(), + contentCache); + var ancestorsOrSelf = content.AncestorsOrSelf(navigationQueryService, publishedContentStatusFilteringService).ToArray(); return ancestorsOrSelf.All(c => c.IsPublished(culture)) ? string.Join("/", ancestorsOrSelf.Reverse().Skip(hideTopLevelNodeFromPath ? 1 : 0).Select(c => c.UrlSegment(variantContextAccessor, culture))).EnsureStartsWith("/") : "#"; @@ -448,7 +451,7 @@ public class ContentRouteBuilderTests : DeliveryApiTests private IApiContentPathProvider SetupApiContentPathProvider(bool hideTopLevelNodeFromPath, IPublishedContentCache contentCache, IDocumentNavigationQueryService navigationQueryService) => new ApiContentPathProvider(SetupPublishedUrlProvider(hideTopLevelNodeFromPath, contentCache, navigationQueryService)); - private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath, bool addTrailingSlash = false, bool isPreview = false, IPublishedContentCache? contentCache = null, IApiContentPathProvider? apiContentPathProvider = null, IDocumentNavigationQueryService navigationQueryService = null) + private ApiContentRouteBuilder CreateApiContentRouteBuilder(bool hideTopLevelNodeFromPath, IDocumentNavigationQueryService navigationQueryService, bool addTrailingSlash = false, bool isPreview = false, IPublishedContentCache? contentCache = null, IApiContentPathProvider? apiContentPathProvider = null) { var requestHandlerSettings = new RequestHandlerSettings { AddTrailingSlash = addTrailingSlash }; var requestHandlerSettingsMonitorMock = new Mock>(); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs index 2764dd21d3..08c09c92f2 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/DeliveryApiTests.cs @@ -113,13 +113,14 @@ public class DeliveryApiTests content.SetupGet(c => c.ContentType).Returns(contentType); content.SetupGet(c => c.Properties).Returns(properties); content.SetupGet(c => c.ItemType).Returns(contentType.ItemType); + content.SetupGet(c => c.Level).Returns(1); content.Setup(c => c.IsPublished(It.IsAny())).Returns(true); } protected string DefaultUrlSegment(string name, string? culture = null) => $"{name.ToLowerInvariant().Replace(" ", "-")}{(culture.IsNullOrWhiteSpace() ? string.Empty : $"-{culture}")}"; - protected ApiContentRouteBuilder CreateContentRouteBuilder( + protected virtual ApiContentRouteBuilder CreateContentRouteBuilder( IApiContentPathProvider contentPathProvider, IOptions globalSettings, IVariationContextAccessor? variationContextAccessor = null, diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs index fc232d6c95..4b6c052392 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/MultiNodeTreePickerValueConverterTests.cs @@ -21,7 +21,7 @@ public class MultiNodeTreePickerValueConverterTests : PropertyValueConverterTest var contentNameProvider = new ApiContentNameProvider(); var apiUrProvider = new ApiMediaUrlProvider(PublishedUrlProvider); - routeBuilder = routeBuilder ?? CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()); + routeBuilder ??= CreateContentRouteBuilder(ApiContentPathProvider, CreateGlobalSettings()); return new MultiNodeTreePickerValueConverter( Mock.Of(), Mock.Of(), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs index 7f952c35da..2751cf6f13 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/PropertyValueConverterTests.cs @@ -1,9 +1,12 @@ +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DeliveryApi; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Core.Services.Navigation; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.DeliveryApi; @@ -31,6 +34,8 @@ public class PropertyValueConverterTests : DeliveryApiTests protected VariationContext VariationContext { get; } = new(); + protected Mock DocumentNavigationQueryServiceMock { get; private set; } + [SetUp] public override void Setup() { @@ -76,6 +81,10 @@ public class PropertyValueConverterTests : DeliveryApiTests .Returns("the-media-url"); PublishedUrlProvider = PublishedUrlProviderMock.Object; ApiContentPathProvider = new ApiContentPathProvider(PublishedUrlProvider); + + DocumentNavigationQueryServiceMock = new Mock(); + IEnumerable ancestorsKeys = []; + DocumentNavigationQueryServiceMock.Setup(x => x.TryGetAncestorsKeys(contentKey, out ancestorsKeys)).Returns(true); } protected Mock SetupPublishedContent(string name, Guid key, PublishedItemType itemType, IPublishedContentType contentType) @@ -109,4 +118,28 @@ public class PropertyValueConverterTests : DeliveryApiTests .Setup(pcc => pcc.GetById(It.IsAny(), media.Key)) .Returns(media); } + + protected override ApiContentRouteBuilder CreateContentRouteBuilder( + IApiContentPathProvider contentPathProvider, + IOptions globalSettings, + IVariationContextAccessor? variationContextAccessor = null, + IRequestPreviewService? requestPreviewService = null, + IOptionsMonitor? requestHandlerSettingsMonitor = null, + IPublishedContentCache? contentCache = null, + IDocumentNavigationQueryService? navigationQueryService = null, + IPublishStatusQueryService? publishStatusQueryService = null) + { + contentCache ??= PublishedContentCacheMock.Object; + navigationQueryService ??= DocumentNavigationQueryServiceMock.Object; + + return base.CreateContentRouteBuilder( + contentPathProvider, + globalSettings, + variationContextAccessor, + requestPreviewService, + requestHandlerSettingsMonitor, + contentCache, + navigationQueryService, + publishStatusQueryService); + } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAliasTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAliasTests.cs index d7f86ff11e..11872abbfb 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAliasTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Routing/ContentFinderByUrlAliasTests.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using AutoFixture.NUnit3; +using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; @@ -30,16 +31,14 @@ public class ContentFinderByUrlAliasTests [Frozen] IPublishedContentCache publishedContentCache, [Frozen] IUmbracoContextAccessor umbracoContextAccessor, [Frozen] IUmbracoContext umbracoContext, - [Frozen] IVariationContextAccessor variationContextAccessor, - [Frozen] IPublishStatusQueryService publishStatusQueryService, + [Frozen] IDocumentNavigationQueryService documentNavigationQueryService, + [Frozen] IPublishedContentStatusFilteringService publishedContentStatusFilteringService, IFileService fileService, - ContentFinderByUrlAlias sut, IPublishedContent[] rootContents, IPublishedProperty urlProperty) { // Arrange var absoluteUrl = "http://localhost" + relativeUrl; - var variationContext = new VariationContext(); var contentItem = rootContents[0]; Mock.Get(umbracoContextAccessor).Setup(x => x.TryGetUmbracoContext(out umbracoContext)).Returns(true); @@ -47,13 +46,22 @@ public class ContentFinderByUrlAliasTests Mock.Get(publishedContentCache).Setup(x => x.GetAtRoot(null)).Returns(rootContents); Mock.Get(contentItem).Setup(x => x.Id).Returns(nodeMatch); Mock.Get(contentItem).Setup(x => x.GetProperty(Constants.Conventions.Content.UrlAlias)).Returns(urlProperty); + Mock.Get(contentItem).Setup(x => x.ItemType).Returns(PublishedItemType.Content); Mock.Get(urlProperty).Setup(x => x.GetValue(null, null)).Returns(relativeUrl); - Mock.Get(variationContextAccessor).Setup(x => x.VariationContext).Returns(variationContext); - Mock.Get(publishStatusQueryService).Setup(x => x.IsDocumentPublished(It.IsAny(), It.IsAny())).Returns(true); + IEnumerable descendantKeys = []; + Mock.Get(documentNavigationQueryService).Setup(x => x.TryGetDescendantsKeys(It.IsAny(), out descendantKeys)).Returns(true); + + Mock.Get(publishedContentStatusFilteringService).Setup(x => x.FilterAvailable(It.IsAny>(), It.IsAny())).Returns([]); var publishedRequestBuilder = new PublishedRequestBuilder(new Uri(absoluteUrl, UriKind.Absolute), fileService); // Act + var sut = new ContentFinderByUrlAlias( + Mock.Of>(), + Mock.Of(), + umbracoContextAccessor, + documentNavigationQueryService, + publishedContentStatusFilteringService); var result = await sut.TryFindContent(publishedRequestBuilder); Assert.IsTrue(result); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringServiceTests.cs new file mode 100644 index 0000000000..674c06d643 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/PublishStatus/PublishedContentStatusFilteringServiceTests.cs @@ -0,0 +1,357 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Navigation; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services.PublishStatus; + +[TestFixture] +public partial class PublishedContentStatusFilteringServiceTests +{ + [Test] + public void FilterAvailable_Invariant_ForNonPreview_YieldsPublishedItems() + { + var (sut, items) = SetupInvariant(false); + + var children = sut.FilterAvailable(items.Keys, null).ToArray(); + Assert.AreEqual(5, children.Length); + Assert.Multiple(() => + { + Assert.AreEqual(0, children[0].Id); + Assert.AreEqual(2, children[1].Id); + Assert.AreEqual(4, children[2].Id); + Assert.AreEqual(6, children[3].Id); + Assert.AreEqual(8, children[4].Id); + }); + } + + [Test] + public void FilterAvailable_Invariant_ForPreview_YieldsUnpublishedItems() + { + var (sut, items) = SetupInvariant(true); + + var children = sut.FilterAvailable(items.Keys, null).ToArray(); + Assert.AreEqual(10, children.Length); + for (var i = 0; i < 10; i++) + { + Assert.AreEqual(i, children[i].Id); + } + } + + [TestCase("da-DK", 3)] + [TestCase("en-US", 4)] + public void FilterAvailable_Variant_ForNonPreview_YieldsPublishedItemsInCulture(string culture, int expectedNumberOfChildren) + { + var (sut, items) = SetupVariant(false, culture); + + var children = sut.FilterAvailable(items.Keys, culture).ToArray(); + Assert.AreEqual(expectedNumberOfChildren, children.Length); + + // IDs 0 through 3 exist in both en-US and da-DK - only even IDs are published + Assert.Multiple(() => + { + Assert.AreEqual(0, children[0].Id); + Assert.AreEqual(2, children[1].Id); + }); + + // IDs 4 through 6 exist only in en-US - only even IDs are published + if (culture == "en-US") + { + Assert.AreEqual(4, children[2].Id); + Assert.AreEqual(6, children[3].Id); + } + + // IDs 7 through 9 exist only in da-DK - only even IDs are published + if (culture == "da-DK") + { + Assert.AreEqual(8, children[2].Id); + } + } + + [TestCase("da-DK")] + [TestCase("en-US")] + public void FilterAvailable_Variant_ForPreview_YieldsUnpublishedItemsInCulture(string culture) + { + var (sut, items) = SetupVariant(true, culture); + + var children = sut.FilterAvailable(items.Keys, culture).ToArray(); + Assert.AreEqual(7, children.Length); + + // IDs 0 through 3 exist in both en-US and da-DK + Assert.Multiple(() => + { + Assert.AreEqual(0, children[0].Id); + Assert.AreEqual(1, children[1].Id); + Assert.AreEqual(2, children[2].Id); + Assert.AreEqual(3, children[3].Id); + }); + + // IDs 4 through 6 exist only in en-US + if (culture == "en-US") + { + Assert.AreEqual(4, children[4].Id); + Assert.AreEqual(5, children[5].Id); + Assert.AreEqual(6, children[6].Id); + } + + // IDs 7 through 9 exist only in da-DK + if (culture == "da-DK") + { + Assert.AreEqual(7, children[4].Id); + Assert.AreEqual(8, children[5].Id); + Assert.AreEqual(9, children[6].Id); + } + } + + [TestCase("da-DK")] + [TestCase("en-US")] + public void FilterAvailable_MixedVariance_ForNonPreview_YieldsPublishedItemsInCultureOrInvariant(string culture) + { + var (sut, items) = SetupMixedVariance(false, culture); + + var children = sut.FilterAvailable(items.Keys, culture).ToArray(); + Assert.AreEqual(4, children.Length); + + // IDs 0 through 2 are invariant - only even IDs are published + Assert.Multiple(() => + { + Assert.AreEqual(0, children[0].Id); + Assert.AreEqual(2, children[1].Id); + }); + + // IDs 3 through 5 exist in both en-US and da-DK - only even IDs are published + Assert.Multiple(() => + { + Assert.AreEqual(4, children[2].Id); + }); + + // IDs 6 and 7 exist only in en-US - only even IDs are published + if (culture == "en-US") + { + Assert.AreEqual(6, children[3].Id); + } + + // IDs 8 and 9 exist only in da-DK - only even IDs are published + if (culture == "da-DK") + { + Assert.AreEqual(8, children[3].Id); + } + } + + [TestCase("da-DK")] + [TestCase("en-US")] + public void FilterAvailable_MixedVariance_FoPreview_YieldsPublishedItemsInCultureOrInvariant(string culture) + { + var (sut, items) = SetupMixedVariance(true, culture); + + var children = sut.FilterAvailable(items.Keys, culture).ToArray(); + Assert.AreEqual(8, children.Length); + + // IDs 0 through 2 are invariant + Assert.Multiple(() => + { + Assert.AreEqual(0, children[0].Id); + Assert.AreEqual(1, children[1].Id); + Assert.AreEqual(2, children[2].Id); + }); + + // IDs 3 through 5 exist in both en-US and da-DK + Assert.Multiple(() => + { + Assert.AreEqual(3, children[3].Id); + Assert.AreEqual(4, children[4].Id); + Assert.AreEqual(5, children[5].Id); + }); + + // IDs 6 and 7 exist only in en-US + if (culture == "en-US") + { + Assert.AreEqual(6, children[6].Id); + Assert.AreEqual(7, children[7].Id); + } + + // IDs 8 and 9 exist only in da-DK + if (culture == "da-DK") + { + Assert.AreEqual(8, children[6].Id); + Assert.AreEqual(9, children[7].Id); + } + } + + // sets up invariant test data: + // - 10 documents with IDs 0 through 9 + // - even IDs (0, 2, ...) are published, odd are unpublished + private (PublishedContentStatusFilteringService PublishedContentStatusFilteringService, Dictionary Items) SetupInvariant(bool forPreview) + { + var contentType = new Mock(); + contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing); + + var items = new Dictionary(); + for (var i = 0; i < 10; i++) + { + var content = new Mock(); + + var key = Guid.NewGuid(); + content.SetupGet(c => c.Key).Returns(key); + content.SetupGet(c => c.ContentType).Returns(contentType.Object); + content.SetupGet(c => c.Cultures).Returns(new Dictionary()); + content.SetupGet(c => c.Id).Returns(i); + + items[key] = content.Object; + } + + var publishedContentCache = SetupPublishedContentCache(forPreview, items); + var previewService = SetupPreviewService(forPreview); + var publishStatusQueryService = SetupPublishStatusQueryService(items); + var variationContextAccessor = SetupVariantContextAccessor(null); + + return ( + new PublishedContentStatusFilteringService( + variationContextAccessor, + publishStatusQueryService, + previewService, + publishedContentCache), + items); + } + + // sets up variant test data: + // - 10 documents with IDs 0 through 9 + // - IDs 0 through 3 exist in both en-US and da-DK + // - IDs 4 through 6 exist only in en-US + // - IDs 7 through 9 exist only in da-DK + // - even IDs (0, 2, ...) are published, odd are unpublished + private (PublishedContentStatusFilteringService PublishedContentStatusFilteringService, Dictionary Items) SetupVariant(bool forPreview, string requestCulture) + { + var contentType = new Mock(); + contentType.SetupGet(c => c.Variations).Returns(ContentVariation.Culture); + + var items = new Dictionary(); + for (var i = 0; i < 10; i++) + { + var content = new Mock(); + + var key = Guid.NewGuid(); + string[] cultures = i <= 3 + ? ["da-DK", "en-US"] + : i <= 6 + ? ["en-US"] + : ["da-DK"]; + var cultureDictionary = cultures.ToDictionary(culture => culture, culture => new PublishedCultureInfo(culture, culture, $"{i}-{culture}", DateTime.MinValue)); + content.SetupGet(c => c.Key).Returns(key); + content.SetupGet(c => c.ContentType).Returns(contentType.Object); + content.SetupGet(c => c.Cultures).Returns(cultureDictionary); + content.SetupGet(c => c.Id).Returns(i); + + items[key] = content.Object; + } + + var publishedContentCache = SetupPublishedContentCache(forPreview, items); + var previewService = SetupPreviewService(forPreview); + var publishStatusQueryService = SetupPublishStatusQueryService(items); + var variationContextAccessor = SetupVariantContextAccessor(requestCulture); + + return ( + new PublishedContentStatusFilteringService( + variationContextAccessor, + publishStatusQueryService, + previewService, + publishedContentCache), + items); + } + + // sets up mixed variant test data: + // - 10 documents with IDs 0 through 9 + // - IDs 0 through 2 are invariant + // - IDs 3 through 5 exist in both en-US and da-DK + // - IDs 6 and 7 exist only in en-US + // - IDs 8 and 9 exist only in da-DK + // - even IDs (0, 2, ...) are published, odd are unpublished + private (PublishedContentStatusFilteringService PublishedContentStatusFilteringService, Dictionary Items) SetupMixedVariance(bool forPreview, string requestCulture) + { + var invariantContentType = new Mock(); + invariantContentType.SetupGet(c => c.Variations).Returns(ContentVariation.Nothing); + + var variantContentType = new Mock(); + variantContentType.SetupGet(c => c.Variations).Returns(ContentVariation.Culture); + + var items = new Dictionary(); + for (var i = 0; i < 10; i++) + { + var content = new Mock(); + var contentType = i <= 2 + ? invariantContentType + : variantContentType; + + var key = Guid.NewGuid(); + string[] cultures = i <= 2 + ? [] + : i <= 5 + ? ["da-DK", "en-US"] + : i <= 7 + ? ["en-US"] + : ["da-DK"]; + var cultureDictionary = cultures.ToDictionary(culture => culture, culture => new PublishedCultureInfo(culture, culture, $"{i}-{culture}", DateTime.MinValue)); + content.SetupGet(c => c.Key).Returns(key); + content.SetupGet(c => c.ContentType).Returns(contentType.Object); + content.SetupGet(c => c.Cultures).Returns(cultureDictionary); + content.SetupGet(c => c.Id).Returns(i); + + items[key] = content.Object; + } + + var publishedContentCache = SetupPublishedContentCache(forPreview, items); + var previewService = SetupPreviewService(forPreview); + var publishStatusQueryService = SetupPublishStatusQueryService(items); + var variationContextAccessor = SetupVariantContextAccessor(requestCulture); + + return ( + new PublishedContentStatusFilteringService( + variationContextAccessor, + publishStatusQueryService, + previewService, + publishedContentCache), + items); + } + + private IPublishStatusQueryService SetupPublishStatusQueryService(Dictionary items) + => SetupPublishStatusQueryService(items, id => id % 2 == 0); + + private IPublishStatusQueryService SetupPublishStatusQueryService(Dictionary items, Func idIsPublished) + { + var publishStatusQueryService = new Mock(); + publishStatusQueryService + .Setup(p => p.IsDocumentPublished(It.IsAny(), It.IsAny())) + .Returns((Guid key, string culture) => items + .TryGetValue(key, out var item) + && idIsPublished(item.Id) + && (item.ContentType.VariesByCulture() is false || item.Cultures.ContainsKey(culture))); + return publishStatusQueryService.Object; + } + + private IPreviewService SetupPreviewService(bool forPreview) + { + var previewService = new Mock(); + previewService.Setup(p => p.IsInPreview()).Returns(forPreview); + return previewService.Object; + } + + private IVariationContextAccessor SetupVariantContextAccessor(string? requestCulture) + { + var variationContextAccessor = new Mock(); + variationContextAccessor.SetupGet(v => v.VariationContext).Returns(new VariationContext(requestCulture)); + return variationContextAccessor.Object; + } + + private IPublishedContentCache SetupPublishedContentCache(bool forPreview, Dictionary items) + { + var publishedContentCache = new Mock(); + publishedContentCache + .Setup(c => c.GetById(forPreview, It.IsAny())) + .Returns((bool preview, Guid key) => items.TryGetValue(key, out var item) ? item : null); + return publishedContentCache.Object; + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs index 1a4a73f800..399633da2a 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlImageSourceParserTests.cs @@ -98,9 +98,8 @@ public class HtmlImageSourceParserTests new UrlProviderCollection(() => Enumerable.Empty()), new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }), Mock.Of(), - Mock.Of(), Mock.Of(), - Mock.Of()); + Mock.Of()); using (var reference = umbracoContextFactory.EnsureUmbracoContext()) { diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs index ad2fb0b231..d1e5e0f494 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Templates/HtmlLocalLinkParserTests.cs @@ -222,8 +222,12 @@ public class HtmlLocalLinkParserTests var webRoutingSettings = new WebRoutingSettings(); var navigationQueryService = new Mock(); - Guid? parentKey = null; - navigationQueryService.Setup(x => x.TryGetParentKey(It.IsAny(), out parentKey)).Returns(true); + // Guid? parentKey = null; + // navigationQueryService.Setup(x => x.TryGetParentKey(It.IsAny(), out parentKey)).Returns(true); + IEnumerable ancestorKeys = []; + navigationQueryService.Setup(x => x.TryGetAncestorsKeys(It.IsAny(), out ancestorKeys)).Returns(true); + + var publishedContentStatusFilteringService = new Mock(); using (var reference = umbracoContextFactory.EnsureUmbracoContext()) { @@ -246,9 +250,8 @@ public class HtmlLocalLinkParserTests new UrlProviderCollection(() => new[] { contentUrlProvider.Object }), new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }), Mock.Of(), - contentCache.Object, navigationQueryService.Object, - publishStatusQueryService.Object); + publishedContentStatusFilteringService.Object); var linkParser = new HtmlLocalLinkParser(publishedUrlProvider); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index 8661e94e37..0a1c88c5b8 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -3,7 +3,7 @@ true Umbraco.Cms.Tests.UnitTests - + $(WarningsNotAsErrors),SYSLIB0013,CS0618,CS1998,SA1117,CS0067,CA1822,CA1416,IDE0028,SA1401,SA1405,IDE0060,ASP0019,CS0114,CS0661,CS0659,CS0414,CS0252,CS0612,IDE1006 - + From 74eb66ef863d7c586fe7ee2b9f1a8a8b70bdd7f5 Mon Sep 17 00:00:00 2001 From: Mole Date: Tue, 25 Feb 2025 13:26:16 +0100 Subject: [PATCH 30/58] V15: Serverside Media Picker Validation (#18429) * Add TypedJsonValidator to avoid duplicate serialization * Add allowed type validator * Validate multiple media toggle * Add startnode validator * Fix tests * Add validation tests * Apply suggestions from code review Co-authored-by: Andy Butland * Add XML docs * Remove unnecessary obsolete constructor * Avoid multiple checks * Use value instead of specific member names * Remove test * Optimize StartNodeValidator * Clarify Validates_Allowed_Type --------- Co-authored-by: Andy Butland --- .../EmbeddedResources/Lang/da.xml | 5 +- .../EmbeddedResources/Lang/en.xml | 3 + .../EmbeddedResources/Lang/en_us.xml | 3 + .../Validation/ITypedJsonValidator.cs | 19 ++ .../Validation/TypedJsonValidatorRunner.cs | 51 +++++ .../MediaPicker3PropertyEditor.cs | 201 ++++++++++++---- ...ataValueReferenceFactoryCollectionTests.cs | 5 +- .../MediaPicker3ValueEditorValidationTests.cs | 215 ++++++++++++++++++ 8 files changed, 453 insertions(+), 49 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/Validation/ITypedJsonValidator.cs create mode 100644 src/Umbraco.Core/PropertyEditors/Validation/TypedJsonValidatorRunner.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/MediaPicker3ValueEditorValidationTests.cs diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml index bd5c490511..7bec4bb600 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml @@ -113,6 +113,9 @@ Mange hilsner fra Umbraco robotten %1% mere.]]> %1% for mange.]]> Ét eller flere områder lever ikke op til kravene for antal indholdselementer. + Den valgte medie type er ugyldig. + Det er kun tilladt at vælge ét medie. + Valgt medie kommer fra en ugyldig mappe. Slettet indhold med Id: {0} Relateret til original "parent" med id: {1} @@ -124,4 +127,4 @@ Mange hilsner fra Umbraco robotten Filskrivning Mediemappeoprettelse - \ No newline at end of file + diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml index c05ea72cfc..c3b71b5ab0 100644 --- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml +++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml @@ -387,6 +387,9 @@ User group name '%0%' is already taken Member group name '%0%' is already taken Username '%0%' is already taken + The chosen media type is invalid. + Multiple selected media is not allowed. + The selected media is from the wrong folder. )?<\/umb-rte-block(?:-inline)?>/gi, ); let blockElement: RegExpExecArray | null; - while ((blockElement = regex.exec(value)) !== null) { + while ((blockElement = regex.exec(markup)) !== null) { if (blockElement.groups?.key) { usedContentKeys.push(blockElement.groups.key); } } - this._filterUnusedBlocks(usedContentKeys); - - this._markup = value; - if (this.value) { this.value = { ...this.value, - markup: this._markup, + markup: markup, }; } else { this.value = { - markup: this._markup, + markup: markup, blocks: { layout: {}, contentData: [], @@ -55,6 +57,9 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem }; } + // lets run this one after we set the value, to make sure we don't reset the value. + this._filterUnusedBlocks(usedContentKeys); + this._fireChangeEvent(); } @@ -64,6 +69,8 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem .configuration=${this._config} .value=${this._markup} ?readonly=${this.readonly} + ?required=${this.mandatory} + ?required-message=${this.mandatoryMessage} @change=${this.#onChange}> `; } From 5ed09ebefa068a539557c1ebd2c66d6fc95ee1ab Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 26 Feb 2025 20:13:27 +0100 Subject: [PATCH 48/58] Backport ShowUnroutableContentWarnings to V13 (#18479) --- src/Umbraco.Core/Configuration/Models/ContentSettings.cs | 7 +++++++ .../Controllers/ContentController.cs | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs index f08ab2abe5..c542bcb030 100644 --- a/src/Umbraco.Core/Configuration/Models/ContentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/ContentSettings.cs @@ -160,6 +160,7 @@ public class ContentSettings internal const bool StaticDisableUnpublishWhenReferenced = false; internal const bool StaticAllowEditInvariantFromNonDefault = false; internal const bool StaticShowDomainWarnings = true; + internal const bool StaticShowUnroutableContentWarnings = true; /// /// Gets or sets a value for the content notification settings. @@ -285,4 +286,10 @@ public class ContentSettings /// [DefaultValue(StaticShowDomainWarnings)] public bool ShowDomainWarnings { get; set; } = StaticShowDomainWarnings; + + /// + /// Gets or sets a value indicating whether to show unroutable content warnings. + /// + [DefaultValue(StaticShowUnroutableContentWarnings)] + public bool ShowUnroutableContentWarnings { get; set; } = StaticShowUnroutableContentWarnings; } diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs index ef935b6b59..a1c5e55ac8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs @@ -1278,6 +1278,11 @@ public class ContentController : ContentControllerBase SimpleNotificationModel globalNotifications, string[]? successfulCultures) { + if (_contentSettings.ShowUnroutableContentWarnings is false) + { + return; + } + IContent? content = publishStatus.FirstOrDefault()?.Content; if (content is null) { From 2bdeefec16cb8a4e444b2873e563665951fffb28 Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 26 Feb 2025 22:48:03 +0100 Subject: [PATCH 49/58] Pass preview to GetEntryOptions (#18470) --- .../Services/DocumentCacheService.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs index a6af3b89ba..21758a1fcd 100644 --- a/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs +++ b/src/Umbraco.PublishedCache.HybridCache/Services/DocumentCacheService.cs @@ -126,7 +126,7 @@ internal sealed class DocumentCacheService : IDocumentCacheService scope.Complete(); return contentCacheNode; }, - GetEntryOptions(key)); + GetEntryOptions(key, preview)); // We don't want to cache removed items, this may cause issues if the L2 serializer changes. if (contentCacheNode is null) @@ -209,13 +209,13 @@ internal sealed class DocumentCacheService : IDocumentCacheService ContentCacheNode? draftNode = await _databaseCacheRepository.GetContentSourceAsync(key, true); if (draftNode is not null) { - await _hybridCache.SetAsync(GetCacheKey(draftNode.Key, true), draftNode, GetEntryOptions(draftNode.Key)); + await _hybridCache.SetAsync(GetCacheKey(draftNode.Key, true), draftNode, GetEntryOptions(draftNode.Key, true)); } ContentCacheNode? publishedNode = await _databaseCacheRepository.GetContentSourceAsync(key, false); if (publishedNode is not null && HasPublishedAncestorPath(publishedNode.Key)) { - await _hybridCache.SetAsync(GetCacheKey(publishedNode.Key, false), publishedNode, GetEntryOptions(publishedNode.Key)); + await _hybridCache.SetAsync(GetCacheKey(publishedNode.Key, false), publishedNode, GetEntryOptions(publishedNode.Key, false)); } scope.Complete(); @@ -276,9 +276,9 @@ internal sealed class DocumentCacheService : IDocumentCacheService LocalCacheExpiration = _cacheSettings.Entry.Document.SeedCacheDuration }; - private HybridCacheEntryOptions GetEntryOptions(Guid key) + private HybridCacheEntryOptions GetEntryOptions(Guid key, bool preview) { - if (SeedKeys.Contains(key)) + if (SeedKeys.Contains(key) && preview is false) { return GetSeedEntryOptions(); } From 9809db4179819b6459665aa988d9374fc8137871 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Thu, 27 Feb 2025 08:42:25 +0000 Subject: [PATCH 50/58] Tiptap: Text color + background color (#18482) adds a reusable "color picker button" kind extension type. --- .../mocks/data/data-type/data-type.data.ts | 33 +++++--- ...tap-toolbar-color-picker-button.element.ts | 80 +++++++++++++++++++ .../packages/tiptap/extensions/manifests.ts | 36 ++++++++- .../extensions/tiptap-toolbar.extension.ts | 12 ++- ...ext-color-background.tiptap-toolbar-api.ts | 8 ++ ...ext-color-foreground.tiptap-toolbar-api.ts | 8 ++ .../src/packages/tiptap/extensions/types.ts | 2 +- 7 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-color-picker-button.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-background.tiptap-toolbar-api.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-foreground.tiptap-toolbar-api.ts diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 194ace84da..56a114c6c0 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -1048,27 +1048,30 @@ export const data: Array = [ 'Umb.Tiptap.Toolbar.SourceEditor', 'Umb.Tiptap.Toolbar.Undo', 'Umb.Tiptap.Toolbar.Redo', - 'Umb.Tiptap.Toolbar.StyleSelect', - 'Umb.Tiptap.Toolbar.FontFamily', - 'Umb.Tiptap.Toolbar.FontSize', 'Umb.Tiptap.Toolbar.ClearFormatting', + ], + ['Umb.Tiptap.Toolbar.StyleSelect', 'Umb.Tiptap.Toolbar.FontFamily', 'Umb.Tiptap.Toolbar.FontSize'], + [ 'Umb.Tiptap.Toolbar.Bold', 'Umb.Tiptap.Toolbar.Italic', + 'Umb.Tiptap.Toolbar.Underline', + 'Umb.Tiptap.Toolbar.Strike', + ], + ['Umb.Tiptap.Toolbar.TextColorForeground', 'Umb.Tiptap.Toolbar.TextColorBackground'], + [ 'Umb.Tiptap.Toolbar.TextAlignLeft', 'Umb.Tiptap.Toolbar.TextAlignCenter', 'Umb.Tiptap.Toolbar.TextAlignRight', - 'Umb.Tiptap.Toolbar.TextDirectionRtl', - 'Umb.Tiptap.Toolbar.TextDirectionLtr', + ], + ['Umb.Tiptap.Toolbar.TextDirectionRtl', 'Umb.Tiptap.Toolbar.TextDirectionLtr'], + [ 'Umb.Tiptap.Toolbar.BulletList', 'Umb.Tiptap.Toolbar.OrderedList', 'Umb.Tiptap.Toolbar.Blockquote', - 'Umb.Tiptap.Toolbar.Link', - 'Umb.Tiptap.Toolbar.Unlink', 'Umb.Tiptap.Toolbar.HorizontalRule', - 'Umb.Tiptap.Toolbar.Table', - 'Umb.Tiptap.Toolbar.MediaPicker', - 'Umb.Tiptap.Toolbar.EmbeddedMedia', ], + ['Umb.Tiptap.Toolbar.Link', 'Umb.Tiptap.Toolbar.Unlink'], + ['Umb.Tiptap.Toolbar.Table', 'Umb.Tiptap.Toolbar.MediaPicker', 'Umb.Tiptap.Toolbar.EmbeddedMedia'], ], ], }, @@ -1112,11 +1115,18 @@ export const data: Array = [ 'styles', 'fontfamily', 'fontsize', + 'forecolor', + 'backcolor', + 'blockquote', + 'removeformat', 'bold', 'italic', + 'underline', + 'strikethrough', 'alignleft', 'aligncenter', 'alignright', + 'alignjustify', 'bullist', 'numlist', 'outdent', @@ -1124,6 +1134,9 @@ export const data: Array = [ 'link', 'unlink', 'anchor', + 'hr', + 'subscript', + 'superscript', 'charmap', 'rtl', 'ltr', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-color-picker-button.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-color-picker-button.element.ts new file mode 100644 index 0000000000..ff19362aec --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-color-picker-button.element.ts @@ -0,0 +1,80 @@ +import { UmbTiptapToolbarButtonElement } from '../../components/toolbar/tiptap-toolbar-button.element.js'; +import { css, customElement, html, state, when } from '@umbraco-cms/backoffice/external/lit'; +import type { UUIColorPickerChangeEvent } from '@umbraco-cms/backoffice/external/uui'; + +@customElement('umb-tiptap-toolbar-color-picker-button') +export class UmbTiptapToolbarColorPickerButtonElement extends UmbTiptapToolbarButtonElement { + #onChange(event: UUIColorPickerChangeEvent) { + this._selectedColor = event.target.value; + this.api?.execute(this.editor, this._selectedColor); + } + + @state() + private _selectedColor?: string; + + override render() { + const label = this.localize.string(this.manifest?.meta.label); + return html` + + this.api?.execute(this.editor, this._selectedColor)}> +
+ ${when( + this.manifest?.meta.icon, + (icon) => html``, + () => html`${label}`, + )} +
+
+
+ + + + + + + + + + +
+ `; + } + + static override readonly styles = [ + css` + uui-button-group:hover { + background-color: var(--uui-color-background); + border-radius: var(--uui-border-radius); + } + + uui-scroll-container { + border-radius: var(--uui-border-radius); + overflow-x: hidden; + } + + umb-icon { + height: 1em; + width: 1em; + margin-bottom: 1px; + } + + #color-selected { + height: var(--uui-size-1); + } + `, + ]; +} + +export { UmbTiptapToolbarColorPickerButtonElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-tiptap-toolbar-color-picker-button': UmbTiptapToolbarColorPickerButtonElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 67c159a6cf..16a22a5e50 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -2,7 +2,6 @@ import { manifests as blockExtensions } from './block/manifests.js'; import { manifests as styleSelectExtensions } from './style-select/manifests.js'; import { manifests as tableExtensions } from './table/manifests.js'; import type { ManifestTiptapExtension } from './tiptap.extension.js'; -import type { ManifestTiptapToolbarExtension } from './tiptap-toolbar.extension.js'; import type { UmbExtensionManifestKind } from '@umbraco-cms/backoffice/extension-registry'; const kinds: Array = [ @@ -15,6 +14,15 @@ const kinds: Array = [ element: () => import('../components/toolbar/tiptap-toolbar-button.element.js'), }, }, + { + type: 'kind', + alias: 'Umb.Kind.TiptapToolbar.ColorPickerButton', + matchKind: 'colorPickerButton', + matchType: 'tiptapToolbarExtension', + manifest: { + element: () => import('../components/toolbar/tiptap-toolbar-color-picker-button.element.js'), + }, + }, ]; const coreExtensions: Array = [ @@ -146,7 +154,7 @@ const coreExtensions: Array = [ }, ]; -const toolbarExtensions: Array = [ +const toolbarExtensions: Array = [ { type: 'tiptapToolbarExtension', kind: 'button', @@ -272,6 +280,30 @@ const toolbarExtensions: Array = [ label: 'Text Align Justify', }, }, + { + type: 'tiptapToolbarExtension', + kind: 'colorPickerButton', + alias: 'Umb.Tiptap.Toolbar.TextColorBackground', + name: 'Text Color Background Tiptap Extension', + api: () => import('./toolbar/text-color-background.tiptap-toolbar-api.js'), + meta: { + alias: 'text-color-background', + icon: 'icon-color-bucket', + label: 'Background color', + }, + }, + { + type: 'tiptapToolbarExtension', + kind: 'colorPickerButton', + alias: 'Umb.Tiptap.Toolbar.TextColorForeground', + name: 'Text Color Foreground Tiptap Extension', + api: () => import('./toolbar/text-color-foreground.tiptap-toolbar-api.js'), + meta: { + alias: 'text-color-foreground', + icon: 'icon-colorpicker', + label: 'Color', + }, + }, { type: 'tiptapToolbarExtension', kind: 'button', diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar.extension.ts index 36d5c74f4a..aa713a9ef5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar.extension.ts @@ -23,8 +23,18 @@ export interface ManifestTiptapToolbarExtensionButtonKind< kind: 'button'; } +export interface ManifestTiptapToolbarExtensionColorPickerButtonKind< + MetaType extends MetaTiptapToolbarExtension = MetaTiptapToolbarExtension, +> extends ManifestTiptapToolbarExtension { + type: 'tiptapToolbarExtension'; + kind: 'colorPickerButton'; +} + declare global { interface UmbExtensionManifestMap { - umbTiptapToolbarExtension: ManifestTiptapToolbarExtension | ManifestTiptapToolbarExtensionButtonKind; + umbTiptapToolbarExtension: + | ManifestTiptapToolbarExtension + | ManifestTiptapToolbarExtensionButtonKind + | ManifestTiptapToolbarExtensionColorPickerButtonKind } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-background.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-background.tiptap-toolbar-api.ts new file mode 100644 index 0000000000..f29d160c77 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-background.tiptap-toolbar-api.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../base.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarTextColorBackgroundExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor, selectedColor?: string) { + editor?.chain().focus().setSpanStyle(`background-color: ${selectedColor};`).run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-foreground.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-foreground.tiptap-toolbar-api.ts new file mode 100644 index 0000000000..e53f7d054a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-color-foreground.tiptap-toolbar-api.ts @@ -0,0 +1,8 @@ +import { UmbTiptapToolbarElementApiBase } from '../base.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarTextColorForegroundExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor, selectedColor?: string) { + editor?.chain().focus().setSpanStyle(`color: ${selectedColor};`).run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts index b172f1dae1..7177fcd940 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/types.ts @@ -48,7 +48,7 @@ export interface UmbTiptapToolbarElementApi extends UmbApi, UmbTiptapExtensionAr /** * Executes the toolbar element action. */ - execute(editor?: Editor): void; + execute(editor?: Editor, ...args: Array): void; /** * Checks if the toolbar element is active. From f6ab6eea7591ce100964cefda7de6a49cb9721bd Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Thu, 27 Feb 2025 09:52:44 +0000 Subject: [PATCH 51/58] Tiptap RTE: Reusable toolbar menu component (#18483) * Adds Tiptap Toolbar Menu extension kind a reusable menu component. Removes the `unique` property for menu-items. * Implements Font Family as toolbar menu kind * Implements Font Size as toolbar menu kind * Implements Style Select as toolbar menu kind * Implements Table as toolbar menu kind * Markup amends * Mock data RTE content addition * "TextDirection" manifest correction * Text Align: made to be toggleable --- .../src/mocks/data/document/document.data.ts | 1 + .../cascading-menu-popover.element.ts | 43 ++--- .../tiptap-toolbar-button-disabled.element.ts | 11 +- .../toolbar/tiptap-toolbar-button.element.ts | 7 +- .../toolbar/tiptap-toolbar-menu.element.ts | 165 ++++++++++++++++++ .../packages/tiptap/extensions/manifests.ts | 37 +++- .../extensions/style-select/manifests.ts | 28 ++- .../style-select-tiptap-toolbar.element.ts | 95 ---------- .../style-select.tiptap-toolbar-api.ts | 20 +++ .../tiptap/extensions/table/manifests.ts | 44 ++++- .../table-tiptap-toolbar-button.element.ts | 55 ------ .../table/table.tiptap-toolbar-api.ts | 131 +++----------- .../extensions/tiptap-toolbar.extension.ts | 26 ++- .../font-family-tiptap-toolbar.element.ts | 88 ---------- .../toolbar/font-family.tiptap-toolbar-api.ts | 10 ++ .../font-size-tiptap-toolbar.element.ts | 52 ------ .../toolbar/font-size.tiptap-toolbar-api.ts | 10 ++ .../text-align-center.tiptap-toolbar-api.ts | 6 +- .../text-align-justify.tiptap-toolbar-api.ts | 6 +- .../text-align-left.tiptap-toolbar-api.ts | 6 +- .../text-align-right.tiptap-toolbar-api.ts | 6 +- 21 files changed, 400 insertions(+), 447 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-menu.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select-tiptap-toolbar.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select.tiptap-toolbar-api.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/table-tiptap-toolbar-button.element.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family.tiptap-toolbar-api.ts delete mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size.tiptap-toolbar-api.ts diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts index c1b72da5fc..98ea26928f 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/document/document.data.ts @@ -914,6 +914,7 @@ export const data: Array = [

Some value for the RTE with an external link and an internal link.

+

All HTML tags

This is a plain old span tag. Hello world. diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/cascading-menu-popover/cascading-menu-popover.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/cascading-menu-popover/cascading-menu-popover.element.ts index 43e95c192f..9a0f518bb3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/cascading-menu-popover/cascading-menu-popover.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/cascading-menu-popover/cascading-menu-popover.element.ts @@ -1,15 +1,14 @@ -import { css, customElement, html, property, repeat, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, ifDefined, property, repeat, when } from '@umbraco-cms/backoffice/external/lit'; import { UUIPopoverContainerElement } from '@umbraco-cms/backoffice/external/uui'; export type UmbCascadingMenuItem = { - unique: string; label: string; icon?: string; items?: Array; element?: HTMLElement; separatorAfter?: boolean; + style?: string; execute?: () => void; - isActive?: () => boolean; }; @customElement('umb-cascading-menu-popover') @@ -21,10 +20,10 @@ export class UmbCascadingMenuPopoverElement extends UUIPopoverContainerElement { return this.shadowRoot?.querySelector(`#${popoverId}`) as UUIPopoverContainerElement; } - #onMouseEnter(item: UmbCascadingMenuItem) { + #onMouseEnter(item: UmbCascadingMenuItem, popoverId: string) { if (!item.items?.length) return; - const popover = this.#getPopoverById(item.unique); + const popover = this.#getPopoverById(popoverId); if (!popover) return; // TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet. @@ -33,8 +32,8 @@ export class UmbCascadingMenuPopoverElement extends UUIPopoverContainerElement { popover.showPopover(); } - #onMouseLeave(item: UmbCascadingMenuItem) { - const popover = this.#getPopoverById(item.unique); + #onMouseLeave(item: UmbCascadingMenuItem, popoverId: string) { + const popover = this.#getPopoverById(popoverId); if (!popover) return; // TODO: This ignorer is just neede for JSON SCHEMA TO WORK, As its not updated with latest TS jet. @@ -43,11 +42,11 @@ export class UmbCascadingMenuPopoverElement extends UUIPopoverContainerElement { popover.hidePopover(); } - #onClick(item: UmbCascadingMenuItem) { + #onClick(item: UmbCascadingMenuItem, popoverId: string) { item.execute?.(); setTimeout(() => { - this.#onMouseLeave(item); + this.#onMouseLeave(item, popoverId); }, 100); } @@ -56,44 +55,40 @@ export class UmbCascadingMenuPopoverElement extends UUIPopoverContainerElement { ${when( this.items?.length, - () => html` - ${repeat( - this.items!, - (item) => item.unique, - (item) => this.#renderItem(item), - )} - ${super.render()} - `, + () => html` ${repeat(this.items!, (item, index) => this.#renderItem(item, index))} ${super.render()} `, () => super.render(), )} `; } - #renderItem(item: UmbCascadingMenuItem) { + #renderItem(item: UmbCascadingMenuItem, index: number) { const element = item.element; + const popoverId = `item-${index}`; if (element) { - element.setAttribute('popovertarget', item.unique); + element.setAttribute('popovertarget', popoverId); } return html` -
this.#onMouseEnter(item)} @mouseleave=${() => this.#onMouseLeave(item)}> +
this.#onMouseEnter(item, popoverId)} + @mouseleave=${() => this.#onMouseLeave(item, popoverId)}> ${when( element, () => element, () => html` this.#onClick(item)}> + popovertarget=${popoverId} + @click-label=${() => this.#onClick(item, popoverId)}> ${when(item.icon, (icon) => html``)} `, )} - +
`; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button-disabled.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button-disabled.element.ts index cbc9b13f54..c50eebe86c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button-disabled.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button-disabled.element.ts @@ -1,21 +1,22 @@ import { UmbTiptapToolbarButtonElement } from './tiptap-toolbar-button.element.js'; -import { customElement, html, ifDefined, when } from '@umbraco-cms/backoffice/external/lit'; +import { customElement, html, when } from '@umbraco-cms/backoffice/external/lit'; @customElement('umb-tiptap-toolbar-button-disabled') export class UmbTiptapToolbarButtonDisabledElement extends UmbTiptapToolbarButtonElement { override render() { + const label = this.localize.string(this.manifest?.meta.label); return html` (this.api && this.editor ? this.api.execute(this.editor) : null)}> + @click=${() => this.api?.execute(this.editor)}> ${when( this.manifest?.meta.icon, () => html``, - () => html`${this.manifest?.meta.label}`, + () => html`${label}`, )} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts index 7a567400b6..c96cb42d1c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-button.element.ts @@ -38,18 +38,19 @@ export class UmbTiptapToolbarButtonElement extends UmbLitElement { }; override render() { + const label = this.localize.string(this.manifest?.meta.label); return html` (this.api && this.editor ? this.api.execute(this.editor) : null)}> + @click=${() => this.api?.execute(this.editor)}> ${when( this.manifest?.meta.icon, (icon) => html``, - () => html`${this.manifest?.meta.label}`, + () => html`${label}`, )} `; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-menu.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-menu.element.ts new file mode 100644 index 0000000000..e30cc9aefc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/toolbar/tiptap-toolbar-menu.element.ts @@ -0,0 +1,165 @@ +import type { + ManifestTiptapToolbarExtensionMenuKind, + MetaTiptapToolbarMenuItem, + UmbTiptapToolbarElementApi, +} from '../../extensions/index.js'; +import type { UmbCascadingMenuItem } from '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; +import { css, customElement, html, ifDefined, state, when } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +import '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; + +@customElement('umb-tiptap-toolbar-menu-element') +export class UmbTiptapToolbarMenuElement extends UmbLitElement { + #menu: Array = []; + + @state() + protected isActive = false; + + public api?: UmbTiptapToolbarElementApi; + + public editor?: Editor; + + public set manifest(value: ManifestTiptapToolbarExtensionMenuKind | undefined) { + this.#manifest = value; + this.#setMenu(); + } + public get manifest(): ManifestTiptapToolbarExtensionMenuKind | undefined { + return this.#manifest; + } + #manifest?: ManifestTiptapToolbarExtensionMenuKind | undefined; + + override connectedCallback() { + super.connectedCallback(); + + if (this.editor) { + this.editor.on('selectionUpdate', this.#onEditorUpdate); + this.editor.on('update', this.#onEditorUpdate); + } + } + + override disconnectedCallback() { + super.disconnectedCallback(); + + if (this.editor) { + this.editor.off('selectionUpdate', this.#onEditorUpdate); + this.editor.off('update', this.#onEditorUpdate); + } + } + + async #setMenu() { + if (!this.#manifest?.meta.items) return; + this.#menu = await this.#getMenuItems(this.#manifest.meta.items); + } + + async #getMenuItems(items: Array): Promise> { + const menu = []; + + for (const item of items) { + const menuItem = await this.#getMenuItem(item); + menu.push(menuItem); + } + + return menu; + } + + async #getMenuItem(item: MetaTiptapToolbarMenuItem): Promise { + let element; + + // TODO: Commented out as needs review of how async/await is being handled here. [LK] + // if (item.element) { + // const elementConstructor = await loadManifestElement(item.element); + // if (elementConstructor) { + // element = new elementConstructor(); + // } + // } + + if (!element && item.elementName) { + element = document.createElement(item.elementName); + } + + if (element) { + // TODO: Enforce a type for the element, that has an `editor` property. [LK] + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + element.editor = this.editor; + } + + let items; + if (item.items) { + items = await this.#getMenuItems(item.items); + } + + return { + icon: item.icon, + items, + label: item.label, + style: item.style, + separatorAfter: item.separatorAfter, + element, + execute: () => this.api?.execute(this.editor, item), + }; + } + + readonly #onEditorUpdate = () => { + if (this.api && this.editor && this.manifest) { + this.isActive = this.api.isActive(this.editor); + } + }; + + override render() { + const label = this.localize.string(this.manifest?.meta.label); + return html` + ${when( + this.manifest?.meta.look === 'icon', + () => html` + + ${when( + this.manifest?.meta.icon, + (icon) => html``, + () => html`${this.manifest?.meta.label}`, + )} + + + `, + () => html` + + ${label} + + + `, + )} + + + `; + } + + static override readonly styles = [ + css` + :host { + --uui-button-font-weight: normal; + --uui-menu-item-flat-structure: 1; + + margin-inline-start: var(--uui-size-space-1); + } + + uui-button > uui-symbol-expand { + margin-left: var(--uui-size-space-4); + } + `, + ]; +} + +export { UmbTiptapToolbarMenuElement as element }; + +declare global { + interface HTMLElementTagNameMap { + 'umb-tiptap-toolbar-menu-element': UmbTiptapToolbarMenuElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts index 16a22a5e50..4042511918 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/manifests.ts @@ -23,6 +23,15 @@ const kinds: Array = [ element: () => import('../components/toolbar/tiptap-toolbar-color-picker-button.element.js'), }, }, + { + type: 'kind', + alias: 'Umb.Kind.TiptapToolbar.Menu', + matchKind: 'menu', + matchType: 'tiptapToolbarExtension', + manifest: { + element: () => import('../components/toolbar/tiptap-toolbar-menu.element.js'), + }, + }, ]; const coreExtensions: Array = [ @@ -310,7 +319,7 @@ const toolbarExtensions: Array = [ alias: 'Umb.Tiptap.Toolbar.TextDirectionRtl', name: 'Text Direction RTL Tiptap Extension', api: () => import('./toolbar/text-direction-rtl.tiptap-toolbar-api.js'), - forExtensions: ['Umb.Tiptap.TextAlign'], + forExtensions: ['Umb.Tiptap.TextDirection'], meta: { alias: 'text-direction-rtl', icon: 'icon-text-direction-rtl', @@ -323,7 +332,7 @@ const toolbarExtensions: Array = [ alias: 'Umb.Tiptap.Toolbar.TextDirectionLtr', name: 'Text Direction LTR Tiptap Extension', api: () => import('./toolbar/text-direction-ltr.tiptap-toolbar-api.js'), - forExtensions: ['Umb.Tiptap.TextAlign'], + forExtensions: ['Umb.Tiptap.TextDirection'], meta: { alias: 'text-direction-ltr', icon: 'icon-text-direction-ltr', @@ -533,24 +542,44 @@ const toolbarExtensions: Array = [ }, { type: 'tiptapToolbarExtension', + kind: 'menu', alias: 'Umb.Tiptap.Toolbar.FontFamily', name: 'Font Family Tiptap Extension', - element: () => import('./toolbar/font-family-tiptap-toolbar.element.js'), + api: () => import('./toolbar/font-family.tiptap-toolbar-api.js'), meta: { alias: 'umbFontFamily', icon: 'icon-ruler-alt', label: 'Font family', + items: [ + { label: 'Sans serif', style: 'font-family: sans-serif;', data: 'sans-serif' }, + { label: 'Serif', style: 'font-family: serif;', data: 'serif' }, + { label: 'Monospace', style: 'font-family: monospace;', data: 'monospace' }, + { label: 'Cursive', style: 'font-family: cursive;', data: 'cursive' }, + { label: 'Fantasy', style: 'font-family: fantasy;', data: 'fantasy' }, + ], }, }, { type: 'tiptapToolbarExtension', + kind: 'menu', alias: 'Umb.Tiptap.Toolbar.FontSize', name: 'Font Size Tiptap Extension', - element: () => import('./toolbar/font-size-tiptap-toolbar.element.js'), + api: () => import('./toolbar/font-size.tiptap-toolbar-api.js'), meta: { alias: 'umbFontSize', icon: 'icon-ruler', label: 'Font size', + items: [ + { label: '8pt', data: '8pt;' }, + { label: '10pt', data: '10pt;' }, + { label: '12pt', data: '12pt;' }, + { label: '14pt', data: '14pt;' }, + { label: '16pt', data: '16pt;' }, + { label: '18pt', data: '18pt;' }, + { label: '24pt', data: '24pt;' }, + { label: '26pt', data: '26pt;' }, + { label: '48pt', data: '48pt;' }, + ], }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts index 8e2bbb6fa5..298270f1f5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts @@ -1,15 +1,35 @@ -import type { ManifestTiptapToolbarExtension } from '../types.js'; - -export const manifests: Array = [ +export const manifests: Array = [ { type: 'tiptapToolbarExtension', + kind: 'menu', alias: 'Umb.Tiptap.Toolbar.StyleSelect', name: 'Style Select Tiptap Extension', - element: () => import('./style-select-tiptap-toolbar.element.js'), + api: () => import('./style-select.tiptap-toolbar-api.js'), meta: { alias: 'umbStyleSelect', icon: 'icon-palette', label: 'Style Select', + items: [ + { + label: 'Headers', + items: [ + { label: 'Page heading', data: 'h2', style: 'font-size: x-large;font-weight: bold;' }, + { label: 'Section heading', data: 'h3', style: 'font-size: large;font-weight: bold;' }, + { label: 'Paragraph heading', data: 'h4', style: 'font-weight: bold;' }, + ], + }, + { + label: 'Blocks', + items: [{ label: 'Paragraph', data: 'p' }], + }, + { + label: 'Containers', + items: [ + { label: 'Quote', data: 'blockquote', style: 'font-style: italic;' }, + { label: 'Code', data: 'codeBlock', style: 'font-family: monospace;' }, + ], + }, + ], }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select-tiptap-toolbar.element.ts deleted file mode 100644 index 07febbc2c4..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select-tiptap-toolbar.element.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { ManifestTiptapToolbarExtension } from '../tiptap-toolbar.extension.js'; -import type { UmbCascadingMenuItem } from '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; -import { css, customElement, html, ifDefined } from '@umbraco-cms/backoffice/external/lit'; -import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; -import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; - -import '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; - -@customElement('umb-tiptap-style-select-toolbar-element') -export class UmbTiptapToolbarStyleSelectToolbarElement extends UmbLitElement { - #menu: Array = [ - { - unique: 'headers', - label: 'Headers', - items: [ - { - unique: 'h2', - label: 'Page heading', - execute: () => this.editor?.chain().focus().toggleHeading({ level: 2 }).run(), - }, - { - unique: 'h3', - label: 'Section heading', - execute: () => this.editor?.chain().focus().toggleHeading({ level: 3 }).run(), - }, - { - unique: 'h4', - label: 'Paragraph heading', - execute: () => this.editor?.chain().focus().toggleHeading({ level: 4 }).run(), - }, - ], - }, - { - unique: 'blocks', - label: 'Blocks', - items: [ - { - unique: 'p', - label: 'Paragraph', - execute: () => this.editor?.chain().focus().setParagraph().run(), - }, - ], - }, - { - unique: 'containers', - label: 'Containers', - items: [ - { unique: 'blockquote', label: 'Quote', execute: () => this.editor?.chain().focus().toggleBlockquote().run() }, - { unique: 'code', label: 'Code', execute: () => this.editor?.chain().focus().toggleCodeBlock().run() }, - ], - }, - ]; - - public editor?: Editor; - - public manifest?: ManifestTiptapToolbarExtension; - - override render() { - return html` - - Style select - - - - - `; - } - - static override readonly styles = [ - css` - :host { - --uui-button-font-weight: normal; - - margin-inline-start: var(--uui-size-space-1); - } - - uui-button > uui-symbol-expand { - margin-left: var(--uui-size-space-4); - } - `, - ]; -} - -export { UmbTiptapToolbarStyleSelectToolbarElement as element }; - -declare global { - interface HTMLElementTagNameMap { - 'umb-tiptap-style-select-toolbar-element': UmbTiptapToolbarStyleSelectToolbarElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select.tiptap-toolbar-api.ts new file mode 100644 index 0000000000..3c65eed2f8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/style-select.tiptap-toolbar-api.ts @@ -0,0 +1,20 @@ +import { UmbTiptapToolbarElementApiBase } from '../base.js'; +import type { MetaTiptapToolbarMenuItem } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarStyleSelectExtensionApi extends UmbTiptapToolbarElementApiBase { + #commands: Record void> = { + h2: (editor) => editor?.chain().focus().toggleHeading({ level: 2 }).run(), + h3: (editor) => editor?.chain().focus().toggleHeading({ level: 3 }).run(), + h4: (editor) => editor?.chain().focus().toggleHeading({ level: 4 }).run(), + p: (editor) => editor?.chain().focus().setParagraph().run(), + blockquote: (editor) => editor?.chain().focus().toggleBlockquote().run(), + codeBlock: (editor) => editor?.chain().focus().toggleCodeBlock().run(), + }; + + override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) { + if (!item?.data) return; + const key = item.data.toString(); + this.#commands[key](editor); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/manifests.ts index 2178664c1f..68263c0e07 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/manifests.ts @@ -1,4 +1,4 @@ -import type { ManifestTiptapExtension, ManifestTiptapToolbarExtension } from '../types.js'; +import type { ManifestTiptapExtension } from '../types.js'; const coreExtensions: Array = [ { @@ -15,18 +15,56 @@ const coreExtensions: Array = [ }, ]; -const toolbarExtensions: Array = [ +const toolbarExtensions: Array = [ { type: 'tiptapToolbarExtension', + kind: 'menu', alias: 'Umb.Tiptap.Toolbar.Table', name: 'Table Tiptap Extension', api: () => import('./table.tiptap-toolbar-api.js'), - element: () => import('./table-tiptap-toolbar-button.element.js'), forExtensions: ['Umb.Tiptap.Table'], meta: { alias: 'table', icon: 'icon-table', label: 'Table', + look: 'icon', + items: [ + { + label: 'Table', + icon: 'icon-table', + items: [{ label: 'Insert table', elementName: 'umb-tiptap-table-insert' }], + separatorAfter: true, + }, + { + label: 'Cell', + items: [ + { label: 'Merge cells', data: 'mergeCells' }, + { label: 'Split cell', data: 'splitCell' }, + { label: 'Merge or split', data: 'mergeOrSplit' }, + { label: 'Toggle header cell', data: 'toggleHeaderCell' }, + ], + }, + { + label: 'Row', + items: [ + { label: 'Add row before', data: 'addRowBefore' }, + { label: 'Add row after', data: 'addRowAfter' }, + { label: 'Delete row', icon: 'icon-trash', data: 'deleteRow' }, + { label: 'Toggle header row', data: 'toggleHeaderRow' }, + ], + }, + { + label: 'Column', + items: [ + { label: 'Add column before', data: 'addColumnBefore' }, + { label: 'Add column after', data: 'addColumnAfter' }, + { label: 'Delete column', icon: 'icon-trash', data: 'deleteColumn' }, + { label: 'Toggle header column', data: 'toggleHeaderColumn' }, + ], + separatorAfter: true, + }, + { label: 'Delete table', icon: 'icon-trash', data: 'deleteTable' }, + ], }, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/table-tiptap-toolbar-button.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/table-tiptap-toolbar-button.element.ts deleted file mode 100644 index 86d59f6080..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/table-tiptap-toolbar-button.element.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { UmbTiptapToolbarButtonElement } from '../../components/toolbar/tiptap-toolbar-button.element.js'; -import type { UmbTiptapToolbarTableExtensionApi } from './table.tiptap-toolbar-api.js'; -import { css, customElement, html, ifDefined, when } from '@umbraco-cms/backoffice/external/lit'; - -import '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; - -@customElement('umb-tiptap-toolbar-table-button') -export class UmbTiptapToolbarTableButtonElement extends UmbTiptapToolbarButtonElement { - override api?: UmbTiptapToolbarTableExtensionApi; - - override render() { - return html` - - ${when( - this.manifest?.meta.icon, - (icon) => html``, - () => html`${this.manifest?.meta.label}`, - )} - - - ${when( - this.api?.getMenu(this.editor), - (menu) => html` - - - `, - )} - `; - } - - static override readonly styles = [ - css` - :host { - --uui-menu-item-flat-structure: 1; - } - - uui-button > uui-symbol-expand { - margin-left: var(--uui-size-space-1); - } - `, - ]; -} - -export { UmbTiptapToolbarTableButtonElement as element }; - -declare global { - interface HTMLElementTagNameMap { - 'umb-tiptap-toolbar-table-button': UmbTiptapToolbarTableButtonElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/table.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/table.tiptap-toolbar-api.ts index b47f6f4c2e..79b5da40a5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/table.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/table/table.tiptap-toolbar-api.ts @@ -1,117 +1,30 @@ -import type { UmbCascadingMenuItem } from '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; import { UmbTiptapToolbarElementApiBase } from '../base.js'; -import { UmbTiptapTableInsertElement } from './components/table-insert.element.js'; +import type { MetaTiptapToolbarMenuItem } from '../types.js'; import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; +import './components/table-insert.element.js'; + export class UmbTiptapToolbarTableExtensionApi extends UmbTiptapToolbarElementApiBase { - public override execute() {} + #commands: Record void> = { + mergeCells: (editor) => editor?.chain().focus().mergeCells().run(), + splitCell: (editor) => editor?.chain().focus().splitCell().run(), + mergeOrSplit: (editor) => editor?.chain().focus().mergeOrSplit().run(), + toggleHeaderCell: (editor) => editor?.chain().focus().toggleHeaderCell().run(), + addRowBefore: (editor) => editor?.chain().focus().addRowBefore().run(), + addRowAfter: (editor) => editor?.chain().focus().addRowAfter().run(), + deleteRow: (editor) => editor?.chain().focus().deleteRow().run(), + toggleHeaderRow: (editor) => editor?.chain().focus().toggleHeaderRow().run(), + addColumnBefore: (editor) => editor?.chain().focus().addColumnBefore().run(), + addColumnAfter: (editor) => editor?.chain().focus().addColumnAfter().run(), + deleteColumn: (editor) => editor?.chain().focus().deleteColumn().run(), + toggleHeaderColumn: (editor) => editor?.chain().focus().toggleHeaderColumn().run(), + deleteTable: (editor) => editor?.chain().focus().deleteTable().run(), + }; - public getMenu(editor?: Editor): Array { - const tableInsertElement = new UmbTiptapTableInsertElement(); - tableInsertElement.editor = editor; - - return [ - { - unique: 'table-menu-table', - label: 'Table', - icon: 'icon-table', - items: [ - { - unique: 'table-insert', - label: 'Insert table', - element: tableInsertElement, - }, - ], - separatorAfter: true, - }, - { - unique: 'table-menu-cell', - label: 'Cell', - items: [ - { - unique: 'table-merge', - label: 'Merge cells', - execute: () => editor?.chain().focus().mergeCells().run(), - }, - { - unique: 'table-split', - label: 'Split cell', - execute: () => editor?.chain().focus().splitCell().run(), - }, - { - unique: 'table-merge-split', - label: 'Merge or split', - execute: () => editor?.chain().focus().mergeOrSplit().run(), - }, - { - unique: 'table-header-cell', - label: 'Toggle header cell', - execute: () => editor?.chain().focus().toggleHeaderCell().run(), - }, - ], - }, - { - unique: 'table-menu-row', - label: 'Row', - items: [ - { - unique: 'table-row-before', - label: 'Add row before', - execute: () => editor?.chain().focus().addRowBefore().run(), - }, - { - unique: 'table-row-after', - label: 'Add row after', - execute: () => editor?.chain().focus().addRowAfter().run(), - }, - { - unique: 'table-row-delete', - label: 'Delete row', - icon: 'icon-trash', - execute: () => editor?.chain().focus().deleteRow().run(), - }, - { - unique: 'table-header-row', - label: 'Toggle header row', - execute: () => editor?.chain().focus().toggleHeaderRow().run(), - }, - ], - }, - { - unique: 'table-menu-column', - label: 'Column', - items: [ - { - unique: 'table-column-before', - label: 'Add column before', - execute: () => editor?.chain().focus().addColumnBefore().run(), - }, - { - unique: 'table-column-after', - label: 'Add column after', - execute: () => editor?.chain().focus().addColumnAfter().run(), - }, - { - unique: 'table-column-delete', - label: 'Delete column', - icon: 'icon-trash', - execute: () => editor?.chain().focus().deleteColumn().run(), - }, - { - unique: 'table-header-column', - label: 'Toggle header column', - execute: () => editor?.chain().focus().toggleHeaderColumn().run(), - }, - ], - separatorAfter: true, - }, - { - unique: 'table-delete', - label: 'Delete table', - icon: 'icon-trash', - execute: () => editor?.chain().focus().deleteTable().run(), - }, - ]; + override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) { + if (!item?.data) return; + const key = item.data.toString(); + this.#commands[key](editor); } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar.extension.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar.extension.ts index aa713a9ef5..35432bca51 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar.extension.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/tiptap-toolbar.extension.ts @@ -1,6 +1,6 @@ import type { UmbTiptapToolbarElementApi } from './types.js'; import type { UmbControllerHostElement } from '@umbraco-cms/backoffice/controller-api'; -import type { ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; +import type { ElementLoaderProperty, ManifestElementAndApi } from '@umbraco-cms/backoffice/extension-api'; export interface ManifestTiptapToolbarExtension< MetaType extends MetaTiptapToolbarExtension = MetaTiptapToolbarExtension, @@ -30,11 +30,35 @@ export interface ManifestTiptapToolbarExtensionColorPickerButtonKind< kind: 'colorPickerButton'; } +export interface MetaTiptapToolbarMenuItem { + data?: unknown; + element?: ElementLoaderProperty; + elementName?: string; + icon?: string; + items?: Array; + label: string; + separatorAfter?: boolean; + style?: string; +} + +export interface MetaTiptapToolbarMenuExtension extends MetaTiptapToolbarExtension { + look?: 'icon' | 'text'; + items: Array; +} + +export interface ManifestTiptapToolbarExtensionMenuKind< + MetaType extends MetaTiptapToolbarMenuExtension = MetaTiptapToolbarMenuExtension, +> extends ManifestTiptapToolbarExtension { + type: 'tiptapToolbarExtension'; + kind: 'menu'; +} + declare global { interface UmbExtensionManifestMap { umbTiptapToolbarExtension: | ManifestTiptapToolbarExtension | ManifestTiptapToolbarExtensionButtonKind | ManifestTiptapToolbarExtensionColorPickerButtonKind + | ManifestTiptapToolbarExtensionMenuKind; } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts deleted file mode 100644 index c210dc997c..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family-tiptap-toolbar.element.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { UmbTiptapToolbarButtonElement } from '../../components/toolbar/tiptap-toolbar-button.element.js'; -import type { UmbCascadingMenuItem } from '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; -import { css, customElement, html, ifDefined } from '@umbraco-cms/backoffice/external/lit'; - -import '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; - -@customElement('umb-tiptap-font-family-toolbar-element') -export class UmbTiptapToolbarFontFamilyToolbarElement extends UmbTiptapToolbarButtonElement { - #menu: Array = [ - { - unique: 'font-family-sans-serif', - label: 'Sans serif', - element: this.#getElement('sans-serif', 'Sans serif'), - }, - { - unique: 'font-family-serif', - label: 'Serif', - element: this.#getElement('serif', 'Serif'), - }, - { - unique: 'font-family-monospace', - label: 'Monospace', - element: this.#getElement('monospace', 'Monospace'), - }, - { - unique: 'font-family-cursive', - label: 'Cursive', - element: this.#getElement('cursive', 'Cursive'), - }, - { - unique: 'font-family-fantasy', - label: 'Fantasy', - element: this.#getElement('fantasy', 'Fantasy'), - }, - ]; - - #getElement(fontFamily: string, label: string) { - const menuItem = document.createElement('uui-menu-item'); - menuItem.addEventListener('click', () => { - //this.editor?.chain().focus().setMark('textStyle', { fontFamily }).run(); - this.editor?.chain().focus().setSpanStyle(`font-family: ${fontFamily};`).run(); - }); - - const element = document.createElement('span'); - element.slot = 'label'; - element.textContent = label; - element.style.fontFamily = fontFamily; - - menuItem.appendChild(element); - - return menuItem; - } - - override render() { - const label = this.localize.string(this.manifest?.meta.label); - return html` - - ${label} - - - - - `; - } - - static override readonly styles = [ - css` - :host { - --uui-button-font-weight: normal; - --uui-menu-item-flat-structure: 1; - - margin-inline-start: var(--uui-size-space-1); - } - - uui-button > uui-symbol-expand { - margin-left: var(--uui-size-space-4); - } - `, - ]; -} - -export { UmbTiptapToolbarFontFamilyToolbarElement as element }; - -declare global { - interface HTMLElementTagNameMap { - 'umb-tiptap-font-family-toolbar-element': UmbTiptapToolbarFontFamilyToolbarElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family.tiptap-toolbar-api.ts new file mode 100644 index 0000000000..02b6b168a3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-family.tiptap-toolbar-api.ts @@ -0,0 +1,10 @@ +import { UmbTiptapToolbarElementApiBase } from '../base.js'; +import type { MetaTiptapToolbarMenuItem } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarFontFamilyExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) { + if (!item?.data) return; + editor?.chain().focus().setSpanStyle(`font-family: ${item.data};`).run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts deleted file mode 100644 index eade259455..0000000000 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size-tiptap-toolbar.element.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { UmbTiptapToolbarButtonElement } from '../../components/toolbar/tiptap-toolbar-button.element.js'; -import type { UmbCascadingMenuItem } from '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; -import { css, customElement, html, ifDefined } from '@umbraco-cms/backoffice/external/lit'; - -import '../../components/cascading-menu-popover/cascading-menu-popover.element.js'; - -@customElement('umb-tiptap-font-size-toolbar-element') -export class UmbTiptapToolbarFontSizeToolbarElement extends UmbTiptapToolbarButtonElement { - #fontSizes = [8, 10, 12, 14, 16, 18, 24, 36, 48].map((fontSize) => `${fontSize}pt`); - - #menu: Array = this.#fontSizes.map((fontSize) => ({ - unique: `font-size-${fontSize}`, - label: fontSize, - // execute: () => this.editor?.chain().focus().setMark('textStyle', { fontSize }).run(), - execute: () => this.editor?.chain().focus().setSpanStyle(`font-size: ${fontSize};`).run(), - })); - - override render() { - const label = this.localize.string(this.manifest?.meta.label); - return html` - - ${label} - - - - - `; - } - - static override readonly styles = [ - css` - :host { - --uui-button-font-weight: normal; - --uui-menu-item-flat-structure: 1; - - margin-inline-start: var(--uui-size-space-1); - } - - uui-button > uui-symbol-expand { - margin-left: var(--uui-size-space-4); - } - `, - ]; -} - -export { UmbTiptapToolbarFontSizeToolbarElement as element }; - -declare global { - interface HTMLElementTagNameMap { - 'umb-tiptap-font-size-toolbar-element': UmbTiptapToolbarFontSizeToolbarElement; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size.tiptap-toolbar-api.ts new file mode 100644 index 0000000000..d62b9c82a1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/font-size.tiptap-toolbar-api.ts @@ -0,0 +1,10 @@ +import { UmbTiptapToolbarElementApiBase } from '../base.js'; +import type { MetaTiptapToolbarMenuItem } from '../types.js'; +import type { Editor } from '@umbraco-cms/backoffice/external/tiptap'; + +export default class UmbTiptapToolbarFontFamilyExtensionApi extends UmbTiptapToolbarElementApiBase { + override execute(editor?: Editor, item?: MetaTiptapToolbarMenuItem) { + if (!item?.data) return; + editor?.chain().focus().setSpanStyle(`font-size: ${item.data};`).run(); + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-center.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-center.tiptap-toolbar-api.ts index 683f9cef2c..f9a48e1b9f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-center.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-center.tiptap-toolbar-api.ts @@ -7,6 +7,10 @@ export default class UmbTiptapToolbarTextAlignCenterExtensionApi extends UmbTipt } override execute(editor?: Editor) { - editor?.chain().focus().setTextAlign('center').run(); + if (!this.isActive(editor)) { + editor?.chain().focus().setTextAlign('center').run(); + } else { + editor?.chain().focus().unsetTextAlign().run(); + } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-justify.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-justify.tiptap-toolbar-api.ts index 10e4825664..584619954e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-justify.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-justify.tiptap-toolbar-api.ts @@ -7,6 +7,10 @@ export default class UmbTiptapToolbarTextAlignJustifyExtensionApi extends UmbTip } override execute(editor?: Editor) { - editor?.chain().focus().setTextAlign('justify').run(); + if (!this.isActive(editor)) { + editor?.chain().focus().setTextAlign('justify').run(); + } else { + editor?.chain().focus().unsetTextAlign().run(); + } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-left.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-left.tiptap-toolbar-api.ts index df6fb9fedb..172ccb785e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-left.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-left.tiptap-toolbar-api.ts @@ -7,6 +7,10 @@ export default class UmbTiptapToolbarTextAlignLeftExtensionApi extends UmbTiptap } override execute(editor?: Editor) { - editor?.chain().focus().setTextAlign('left').run(); + if (!this.isActive(editor)) { + editor?.chain().focus().setTextAlign('left').run(); + } else { + editor?.chain().focus().unsetTextAlign().run(); + } } } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-right.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-right.tiptap-toolbar-api.ts index 1f0c703b87..5a1032395b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-right.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/toolbar/text-align-right.tiptap-toolbar-api.ts @@ -7,6 +7,10 @@ export default class UmbTiptapToolbarTextAlignRightExtensionApi extends UmbTipta } override execute(editor?: Editor) { - editor?.chain().focus().setTextAlign('right').run(); + if (!this.isActive(editor)) { + editor?.chain().focus().setTextAlign('right').run(); + } else { + editor?.chain().focus().unsetTextAlign().run(); + } } } From a7a6eb8af6fa726cb7c4374c230e8084ac9321f0 Mon Sep 17 00:00:00 2001 From: Nhu Dinh <150406148+nhudinh0309@users.noreply.github.com> Date: Thu, 27 Feb 2025 18:44:39 +0700 Subject: [PATCH 52/58] V15 QA Fixing the failing acceptance tests in the nightly build (#18466) * Updated permission name due to UI changes * Added skip to flaky tests * Add skip to flaky tests * Bumped version * Updated content tests due to test helper changes --- tests/Umbraco.Tests.AcceptanceTest/package-lock.json | 9 ++++----- tests/Umbraco.Tests.AcceptanceTest/package.json | 2 +- .../tests/DefaultConfig/Content/ContentInfoTab.spec.ts | 4 ++-- .../DefaultConfig/Content/ContentWithBlockList.spec.ts | 6 ++++-- .../tests/DefaultConfig/Users/UserGroups.spec.ts | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 0139e39172..8ea239c327 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -8,7 +8,7 @@ "hasInstallScript": true, "dependencies": { "@umbraco/json-models-builders": "^2.0.29", - "@umbraco/playwright-testhelpers": "^15.0.23", + "@umbraco/playwright-testhelpers": "^15.0.24", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" @@ -67,10 +67,9 @@ } }, "node_modules/@umbraco/playwright-testhelpers": { - "version": "15.0.23", - "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.23.tgz", - "integrity": "sha512-7r7tV45wP47b26lcrv427xINanQ67HTlr89QxprwLgG4otbeDqJO908H9OqsLFoKyk958e1SaGCqBRYbUpJ9EQ==", - "license": "MIT", + "version": "15.0.24", + "resolved": "https://registry.npmjs.org/@umbraco/playwright-testhelpers/-/playwright-testhelpers-15.0.24.tgz", + "integrity": "sha512-cv7sr3e1vhOoqAKOgj82kKgWY9dCQCnQdP+4rGllM/Dhvup+nSs93XKOAnTc2Fn3ZqhpwA8PDL8Pg9riUpt5JQ==", "dependencies": { "@umbraco/json-models-builders": "2.0.30", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/package.json b/tests/Umbraco.Tests.AcceptanceTest/package.json index 48d4b1794e..9c542f9bf0 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package.json @@ -21,7 +21,7 @@ }, "dependencies": { "@umbraco/json-models-builders": "^2.0.29", - "@umbraco/playwright-testhelpers": "^15.0.23", + "@umbraco/playwright-testhelpers": "^15.0.24", "camelize": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^2.6.7" diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts index df67c034ab..24e84cb79e 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentInfoTab.spec.ts @@ -29,7 +29,7 @@ test('can see correct information when published', async ({umbracoApi, umbracoUi // Act await umbracoUi.content.goToContentWithName(contentName); await umbracoUi.content.clickInfoTab(); - await umbracoUi.content.doesLinkHaveText(notPublishContentLink); + await umbracoUi.content.doesDocumentHaveLink(notPublishContentLink); await umbracoUi.content.clickSaveAndPublishButton(); // Assert @@ -46,7 +46,7 @@ test('can see correct information when published', async ({umbracoApi, umbracoUi hour12: true, }); await umbracoUi.content.doesCreatedDateHaveText(expectedCreatedDate); - await umbracoUi.content.doesLinkHaveText(contentData.urls[0].url ? contentData.urls[0].url : '/'); + await umbracoUi.content.doesDocumentHaveLink(contentData.urls[0].url ? contentData.urls[0].url : '/'); // TODO: Uncomment this when front-end is ready. Currently the publication status of content is not changed to "Published" immediately after publishing it //await umbracoUi.content.doesPublicationStatusHaveText(contentData.variants[0].state === 'Draft' ? 'Unpublished' : contentData.variants[0].state); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithBlockList.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithBlockList.spec.ts index de5dccd627..fccdb0f9de 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithBlockList.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Content/ContentWithBlockList.spec.ts @@ -132,7 +132,8 @@ test('can delete block element in the content', async ({umbracoApi, umbracoUi}) expect(blockGridValue).toBeFalsy(); }); -test('cannot add number of block element greater than the maximum amount', async ({umbracoApi, umbracoUi}) => { +// Skip this flaky tests as sometimes the modal to choose block item is not displayed +test.skip('cannot add number of block element greater than the maximum amount', async ({umbracoApi, umbracoUi}) => { // Arrange const customDataTypeId = await umbracoApi.dataType.createBlockListWithABlockAndMinAndMaxAmount(customDataTypeName, elementTypeId, 0, 1); const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, customDataTypeName, customDataTypeId); @@ -154,7 +155,8 @@ test('cannot add number of block element greater than the maximum amount', async await umbracoUi.content.doesFormValidationMessageContainText('too many'); }); -test('can set the label of block element in the content', async ({umbracoApi, umbracoUi}) => { +// Skip this flaky tests as sometimes the modal to choose block item is not displayed +test.skip('can set the label of block element in the content', async ({umbracoApi, umbracoUi}) => { // Arrange const blockLabel = 'Test Block Label'; const customDataTypeId = await umbracoApi.dataType.createBlockListDataTypeWithLabel(customDataTypeName, elementTypeId, blockLabel); diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts index 67cd958a48..c56fa1675c 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DefaultConfig/Users/UserGroups.spec.ts @@ -3,7 +3,7 @@ import {expect} from "@playwright/test"; const allPermissions = { uiPermission: - ['Browse Node', + ['Browse', 'Create Document Blueprint', 'Delete', 'Create', From 96febee55918a52a7bfe2dea42c610fa1cbabd32 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 28 Feb 2025 13:55:06 +0100 Subject: [PATCH 53/58] add mandatory attribute (#18488) --- .../details/data-type-details-workspace-view.element.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts index 8c0345238a..63e5610914 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/data-type/workspace/views/details/data-type-details-workspace-view.element.ts @@ -96,7 +96,8 @@ export class UmbDataTypeDetailsWorkspaceViewEditElement extends UmbLitElement im + description=${this.localize.term('propertyEditorPicker_title')} + mandatory> + description=${this.localize.term('propertyEditorPicker_title')} + mandatory> Date: Fri, 28 Feb 2025 13:56:05 +0100 Subject: [PATCH 54/58] align tags (#18487) --- .../column-layouts/document-table-column-state.element.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-table-column-state.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-table-column-state.element.ts index 37bde5b831..e7d2c56c2d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-table-column-state.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/collection/views/table/column-layouts/document-table-column-state.element.ts @@ -36,17 +36,17 @@ export class UmbDocumentTableColumnStateElement extends UmbLitElement implements override render() { switch (this._state) { case 'Published': - return html`${this.localize.term('content_published')}`; + return html`${this.localize.term('content_published')}`; case 'PublishedPendingChanges': - return html`${this.localize.term('content_publishedPendingChanges')}`; case 'Draft': return html`${this.localize.term('content_unpublished')}`; case 'NotCreated': - return html`${this.localize.term('content_notCreated')}`; + return html`${this.localize.term('content_notCreated')}`; default: - return html`${fromCamelCase(this.value.item.state)}`; + return html`${fromCamelCase(this._state)}`; } } } From 4795609973c1bc7a8e20563976c2e4d836d9de64 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 28 Feb 2025 14:01:54 +0100 Subject: [PATCH 55/58] Removet the Tip tap placeholder text (#18486) --- .../core/rich-text-essentials.tiptap-api.ts | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts index bfdb2ac4b6..48f1708a66 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts @@ -1,20 +1,9 @@ import { UmbTiptapExtensionApiBase } from '../base.js'; -import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api'; -import { - Div, - HtmlGlobalAttributes, - Placeholder, - Span, - StarterKit, - TrailingNode, -} from '@umbraco-cms/backoffice/external/tiptap'; +import { Div, HtmlGlobalAttributes, Span, StarterKit, TrailingNode } from '@umbraco-cms/backoffice/external/tiptap'; export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionApiBase { - #localize = new UmbLocalizationController(this); - getTiptapExtensions = () => [ StarterKit, - Placeholder.configure({ placeholder: this.#localize.term('placeholders_rteParagraph') }), Div, Span, HtmlGlobalAttributes.configure({ From a20f11e48ff09c614e3eec2ada484bf7fe953fdf Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Thu, 27 Feb 2025 17:28:33 +0100 Subject: [PATCH 56/58] Makes a few labels friendlier for screen readers. --- src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts | 12 ++++++------ src/Umbraco.Web.UI.Client/src/assets/lang/en.ts | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts index a04089c694..cb7ab76554 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en-us.ts @@ -276,9 +276,9 @@ export default { noVariantsToProcess: 'There are no available variants', releaseDate: 'Publish at', unpublishDate: 'Unpublish at', - removeDate: 'Clear Date', + removeDate: 'Clear date', setDate: 'Set date', - sortDone: 'Sortorder is updated', + sortDone: 'Sort order is updated', sortHelp: 'To sort the nodes, simply drag the nodes or click one of the column headers. You can select\n multiple nodes by holding the "shift" or "control" key while selecting\n ', statistics: 'Statistics', @@ -520,8 +520,8 @@ export default { confirmlogout: 'Are you sure?', confirmSure: 'Are you sure?', cut: 'Cut', - editDictionary: 'Edit Dictionary Item', - editLanguage: 'Edit Language', + editDictionary: 'Edit dictionary item', + editLanguage: 'Edit language', editSelectedMedia: 'Edit selected media', insertAnchor: 'Insert local link', insertCharacter: 'Insert character', @@ -533,7 +533,7 @@ export default { languagedeletewarning: 'This will delete the language', languageChangeWarning: 'Changing the culture for a language may be an expensive operation and will result\n in the content cache and indexes being rebuilt\n ', - lastEdited: 'Last Edited', + lastEdited: 'Last edited', link: 'Link', linkinternal: 'Internal link', linklocaltip: 'When using local links, insert "#" in front of link', @@ -956,7 +956,7 @@ export default { avatar: 'Avatar for', header: 'Header', systemField: 'system field', - lastUpdated: 'Last Updated', + lastUpdated: 'Last updated', selectAll: 'Select all', skipToMenu: 'Skip to menu', skipToContent: 'Skip to content', diff --git a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts index f8764f9127..02354a5a98 100644 --- a/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts +++ b/src/Umbraco.Web.UI.Client/src/assets/lang/en.ts @@ -235,7 +235,7 @@ export default { createDateDesc: 'Date/time this document was created', documentType: 'Document Type', editing: 'Editing', - expireDate: 'Remove At', + expireDate: 'Remove at', itemChanged: 'This item has been changed after publication', itemNotPublished: 'This item is not published', lastPublished: 'Last published', @@ -270,11 +270,11 @@ export default { publishDescendantsWithVariantsHelp: 'Publish variants and variants of same type underneath and thereby making their content publicly available.', noVariantsToProcess: 'There are no available variants', - releaseDate: 'Publish At', - unpublishDate: 'Unpublish At', - removeDate: 'Clear Date', + releaseDate: 'Publish at', + unpublishDate: 'Unpublish at', + removeDate: 'Clear date', setDate: 'Set date', - sortDone: 'Sortorder is updated', + sortDone: 'Sort order is updated', sortHelp: 'To sort the nodes, simply drag the nodes or click one of the column headers. You can select\n multiple nodes by holding the "shift" or "control" key while selecting\n ', statistics: 'Statistics', @@ -533,7 +533,7 @@ export default { languagedeletewarning: 'This will delete the language and all content related to the language', languageChangeWarning: 'Changing the culture for a language may be an expensive operation and will result\n in the content cache and indexes being rebuilt\n ', - lastEdited: 'Last Edited', + lastEdited: 'Last edited', link: 'Link', linkinternal: 'Internal link', linklocaltip: 'When using local links, insert "#" in front of link', @@ -960,7 +960,7 @@ export default { avatar: 'Avatar for', header: 'Header', systemField: 'system field', - lastUpdated: 'Last Updated', + lastUpdated: 'Last updated', selectAll: 'Select all', skipToMenu: 'Skip to menu', skipToContent: 'Skip to content', From 3ef9d64f1a674cced71b4f616bb361831b0183c8 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Fri, 28 Feb 2025 15:37:52 +0000 Subject: [PATCH 57/58] Tiptap RTE: Stylesheets (#18502) * Copies the "TinyMCE Stylesheets Property Editor UI" to the Templating/Stylesheet package * Deprecates the "TinyMCE Stylesheets Property Editor UI" as it may be used in 3rd party extensions. TinyMCE will be removed in v16, but this deprecation notice in 15.4 will at least notify that the "Umb.PropertyEditorUi.StylesheetPicker" UI can be used. * Updates TinyMCE to use the Stylesheet Picker UI * Prevent Stylesheet folders from being picked + UI tweaks * Updates Stylesheet mock data * Stylesheet Rule Input - removes Edit button uses `@open` event for UI consistency with other pickers. * Updates pangram * Amending to be "header" instead of "heading" * Adds "stylesheets" config to Tiptap data-type * Updates RTE styles for Tiptap Moves "StarterKit" styles to the RTE Essentials extension registration. Scopes TinyMCE in "rte-content.css" (for now). --- .../src/css/rte-content.css | 84 ++++++++--------- .../mocks/data/data-type/data-type.data.ts | 3 +- .../mocks/data/stylesheet/stylesheet.data.ts | 90 +++++++++---------- .../stylesheet-input.element.ts | 7 +- .../stylesheet-rule-input.element.ts | 9 +- .../stylesheet-rule-settings-modal.element.ts | 2 +- .../templating/stylesheets/manifests.ts | 2 + .../stylesheets/property-editors/manifests.ts | 3 + .../stylesheet-picker/manifests.ts | 13 +++ ...rty-editor-ui-stylesheet-picker.element.ts | 46 ++++++++++ ...rty-editor-ui-stylesheet-picker.stories.ts | 21 +++++ ...operty-editor-ui-stylesheet-picker.test.ts | 23 +++++ ...y-mce-stylesheets-configuration.element.ts | 11 +++ .../property-editors/tiny-mce/manifests.ts | 2 +- .../input-tiptap/input-tiptap.element.ts | 55 +++--------- .../core/rich-text-essentials.tiptap-api.ts | 41 +++++++++ .../extensions/style-select/manifests.ts | 6 +- .../property-editors/tiptap/manifests.ts | 9 +- 18 files changed, 281 insertions(+), 146 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/manifests.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.element.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.stories.ts create mode 100644 src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.test.ts diff --git a/src/Umbraco.Web.UI.Client/src/css/rte-content.css b/src/Umbraco.Web.UI.Client/src/css/rte-content.css index 2e6d1c23dc..05aa9227a5 100644 --- a/src/Umbraco.Web.UI.Client/src/css/rte-content.css +++ b/src/Umbraco.Web.UI.Client/src/css/rte-content.css @@ -1,49 +1,51 @@ -.umb-macro-holder { - border: 3px dotted red; - padding: 7px; - margin: 3px; - display: block; - position: relative; -} +#tinymce { + .umb-macro-holder { + border: 3px dotted red; + padding: 7px; + margin: 3px; + display: block; + position: relative; + } -.umb-macro-holder::after { - content: 'Macros are no longer supported. Please use the block picker instead.'; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: white; - background-color: rgba(0, 0, 0, 0.7); - padding: 10px; - border-radius: 5px; -} + .umb-macro-holder::after { + content: 'Macros are no longer supported. Please use the block picker instead.'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + background-color: rgba(0, 0, 0, 0.7); + padding: 10px; + border-radius: 5px; + } -.umb-embed-holder { - position: relative; -} + .umb-embed-holder { + position: relative; + } -.umb-embed-holder > * { - user-select: none; - pointer-events: none; -} + .umb-embed-holder > * { + user-select: none; + pointer-events: none; + } -.umb-embed-holder[data-mce-selected] { - outline: 2px solid var(--uui-palette-spanish-pink-light); -} + .umb-embed-holder[data-mce-selected] { + outline: 2px solid var(--uui-palette-spanish-pink-light); + } -.umb-embed-holder::before { - z-index: 1000; - width: 100%; - height: 100%; - position: absolute; - content: ' '; -} + .umb-embed-holder::before { + z-index: 1000; + width: 100%; + height: 100%; + position: absolute; + content: ' '; + } -.umb-embed-holder[data-mce-selected]::before { - background: rgba(0, 0, 0, 0.025); -} + .umb-embed-holder[data-mce-selected]::before { + background: rgba(0, 0, 0, 0.025); + } -*[data-mce-selected='inline-boundary'] { - background: rgba(0, 0, 0, 0.025); - outline: 2px solid var(--uui-palette-spanish-pink-light); + *[data-mce-selected='inline-boundary'] { + background: rgba(0, 0, 0, 0.025); + outline: 2px solid var(--uui-palette-spanish-pink-light); + } } diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts index 56a114c6c0..2b858b9d23 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/data-type/data-type.data.ts @@ -1075,6 +1075,7 @@ export const data: Array = [ ], ], }, + { alias: 'stylesheets', value: ['/rte-styles.css'] }, { alias: 'dimensions', value: { height: 500 } }, { alias: 'maxImageSize', value: 500 }, { alias: 'ignoreUserStartNodes', value: false }, @@ -1105,7 +1106,7 @@ export const data: Array = [ '+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,video[*],audio[*],picture[*],source[*],canvas[*]', }, { alias: 'invalidElements', value: 'font' }, - { alias: 'stylesheets', value: [] }, + { alias: 'stylesheets', value: ['/rte-styles.css'] }, { alias: 'toolbar', value: [ diff --git a/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet/stylesheet.data.ts b/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet/stylesheet.data.ts index 1145bb66e9..14b53b5bc3 100644 --- a/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet/stylesheet.data.ts +++ b/src/Umbraco.Web.UI.Client/src/mocks/data/stylesheet/stylesheet.data.ts @@ -10,93 +10,89 @@ export type UmbMockStylesheetModel = StylesheetResponseModel & export const data: Array = [ { - name: 'Stylesheet File 1.css', - path: '/Stylesheet File 1.css', + name: 'RTE Styles', + path: '/rte-styles.css', parent: null, isFolder: false, hasChildren: false, content: ` - /** Stylesheet 1 */ +/** RTE Stylesheet */ - h1 { - color: blue; +#editor, #tinymce { + background-color: pink; + font-size: 1.5rem; } -/**umb_name:bjjh*/ -h1 { - color: blue; +/**umb_name:Page header*/ +h2 { + color: red; + font-size: 2rem; } -/**umb_name:comeone*/ -h1 { +/**umb_name:Section header*/ +h3 { color: blue; + font-size: 1.75rem; } -/**umb_name:lol*/ -h1 { - color: blue; +/**umb_name:Paragraph header*/ +h4 { + color: green; + font-size: 1.5rem; }`, }, { - name: 'Stylesheet File 2.css', - path: '/Stylesheet File 2.css', + name: 'RTE Styles 2', + path: '/rte-styles-2.css', parent: null, isFolder: false, hasChildren: false, content: ` - /** Stylesheet 2 */ -h1 { - color: green; +/** RTE Stylesheet 2 */ + +body { + font-family: cursive; } -/**umb_name:HELLO*/ -h1 { - color: green; +/**umb_name:Red*/ +span { + color: red; } -/**umb_name:SOMETHING*/ -h1 { - color: green; +/**umb_name:Blue*/ +span { + color: blue; } -/**umb_name:NIOCE*/ -h1 { +/**umb_name:Green*/ +span { color: green; }`, }, { - name: 'Folder 1', - path: '/Folder 1', + name: 'Folder for website', + path: '/folder-for-website', parent: null, isFolder: true, hasChildren: true, content: '', }, { - name: 'Stylesheet File 3.css', - path: '/Folder 1/Stylesheet File 3.css', + name: 'Website Styles', + path: '/folder-for-website/website-styles.css', parent: { - path: '/Folder 1', + path: '/folder-for-website', }, hasChildren: false, isFolder: false, - content: `h1 { - color: pink; -} + content: ` +/** Website Stylesheet */ -/**umb_name:ONE*/ -h1 { - color: pink; +body { + background-color: #ffb7d3; + color: #b57790; + font-family: sans-serif; } - -/**umb_name:TWO*/ -h1 { - color: pink; -} - -/**umb_name:THREE*/ -h1 { - color: pink; -}`, +`, }, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-input/stylesheet-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-input/stylesheet-input.element.ts index 6e6708ab8a..30f0bc5b21 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-input/stylesheet-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-input/stylesheet-input.element.ts @@ -110,7 +110,7 @@ export class UmbStylesheetInputElement extends UUIFormControlMixin(UmbLitElement this.#pickerContext.openPicker()} + @click=${() => this.#pickerContext.openPicker({ pickableFilter: (item) => !item.isFolder })} label="Add stylesheet"> `; } @@ -118,13 +118,14 @@ export class UmbStylesheetInputElement extends UUIFormControlMixin(UmbLitElement #renderItem(item: UmbStylesheetItemModel) { if (!item.unique) return; return html` - + + this.#pickerContext.requestRemoveItem(item.unique!)} label=${this.localize.term('general_remove')}> - + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-rule-input/stylesheet-rule-input.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-rule-input/stylesheet-rule-input.element.ts index 04d8b7d3b5..103e7f7a1b 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-rule-input/stylesheet-rule-input.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-rule-input/stylesheet-rule-input.element.ts @@ -79,11 +79,12 @@ export class UmbStylesheetRuleInputElement extends UUIFormControlMixin(UmbLitEle this.rules, (rule, index) => rule.name + index, (rule, index) => html` - + this.#editRule(rule, index)}> - this.#editRule(rule, index)}> this.#removeRule(rule)}> diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-rule-input/stylesheet-rule-settings-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-rule-input/stylesheet-rule-settings-modal.element.ts index 2875e4e486..343d94599f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-rule-input/stylesheet-rule-settings-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/global-components/stylesheet-rule-input/stylesheet-rule-settings-modal.element.ts @@ -97,7 +97,7 @@ export default class UmbStylesheetRuleSettingsModalElement extends UmbModalBaseE
1 2 3 4 5 6 7 8 9 0 € £ $ % & (.,;:'"!?)
- Just keep examining every bid quoted for zinc etchings. + Amazingly few discotheques provide jukeboxes.
diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts index f2d89860f0..69d917f09e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/manifests.ts @@ -4,6 +4,7 @@ import { manifests as treeManifests } from './tree/manifests.js'; import { manifests as workspaceManifests } from './workspace/manifests.js'; import { manifests as entityActionManifests } from './entity-actions/manifests.js'; import { manifests as componentManifests } from './global-components/manifests.js'; +import { manifests as propertyEditorsManifests } from './property-editors/manifests.js'; export const manifests: Array = [ ...repositoryManifests, @@ -12,4 +13,5 @@ export const manifests: Array = [ ...workspaceManifests, ...entityActionManifests, ...componentManifests, + ...propertyEditorsManifests, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/manifests.ts new file mode 100644 index 0000000000..1207b62ca8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/manifests.ts @@ -0,0 +1,3 @@ +import { manifest as stylesheetPickerManifest } from './stylesheet-picker/manifests.js'; + +export const manifests: Array = [stylesheetPickerManifest]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/manifests.ts new file mode 100644 index 0000000000..a0990dec61 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/manifests.ts @@ -0,0 +1,13 @@ +import type { ManifestPropertyEditorUi } from '@umbraco-cms/backoffice/property-editor'; + +export const manifest: ManifestPropertyEditorUi = { + type: 'propertyEditorUi', + alias: 'Umb.PropertyEditorUi.StylesheetPicker', + name: 'Stylesheet Picker Property Editor UI', + js: () => import('./property-editor-ui-stylesheet-picker.element.js'), + meta: { + label: 'Stylesheet Picker', + icon: 'icon-document', + group: 'common', + }, +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.element.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.element.ts new file mode 100644 index 0000000000..af764134c6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.element.ts @@ -0,0 +1,46 @@ +import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; +import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; +import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; +import type { + UmbPropertyEditorConfigCollection, + UmbPropertyEditorUiElement, +} from '@umbraco-cms/backoffice/property-editor'; +import type { UmbStylesheetInputElement } from '@umbraco-cms/backoffice/stylesheet'; + +@customElement('umb-property-editor-ui-stylesheet-picker') +export class UmbPropertyEditorUIStylesheetPickerElement extends UmbLitElement implements UmbPropertyEditorUiElement { + readonly #serverFilePathUniqueSerializer = new UmbServerFilePathUniqueSerializer(); + + @property({ type: Array }) + public set value(value: Array) { + if (!value) return; + this.#value = value.map((unique) => this.#serverFilePathUniqueSerializer.toUnique(unique)); + } + public get value(): Array { + if (!this.#value) return []; + return this.#value.map((unique) => this.#serverFilePathUniqueSerializer.toServerPath(unique)) as string[]; + } + #value: Array = []; + + @property({ type: Object, attribute: false }) + public config?: UmbPropertyEditorConfigCollection; + + #onChange(event: CustomEvent) { + const target = event.target as UmbStylesheetInputElement; + this.#value = target.selection ?? []; + this.dispatchEvent(new UmbPropertyValueChangeEvent()); + } + + override render() { + return html``; + } +} + +export default UmbPropertyEditorUIStylesheetPickerElement; + +declare global { + interface HTMLElementTagNameMap { + 'umb-property-editor-ui-stylesheet-picker': UmbPropertyEditorUIStylesheetPickerElement; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.stories.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.stories.ts new file mode 100644 index 0000000000..8363c00a0c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.stories.ts @@ -0,0 +1,21 @@ +import { umbDataTypeMockDb } from '../../../../../mocks/data/data-type/data-type.db.js'; +import { html } from '@umbraco-cms/backoffice/external/lit'; +import type { Meta } from '@storybook/web-components'; + +import './property-editor-ui-tiny-mce-stylesheets-configuration.element.js'; +import type { UmbDataTypeDetailModel } from '@umbraco-cms/backoffice/data-type'; + +const dataTypeData = umbDataTypeMockDb.read('dt-richTextEditor') as unknown as UmbDataTypeDetailModel; + +export default { + title: 'Property Editor UIs/Stylesheet Picker', + component: 'umb-property-editor-ui-stylesheet-picker', + id: 'umb-property-editor-ui-sstylesheet-picker', +} as Meta; + +export const AAAOverview = ({ value }: any) => + html``; +AAAOverview.storyName = 'Overview'; +AAAOverview.args = { + value: dataTypeData?.values?.find((x) => x.alias === 'stylesheets')?.value ?? [], +}; diff --git a/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.test.ts b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.test.ts new file mode 100644 index 0000000000..3a53652f45 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/packages/templating/stylesheets/property-editors/stylesheet-picker/property-editor-ui-stylesheet-picker.test.ts @@ -0,0 +1,23 @@ +import { UmbPropertyEditorUIStylesheetPickerElement } from './property-editor-ui-stylesheet-picker.element.js'; +import { expect, fixture, html } from '@open-wc/testing'; +import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils'; + +describe('UmbPropertyEditorUIStylesheetPickerElement', () => { + let element: UmbPropertyEditorUIStylesheetPickerElement; + + beforeEach(async () => { + element = await fixture(html` + + `); + }); + + it('is defined with its own instance', () => { + expect(element).to.be.instanceOf(UmbPropertyEditorUIStylesheetPickerElement); + }); + + if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) { + it('passes the a11y audit', async () => { + await expect(element).shadowDom.to.be.accessible(defaultA11yConfig); + }); + } +}); diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts index a0efa34c22..048383857c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/stylesheets/property-editor-ui-tiny-mce-stylesheets-configuration.element.ts @@ -1,4 +1,5 @@ import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit'; +import { UmbDeprecation } from '@umbraco-cms/backoffice/utils'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; import { UmbPropertyValueChangeEvent } from '@umbraco-cms/backoffice/property-editor'; import { UmbServerFilePathUniqueSerializer } from '@umbraco-cms/backoffice/server-file-system'; @@ -38,6 +39,16 @@ export class UmbPropertyEditorUITinyMceStylesheetsConfigurationElement this.dispatchEvent(new UmbPropertyValueChangeEvent()); } + constructor() { + super(); + new UmbDeprecation({ + deprecated: 'umb-property-editor-ui-tiny-mce-stylesheets-configuration', + removeInVersion: '16.0.0', + solution: + "Use `` instead, or the 'Umb.PropertyEditorUi.StylesheetPicker' manifest.", + }).warn(); + } + override render() { return html``; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts index 902911315a..b3c14d87c3 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiny-mce/property-editors/tiny-mce/manifests.ts @@ -66,7 +66,7 @@ export const manifests: Array = [ alias: 'stylesheets', label: '#treeHeaders_stylesheets', description: 'Pick the stylesheets whose editor styles should be available when editing', - propertyEditorUiAlias: 'Umb.PropertyEditorUI.TinyMCE.StylesheetsConfiguration', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.StylesheetPicker', weight: 20, }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts index 03b4824fc7..4541209e21 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/components/input-tiptap/input-tiptap.element.ts @@ -1,6 +1,6 @@ import type { UmbTiptapExtensionApi } from '../../extensions/types.js'; import type { UmbTiptapToolbarValue } from '../types.js'; -import { css, customElement, html, property, state, unsafeCSS, when } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, map, property, state, unsafeCSS, when } from '@umbraco-cms/backoffice/external/lit'; import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api'; import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; import { Editor } from '@umbraco-cms/backoffice/external/tiptap'; @@ -17,6 +17,8 @@ const TIPTAP_CORE_EXTENSION_ALIAS = 'Umb.Tiptap.RichTextEssentials'; @customElement('umb-input-tiptap') export class UmbInputTiptapElement extends UmbFormControlMixin(UmbLitElement) { + #stylesheets = new Set(['/umbraco/backoffice/css/rte-content.css']); + @property({ type: String }) override set value(value: string) { if (value === this.#value) return; @@ -121,6 +123,11 @@ export class UmbInputTiptapElement extends UmbFormControlMixin>('stylesheets'); + if (stylesheets?.length) { + stylesheets.forEach((x) => this.#stylesheets.add(x)); + } + this._toolbar = this.configuration?.getValueByAlias('toolbar') ?? [[[]]]; const tiptapExtensions: Extensions = []; @@ -179,6 +186,7 @@ export class UmbInputTiptapElement extends UmbFormControlMixin html``)} @@ -217,7 +225,6 @@ export class UmbInputTiptapElement extends UmbFormControlMixin .tiptap { height: 100%; width: 100%; outline: none; @@ -246,47 +254,6 @@ export class UmbInputTiptapElement extends UmbFormControlMixin code) { - background-color: var(--uui-color-surface-alt); - padding: var(--uui-size-space-1) var(--uui-size-space-2); - border-radius: calc(var(--uui-border-radius) * 2); - } - - code { - font-family: 'Roboto Mono', monospace; - background: none; - color: inherit; - font-size: 0.8rem; - padding: 0; - } - - h1, - h2, - h3, - h4, - h5, - h6 { - margin-top: 0; - margin-bottom: 0.5em; - } - - max-width: 100%; - - li { - > p { - margin: 0; - padding: 0; - } - } } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts index 48f1708a66..e51609936d 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/core/rich-text-essentials.tiptap-api.ts @@ -1,4 +1,5 @@ import { UmbTiptapExtensionApiBase } from '../base.js'; +import { css } from '@umbraco-cms/backoffice/external/lit'; import { Div, HtmlGlobalAttributes, Span, StarterKit, TrailingNode } from '@umbraco-cms/backoffice/external/tiptap'; export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionApiBase { @@ -36,6 +37,46 @@ export class UmbTiptapRichTextEssentialsExtensionApi extends UmbTiptapExtensionA }), TrailingNode, ]; + + override getStyles = () => css` + pre { + background-color: var(--uui-color-surface-alt); + padding: var(--uui-size-space-2) var(--uui-size-space-4); + border-radius: calc(var(--uui-border-radius) * 2); + overflow-x: auto; + } + + code:not(pre > code) { + background-color: var(--uui-color-surface-alt); + padding: var(--uui-size-space-1) var(--uui-size-space-2); + border-radius: calc(var(--uui-border-radius) * 2); + } + + code { + font-family: 'Roboto Mono', monospace; + background: none; + color: inherit; + font-size: 0.8rem; + padding: 0; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + margin-top: 0; + margin-bottom: 0.5em; + } + + li { + > p { + margin: 0; + padding: 0; + } + } + `; } export default UmbTiptapRichTextEssentialsExtensionApi; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts index 298270f1f5..915c7c939e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/style-select/manifests.ts @@ -13,9 +13,9 @@ export const manifests: Array = [ { label: 'Headers', items: [ - { label: 'Page heading', data: 'h2', style: 'font-size: x-large;font-weight: bold;' }, - { label: 'Section heading', data: 'h3', style: 'font-size: large;font-weight: bold;' }, - { label: 'Paragraph heading', data: 'h4', style: 'font-weight: bold;' }, + { label: 'Page header', data: 'h2', style: 'font-size: x-large;font-weight: bold;' }, + { label: 'Section header', data: 'h3', style: 'font-size: large;font-weight: bold;' }, + { label: 'Paragraph header', data: 'h4', style: 'font-weight: bold;' }, ], }, { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/manifests.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/manifests.ts index 9b7c323e53..737186a385 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/manifests.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap/manifests.ts @@ -31,12 +31,19 @@ _Drag and drop the available actions onto the toolbar._`, propertyEditorUiAlias: 'Umb.PropertyEditorUi.Tiptap.ToolbarConfiguration', weight: 15, }, + { + alias: 'stylesheets', + label: '#treeHeaders_stylesheets', + description: 'Pick the stylesheets whose editor styles should be available when editing!!!', + propertyEditorUiAlias: 'Umb.PropertyEditorUi.StylesheetPicker', + weight: 20, + }, { alias: 'dimensions', label: '#general_dimensions', description: '{#tiptap_config_dimensions_description}', propertyEditorUiAlias: 'Umb.PropertyEditorUI.TinyMCE.DimensionsConfiguration', - weight: 20, + weight: 30, }, { alias: 'maxImageSize', From 2bb894c038a5cefffe66254c00b9b29177118402 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Fri, 28 Feb 2025 13:38:54 +0100 Subject: [PATCH 58/58] 15.3: Hotfix: Content type discard changes (#18490) * wrap in entity detail workspace element * use element on media type and member type * add developer console warning * sync current data with owner content type * Update content-type-workspace-context-base.ts * fix lint error --- .../content-type-workspace-context-base.ts | 49 ++++++++++++++----- .../document-type-workspace-editor.element.ts | 4 +- .../media-type-workspace-editor.element.ts | 4 +- .../member-type-workspace-editor.element.ts | 4 +- 4 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts index a37960f622..74c3f510d1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/content-type/workspace/content-type-workspace-context-base.ts @@ -10,7 +10,7 @@ import { type UmbRoutableWorkspaceContext, } from '@umbraco-cms/backoffice/workspace'; import type { UmbReferenceByUnique } from '@umbraco-cms/backoffice/models'; -import { jsonStringComparison, type Observable } from '@umbraco-cms/backoffice/observable-api'; +import type { Observable } from '@umbraco-cms/backoffice/observable-api'; import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action'; import { UmbRequestReloadChildrenOfEntityEvent, @@ -21,6 +21,8 @@ import type { UmbEntityModel } from '@umbraco-cms/backoffice/entity'; // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface UmbContentTypeWorkspaceContextArgs extends UmbEntityDetailWorkspaceContextArgs {} +const LOADING_STATE_UNIQUE = 'umbLoadingContentTypeDetail'; + export abstract class UmbContentTypeWorkspaceContextBase< DetailModelType extends UmbContentTypeDetailModel = UmbContentTypeDetailModel, DetailRepositoryType extends UmbDetailRepository = UmbDetailRepository, @@ -61,6 +63,9 @@ export abstract class UmbContentTypeWorkspaceContextBase< this.allowedContentTypes = this.structure.ownerContentTypeObservablePart((data) => data?.allowedContentTypes); this.compositions = this.structure.ownerContentTypeObservablePart((data) => data?.compositions); this.collection = this.structure.ownerContentTypeObservablePart((data) => data?.collection); + + // Keep current data in sync with the owner content type - This is used for the discard changes feature + this.observe(this.structure.ownerContentType, (data) => this._data.setCurrent(data)); } /** @@ -72,21 +77,27 @@ export abstract class UmbContentTypeWorkspaceContextBase< args: UmbEntityDetailWorkspaceContextCreateArgs, ): Promise { this.resetState(); + this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Creating ${this.getEntityType()} scaffold` }); this.setParent(args.parent); const request = this.structure.createScaffold(args.preset); this._getDataPromise = request; let { data } = await request; - if (!data) return undefined; - this.setUnique(data.unique); + if (data) { + data = await this._scaffoldProcessData(data); - if (this.modalContext) { - data = { ...data, ...this.modalContext.data.preset }; + if (this.modalContext) { + // Notice if the preset comes with values, they will overwrite the scaffolded values... [NL] + data = { ...data, ...this.modalContext.data.preset }; + } + + this.setUnique(data.unique); + this.setIsNew(true); + this._data.setPersisted(data); } - this.setIsNew(true); - this._data.setPersisted(data); + this.loading.removeState(LOADING_STATE_UNIQUE); return data; } @@ -97,8 +108,13 @@ export abstract class UmbContentTypeWorkspaceContextBase< * @returns { Promise } The loaded data */ override async load(unique: string) { + if (unique === this.getUnique() && this._getDataPromise) { + return (await this._getDataPromise) as any; + } + this.resetState(); this.setUnique(unique); + this.loading.addState({ unique: LOADING_STATE_UNIQUE, message: `Loading ${this.getEntityType()} Details` }); this._getDataPromise = this.structure.loadType(unique); const response = await this._getDataPromise; const data = response.data; @@ -106,11 +122,24 @@ export abstract class UmbContentTypeWorkspaceContextBase< if (data) { this._data.setPersisted(data); this.setIsNew(false); + + this.observe( + response.asObservable(), + (entity: any) => this.#onDetailStoreChange(entity), + 'umbContentTypeDetailStoreObserver', + ); } + this.loading.removeState(LOADING_STATE_UNIQUE); return response; } + #onDetailStoreChange(entity: DetailModelType | undefined) { + if (!entity) { + this._data.clear(); + } + } + /** * Creates the Content Type * @param { DetailModelType } currentData The current data @@ -232,12 +261,6 @@ export abstract class UmbContentTypeWorkspaceContextBase< return this.structure.getOwnerContentType(); } - protected override _getHasUnpersistedChanges(): boolean { - const currentData = this.structure.getOwnerContentType(); - const persistedData = this._data.getPersisted(); - return jsonStringComparison(persistedData, currentData) === false; - } - public override destroy(): void { this.structure.destroy(); super.destroy(); diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace-editor.element.ts index 6fe2558d9d..be615b0227 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/document-types/workspace/document-type/document-type-workspace-editor.element.ts @@ -78,7 +78,7 @@ export class UmbDocumentTypeWorkspaceEditorElement extends UmbLitElement { override render() { return html` - +
- + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts index 4c7fd19afb..10c68fbe84 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/media/media-types/workspace/media-type-workspace-editor.element.ts @@ -82,7 +82,7 @@ export class UmbMediaTypeWorkspaceEditorElement extends UmbLitElement { override render() { return html` - +
- + `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts index c9d8a5f545..521a3a54c5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member-type/workspace/member-type-workspace-editor.element.ts @@ -77,7 +77,7 @@ export class UmbMemberTypeWorkspaceEditorElement extends UmbLitElement { override render() { return html` - +
- + `; }