From 3b6be8e7c4c791713c86c5b243d4dcf7968574fb Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 15 Sep 2025 15:08:15 +0200 Subject: [PATCH 1/6] Feature: Redirect to the last visited path when navigating between sections (#20084) * redirect to the last visited path in a section * Update src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update backoffice-header-sections.element.ts --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../backoffice-header-sections.element.ts | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts index b83133f761..28d8a80778 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts @@ -16,6 +16,8 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement { private _backofficeContext?: UmbBackofficeContext; + #sectionPathMap = new Map(); + constructor() { super(); @@ -52,6 +54,42 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement { ); } + #getSectionPath(manifest: ManifestSection | undefined) { + return `section/${manifest?.meta.pathname}`; + } + + #onSectionClick(event: PointerEvent, manifest: ManifestSection | undefined) { + // Let the browser handle the click if the Ctrl or Meta key is pressed + if (event.ctrlKey || event.metaKey) { + return; + } + + event.stopPropagation(); + event.preventDefault(); + + // Store the current path for the section so we can redirect to it next time the section is visited + if (this._currentSectionAlias) { + const currentPath = window.location.pathname; + this.#sectionPathMap.set(this._currentSectionAlias, currentPath); + } + + if (!manifest) { + throw new Error('Section manifest is missing'); + } + + const clickedSectionAlias = manifest.alias; + + // Check if we have a stored path for the clicked section + if (this.#sectionPathMap.has(clickedSectionAlias)) { + const storedPath = this.#sectionPathMap.get(clickedSectionAlias); + history.pushState(null, '', storedPath); + } else { + // Nothing stored, so we navigate to the regular section path + const sectionPath = this.#getSectionPath(manifest); + history.pushState(null, '', sectionPath); + } + } + override render() { return html` @@ -61,7 +99,8 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement { (section) => html` this.#onSectionClick(event, section.manifest)} + href="${this.#getSectionPath(section.manifest)}" label="${ifDefined( section.manifest?.meta.label ? this.localize.string(section.manifest?.meta.label) From 40fe4995e8208f3c6bb0bd0c1b11b7e7694edea5 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:11:01 +0200 Subject: [PATCH 2/6] V16: keepUserLoggedIn has no effect (#20123) * feat: exports all current-user config-related items * fix: observes the current-user config for the 'keepUserLoggedIn' value and simply try to refresh the token when the worker makes an attempt to log out the user * fix: moves current user config repository and related dependencies to the 'current-user' package previously, it was not exported, so is not a breaking change * chore: moves current-user-allow-mfa condition to the 'current-user' package to avoid circular dependencies (and because it naturally belongs there) * fix: checks for `keepUserLoggedIn` directly * Revert "chore: moves current-user-allow-mfa condition to the 'current-user' package to avoid circular dependencies (and because it naturally belongs there)" This reverts commit 17bebfba41f6996205f0649d70c0d210808f6081. * Revert "fix: moves current user config repository and related dependencies to the 'current-user' package" This reverts commit 0c114628985643a2ac1c7dc135e75d64db972bc6. * Revert "feat: exports all current-user config-related items" This reverts commit a6586aff1dcc293ae5485bcf436297341fc126bf. * fix: avoids depending on 'resources' --- .../auth-session-timeout.controller.ts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/auth/controllers/auth-session-timeout.controller.ts b/src/Umbraco.Web.UI.Client/src/packages/core/auth/controllers/auth-session-timeout.controller.ts index 62a3d994e8..4d522bd673 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/auth/controllers/auth-session-timeout.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/auth/controllers/auth-session-timeout.controller.ts @@ -2,10 +2,13 @@ import type { UmbAuthFlow } from '../auth-flow.js'; import type { UmbAuthContext } from '../auth.context.js'; import { UMB_MODAL_AUTH_TIMEOUT } from '../modals/umb-auth-timeout-modal.token.js'; import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api'; +import { UserService } from '@umbraco-cms/backoffice/external/backend-api'; export class UmbAuthSessionTimeoutController extends UmbControllerBase { #tokenCheckWorker?: SharedWorker; #host: UmbAuthContext; + #keepUserLoggedIn = false; + #hasCheckedKeepUserLoggedIn = false; constructor(host: UmbAuthContext, authFlow: UmbAuthFlow) { super(host, 'UmbAuthSessionTimeoutController'); @@ -22,6 +25,15 @@ export class UmbAuthSessionTimeoutController extends UmbControllerBase { // Listen for messages from the token check worker this.#tokenCheckWorker.port.onmessage = async (event) => { + // If the user has chosen to stay logged in, we ignore the logout command and instead request a new token + if (this.#keepUserLoggedIn) { + console.log( + '[Auth Context] User chose to stay logged in, attempting to validate token instead of logging out.', + ); + await this.#tryValidateToken(); + return; + } + if (event.data?.command === 'logout') { // If the worker signals a logout, we clear the token storage and set the user as unauthorized host.timeOut(); @@ -60,6 +72,16 @@ export class UmbAuthSessionTimeoutController extends UmbControllerBase { }, '_authFlowTimeoutSignal', ); + + this.observe( + host.isAuthorized, + (isAuthorized) => { + if (isAuthorized) { + this.#observeKeepUserLoggedIn(); + } + }, + '_authFlowIsAuthorizedSignal', + ); } override destroy(): void { @@ -68,6 +90,20 @@ export class UmbAuthSessionTimeoutController extends UmbControllerBase { this.#tokenCheckWorker = undefined; } + /** + * Observe the user's preference for staying logged in + * and update the internal state accordingly. + * This method fetches the current user configuration from the server to find the value. + * // TODO: We cannot observe the config store directly here yet, as it would create a circular dependency, so maybe we need to move the config option somewhere else? + */ + async #observeKeepUserLoggedIn() { + if (this.#hasCheckedKeepUserLoggedIn) return; + this.#hasCheckedKeepUserLoggedIn = true; + // eslint-disable-next-line local-rules/no-direct-api-import + const { data } = await UserService.getUserCurrentConfiguration(); + this.#keepUserLoggedIn = data?.keepUserLoggedIn ?? false; + } + async #closeTimeoutModal() { const contextToken = (await import('@umbraco-cms/backoffice/modal')).UMB_MODAL_MANAGER_CONTEXT; const modalManager = await this.getContext(contextToken); From cbf5665f15fc10ade4f0ba37eb9b28320d49719b Mon Sep 17 00:00:00 2001 From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:14:39 +0200 Subject: [PATCH 3/6] V16: Fix member validation endpoints (#20116) * Call the validation of member data * Fix return status * Refactor to remove duplicate code * Update src/Umbraco.Infrastructure/Services/MemberEditingService.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Removed `disableNotifications` from members validation --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: leekelleher --- .../Services/MemberEditingService.cs | 84 +++++++++++++++++-- .../member-validation.server.data-source.ts | 1 - 2 files changed, 79 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Infrastructure/Services/MemberEditingService.cs b/src/Umbraco.Infrastructure/Services/MemberEditingService.cs index 974a70254f..6f062f1d43 100644 --- a/src/Umbraco.Infrastructure/Services/MemberEditingService.cs +++ b/src/Umbraco.Infrastructure/Services/MemberEditingService.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Models.ContentEditing.Validation; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services.OperationStatus; @@ -48,15 +49,21 @@ internal sealed class MemberEditingService : IMemberEditingService public Task GetAsync(Guid key) => Task.FromResult(_memberService.GetById(key)); - public async Task> ValidateCreateAsync(MemberCreateModel createModel) - => await _memberContentEditingService.ValidateAsync(createModel, createModel.ContentTypeKey); + public async Task> ValidateCreateAsync( + MemberCreateModel createModel) => + await ValidateMember(createModel, null, createModel.Password, createModel.ContentTypeKey); + + public async Task> ValidateUpdateAsync(Guid key, MemberUpdateModel updateModel) { IMember? member = _memberService.GetById(key); - return member is not null - ? await _memberContentEditingService.ValidateAsync(updateModel, member.ContentType.Key) - : Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentValidationResult()); + if (member is null) + { + return Attempt.FailWithStatus(ContentEditingOperationStatus.NotFound, new ContentValidationResult()); + } + + return await ValidateMember(updateModel, key, updateModel.NewPassword, member.ContentType.Key); } public async Task> CreateAsync(MemberCreateModel createModel, IUser user) @@ -221,6 +228,73 @@ internal sealed class MemberEditingService : IMemberEditingService contentDeleteResult.Result); } + private async Task> ValidateMember(MemberEditingModelBase model, Guid? memberKey, string? password, Guid memberTypeKey) + { + var validationErrors = new List(); + MemberEditingOperationStatus validationStatus = await ValidateMemberDataAsync(model, memberKey, password); + if (validationStatus is not MemberEditingOperationStatus.Success) + { + validationErrors.Add(MapStatusToPropertyValidationError(validationStatus)); + } + Attempt propertyValidation = await _memberContentEditingService.ValidateAsync(model, memberTypeKey); + + if (propertyValidation.Success is false) + { + if (propertyValidation.Status is ContentEditingOperationStatus.ContentTypeNotFound) + { + return Attempt.FailWithStatus(ContentEditingOperationStatus.ContentTypeNotFound, new ContentValidationResult()); + } + else + { + validationErrors.AddRange(propertyValidation.Result.ValidationErrors); + } + } + + var result = new ContentValidationResult { ValidationErrors = validationErrors }; + return result.ValidationErrors.Any() is false + ? Attempt.SucceedWithStatus(ContentEditingOperationStatus.Success, result) + : Attempt.FailWithStatus(ContentEditingOperationStatus.PropertyValidationError, result); + } + + private PropertyValidationError MapStatusToPropertyValidationError(MemberEditingOperationStatus memberEditingOperationStatus) + { + string alias; + string[] errorMessages; + switch (memberEditingOperationStatus) + { + case MemberEditingOperationStatus.InvalidName: + alias = "name"; + errorMessages = ["Invalid or empty name"]; + break; + case MemberEditingOperationStatus.InvalidPassword: + alias = "password"; + errorMessages = ["Invalid password"]; + break; + case MemberEditingOperationStatus.InvalidUsername: + alias = "username"; + errorMessages = ["Invalid username"]; + break; + case MemberEditingOperationStatus.InvalidEmail: + alias = "email"; + errorMessages = ["Invalid email"]; + break; + case MemberEditingOperationStatus.DuplicateUsername: + alias = "username"; + errorMessages = ["Duplicate username"]; + break; + case MemberEditingOperationStatus.DuplicateEmail: + alias = "email"; + errorMessages = ["Duplicate email"]; + break; + default: + alias = string.Empty; + errorMessages = []; + break; + } + + return new PropertyValidationError { Alias = alias, Culture = null, Segment = null, ErrorMessages = errorMessages, JsonPath = string.Empty }; + } + private async Task ValidateMemberDataAsync(MemberEditingModelBase model, Guid? memberKey, string? password) { if (model.Variants.FirstOrDefault(v => v.Culture is null && v.Segment is null)?.Name.IsNullOrWhiteSpace() is not false) diff --git a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/validation/member-validation.server.data-source.ts b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/validation/member-validation.server.data-source.ts index 544069add2..b6889b741f 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/validation/member-validation.server.data-source.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/members/member/repository/validation/member-validation.server.data-source.ts @@ -94,7 +94,6 @@ export class UmbMemberValidationServerDataSource { path: { id: model.unique }, body, }), - { disableNotifications: true }, ); if (data && typeof data === 'string') { From 4207e0360cc986e7fc716c19b5c9ce5b266b30f9 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Mon, 15 Sep 2025 19:56:22 +0200 Subject: [PATCH 4/6] Reload section root on repeated header section click (#20141) * Reload section root on repeated header section click Adds logic to reload the root of a section if its header is clicked while already active. This improves navigation consistency by resetting the section view when the user clicks the current section again. * Update backoffice-header-sections.element.ts --- .../components/backoffice-header-sections.element.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts index 28d8a80778..89c0ffbb51 100644 --- a/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts +++ b/src/Umbraco.Web.UI.Client/src/apps/backoffice/components/backoffice-header-sections.element.ts @@ -79,6 +79,13 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement { const clickedSectionAlias = manifest.alias; + // If the clicked section is the same as the current section, we just load the original section path to load the section root + if (this._currentSectionAlias === clickedSectionAlias) { + const sectionPath = this.#getSectionPath(manifest); + history.pushState(null, '', sectionPath); + return; + } + // Check if we have a stored path for the clicked section if (this.#sectionPathMap.has(clickedSectionAlias)) { const storedPath = this.#sectionPathMap.get(clickedSectionAlias); From 343a07de71529a950b80417c980cfb15372a06db Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Tue, 16 Sep 2025 09:14:20 +0200 Subject: [PATCH 5/6] Tiptap RTE: Fixes undo when RTE is emptied (#20133) * Tiptap RTE: prevent `undefined` value If the `value` becomes `undefined`, then the block data can't be tracked (for undo/redo). The scenario comes when a user "selects all" contents, cuts it, and pasted it back in. Fixes #20076 * Tiptap RTE: fixes selection white text bug * Tiptap RTE: amends heading styles (for first-child) --- .../components/ref-rte-block/ref-rte-block.element.ts | 4 ---- .../tiptap/extensions/heading/heading.tiptap-api.ts | 6 +++++- .../tiptap-rte/property-editor-ui-tiptap.element.ts | 7 ------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts index 3f0dc522fc..20adfe8be5 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/block/block-rte/components/ref-rte-block/ref-rte-block.element.ts @@ -84,10 +84,6 @@ export class UmbRefRteBlockElement extends UmbLitElement { umb-icon, umb-ufm-render { z-index: 1; - - &::selection { - color: var(--uui-color-default-contrast); - } } `, ]; diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/heading/heading.tiptap-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/heading/heading.tiptap-api.ts index d733e83234..240f330e70 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/heading/heading.tiptap-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/heading/heading.tiptap-api.ts @@ -13,7 +13,11 @@ export default class UmbTiptapHeadingExtensionApi extends UmbTiptapExtensionApiB h5, h6 { margin-top: 0; - margin-bottom: 0.5em; + margin-bottom: 1rem; + + &:first-child { + margin-top: 0.25rem; + } } `; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap-rte/property-editor-ui-tiptap.element.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap-rte/property-editor-ui-tiptap.element.ts index c979385c79..2ac8f55a88 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap-rte/property-editor-ui-tiptap.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/property-editors/tiptap-rte/property-editor-ui-tiptap.element.ts @@ -20,13 +20,6 @@ export class UmbPropertyEditorUiTiptapElement extends UmbPropertyEditorUiRteElem const tipTapElement = event.target; const markup = tipTapElement.value; - // If we don't get any markup clear the property editor value. - if (tipTapElement.isEmpty()) { - this.value = undefined; - this._fireChangeEvent(); - return; - } - // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup. const usedContentKeys: string[] = []; From 9aa9d4499fe67f6495af12eeb36f5ff25f0054a8 Mon Sep 17 00:00:00 2001 From: Lee Kelleher Date: Tue, 16 Sep 2025 09:15:56 +0200 Subject: [PATCH 6/6] Fixes regression with hidden tab labels in the Content Editor (#20140) Fixes regression with hidden tabs in the Content Editor Regression occurred in #19255, originally fixed in #19370. --- .../content/workspace/views/edit/content-editor.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts index f5b9941d44..45c8d3b817 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/content/content/workspace/views/edit/content-editor.element.ts @@ -201,7 +201,7 @@ export class UmbContentWorkspaceViewEditElement extends UmbLitElement implements fullPath === this._activePath || (!this._hasRootGroups && index === 0 && this._routerPath + '/' === this._activePath); return html`